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