1 /************************************************************************
2  *									*
3  *  This file is part of Kooka, a scanning/OCR application using	*
4  *  Qt <http://www.qt.io> and KDE Frameworks <http://www.kde.org>.	*
5  *									*
6  *  Copyright (C) 2003-2016 Klaas Freitag <freitag@suse.de>		*
7  *                          Jonathan Marten <jjm@keelhaul.me.uk>	*
8  *									*
9  *  Kooka is free software; you can redistribute it and/or modify it	*
10  *  under the terms of the GNU Library General Public License as	*
11  *  published by the Free Software Foundation and appearing in the	*
12  *  file COPYING included in the packaging of this file;  either	*
13  *  version 2 of the License, or (at your option) any later version.	*
14  *									*
15  *  As a special exception, permission is given to link this program	*
16  *  with any version of the KADMOS OCR/ICR engine (a product of		*
17  *  reRecognition GmbH, Kreuzlingen), and distribute the resulting	*
18  *  executable without including the source code for KADMOS in the	*
19  *  source distribution.						*
20  *									*
21  *  This program is distributed in the hope that it will be useful,	*
22  *  but WITHOUT ANY WARRANTY; without even the implied warranty of	*
23  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the	*
24  *  GNU General Public License for more details.			*
25  *									*
26  *  You should have received a copy of the GNU General Public		*
27  *  License along with this program;  see the file COPYING.  If		*
28  *  not, see <http://www.gnu.org/licenses/>.				*
29  *									*
30  ************************************************************************/
31 
32 #include "kookaprint.h"
33 
34 #include <math.h>
35 
36 #include <qpainter.h>
37 #include <qfontmetrics.h>
38 #include <qguiapplication.h>
39 #include <qdebug.h>
40 
41 #include <klocalizedstring.h>
42 
43 #include "imgprintdialog.h"
44 #include "kookaimage.h"
45 #include "kookasettings.h"
46 
47 
48 #define CUT_MARGIN		5			// margin in millimetres
49 
50 #define PRINT_ORDER_COLUMNS
51 #define CUTMARKS_COLOURSEGS
52 
53 
54 
55 // TODO: somewhere common (in libkookascan)
56 #define DPM_TO_DPI(d)		qRound((d)*2.54/100)	// dots/metre -> dots/inch
57 #define DPI_TO_DPM(d)		qRound((d)*100/2.54)	// dots/inch -> dots/metre
58 
59 
60 
KookaPrint()61 KookaPrint::KookaPrint()
62     : QPrinter(QPrinter::HighResolution)
63 {
64     qDebug();
65     m_image = nullptr;
66 
67     // Initial default print parameters
68     m_scaleOption = static_cast<KookaPrint::ScaleOption>(KookaSettings::printScaleOption());
69     m_printSize = KookaSettings::printPrintSize();
70     m_maintainAspect = KookaSettings::printMaintainAspect();
71     m_lowResDraft = KookaSettings::printLowResDraft();
72     m_cutsOption = static_cast<KookaPrint::CutMarksOption>(KookaSettings::printCutsOption());
73 
74     setOutputFileName(KookaSettings::printFileName());
75 
76     m_screenResolution = -1;				// set by caller
77     m_scanResolution = -1;				// taken from image
78 }
79 
80 
recalculatePrintParameters()81 void KookaPrint::recalculatePrintParameters()
82 {
83     if (m_image==nullptr) return;				// no image to print
84 
85     qDebug() << "image:";
86     qDebug() << "  size (pix) =" << m_image->size();
87     qDebug() << "  dpi X =" << DPM_TO_DPI(m_image->dotsPerMeterX());
88     qDebug() << "  dpi Y =" << DPM_TO_DPI(m_image->dotsPerMeterY());
89     qDebug() << "printer:";
90     qDebug() << "  name =" << printerName();
91     qDebug() << "  colour mode =" << colorMode();
92     qDebug() << "  full page?" << fullPage();
93     qDebug() << "  output format =" << outputFormat();
94     qDebug() << "  paper rect (mm) =" << paperRect(QPrinter::Millimeter);
95     qDebug() << "  page rect (mm) =" << pageRect(QPrinter::Millimeter);
96     qDebug() << "  resolution =" << resolution();
97     qDebug() << "options:";
98     qDebug() << "  scale mode =" << m_scaleOption;
99     qDebug() << "  print size (mm) =" << m_printSize;
100     qDebug() << "  scan resolution =" << m_scanResolution;
101     qDebug() << "  screen resolution =" << m_screenResolution;
102     qDebug() << "  cuts option =" << m_cutsOption;
103     qDebug() << "  maintain aspect?" << m_maintainAspect;
104     qDebug() << "  low res draft?" << m_lowResDraft;
105 
106     // Calculate the available page size, in real world units
107     QRectF r = pageRect(QPrinter::Millimeter);
108     mPageWidthMm = r.width();
109     mPageHeightMm = r.height();
110 
111     // Calculate the size at which the image is to be printed,
112     // depending on the scaling option.
113 
114     mImageWidthPix = m_image->width();			// image size in pixels
115     mImageHeightPix = m_image->height();
116 
117     if (m_scaleOption==KookaPrint::ScaleScan)		// Original scan size
118     {
119         const int imageRes = m_scanResolution!=-1 ? DPI_TO_DPM(m_scanResolution) : m_image->dotsPerMeterX();
120 	Q_ASSERT(imageRes>0);				// dots per metre
121         mPrintWidthMm = double(mImageWidthPix)/imageRes*1000;
122         mPrintHeightMm = double(mImageHeightPix)/imageRes*1000;
123     }
124     else if (m_scaleOption==KookaPrint::ScaleScreen)	// Scale to screen resolution
125     {
126         int screenRes = DPI_TO_DPM(m_screenResolution);
127         Q_ASSERT(screenRes>0);				// dots per metre
128         mPrintWidthMm = double(mImageWidthPix)/screenRes*1000;
129         mPrintHeightMm = double(mImageHeightPix)/screenRes*1000;
130     }
131     else if (m_scaleOption==KookaPrint::ScaleCustom)	// Custom size
132     {
133         // For this option, "Maintain aspect ratio" can be enabled in the GUI.
134         // There is however no need to take account of it here, because the
135         // values are already scaled/adjusted there.
136 
137         mPrintWidthMm = double(m_printSize.width());
138         mPrintHeightMm = double(m_printSize.height());
139         Q_ASSERT(mPrintWidthMm>0 && mPrintHeightMm>0);
140     }
141     else if (m_scaleOption==KookaPrint::ScaleFitPage)	// Fit to one page
142     {
143         mPrintWidthMm = mPageWidthMm;
144         mPrintHeightMm = mPageHeightMm;
145 
146         // If cut marks are being "always" shown, then reduce the printed
147         // image size here to account for them.  For the other cut marks
148         // options, the image for this scale will by definition fit on one page
149         // and so they will not be shown.
150 
151         if (m_cutsOption==KookaPrint::CutMarksAlways)
152         {
153             mPrintWidthMm -= 2*CUT_MARGIN;
154             mPrintHeightMm -= 2*CUT_MARGIN;
155         }
156 
157         if (m_maintainAspect)				// maintain the aspect ratio
158         {
159             QRectF r = pageRect(QPrinter::DevicePixel);
160             double wAspect = r.width()/mImageWidthPix;	// scaling ratio image -> page
161             double hAspect = r.height()/mImageHeightPix;
162 
163             if (wAspect>hAspect)
164             {
165                 // More scaling up is needed in the horizontal direction,
166                 // so reduce that to maintain the aspect ratio
167                 mPrintWidthMm *= hAspect/wAspect;
168             }
169             else if (hAspect>wAspect)
170             {
171                 // More scaling up is needed in the vertical direction,
172                 // so reduce that to maintain the aspect ratio
173                 mPrintHeightMm *= wAspect/hAspect;
174             }
175         }
176     }
177     else Q_ASSERT(false);
178 
179     qDebug() << "scaled image size (mm) =" << QSizeF(mPrintWidthMm, mPrintHeightMm);
180 
181     mPrintResolution = DPI_TO_DPM(resolution())/1000;	// dots per millimetre
182 
183     // Now that we have the image size to be printed,
184     // see if cut marks are required.
185 
186     mPageWidthAdjustedMm = mPageWidthMm;
187     mPageHeightAdjustedMm = mPageHeightMm;
188 
189     bool withCutMarks;
190     if (m_cutsOption==KookaPrint::CutMarksMultiple) withCutMarks = !(mPrintWidthMm<=mPageWidthMm && mPrintHeightMm<=mPageHeightMm);
191     else if (m_cutsOption==KookaPrint::CutMarksAlways) withCutMarks = true;
192     else if (m_cutsOption==KookaPrint::CutMarksNone) withCutMarks = false;
193     else Q_ASSERT(false);
194     qDebug() << "for cuts" << m_cutsOption << "with marks?" << withCutMarks;
195 
196     // If cut marks are required, reduce the available page size
197     // to allow for them.
198 
199     mPrintLeftPix = 0;					// page origin of print
200     mPrintTopPix = 0;
201 
202     if (withCutMarks)
203     {
204         mPageWidthAdjustedMm -= 2*CUT_MARGIN;
205         mPageHeightAdjustedMm -= 2*CUT_MARGIN;
206         qDebug() << "adjusted page size (mm) =" << QSizeF(mPageWidthAdjustedMm, mPageHeightAdjustedMm);
207 
208         mPrintLeftPix = mPrintTopPix = CUT_MARGIN*mPrintResolution;
209     }
210 
211     bool onOnePage = (mPrintWidthMm<=mPageWidthAdjustedMm && mPrintHeightMm<=mPageHeightAdjustedMm);
212     qDebug() << "on one page?" << onOnePage;		// see if fits on one page
213 							// must be true for this
214     if (m_scaleOption==KookaPrint::ScaleFitPage) Q_ASSERT(onOnePage);
215 
216     // If the image fits on one page, then adjust the print margins so
217     // that it is centered.  I'm not sure whether this is the right thing
218     // to do, but it is implied by tool tips set in ImgPrintDialog.
219     // TODO: maybe make it an option
220 
221     if (onOnePage)
222     {
223         int widthSpareMm = mPageWidthAdjustedMm-mPrintWidthMm;
224         mPrintLeftPix += (widthSpareMm/2)*mPrintResolution;
225         int heightSpareMm = mPageHeightAdjustedMm-mPrintHeightMm;
226         mPrintTopPix += (heightSpareMm/2)*mPrintResolution;
227     }
228 
229     // Calculate how many parts (including partial parts)
230     // the image needs to be sliced up into.
231 
232     double ipart;
233     double fpart = modf(mPrintWidthMm/mPageWidthAdjustedMm, &ipart);
234     //qDebug() << "for cols ipart" << ipart << "fpart" << fpart;
235     mPrintColumns = qRound(ipart)+(fpart>0 ? 1 : 0);
236     fpart = modf(mPrintHeightMm/mPageHeightAdjustedMm, &ipart);
237     //qDebug() << "for rows ipart" << ipart << "fpart" << fpart;
238     mPrintRows = qRound(ipart)+(fpart>0 ? 1 : 0);
239 
240     int totalPages = mPrintColumns*mPrintRows;
241     qDebug() << "print cols" << mPrintColumns << "rows" << mPrintRows << "pages" << totalPages;
242     Q_ASSERT(totalPages>0);				// checks for sanity
243     if (onOnePage) Q_ASSERT(totalPages==1);
244 
245     qDebug() << "done";
246 }
247 
248 
printImage()249 void KookaPrint::printImage()
250 {
251     if (m_image==nullptr) return;				// no image to print
252     qDebug() << "starting";
253     QGuiApplication::setOverrideCursor(Qt::WaitCursor);	// this may take some time
254 
255     // Save the print parameters used
256     KookaSettings::setPrintScaleOption(m_scaleOption);
257     KookaSettings::setPrintPrintSize(m_printSize);
258     KookaSettings::setPrintMaintainAspect(m_maintainAspect);
259     KookaSettings::setPrintLowResDraft(m_lowResDraft);
260     KookaSettings::setPrintCutsOption(m_cutsOption);
261     KookaSettings::setPrintFileName(outputFileName());
262     KookaSettings::self()->save();
263 
264 #if 1
265     // TODO: does this work?
266     if (m_lowResDraft) setResolution(75);
267 #endif
268 
269     // Create the painter after all the print parameters have been set.
270     QPainter painter(this);
271     painter.setRenderHint(QPainter::SmoothPixmapTransform);
272 
273     // The image is to be printed as 'mPrintColumns' columns in 'mPrintRows'
274     // rows. Each whole slice is 'sliceWidthPix' pixels wide and 'sliceHeightPix'
275     // pixels high;  the last slice in each row and column may be smaller.
276 
277     int sliceWidthPix = qRound(mImageWidthPix*mPageWidthAdjustedMm/mPrintWidthMm);
278     int sliceHeightPix = qRound(mImageHeightPix*mPageHeightAdjustedMm/mPrintHeightMm);
279     qDebug() << "slice size =" << QSize(sliceWidthPix, sliceHeightPix);
280 
281     // Print columns and rows in this order, so that in the usual printer
282     // and alignment case the sheets line up corresponding with the columns.
283 
284     const int totalPages = mPrintColumns*mPrintRows;
285     int page = 1;
286 #ifdef PRINT_ORDER_COLUMNS
287     for (int col = 0; col<mPrintColumns; ++col)
288 #else
289     for (int row = 0; row<mPrintRows; ++row)
290 #endif
291     {
292 #ifdef PRINT_ORDER_COLUMNS
293         for (int row = 0; row<mPrintRows; ++row)
294 #else
295         for (int col = 0; col<mPrintColumns; ++col)
296 #endif
297         {
298             qDebug() << "print page" << page << "col" << col << "row" << row;
299 
300             // The slice starts at 'sliceLeftPix' and 'sliceTopPix' in the image.
301             int sliceLeftPix = col*sliceWidthPix;
302             int sliceTopPix = row*sliceHeightPix;
303 
304             // Calculate the source rectangle within the image
305             int thisWidthPix = qMin(sliceWidthPix, (mImageWidthPix-sliceLeftPix));
306             int thisHeightPix = qMin(sliceHeightPix, (mImageHeightPix-sliceTopPix));
307             const QRect sourceRect(sliceLeftPix, sliceTopPix, thisWidthPix, thisHeightPix);
308 
309             // Calculate the target rectangle on the painter
310             int targetWidthPix = qRound((mPageWidthAdjustedMm*mPrintResolution)*(double(thisWidthPix)/sliceWidthPix));
311             int targetHeightPix = qRound((mPageHeightAdjustedMm*mPrintResolution)*(double(thisHeightPix)/sliceHeightPix));
312             const QRect targetRect(mPrintLeftPix, mPrintTopPix, targetWidthPix, targetHeightPix);
313 
314             qDebug() << " " << sourceRect << "->" << targetRect;
315             // The markers are drawn first so that the image will overwrite them.
316             drawCornerMarkers(&painter, targetRect, row, col, mPrintRows, mPrintColumns);
317             // Then the image rectangle.
318             painter.drawImage(targetRect, *m_image, sourceRect);
319 
320             ++page;
321             if (page<=totalPages) newPage();		// not for the last page
322         }
323     }
324 
325     QGuiApplication::restoreOverrideCursor();
326     qDebug() << "done";
327 }
328 
329 
330 // Draw a cross marker centered on that corner of the printed image.
drawMarkerAroundPoint(QPainter * painter,const QPoint & p)331 void KookaPrint::drawMarkerAroundPoint(QPainter *painter, const QPoint &p)
332 {
333     const int len = (CUT_MARGIN-1)*mPrintResolution;	// length in device pixels
334 
335     painter->save();
336     painter->setPen(QPen(QBrush(Qt::black), 0));	// cosmetic pen width
337     painter->drawLine(p-QPoint(len, 0), p+QPoint(len, 0));
338     painter->drawLine(p-QPoint(0, len), p+QPoint(0, len));
339     painter->restore();
340 }
341 
342 
drawCutSign(QPainter * painter,const QPoint & p,int num,Qt::Corner dir)343 void KookaPrint::drawCutSign(QPainter *painter, const QPoint &p, int num, Qt::Corner dir)
344 {
345     painter->save();
346     int start = 0;
347     const int radius = (CUT_MARGIN-2)*mPrintResolution;	// offset in device pixels
348 
349     QColor brushColor(Qt::red);
350     int toffX = 0;
351     int toffY = 0;
352     QString numStr = QString::number(num);
353 
354     QFontMetrics fm = painter->fontMetrics();
355     int textWidth = fm.horizontalAdvance(numStr)/2;
356     int textHeight = fm.height()/2;
357     int textYOff = 0;
358     int textXOff = 0;
359     switch (dir) {
360     case Qt::BottomLeftCorner:
361         start = -90;
362         brushColor = Qt::green;
363         toffX = -1;
364         toffY = 1;
365         textXOff = -1 * textWidth;
366         textYOff = textHeight;
367         break;
368     case Qt::TopLeftCorner:
369         start = -180;
370         brushColor = Qt::blue;
371         toffX = -1;
372         toffY = -1;
373         textXOff = -1 * textWidth;
374         textYOff = textHeight;
375         break;
376     case Qt::TopRightCorner:
377         start = -270;
378         brushColor = Qt::yellow;
379         toffX = 1;
380         toffY = -1;
381         textXOff = -1 * textWidth;
382         textYOff = textHeight;
383         break;
384     case Qt::BottomRightCorner:
385         start = 0;
386         brushColor = Qt::magenta;
387         toffX = 1;
388         toffY = 1;
389         textXOff = -1 * textWidth;
390         textYOff = textHeight;
391         break;
392     default:
393         start = 0;
394     }
395 
396     /* to draw around the point p, subtraction of the half radius is needed */
397     int x = p.x() - radius / 2;
398     int y = p.y() - radius / 2;
399 
400     // painter->drawRect( x, y, radius, radius );  /* debug !!! */
401     const int tAway = radius * 3 / 4;
402 
403     int textX = p.x() + tAway * toffX + textXOff;
404     int textY = p.y() + tAway * toffY + textYOff;
405 
406     // painter->drawRect( textX, textY, bRect.width(), bRect.height() );
407     //qDebug() << "Drawing to position [" << textX << "," << textY << "]";
408     painter->drawText(textX,
409                         textY,
410                         numStr);
411 
412 #ifdef CUTMARKS_COLOURSEGS
413     QBrush b(brushColor);				// draw a colour segment
414     painter->setBrush(b);				// to show correct orientation
415     painter->drawPie(x, y, radius, radius, start*16, -90*16);
416 #endif
417 
418     painter->restore();
419 }
420 
421 
422 // Draw the circle and the numbers that indicate the adjacent pages
drawCornerMarkers(QPainter * painter,const QRect & targetRect,int row,int col,int maxRows,int maxCols)423 void KookaPrint::drawCornerMarkers(QPainter *painter, const QRect &targetRect,
424                                    int row, int col, int maxRows, int maxCols)
425 {
426     const bool multiPages = (maxRows>1 || maxCols>1);
427 
428     const bool firstColumn = (col==0);
429     const bool lastColumn = (col==(maxCols-1));
430     const bool firstRow = (row==0);
431     const bool lastRow = (row==(maxRows-1));
432 
433 #ifdef PRINT_ORDER_COLUMNS
434     const int indx = maxRows*col + row + 1;
435 #else
436     const int indx = maxCols*row + col + 1;
437 #endif
438     //qDebug() << "for col" << col << "/" << maxCols << "row" << row << "/" << maxRows << "-> index" << indx;
439 
440     // All the measurements here are derived from 'targetRect',
441     // which is in device pixels.
442 
443     // Page index, if required
444     if (multiPages)
445     {
446         const QString numStr = QString("= %1 =").arg(indx);
447         const QFontMetrics &fm = painter->fontMetrics();
448         const int xoff = targetRect.left()+((targetRect.width()-fm.horizontalAdvance(numStr))/2);
449 
450         painter->setPen(Qt::black);
451         painter->drawText(xoff, fm.height()-1, numStr);
452     }
453 
454     // Top left
455     QPoint p = targetRect.topLeft();
456     drawMarkerAroundPoint(painter, p);
457     if (multiPages)
458     {
459 #ifdef PRINT_ORDER_COLUMNS
460         if (!firstColumn) drawCutSign(painter, p, indx - maxRows, Qt::BottomLeftCorner);
461         if (!firstRow) drawCutSign(painter, p, indx - 1, Qt::TopRightCorner);
462         if (!firstColumn && !firstRow) drawCutSign(painter, p, indx - maxRows - 1, Qt::TopLeftCorner);
463 #else
464         if (!firstColumn) drawCutSign(painter, p, indx - 1, Qt::BottomLeftCorner);
465         if (!firstRow) drawCutSign(painter, p, indx - maxCols, Qt::TopRightCorner);
466         if (!firstColumn && !firstRow) drawCutSign(painter, p, indx - maxCols - 1, Qt::TopLeftCorner);
467 #endif
468     }
469 
470     // Top right
471     p = targetRect.topRight();
472     drawMarkerAroundPoint(painter, p);
473     if (multiPages)
474     {
475 #ifdef PRINT_ORDER_COLUMNS
476         if (!lastColumn) drawCutSign(painter, p, indx + maxRows, Qt::BottomRightCorner);
477         if (!firstRow) drawCutSign(painter, p, indx - 1, Qt::TopLeftCorner);
478         if (!lastColumn && !firstRow) drawCutSign(painter, p, indx + maxRows - 1, Qt::TopRightCorner);
479 #else
480         if (!lastColumn) drawCutSign(painter, p, indx + 1, Qt::BottomRightCorner);
481         if (!firstRow) drawCutSign(painter, p, indx - maxCols, Qt::TopLeftCorner);
482         if (!lastColumn && !firstRow) drawCutSign(painter, p, indx - maxCols + 1, Qt::TopRightCorner);
483 #endif
484     }
485 
486     // Bottom right
487     p = targetRect.bottomRight();
488     drawMarkerAroundPoint(painter, p);
489     if (multiPages)
490     {
491 #ifdef PRINT_ORDER_COLUMNS
492         if (!lastColumn) drawCutSign(painter, p, indx + maxRows, Qt::TopRightCorner);
493         if (!lastRow) drawCutSign(painter, p, indx + 1, Qt::BottomLeftCorner);
494         if (!lastColumn && !lastRow) drawCutSign(painter, p, indx + maxRows + 1, Qt::BottomRightCorner);
495 #else
496         if (!lastColumn) drawCutSign(painter, p, indx + 1, Qt::TopRightCorner);
497         if (!lastRow) drawCutSign(painter, p, indx + maxCols, Qt::BottomLeftCorner);
498         if (!lastColumn && !lastRow) drawCutSign(painter, p, indx + maxCols + 1, Qt::BottomRightCorner);
499 #endif
500     }
501 
502     // Bottom left
503     p = targetRect.bottomLeft();
504     drawMarkerAroundPoint(painter, p);
505     if (multiPages)
506     {
507 #ifdef PRINT_ORDER_COLUMNS
508         if (!firstColumn) drawCutSign(painter, p, indx - maxRows, Qt::TopLeftCorner);
509         if (!lastRow) drawCutSign(painter, p, indx + 1, Qt::BottomRightCorner);
510         if (!firstColumn && !lastRow) drawCutSign(painter, p, indx - maxRows + 1, Qt::BottomLeftCorner);
511 #else
512         if (!firstColumn) drawCutSign(painter, p, indx - 1, Qt::TopLeftCorner);
513         if (!lastRow) drawCutSign(painter, p, indx + maxCols, Qt::BottomRightCorner);
514         if (!firstColumn && !lastRow) drawCutSign(painter, p, indx + maxCols - 1, Qt::BottomLeftCorner);
515 #endif
516     }
517 }
518