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