File indexing completed on 2025-01-05 03:53:12

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2007-11-07
0007  * Description : a tool to print images
0008  *
0009  * SPDX-FileCopyrightText: 2017-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  *
0011  * SPDX-License-Identifier: GPL-2.0-or-later
0012  *
0013  * ============================================================ */
0014 
0015 #include "advprinttask.h"
0016 
0017 // C++ includes
0018 
0019 #include <cmath>
0020 
0021 // Qt includes
0022 
0023 #include <QSize>
0024 #include <QFileInfo>
0025 #include <QScopedPointer>
0026 
0027 // KDE includes
0028 
0029 #include <klocalizedstring.h>
0030 
0031 // Local includes
0032 
0033 #include "advprintwizard.h"
0034 #include "advprintphoto.h"
0035 #include "advprintcaptionpage.h"
0036 #include "dmetadata.h"
0037 #include "dfileoperations.h"
0038 #include "dimg.h"
0039 #include "digikam_debug.h"
0040 #include "digikam_config.h"
0041 
0042 namespace DigikamGenericPrintCreatorPlugin
0043 {
0044 
0045 class Q_DECL_HIDDEN AdvPrintTask::Private
0046 {
0047 public:
0048 
0049     explicit Private()
0050       : settings (nullptr),
0051         mode     (AdvPrintTask::PRINT),
0052         sizeIndex(0)
0053     {
0054     }
0055 
0056 public:
0057 
0058     AdvPrintSettings* settings;
0059 
0060     PrintMode         mode;
0061     QSize             size;
0062 
0063     int               sizeIndex;
0064 };
0065 
0066 // -------------------------------------------------------
0067 
0068 AdvPrintTask::AdvPrintTask(AdvPrintSettings* const settings,
0069                            PrintMode mode,
0070                            const QSize& size,
0071                            int sizeIndex)
0072     : ActionJob(),
0073       d        (new Private)
0074 {
0075     d->settings  = settings;
0076     d->mode      = mode;
0077     d->size      = size;
0078     d->sizeIndex = sizeIndex;
0079 }
0080 
0081 AdvPrintTask::~AdvPrintTask()
0082 {
0083     cancel();
0084     delete d;
0085 }
0086 
0087 void AdvPrintTask::run()
0088 {
0089     switch (d->mode)
0090     {
0091         case PREPAREPRINT:
0092 
0093             qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Start prepare to print";
0094             preparePrint();
0095             Q_EMIT signalDone(!m_cancel);
0096             qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Prepare to print is done";
0097 
0098             break;
0099 
0100         case PRINT:
0101 
0102             qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Start to print";
0103 
0104             if ((d->settings->printerName != d->settings->outputName(AdvPrintSettings::FILES)) &&
0105                 (d->settings->printerName != d->settings->outputName(AdvPrintSettings::GIMP)))
0106             {
0107                 printPhotos();
0108                 Q_EMIT signalDone(!m_cancel);
0109             }
0110             else
0111             {
0112                 QStringList files = printPhotosToFile();
0113 
0114                 if (d->settings->printerName == d->settings->outputName(AdvPrintSettings::GIMP))
0115                 {
0116                     d->settings->gimpFiles << files;
0117                 }
0118 
0119                 Q_EMIT signalDone(!m_cancel && !files.isEmpty());
0120             }
0121 
0122             qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Print is done";
0123 
0124             break;
0125 
0126         default:    // PREVIEW
0127 
0128             qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Start to compute preview";
0129 
0130             QImage img(d->size, QImage::Format_ARGB32_Premultiplied);
0131             QPainter p(&img);
0132             p.setCompositionMode(QPainter::CompositionMode_Clear);
0133             p.fillRect(img.rect(), Qt::color0);
0134             p.setCompositionMode(QPainter::CompositionMode_SourceOver);
0135             paintOnePage(p,
0136                          d->settings->photos,
0137                          d->settings->outputLayouts->m_layouts,
0138                          d->settings->currentPreviewPage,
0139                          d->settings->disableCrop,
0140                          true);
0141             p.end();
0142 
0143             if (!m_cancel)
0144             {
0145                 Q_EMIT signalPreview(img);
0146             }
0147 
0148             qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Preview computation is done";
0149 
0150             break;
0151     }
0152 }
0153 
0154 void AdvPrintTask::preparePrint()
0155 {
0156     int photoIndex = 0;
0157 
0158     for (QList<AdvPrintPhoto*>::iterator it = d->settings->photos.begin() ;
0159          it != d->settings->photos.end() ; ++it)
0160     {
0161         AdvPrintPhoto* const photo = static_cast<AdvPrintPhoto*>(*it);
0162 
0163         if (photo && (photo->m_cropRegion == QRect(-1, -1, -1, -1)))
0164         {
0165             QRect* const curr = d->settings->getLayout(photoIndex, d->sizeIndex);
0166 
0167             photo->updateCropRegion(curr->width(),
0168                                     curr->height(),
0169                                     d->settings->outputLayouts->m_autoRotate);
0170         }
0171 
0172         photoIndex++;
0173         Q_EMIT signalProgress(photoIndex);
0174 
0175         if (m_cancel)
0176         {
0177             Q_EMIT signalMessage(i18n("Printing canceled"), true);
0178             return;
0179         }
0180     }
0181 }
0182 
0183 void AdvPrintTask::printPhotos()
0184 {
0185     AdvPrintPhotoSize* const layouts = d->settings->outputLayouts;
0186     QPrinter* const printer          = d->settings->outputPrinter;
0187 
0188     Q_ASSERT(layouts);
0189     Q_ASSERT(printer);
0190     Q_ASSERT(layouts->m_layouts.count() > 1);
0191 
0192     QList<AdvPrintPhoto*> photos = d->settings->photos;
0193     QPainter p;
0194     p.begin(printer);
0195 
0196     int current   = 0;
0197     int pageCount = 1;
0198     bool printing = true;
0199 
0200     while (printing)
0201     {
0202         Q_EMIT signalMessage(i18n("Processing page %1", pageCount), false);
0203 
0204         printing = paintOnePage(p,
0205                                 photos,
0206                                 layouts->m_layouts,
0207                                 current,
0208                                 d->settings->disableCrop);
0209 
0210         if (printing)
0211         {
0212             printer->newPage();
0213         }
0214 
0215         pageCount++;
0216         Q_EMIT signalProgress(current);
0217 
0218         if (m_cancel)
0219         {
0220             printer->abort();
0221             Q_EMIT signalMessage(i18n("Printing canceled"), true);
0222             return;
0223         }
0224     }
0225 
0226     p.end();
0227 }
0228 
0229 QStringList AdvPrintTask::printPhotosToFile()
0230 {
0231     AdvPrintPhotoSize* const layouts = d->settings->outputLayouts;
0232     QString dir                      = d->settings->outputPath;
0233 
0234     Q_ASSERT(layouts);
0235     Q_ASSERT(!dir.isEmpty());
0236     Q_ASSERT(layouts->m_layouts.count() > 1);
0237 
0238     QList<AdvPrintPhoto*> photos     = d->settings->photos;
0239 
0240     QStringList files;
0241     int current                      = 0;
0242     int pageCount                    = 1;
0243     bool printing                    = true;
0244     QRect* const srcPage             = layouts->m_layouts.at(0);
0245 
0246     while (printing)
0247     {
0248         // make a pixmap to save to file.  Make it just big enough to show the
0249         // highest-dpi image on the page without losing data.
0250 
0251         double dpi       = layouts->m_dpi;
0252 
0253         if (dpi == 0.0)
0254         {
0255             dpi = getMaxDPI(photos, layouts->m_layouts, current) * 1.1;
0256             (void)dpi; // Remove clang warnings.
0257         }
0258 
0259         int w            = AdvPrintWizard::normalizedInt(srcPage->width());
0260         int h            = AdvPrintWizard::normalizedInt(srcPage->height());
0261 
0262         QImage image(w, h, QImage::Format_ARGB32_Premultiplied);
0263         QPainter painter;
0264         painter.begin(&image);
0265 
0266         QString ext      = d->settings->format();
0267         QString name     = QLatin1String("output");
0268         QString filename = dir  + QLatin1Char('/')    +
0269                            name + QLatin1Char('_')    +
0270                            QString::number(pageCount) +
0271                            QLatin1Char('.') + ext;
0272 
0273         if (QFile::exists(filename) &&
0274             (d->settings->conflictRule != FileSaveConflictBox::OVERWRITE))
0275         {
0276             filename = DFileOperations::getUniqueFileUrl(QUrl::fromLocalFile(filename)).toLocalFile();
0277         }
0278 
0279         Q_EMIT signalMessage(i18n("Processing page %1", pageCount), false);
0280 
0281         printing = paintOnePage(painter,
0282                                 photos,
0283                                 layouts->m_layouts,
0284                                 current,
0285                                 d->settings->disableCrop);
0286 
0287         painter.end();
0288 
0289         if (!image.save(filename, nullptr, 100))
0290         {
0291             Q_EMIT signalMessage(i18n("Could not save file %1", filename), true);
0292             break;
0293         }
0294         else
0295         {
0296             files.append(filename);
0297             Q_EMIT signalMessage(i18n("Page %1 saved as %2", pageCount, filename), false);
0298         }
0299 
0300         pageCount++;
0301         Q_EMIT signalProgress(current);
0302 
0303         if (m_cancel)
0304         {
0305             Q_EMIT signalMessage(i18n("Printing canceled"), true);
0306             break;
0307         }
0308     }
0309 
0310     return files;
0311 }
0312 
0313 bool AdvPrintTask::paintOnePage(QPainter& p,
0314                                 const QList<AdvPrintPhoto*>& photos,
0315                                 const QList<QRect*>& layouts,
0316                                 int& current,
0317                                 bool cropDisabled,
0318                                 bool useThumbnails)
0319 {
0320     if (layouts.isEmpty())
0321     {
0322         qCWarning(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Invalid layout content";
0323         return true;
0324     }
0325 
0326     if (photos.count() == 0)
0327     {
0328         qCWarning(DIGIKAM_DPLUGIN_GENERIC_LOG) << "no photo to print";
0329 
0330         // no photos => last photo
0331 
0332         return true;
0333     }
0334 
0335     QList<QRect*>::const_iterator it = layouts.begin();
0336     QRect* const srcPage             = static_cast<QRect*>(*it);
0337     ++it;
0338     QRect* layout                    = static_cast<QRect*>(*it);
0339 
0340     // scale the page size to best fit the painter
0341     // size the rectangle based on the minimum image dimension
0342 
0343     int destW = p.window().width();
0344     int destH = p.window().height();
0345     int srcW  = srcPage->width();
0346     int srcH  = srcPage->height();
0347 
0348     if (destW < destH)
0349     {
0350         destH = AdvPrintWizard::normalizedInt((double) destW * ((double) srcH / (double) srcW));
0351 
0352         if (destH > p.window().height())
0353         {
0354             destH = p.window().height();
0355             destW = AdvPrintWizard::normalizedInt((double) destH * ((double) srcW / (double) srcH));
0356         }
0357     }
0358     else
0359     {
0360         destW = AdvPrintWizard::normalizedInt((double) destH * ((double) srcW / (double) srcH));
0361 
0362         if (destW > p.window().width())
0363         {
0364             destW = p.window().width();
0365             destH = AdvPrintWizard::normalizedInt((double) destW * ((double) srcH / (double) srcW));
0366         }
0367     }
0368 
0369     double xRatio1 = (double) destW / (double) srcPage->width();
0370     double yRatio1 = (double) destH / (double) srcPage->height();
0371     int left       = (p.window().width()  - destW) / 2;
0372     int top        = (p.window().height() - destH) / 2;
0373 
0374     // FIXME: may not want to erase the background page
0375 
0376     p.eraseRect(left, top,
0377                 AdvPrintWizard::normalizedInt((double) srcPage->width()  * xRatio1),
0378                 AdvPrintWizard::normalizedInt((double) srcPage->height() * yRatio1));
0379 
0380     for ( ; (current < photos.count()) && !m_cancel ; ++current)
0381     {
0382         AdvPrintPhoto* const photo = photos.at(current);
0383 
0384         // crop
0385 
0386         QImage img;
0387 
0388         if (useThumbnails)
0389         {
0390             img = photo->thumbnail().copyQImage();
0391         }
0392         else
0393         {
0394             img = photo->loadPhoto().copyQImage();
0395         }
0396 
0397         // next, do we rotate?
0398 
0399         if (photo->m_rotation != 0)
0400         {
0401             // rotate
0402 
0403             QTransform matrix;
0404             matrix.rotate(photo->m_rotation);
0405             img = img.transformed(matrix);
0406         }
0407 
0408         if      (useThumbnails)
0409         {
0410             // scale the crop region to thumbnail coords
0411 
0412             double xRatio2 = 0.0;
0413             double yRatio2 = 0.0;
0414 
0415             if (photo->thumbnail().width() != 0)
0416             {
0417                 xRatio2 = (double)photo->thumbnail().width()  / (double)photo->width();
0418             }
0419 
0420             if (photo->thumbnail().height() != 0)
0421             {
0422                 yRatio2 = (double)photo->thumbnail().height() / (double)photo->height();
0423             }
0424 
0425             int x1 = AdvPrintWizard::normalizedInt((double)photo->m_cropRegion.left()   * xRatio2);
0426             int y1 = AdvPrintWizard::normalizedInt((double)photo->m_cropRegion.top()    * yRatio2);
0427             int w  = AdvPrintWizard::normalizedInt((double)photo->m_cropRegion.width()  * xRatio2);
0428             int h  = AdvPrintWizard::normalizedInt((double)photo->m_cropRegion.height() * yRatio2);
0429             img    = img.copy(QRect(x1, y1, w, h));
0430         }
0431         else if (!cropDisabled)
0432         {
0433             img = img.copy(photo->m_cropRegion);
0434         }
0435 
0436         int x1 = AdvPrintWizard::normalizedInt((double) layout->left()   * xRatio1);
0437         int y1 = AdvPrintWizard::normalizedInt((double) layout->top()    * yRatio1);
0438         int w  = AdvPrintWizard::normalizedInt((double) layout->width()  * xRatio1);
0439         int h  = AdvPrintWizard::normalizedInt((double) layout->height() * yRatio1);
0440 
0441         QRect rectViewPort    = p.viewport();
0442         QRect newRectViewPort = QRect(x1 + left, y1 + top, w, h);
0443         QSize imageSize       = img.size();
0444 /*
0445         qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Image         "
0446                                      << photo->filename
0447                                      << " size " << imageSize;
0448         qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "viewport size "
0449                                      << newRectViewPort.size();
0450 */
0451         QPoint point;
0452 
0453         if (cropDisabled)
0454         {
0455             imageSize.scale(newRectViewPort.size(), Qt::KeepAspectRatio);
0456             int spaceLeft = (newRectViewPort.width()  - imageSize.width())  / 2;
0457             int spaceTop  = (newRectViewPort.height() - imageSize.height()) / 2;
0458             p.setViewport(spaceLeft + newRectViewPort.x(),
0459                           spaceTop  + newRectViewPort.y(),
0460                           imageSize.width(),
0461                           imageSize.height());
0462             point         = QPoint(newRectViewPort.x() + spaceLeft + imageSize.width(),
0463                                    newRectViewPort.y() + spaceTop  + imageSize.height());
0464         }
0465         else
0466         {
0467             p.setViewport(newRectViewPort);
0468             point = QPoint(x1 + left + w, y1 + top + w);
0469         }
0470 
0471         QRect rectWindow = p.window();
0472         p.setWindow(img.rect());
0473         p.drawImage(0, 0, img);
0474         p.setViewport(rectViewPort);
0475         p.setWindow(rectWindow);
0476         p.setBrushOrigin(point);
0477 
0478         if (photo->m_pAdvPrintCaptionInfo &&
0479             (photo->m_pAdvPrintCaptionInfo->m_captionType != AdvPrintSettings::NONE))
0480         {
0481             p.save();
0482             QString caption = AdvPrintCaptionPage::captionFormatter(photo);
0483 
0484             qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Caption for"
0485                                                  << photo->m_url
0486                                                  << ":"
0487                                                  << caption;
0488 
0489             // draw the text at (0,0), but we will translate and rotate the world
0490             // before drawing so the text will be in the correct location
0491             // next, do we rotate?
0492 
0493             int captionW        = w - 2;
0494             double ratio        = photo->m_pAdvPrintCaptionInfo->m_captionSize * 0.01;
0495             int captionH        = (int)(qMin(w, h) * ratio);
0496             int orientatation   = photo->m_rotation;
0497             int exifOrientation = DMetadata::ORIENTATION_NORMAL;
0498             (void)exifOrientation; // prevent cppcheck warning.
0499 
0500             if (photo->m_iface)
0501             {
0502                 DItemInfo info(photo->m_iface->itemInfo(photo->m_url));
0503                 exifOrientation = info.orientation();
0504             }
0505             else
0506             {
0507                 QScopedPointer<DMetadata> meta(new DMetadata(photo->m_url.toLocalFile()));
0508                 exifOrientation = meta->getItemOrientation();
0509             }
0510 
0511             // ROT_90_HFLIP .. ROT_270
0512 
0513             if (
0514                 (exifOrientation == DMetadata::ORIENTATION_ROT_90_HFLIP) ||
0515                 (exifOrientation == DMetadata::ORIENTATION_ROT_90)       ||
0516                 (exifOrientation == DMetadata::ORIENTATION_ROT_90_VFLIP) ||
0517                 (exifOrientation == DMetadata::ORIENTATION_ROT_270)
0518                )
0519             {
0520                 orientatation = (photo->m_rotation + 270) % 360;   // -90 degrees
0521             }
0522 
0523             if ((orientatation == 90) || (orientatation == 270))
0524             {
0525                 captionW = h;
0526             }
0527 
0528             p.rotate(orientatation);
0529             qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "rotation "
0530                                          << photo->m_rotation
0531                                          << " orientation "
0532                                          << orientatation;
0533             int tx = left;
0534             int ty = top;
0535 
0536             switch (orientatation)
0537             {
0538                 case 0:
0539                 {
0540                     tx += x1 + 1;
0541                     ty += y1 + (h - captionH - 1);
0542                     break;
0543                 }
0544 
0545                 case 90:
0546                 {
0547                     tx = top + y1 + 1;
0548                     ty = -left - x1 - captionH - 1;
0549                     break;
0550                 }
0551 
0552                 case 180:
0553                 {
0554                     tx = -left - x1 - w + 1;
0555                     ty = -top - y1 - (captionH + 1);
0556                     break;
0557                 }
0558 
0559                 case 270:
0560                 {
0561                     tx = -top - y1 - h + 1;
0562                     ty = left + x1 + (w - captionH) - 1;
0563                     break;
0564                 }
0565             }
0566 
0567             p.translate(tx, ty);
0568             printCaption(p, photo, captionW, captionH, caption);
0569             p.restore();
0570         }
0571 
0572         // iterate to the next position
0573 
0574         ++it;
0575         layout = (it == layouts.end()) ? nullptr : static_cast<QRect*>(*it);
0576 
0577         if (layout == nullptr)
0578         {
0579             current++;
0580             break;
0581         }
0582     }
0583 
0584     // did we print the last photo?
0585 
0586     return (current < photos.count());
0587 }
0588 
0589 double AdvPrintTask::getMaxDPI(const QList<AdvPrintPhoto*>& photos,
0590                                const QList<QRect*>& layouts,
0591                                int current)
0592 {
0593     Q_ASSERT(layouts.count() > 1);
0594 
0595     QList<QRect*>::const_iterator it = layouts.begin();
0596     QRect* layout                    = static_cast<QRect*>(*it);
0597     double maxDPI                    = 0.0;
0598 
0599     for ( ; current < photos.count() ; ++current)
0600     {
0601         AdvPrintPhoto* const photo   = photos.at(current);
0602         double dpi                   = ((double) photo->m_cropRegion.width() +
0603                                         (double) photo->m_cropRegion.height()) /
0604                                        (((double) layout->width()  / 1000.0) +
0605                                         ((double) layout->height() / 1000.0));
0606 
0607         if (dpi > maxDPI)
0608         {
0609             maxDPI = dpi;
0610         }
0611 
0612         // iterate to the next position
0613 
0614         ++it;
0615         layout = (it == layouts.end()) ? nullptr : static_cast<QRect*>(*it);
0616 
0617         if (layout == nullptr)
0618         {
0619             break;
0620         }
0621     }
0622 
0623     return maxDPI;
0624 }
0625 
0626 void AdvPrintTask::printCaption(QPainter& p,
0627                                 AdvPrintPhoto* const photo,
0628                                 int captionW,
0629                                 int captionH,
0630                                 const QString& caption)
0631 {
0632     QStringList captionByLines;
0633 
0634     int captionIndex = 0;
0635 
0636     while (captionIndex < caption.length())
0637     {
0638         QString newLine;
0639         bool breakLine = false; // End Of Line found
0640         int currIndex;          // Caption QString current index
0641 
0642         // Check minimal lines dimension
0643         // TODO: fix length, maybe useless
0644 
0645         int captionLineLocalLength = 40;
0646 
0647         for (currIndex = captionIndex ;
0648              (currIndex < caption.length()) && !breakLine ; ++currIndex)
0649         {
0650             if ((caption[currIndex] == QLatin1Char('\n')) ||
0651                 caption[currIndex].isSpace())
0652             {
0653                 breakLine = true;
0654             }
0655         }
0656 
0657         if (captionLineLocalLength <= (currIndex - captionIndex))
0658         {
0659             captionLineLocalLength = (currIndex - captionIndex);
0660         }
0661 
0662         breakLine = false;
0663 
0664         for (currIndex = captionIndex ;
0665              (currIndex <= (captionIndex + captionLineLocalLength)) &&
0666              (currIndex < caption.length()) && !breakLine ;
0667              ++currIndex)
0668         {
0669             breakLine = (caption[currIndex] == QLatin1Char('\n')) ? true : false;
0670 
0671             if (breakLine)
0672             {
0673                 newLine.append(QLatin1Char(' '));
0674             }
0675             else
0676             {
0677                 newLine.append(caption[currIndex]);
0678             }
0679         }
0680 
0681         captionIndex = currIndex; // The line is ended
0682 
0683         if (captionIndex != caption.length())
0684         {
0685             while (!newLine.endsWith(QLatin1Char(' ')))
0686             {
0687                 newLine.truncate(newLine.length() - 1);
0688                 captionIndex--;
0689             }
0690         }
0691 
0692         captionByLines.prepend(newLine.trimmed());
0693     }
0694 
0695     QFont font(photo->m_pAdvPrintCaptionInfo->m_captionFont);
0696     font.setStyleHint(QFont::SansSerif);
0697     font.setPixelSize((int)(captionH * 0.8F)); // Font height ratio
0698     font.setWeight(QFont::Normal);
0699 
0700     QFontMetrics fm(font);
0701     int pixelsHigh = fm.height();
0702 
0703     p.setFont(font);
0704     p.setPen(photo->m_pAdvPrintCaptionInfo->m_captionColor);
0705 
0706     qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Number of lines "
0707                                  << (int) captionByLines.count() ;
0708 
0709     // Now draw the caption
0710     // TODO allow printing captions  per photo and on top, bottom and vertically
0711 
0712     for (int lineNumber = 0 ;
0713          lineNumber < (int)captionByLines.count() ; ++lineNumber)
0714     {
0715         if (lineNumber > 0)
0716         {
0717             p.translate(0, - (int)(pixelsHigh));
0718         }
0719 
0720         QRect r(0, 0, captionW, captionH);
0721 
0722         p.drawText(r, Qt::AlignLeft, captionByLines[lineNumber], &r);
0723     }
0724 }
0725 
0726 } // namespace DigikamGenericPrintCreatorPlugin
0727 
0728 #include "moc_advprinttask.cpp"