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"