1 /* ============================================================
2 *
3 * This file is a part of digiKam project
4 * https://www.digikam.org
5 *
6 * Date : 2007-11-07
7 * Description : a tool to print images
8 *
9 * Copyright (C) 2017-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10 *
11 * This program is free software; you can redistribute it
12 * and/or modify it under the terms of the GNU General
13 * Public License as published by the Free Software Foundation;
14 * either version 2, or (at your option)
15 * any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * ============================================================ */
23
24 #include "advprinttask.h"
25
26 // C++ includes
27
28 #include <cmath>
29
30 // Qt includes
31
32 #include <QImage>
33 #include <QSize>
34 #include <QPainter>
35 #include <QFileInfo>
36 #include <QScopedPointer>
37
38 // KDE includes
39
40 #include <klocalizedstring.h>
41
42 // Local includes
43
44 #include "advprintwizard.h"
45 #include "advprintphoto.h"
46 #include "advprintcaptionpage.h"
47 #include "dmetadata.h"
48 #include "dfileoperations.h"
49 #include "dimg.h"
50 #include "digikam_debug.h"
51 #include "digikam_config.h"
52
53 namespace DigikamGenericPrintCreatorPlugin
54 {
55
56 class Q_DECL_HIDDEN AdvPrintTask::Private
57 {
58 public:
59
Private()60 explicit Private()
61 : settings (nullptr),
62 mode (AdvPrintTask::PRINT),
63 sizeIndex(0)
64 {
65 }
66
67 public:
68
69 AdvPrintSettings* settings;
70
71 PrintMode mode;
72 QSize size;
73
74 int sizeIndex;
75 };
76
77 // -------------------------------------------------------
78
AdvPrintTask(AdvPrintSettings * const settings,PrintMode mode,const QSize & size,int sizeIndex)79 AdvPrintTask::AdvPrintTask(AdvPrintSettings* const settings,
80 PrintMode mode,
81 const QSize& size,
82 int sizeIndex)
83 : ActionJob(),
84 d (new Private)
85 {
86 d->settings = settings;
87 d->mode = mode;
88 d->size = size;
89 d->sizeIndex = sizeIndex;
90 }
91
~AdvPrintTask()92 AdvPrintTask::~AdvPrintTask()
93 {
94 cancel();
95 delete d;
96 }
97
run()98 void AdvPrintTask::run()
99 {
100 switch (d->mode)
101 {
102 case PREPAREPRINT:
103
104 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Start prepare to print";
105 preparePrint();
106 emit signalDone(!m_cancel);
107 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Prepare to print is done";
108
109 break;
110
111 case PRINT:
112
113 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Start to print";
114
115 if ((d->settings->printerName != d->settings->outputName(AdvPrintSettings::FILES)) &&
116 (d->settings->printerName != d->settings->outputName(AdvPrintSettings::GIMP)))
117 {
118 printPhotos();
119 emit signalDone(!m_cancel);
120 }
121 else
122 {
123 QStringList files = printPhotosToFile();
124
125 if (d->settings->printerName == d->settings->outputName(AdvPrintSettings::GIMP))
126 {
127 d->settings->gimpFiles << files;
128 }
129
130 emit signalDone(!m_cancel && !files.isEmpty());
131 }
132
133 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Print is done";
134
135 break;
136
137 default: // PREVIEW
138
139 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Start to compute preview";
140
141 QImage img(d->size, QImage::Format_ARGB32_Premultiplied);
142 QPainter p(&img);
143 p.setCompositionMode(QPainter::CompositionMode_Clear);
144 p.fillRect(img.rect(), Qt::color0);
145 p.setCompositionMode(QPainter::CompositionMode_SourceOver);
146 paintOnePage(p,
147 d->settings->photos,
148 d->settings->outputLayouts->m_layouts,
149 d->settings->currentPreviewPage,
150 d->settings->disableCrop,
151 true);
152 p.end();
153
154 if (!m_cancel)
155 {
156 emit signalPreview(img);
157 }
158
159 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Preview computation is done";
160
161 break;
162 }
163 }
164
preparePrint()165 void AdvPrintTask::preparePrint()
166 {
167 int photoIndex = 0;
168
169 for (QList<AdvPrintPhoto*>::iterator it = d->settings->photos.begin() ;
170 it != d->settings->photos.end() ; ++it)
171 {
172 AdvPrintPhoto* const photo = static_cast<AdvPrintPhoto*>(*it);
173
174 if (photo && (photo->m_cropRegion == QRect(-1, -1, -1, -1)))
175 {
176 QRect* const curr = d->settings->getLayout(photoIndex, d->sizeIndex);
177
178 photo->updateCropRegion(curr->width(),
179 curr->height(),
180 d->settings->outputLayouts->m_autoRotate);
181 }
182
183 photoIndex++;
184 emit signalProgress(photoIndex);
185
186 if (m_cancel)
187 {
188 emit signalMessage(i18n("Printing canceled"), true);
189 return;
190 }
191 }
192 }
193
printPhotos()194 void AdvPrintTask::printPhotos()
195 {
196 AdvPrintPhotoSize* const layouts = d->settings->outputLayouts;
197 QPrinter* const printer = d->settings->outputPrinter;
198
199 Q_ASSERT(layouts);
200 Q_ASSERT(printer);
201 Q_ASSERT(layouts->m_layouts.count() > 1);
202
203 QList<AdvPrintPhoto*> photos = d->settings->photos;
204 QPainter p;
205 p.begin(printer);
206
207 int current = 0;
208 int pageCount = 1;
209 bool printing = true;
210
211 while (printing)
212 {
213 emit signalMessage(i18n("Processing page %1", pageCount), false);
214
215 printing = paintOnePage(p,
216 photos,
217 layouts->m_layouts,
218 current,
219 d->settings->disableCrop);
220
221 if (printing)
222 {
223 printer->newPage();
224 }
225
226 pageCount++;
227 emit signalProgress(current);
228
229 if (m_cancel)
230 {
231 printer->abort();
232 emit signalMessage(i18n("Printing canceled"), true);
233 return;
234 }
235 }
236
237 p.end();
238 }
239
printPhotosToFile()240 QStringList AdvPrintTask::printPhotosToFile()
241 {
242 AdvPrintPhotoSize* const layouts = d->settings->outputLayouts;
243 QString dir = d->settings->outputPath;
244
245 Q_ASSERT(layouts);
246 Q_ASSERT(!dir.isEmpty());
247 Q_ASSERT(layouts->m_layouts.count() > 1);
248
249 QList<AdvPrintPhoto*> photos = d->settings->photos;
250
251 QStringList files;
252 int current = 0;
253 int pageCount = 1;
254 bool printing = true;
255 QRect* const srcPage = layouts->m_layouts.at(0);
256
257 while (printing)
258 {
259 // make a pixmap to save to file. Make it just big enough to show the
260 // highest-dpi image on the page without losing data.
261
262 double dpi = layouts->m_dpi;
263
264 if (dpi == 0.0)
265 {
266 dpi = getMaxDPI(photos, layouts->m_layouts, current) * 1.1;
267 (void)dpi; // Remove clang warnings.
268 }
269
270 int w = AdvPrintWizard::normalizedInt(srcPage->width());
271 int h = AdvPrintWizard::normalizedInt(srcPage->height());
272
273 QImage image(w, h, QImage::Format_ARGB32_Premultiplied);
274 QPainter painter;
275 painter.begin(&image);
276
277 QString ext = d->settings->format();
278 QString name = QLatin1String("output");
279 QString filename = dir + QLatin1Char('/') +
280 name + QLatin1Char('_') +
281 QString::number(pageCount) +
282 QLatin1Char('.') + ext;
283
284 if (QFile::exists(filename) &&
285 (d->settings->conflictRule != FileSaveConflictBox::OVERWRITE))
286 {
287 filename = DFileOperations::getUniqueFileUrl(QUrl::fromLocalFile(filename)).toLocalFile();
288 }
289
290 emit signalMessage(i18n("Processing page %1", pageCount), false);
291
292 printing = paintOnePage(painter,
293 photos,
294 layouts->m_layouts,
295 current,
296 d->settings->disableCrop);
297
298 painter.end();
299
300 if (!image.save(filename, nullptr, 100))
301 {
302 emit signalMessage(i18n("Could not save file %1", filename), true);
303 break;
304 }
305 else
306 {
307 files.append(filename);
308 emit signalMessage(i18n("Page %1 saved as %2", pageCount, filename), false);
309 }
310
311 pageCount++;
312 emit signalProgress(current);
313
314 if (m_cancel)
315 {
316 emit signalMessage(i18n("Printing canceled"), true);
317 break;
318 }
319 }
320
321 return files;
322 }
323
paintOnePage(QPainter & p,const QList<AdvPrintPhoto * > & photos,const QList<QRect * > & layouts,int & current,bool cropDisabled,bool useThumbnails)324 bool AdvPrintTask::paintOnePage(QPainter& p,
325 const QList<AdvPrintPhoto*>& photos,
326 const QList<QRect*>& layouts,
327 int& current,
328 bool cropDisabled,
329 bool useThumbnails)
330 {
331 if (layouts.isEmpty())
332 {
333 qCWarning(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Invalid layout content";
334 return true;
335 }
336
337 if (photos.count() == 0)
338 {
339 qCWarning(DIGIKAM_DPLUGIN_GENERIC_LOG) << "no photo to print";
340
341 // no photos => last photo
342
343 return true;
344 }
345
346 QList<QRect*>::const_iterator it = layouts.begin();
347 QRect* const srcPage = static_cast<QRect*>(*it);
348 ++it;
349 QRect* layout = static_cast<QRect*>(*it);
350
351 // scale the page size to best fit the painter
352 // size the rectangle based on the minimum image dimension
353
354 int destW = p.window().width();
355 int destH = p.window().height();
356 int srcW = srcPage->width();
357 int srcH = srcPage->height();
358
359 if (destW < destH)
360 {
361 destH = AdvPrintWizard::normalizedInt((double) destW * ((double) srcH / (double) srcW));
362
363 if (destH > p.window().height())
364 {
365 destH = p.window().height();
366 destW = AdvPrintWizard::normalizedInt((double) destH * ((double) srcW / (double) srcH));
367 }
368 }
369 else
370 {
371 destW = AdvPrintWizard::normalizedInt((double) destH * ((double) srcW / (double) srcH));
372
373 if (destW > p.window().width())
374 {
375 destW = p.window().width();
376 destH = AdvPrintWizard::normalizedInt((double) destW * ((double) srcH / (double) srcW));
377 }
378 }
379
380 double xRatio1 = (double) destW / (double) srcPage->width();
381 double yRatio1 = (double) destH / (double) srcPage->height();
382 int left = (p.window().width() - destW) / 2;
383 int top = (p.window().height() - destH) / 2;
384
385 // FIXME: may not want to erase the background page
386
387 p.eraseRect(left, top,
388 AdvPrintWizard::normalizedInt((double) srcPage->width() * xRatio1),
389 AdvPrintWizard::normalizedInt((double) srcPage->height() * yRatio1));
390
391 for ( ; (current < photos.count()) && !m_cancel ; ++current)
392 {
393 AdvPrintPhoto* const photo = photos.at(current);
394
395 // crop
396
397 QImage img;
398
399 if (useThumbnails)
400 {
401 img = photo->thumbnail().copyQImage();
402 }
403 else
404 {
405 img = photo->loadPhoto().copyQImage();
406 }
407
408 // next, do we rotate?
409
410 if (photo->m_rotation != 0)
411 {
412 // rotate
413
414 QMatrix matrix;
415 matrix.rotate(photo->m_rotation);
416 img = img.transformed(matrix);
417 }
418
419 if (useThumbnails)
420 {
421 // scale the crop region to thumbnail coords
422
423 double xRatio2 = 0.0;
424 double yRatio2 = 0.0;
425
426 if (photo->thumbnail().width() != 0)
427 {
428 xRatio2 = (double)photo->thumbnail().width() / (double)photo->width();
429 }
430
431 if (photo->thumbnail().height() != 0)
432 {
433 yRatio2 = (double)photo->thumbnail().height() / (double)photo->height();
434 }
435
436 int x1 = AdvPrintWizard::normalizedInt((double)photo->m_cropRegion.left() * xRatio2);
437 int y1 = AdvPrintWizard::normalizedInt((double)photo->m_cropRegion.top() * yRatio2);
438 int w = AdvPrintWizard::normalizedInt((double)photo->m_cropRegion.width() * xRatio2);
439 int h = AdvPrintWizard::normalizedInt((double)photo->m_cropRegion.height() * yRatio2);
440 img = img.copy(QRect(x1, y1, w, h));
441 }
442 else if (!cropDisabled)
443 {
444 img = img.copy(photo->m_cropRegion);
445 }
446
447 int x1 = AdvPrintWizard::normalizedInt((double) layout->left() * xRatio1);
448 int y1 = AdvPrintWizard::normalizedInt((double) layout->top() * yRatio1);
449 int w = AdvPrintWizard::normalizedInt((double) layout->width() * xRatio1);
450 int h = AdvPrintWizard::normalizedInt((double) layout->height() * yRatio1);
451
452 QRect rectViewPort = p.viewport();
453 QRect newRectViewPort = QRect(x1 + left, y1 + top, w, h);
454 QSize imageSize = img.size();
455 /*
456 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Image "
457 << photo->filename
458 << " size " << imageSize;
459 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "viewport size "
460 << newRectViewPort.size();
461 */
462 QPoint point;
463
464 if (cropDisabled)
465 {
466 imageSize.scale(newRectViewPort.size(), Qt::KeepAspectRatio);
467 int spaceLeft = (newRectViewPort.width() - imageSize.width()) / 2;
468 int spaceTop = (newRectViewPort.height() - imageSize.height()) / 2;
469 p.setViewport(spaceLeft + newRectViewPort.x(),
470 spaceTop + newRectViewPort.y(),
471 imageSize.width(),
472 imageSize.height());
473 point = QPoint(newRectViewPort.x() + spaceLeft + imageSize.width(),
474 newRectViewPort.y() + spaceTop + imageSize.height());
475 }
476 else
477 {
478 p.setViewport(newRectViewPort);
479 point = QPoint(x1 + left + w, y1 + top + w);
480 }
481
482 QRect rectWindow = p.window();
483 p.setWindow(img.rect());
484 p.drawImage(0, 0, img);
485 p.setViewport(rectViewPort);
486 p.setWindow(rectWindow);
487 p.setBrushOrigin(point);
488
489 if (photo->m_pAdvPrintCaptionInfo &&
490 (photo->m_pAdvPrintCaptionInfo->m_captionType != AdvPrintSettings::NONE))
491 {
492 p.save();
493 QString caption = AdvPrintCaptionPage::captionFormatter(photo);
494
495 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Caption for"
496 << photo->m_url
497 << ":"
498 << caption;
499
500 // draw the text at (0,0), but we will translate and rotate the world
501 // before drawing so the text will be in the correct location
502 // next, do we rotate?
503
504 int captionW = w - 2;
505 double ratio = photo->m_pAdvPrintCaptionInfo->m_captionSize * 0.01;
506 int captionH = (int)(qMin(w, h) * ratio);
507 int orientatation = photo->m_rotation;
508 int exifOrientation = DMetadata::ORIENTATION_NORMAL;
509 (void)exifOrientation; // prevent cppcheck warning.
510
511 if (photo->m_iface)
512 {
513 DItemInfo info(photo->m_iface->itemInfo(photo->m_url));
514 exifOrientation = info.orientation();
515 }
516 else
517 {
518 QScopedPointer<DMetadata> meta(new DMetadata(photo->m_url.toLocalFile()));
519 exifOrientation = meta->getItemOrientation();
520 }
521
522 // ROT_90_HFLIP .. ROT_270
523
524 if (
525 (exifOrientation == DMetadata::ORIENTATION_ROT_90_HFLIP) ||
526 (exifOrientation == DMetadata::ORIENTATION_ROT_90) ||
527 (exifOrientation == DMetadata::ORIENTATION_ROT_90_VFLIP) ||
528 (exifOrientation == DMetadata::ORIENTATION_ROT_270)
529 )
530 {
531 orientatation = (photo->m_rotation + 270) % 360; // -90 degrees
532 }
533
534 if ((orientatation == 90) || (orientatation == 270))
535 {
536 captionW = h;
537 }
538
539 p.rotate(orientatation);
540 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "rotation "
541 << photo->m_rotation
542 << " orientation "
543 << orientatation;
544 int tx = left;
545 int ty = top;
546
547 switch (orientatation)
548 {
549 case 0:
550 {
551 tx += x1 + 1;
552 ty += y1 + (h - captionH - 1);
553 break;
554 }
555
556 case 90:
557 {
558 tx = top + y1 + 1;
559 ty = -left - x1 - captionH - 1;
560 break;
561 }
562
563 case 180:
564 {
565 tx = -left - x1 - w + 1;
566 ty = -top - y1 - (captionH + 1);
567 break;
568 }
569
570 case 270:
571 {
572 tx = -top - y1 - h + 1;
573 ty = left + x1 + (w - captionH) - 1;
574 break;
575 }
576 }
577
578 p.translate(tx, ty);
579 printCaption(p, photo, captionW, captionH, caption);
580 p.restore();
581 }
582
583 // iterate to the next position
584
585 ++it;
586 layout = (it == layouts.end()) ? nullptr : static_cast<QRect*>(*it);
587
588 if (layout == nullptr)
589 {
590 current++;
591 break;
592 }
593 }
594
595 // did we print the last photo?
596
597 return (current < photos.count());
598 }
599
getMaxDPI(const QList<AdvPrintPhoto * > & photos,const QList<QRect * > & layouts,int current)600 double AdvPrintTask::getMaxDPI(const QList<AdvPrintPhoto*>& photos,
601 const QList<QRect*>& layouts,
602 int current)
603 {
604 Q_ASSERT(layouts.count() > 1);
605
606 QList<QRect*>::const_iterator it = layouts.begin();
607 QRect* layout = static_cast<QRect*>(*it);
608 double maxDPI = 0.0;
609
610 for ( ; current < photos.count() ; ++current)
611 {
612 AdvPrintPhoto* const photo = photos.at(current);
613 double dpi = ((double) photo->m_cropRegion.width() +
614 (double) photo->m_cropRegion.height()) /
615 (((double) layout->width() / 1000.0) +
616 ((double) layout->height() / 1000.0));
617
618 if (dpi > maxDPI)
619 {
620 maxDPI = dpi;
621 }
622
623 // iterate to the next position
624
625 ++it;
626 layout = (it == layouts.end()) ? nullptr : static_cast<QRect*>(*it);
627
628 if (layout == nullptr)
629 {
630 break;
631 }
632 }
633
634 return maxDPI;
635 }
636
printCaption(QPainter & p,AdvPrintPhoto * const photo,int captionW,int captionH,const QString & caption)637 void AdvPrintTask::printCaption(QPainter& p,
638 AdvPrintPhoto* const photo,
639 int captionW,
640 int captionH,
641 const QString& caption)
642 {
643 QStringList captionByLines;
644
645 int captionIndex = 0;
646
647 while (captionIndex < caption.length())
648 {
649 QString newLine;
650 bool breakLine = false; // End Of Line found
651 int currIndex; // Caption QString current index
652
653 // Check minimal lines dimension
654 // TODO: fix length, maybe useless
655
656 int captionLineLocalLength = 40;
657
658 for (currIndex = captionIndex ;
659 (currIndex < caption.length()) && !breakLine ; ++currIndex)
660 {
661 if ((caption[currIndex] == QLatin1Char('\n')) ||
662 caption[currIndex].isSpace())
663 {
664 breakLine = true;
665 }
666 }
667
668 if (captionLineLocalLength <= (currIndex - captionIndex))
669 {
670 captionLineLocalLength = (currIndex - captionIndex);
671 }
672
673 breakLine = false;
674
675 for (currIndex = captionIndex ;
676 (currIndex <= (captionIndex + captionLineLocalLength)) &&
677 (currIndex < caption.length()) && !breakLine ;
678 ++currIndex)
679 {
680 breakLine = (caption[currIndex] == QLatin1Char('\n')) ? true : false;
681
682 if (breakLine)
683 {
684 newLine.append(QLatin1Char(' '));
685 }
686 else
687 {
688 newLine.append(caption[currIndex]);
689 }
690 }
691
692 captionIndex = currIndex; // The line is ended
693
694 if (captionIndex != caption.length())
695 {
696 while (!newLine.endsWith(QLatin1Char(' ')))
697 {
698 newLine.truncate(newLine.length() - 1);
699 captionIndex--;
700 }
701 }
702
703 captionByLines.prepend(newLine.trimmed());
704 }
705
706 QFont font(photo->m_pAdvPrintCaptionInfo->m_captionFont);
707 font.setStyleHint(QFont::SansSerif);
708 font.setPixelSize((int)(captionH * 0.8F)); // Font height ratio
709 font.setWeight(QFont::Normal);
710
711 QFontMetrics fm(font);
712 int pixelsHigh = fm.height();
713
714 p.setFont(font);
715 p.setPen(photo->m_pAdvPrintCaptionInfo->m_captionColor);
716
717 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Number of lines "
718 << (int) captionByLines.count() ;
719
720 // Now draw the caption
721 // TODO allow printing captions per photo and on top, bottom and vertically
722
723 for (int lineNumber = 0 ;
724 lineNumber < (int)captionByLines.count() ; ++lineNumber)
725 {
726 if (lineNumber > 0)
727 {
728 p.translate(0, - (int)(pixelsHigh));
729 }
730
731 QRect r(0, 0, captionW, captionH);
732
733 p.drawText(r, Qt::AlignLeft, captionByLines[lineNumber], &r);
734 }
735 }
736
737 } // namespace DigikamGenericPrintCreatorPlugin
738