File indexing completed on 2024-04-21 04:21:20

0001 // SPDX-FileCopyrightText: 2006-2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0002 // SPDX-FileCopyrightText: 2007 Dirk Mueller <mueller@kde.org>
0003 // SPDX-FileCopyrightText: 2007-2008 Laurent Montel <montel@kde.org>
0004 // SPDX-FileCopyrightText: 2007-2010 Jan Kundrát <jkt@flaska.net>
0005 // SPDX-FileCopyrightText: 2007-2010 Tuomas Suutari <tuomas@nepnep.net>
0006 // SPDX-FileCopyrightText: 2008 Henner Zeller <h.zeller@acm.org>
0007 // SPDX-FileCopyrightText: 2009-2013 Miika Turkia <miika.turkia@gmail.com>
0008 // SPDX-FileCopyrightText: 2012-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0009 // SPDX-FileCopyrightText: 2013 Dominik Broj <broj.dominik@gmail.com>
0010 // SPDX-FileCopyrightText: 2016-2022 Tobias Leupold <tl@stonemx.de>
0011 
0012 // SPDX-License-Identifier: GPL-2.0-or-later
0013 
0014 #include "Generator.h"
0015 
0016 #include "ImageSizeCheckBox.h"
0017 #include "Logging.h"
0018 #include "Setup.h"
0019 
0020 #include <DB/CategoryCollection.h>
0021 #include <DB/ImageDB.h>
0022 #include <DB/ImageInfo.h>
0023 #include <ImageManager/AsyncLoader.h>
0024 #include <ImportExport/Export.h>
0025 #include <MainWindow/Window.h>
0026 #include <Utilities/FileUtil.h>
0027 #include <kpabase/FileExtensions.h>
0028 #include <kpaexif/Info.h>
0029 
0030 #include <KConfig>
0031 #include <KConfigGroup>
0032 #include <KIO/CopyJob>
0033 #include <KLocalizedString>
0034 #include <KMessageBox>
0035 #include <kio_version.h>
0036 #if KIO_VERSION > QT_VERSION_CHECK(5, 69, 0)
0037 #include <KIO/CommandLauncherJob>
0038 #include <KIO/JobUiDelegate>
0039 #endif
0040 #include <KRun>
0041 #include <QApplication>
0042 #include <QDebug>
0043 #include <QDir>
0044 #include <QDomDocument>
0045 #include <QFile>
0046 #include <QList>
0047 #include <QMimeDatabase>
0048 #include <QStandardPaths>
0049 #include <sys/types.h>
0050 #include <sys/wait.h>
0051 
0052 namespace
0053 {
0054 QString readFile(const QString &fileName)
0055 {
0056     if (fileName.isEmpty()) {
0057         KMessageBox::error(nullptr, i18n("<p>No file name given!</p>"));
0058         return QString();
0059     }
0060 
0061     QFile file(fileName);
0062     if (!file.open(QIODevice::ReadOnly)) {
0063         // KMessageBox::error( nullptr, i18n("Could not open file %1").arg( fileName ) );
0064         return QString();
0065     }
0066 
0067     QTextStream stream(&file);
0068     QString content = stream.readAll();
0069     file.close();
0070 
0071     return content;
0072 }
0073 } // namespace
0074 
0075 HTMLGenerator::Generator::Generator(const Setup &setup, QWidget *parent)
0076     : QProgressDialog(parent)
0077     , m_waitCounter(0)
0078     , m_total(0)
0079     , m_tempDirHandle()
0080     , m_tempDir(m_tempDirHandle.path())
0081     , m_hasEnteredLoop(false)
0082 {
0083     setLabelText(i18n("Generating images for HTML page "));
0084     m_setup = setup;
0085     m_eventLoop = new QEventLoop;
0086     m_avconv = QStandardPaths::findExecutable(QString::fromUtf8("avconv"));
0087     if (m_avconv.isNull())
0088         m_avconv = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg"));
0089     m_tempDirHandle.setAutoRemove(true);
0090 }
0091 
0092 HTMLGenerator::Generator::~Generator()
0093 {
0094     delete m_eventLoop;
0095 }
0096 void HTMLGenerator::Generator::generate()
0097 {
0098     qCDebug(HTMLGeneratorLog) << "Generating gallery" << m_setup.title() << "containing" << m_setup.imageList().size() << "entries in" << m_setup.baseDir();
0099     // Generate .kim file
0100     if (m_setup.generateKimFile()) {
0101         qCDebug(HTMLGeneratorLog) << "Generating .kim file...";
0102         bool ok;
0103         QString destURL = m_setup.destURL();
0104 
0105         ImportExport::Export exp(m_setup.imageList(), kimFileName(false),
0106                                  false, -1, ImportExport::ManualCopy,
0107                                  destURL + QDir::separator() + m_setup.outputDir(), true, &ok);
0108         if (!ok) {
0109             qCDebug(HTMLGeneratorLog) << ".kim file failed!";
0110             return;
0111         }
0112     }
0113 
0114     // prepare the progress dialog
0115     m_total = m_waitCounter = calculateSteps();
0116     setMaximum(m_total);
0117     setValue(0);
0118     connect(this, &QProgressDialog::canceled, this, &Generator::slotCancelGenerate);
0119 
0120     m_filenameMapper.reset();
0121 
0122     qCDebug(HTMLGeneratorLog) << "Generating content pages...";
0123     // Iterate over each of the image sizes needed.
0124     for (QList<ImageSizeCheckBox *>::ConstIterator sizeIt = m_setup.activeResolutions().begin();
0125          sizeIt != m_setup.activeResolutions().end(); ++sizeIt) {
0126         bool ok = generateIndexPage((*sizeIt)->width(), (*sizeIt)->height());
0127         if (!ok)
0128             return;
0129         const DB::FileNameList imageList = m_setup.imageList();
0130         for (int index = 0; index < imageList.size(); ++index) {
0131             DB::FileName current = imageList.at(index);
0132             DB::FileName prev;
0133             DB::FileName next;
0134             if (index != 0)
0135                 prev = imageList.at(index - 1);
0136             if (index != imageList.size() - 1)
0137                 next = imageList.at(index + 1);
0138             ok = generateContentPage((*sizeIt)->width(), (*sizeIt)->height(),
0139                                      prev, current, next);
0140             if (!ok)
0141                 return;
0142         }
0143     }
0144 
0145     // Now generate the thumbnail images
0146     qCDebug(HTMLGeneratorLog) << "Generating thumbnail images...";
0147     for (const DB::FileName &fileName : m_setup.imageList()) {
0148         if (wasCanceled())
0149             return;
0150 
0151         createImage(fileName, m_setup.thumbSize());
0152     }
0153 
0154     if (wasCanceled())
0155         return;
0156 
0157     if (m_waitCounter > 0) {
0158         m_hasEnteredLoop = true;
0159         m_eventLoop->exec();
0160     }
0161 
0162     if (wasCanceled())
0163         return;
0164 
0165     qCDebug(HTMLGeneratorLog) << "Linking image file...";
0166     bool ok = linkIndexFile();
0167     if (!ok)
0168         return;
0169 
0170     qCDebug(HTMLGeneratorLog) << "Copying theme files...";
0171     // Copy over the mainpage.css, imagepage.css
0172     QString themeDir, themeAuthor, themeName;
0173     getThemeInfo(&themeDir, &themeName, &themeAuthor);
0174     QDir dir(themeDir);
0175     QStringList files = dir.entryList(QDir::Files);
0176     if (files.count() < 1)
0177         qCWarning(HTMLGeneratorLog) << QString::fromLatin1("theme '%1' doesn't have enough files to be a theme").arg(themeDir);
0178 
0179     for (QStringList::Iterator it = files.begin(); it != files.end(); ++it) {
0180         if (*it == QString::fromLatin1("kphotoalbum.theme") || *it == QString::fromLatin1("mainpage.html") || *it == QString::fromLatin1("imagepage.html"))
0181             continue;
0182         QString from = QString::fromLatin1("%1%2").arg(themeDir, *it);
0183         QString to = m_tempDir.filePath(*it);
0184         ok = Utilities::copyOrOverwrite(from, to);
0185         if (!ok) {
0186             KMessageBox::error(this, i18n("Error copying %1 to %2", from, to));
0187             return;
0188         }
0189     }
0190 
0191     // Copy files over to destination.
0192     QString outputDir = m_setup.baseDir() + QString::fromLatin1("/") + m_setup.outputDir();
0193     qCDebug(HTMLGeneratorLog) << "Copying files from" << m_tempDir.path() << "to final location" << outputDir << "...";
0194     KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(m_tempDir.path()), QUrl::fromUserInput(outputDir));
0195     connect(job, &KIO::CopyJob::result, this, &Generator::showBrowser);
0196 
0197     m_eventLoop->exec();
0198     return;
0199 }
0200 
0201 bool HTMLGenerator::Generator::generateIndexPage(int width, int height)
0202 {
0203     QString themeDir, themeAuthor, themeName;
0204     getThemeInfo(&themeDir, &themeName, &themeAuthor);
0205     QString content = readFile(QString::fromLatin1("%1mainpage.html").arg(themeDir));
0206     if (content.isEmpty())
0207         return false;
0208 
0209     // Adding the copyright comment after DOCTYPE not before (HTML standard requires the DOCTYPE to be first within the document)
0210     QRegExp rx(QString::fromLatin1("^(<!DOCTYPE[^>]*>)"));
0211     int position;
0212 
0213     rx.setCaseSensitivity(Qt::CaseInsensitive);
0214     position = rx.indexIn(content);
0215     if ((position += rx.matchedLength()) < 0)
0216         content = QString::fromLatin1("<!--\nMade with KPhotoAlbum. (https://www.kphotoalbum.org/)\nCopyright &copy; Jesper K. Pedersen\nTheme %1 by %2\n-->\n").arg(themeName, themeAuthor) + content;
0217     else
0218         content.insert(position, QString::fromLatin1("\n<!--\nMade with KPhotoAlbum. (https://www.kphotoalbum.org/)\nCopyright &copy; Jesper K. Pedersen\nTheme %1 by %2\n-->\n").arg(themeName, themeAuthor));
0219 
0220     content.replace(QString::fromLatin1("**DESCRIPTION**"), m_setup.description());
0221     content.replace(QString::fromLatin1("**TITLE**"), m_setup.title());
0222 
0223     QString copyright;
0224     if (!m_setup.copyright().isEmpty())
0225         copyright = QString::fromLatin1("&#169; %1").arg(m_setup.copyright());
0226     else
0227         copyright = QString::fromLatin1("&nbsp;");
0228     content.replace(QString::fromLatin1("**COPYRIGHT**"), copyright);
0229 
0230     QString kimLink = QString::fromLatin1("Share and Enjoy <a href=\"%1\">KPhotoAlbum export file</a>").arg(kimFileName(true));
0231     if (m_setup.generateKimFile())
0232         content.replace(QString::fromLatin1("**KIMFILE**"), kimLink);
0233     else
0234         content.remove(QString::fromLatin1("**KIMFILE**"));
0235     QDomDocument doc;
0236 
0237     QDomElement elm;
0238     QDomElement col;
0239 
0240     // -------------------------------------------------- Thumbnails
0241     // Initially all of the HTML generation was done using QDom, but it turned out in the end
0242     // to be much less code simply concatenating strings. This part, however, is easier using QDom
0243     // so we keep it using QDom.
0244     int count = 0;
0245     int cols = m_setup.numOfCols();
0246     int minWidth = 0;
0247     int minHeight = 0;
0248     int enableVideo = 0;
0249     QString first, last, images;
0250 
0251     images += QString::fromLatin1("var gallery=new Array()\nvar width=%1\nvar height=%2\nvar tsize=%3\nvar inlineVideo=%4\nvar generatedVideo=%5\n").arg(width).arg(height).arg(m_setup.thumbSize()).arg(m_setup.inlineMovies()).arg(m_setup.html5VideoGenerate());
0252     minImageSize(minWidth, minHeight);
0253     if (minWidth == 0 && minHeight == 0) { // full size only
0254         images += QString::fromLatin1("var minPage=\"index-fullsize.html\"\n");
0255     } else {
0256         images += QString::fromLatin1("var minPage=\"index-%1x%2.html\"\n").arg(minWidth).arg(minHeight);
0257     }
0258 
0259     QDomElement row;
0260     for (const DB::FileName &fileName : m_setup.imageList()) {
0261         const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName);
0262         if (wasCanceled())
0263             return false;
0264 
0265         if (count % cols == 0) {
0266             row = doc.createElement(QString::fromLatin1("tr"));
0267             row.setAttribute(QString::fromLatin1("class"), QString::fromLatin1("thumbnail-row"));
0268             doc.appendChild(row);
0269             count = 0;
0270         }
0271 
0272         col = doc.createElement(QString::fromLatin1("td"));
0273         col.setAttribute(QString::fromLatin1("class"), QString::fromLatin1("thumbnail-col"));
0274         row.appendChild(col);
0275 
0276         if (first.isEmpty())
0277             first = namePage(width, height, fileName);
0278         else
0279             last = namePage(width, height, fileName);
0280 
0281         if (!KPABase::isVideo(fileName)) {
0282             QMimeDatabase db;
0283             images += QString::fromLatin1("gallery.push([\"%1\", \"%2\", \"%3\", \"%4\", \"")
0284                           .arg(nameImage(fileName, width))
0285                           .arg(nameImage(fileName, m_setup.thumbSize()))
0286                           .arg(nameImage(fileName, maxImageSize()))
0287                           .arg(db.mimeTypeForFile(nameImage(fileName, maxImageSize())).name());
0288         } else {
0289             QMimeDatabase db;
0290             images += QString::fromLatin1("gallery.push([\"%1\", \"%2\", \"%3\"")
0291                           .arg(nameImage(fileName, m_setup.thumbSize()))
0292                           .arg(nameImage(fileName, m_setup.thumbSize()))
0293                           .arg(nameImage(fileName, maxImageSize()));
0294             if (m_setup.html5VideoGenerate()) {
0295                 images += QString::fromLatin1(", \"%1\", \"")
0296                               .arg(QString::fromLatin1("video/ogg"));
0297             } else {
0298                 images += QString::fromLatin1(", \"%1\", \"")
0299                               .arg(db.mimeTypeForFile(fileName.relative(), QMimeDatabase::MatchExtension).name());
0300             }
0301             enableVideo = 1;
0302         }
0303 
0304         // -------------------------------------------------- Description
0305         if (!info->description().isEmpty() && m_setup.includeCategory(QString::fromLatin1("**DESCRIPTION**"))) {
0306             images += QString::fromLatin1("%1\", \"")
0307                           .arg(info->description()
0308                                    .replace(QString::fromLatin1("\n$"), QString::fromLatin1(""))
0309                                    .replace(QString::fromLatin1("\n"), QString::fromLatin1(" "))
0310                                    .replace(QString::fromLatin1("\""), QString::fromLatin1("\\\"")));
0311         } else {
0312             images += QString::fromLatin1("\", \"");
0313         }
0314         QString description = populateDescription(DB::ImageDB::instance()->categoryCollection()->categories(), info);
0315 
0316         if (!description.isEmpty()) {
0317             description = QString::fromLatin1("<ul>%1</ul>").arg(description);
0318         } else {
0319             description = QString::fromLatin1("");
0320         }
0321 
0322         description.replace(QString::fromLatin1("\n$"), QString::fromLatin1(""));
0323         description.replace(QString::fromLatin1("\n"), QString::fromLatin1(" "));
0324         description.replace(QString::fromLatin1("\""), QString::fromLatin1("\\\""));
0325 
0326         images += description;
0327         images += QString::fromLatin1("\"]);\n");
0328 
0329         QDomElement href = doc.createElement(QString::fromLatin1("a"));
0330         href.setAttribute(QString::fromLatin1("href"),
0331                           namePage(width, height, fileName));
0332         col.appendChild(href);
0333 
0334         QDomElement img = doc.createElement(QString::fromLatin1("img"));
0335         img.setAttribute(QString::fromLatin1("src"),
0336                          nameImage(fileName, m_setup.thumbSize()));
0337         img.setAttribute(QString::fromLatin1("alt"),
0338                          nameImage(fileName, m_setup.thumbSize()));
0339         href.appendChild(img);
0340         ++count;
0341     }
0342 
0343     // Adding TD elements to match the selected column amount for valid HTML
0344     if (count % cols != 0) {
0345         for (int i = count; i % cols != 0; ++i) {
0346             col = doc.createElement(QString::fromLatin1("td"));
0347             col.setAttribute(QString::fromLatin1("class"), QString::fromLatin1("thumbnail-col"));
0348             QDomText sp = doc.createTextNode(QString::fromLatin1(" "));
0349             col.appendChild(sp);
0350             row.appendChild(col);
0351         }
0352     }
0353 
0354     content.replace(QString::fromLatin1("**THUMBNAIL-TABLE**"), doc.toString());
0355 
0356     images += QString::fromLatin1("var enableVideo=%1\n").arg(enableVideo ? 1 : 0);
0357     content.replace(QString::fromLatin1("**JSIMAGES**"), images);
0358     if (!first.isEmpty())
0359         content.replace(QString::fromLatin1("**FIRST**"), first);
0360     if (!last.isEmpty())
0361         content.replace(QString::fromLatin1("**LAST**"), last);
0362 
0363     // -------------------------------------------------- Resolutions
0364     QString resolutions;
0365     QList<ImageSizeCheckBox *> actRes = m_setup.activeResolutions();
0366     std::sort(actRes.begin(), actRes.end());
0367 
0368     if (actRes.count() > 1) {
0369         resolutions += QString::fromLatin1("Resolutions: ");
0370         for (QList<ImageSizeCheckBox *>::ConstIterator sizeIt = actRes.constBegin();
0371              sizeIt != actRes.constEnd(); ++sizeIt) {
0372 
0373             int w = (*sizeIt)->width();
0374             int h = (*sizeIt)->height();
0375             QString page = QString::fromLatin1("index-%1.html").arg(ImageSizeCheckBox::text(w, h, true));
0376             QString text = (*sizeIt)->text(false);
0377 
0378             resolutions += QString::fromLatin1(" ");
0379             if (width == w && height == h) {
0380                 resolutions += text;
0381             } else {
0382                 resolutions += QString::fromLatin1("<a href=\"%1\">%2</a>").arg(page).arg(text);
0383             }
0384         }
0385     }
0386 
0387     content.replace(QString::fromLatin1("**RESOLUTIONS**"), resolutions);
0388 
0389     if (wasCanceled())
0390         return false;
0391 
0392     // -------------------------------------------------- write to file
0393     QString fileName = m_tempDir.filePath(
0394         QString::fromLatin1("index-%1.html")
0395             .arg(ImageSizeCheckBox::text(width, height, true)));
0396     bool ok = writeToFile(fileName, content);
0397 
0398     if (!ok)
0399         return false;
0400 
0401     return true;
0402 }
0403 
0404 bool HTMLGenerator::Generator::generateContentPage(int width, int height,
0405                                                    const DB::FileName &prev, const DB::FileName &current, const DB::FileName &next)
0406 {
0407     QString themeDir, themeAuthor, themeName;
0408     getThemeInfo(&themeDir, &themeName, &themeAuthor);
0409     QString content = readFile(QString::fromLatin1("%1imagepage.html").arg(themeDir));
0410     if (content.isEmpty())
0411         return false;
0412 
0413     const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(current);
0414     // Note(jzarl): is there any reason why currentFile could be different from current?
0415     const DB::FileName currentFile = info->fileName();
0416 
0417     // Adding the copyright comment after DOCTYPE not before (HTML standard requires the DOCTYPE to be first within the document)
0418     QRegExp rx(QString::fromLatin1("^(<!DOCTYPE[^>]*>)"));
0419     int position;
0420 
0421     rx.setCaseSensitivity(Qt::CaseInsensitive);
0422     position = rx.indexIn(content);
0423     if ((position += rx.matchedLength()) < 0)
0424         content = QString::fromLatin1("<!--\nMade with KPhotoAlbum. (https://www.kphotoalbum.org/)\nCopyright &copy; Jesper K. Pedersen\nTheme %1 by %2\n-->\n").arg(themeName, themeAuthor) + content;
0425     else
0426         content.insert(position, QString::fromLatin1("\n<!--\nMade with KPhotoAlbum. (https://www.kphotoalbum.org/)\nCopyright &copy; Jesper K. Pedersen\nTheme %1 by %2\n-->\n").arg(themeName, themeAuthor));
0427 
0428     // TODO: Hardcoded non-standard category names is not good practice
0429     QString title = QString::fromLatin1("");
0430     QString name = QString::fromLatin1("Common Name");
0431     const auto itemsOfCategory = info->itemsOfCategory(name);
0432     if (!itemsOfCategory.empty()) {
0433         title += QStringList(itemsOfCategory.begin(), itemsOfCategory.end()).join(QLatin1String(" - "));
0434     } else {
0435         name = QString::fromLatin1("Latin Name");
0436         if (!itemsOfCategory.empty()) {
0437             title += QStringList(itemsOfCategory.begin(), itemsOfCategory.end()).join(QString::fromLatin1(" - "));
0438         } else {
0439             title = info->label();
0440         }
0441     }
0442     content.replace(QString::fromLatin1("**TITLE**"), title);
0443 
0444     // Image or video content
0445     if (KPABase::isVideo(currentFile)) {
0446         QString videoFile = createVideo(currentFile);
0447         QString videoBase = videoFile.replace(QRegExp(QString::fromLatin1("\\..*")), QString::fromLatin1(""));
0448         if (m_setup.inlineMovies())
0449             if (m_setup.html5Video())
0450                 content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("<video controls><source src=\"%4\" type=\"video/mp4\" /><source src=\"%5\" type=\"video/ogg\" /><object data=\"%1\"><img src=\"%2\" alt=\"download\"/></object></video><a href=\"%3\"><img src=\"download.png\" /></a>").arg(QString::fromLatin1("%1.mp4").arg(videoBase)).arg(createImage(current, 256)).arg(QString::fromLatin1("%1.mp4").arg(videoBase)).arg(QString::fromLatin1("%1.mp4").arg(videoBase)).arg(QString::fromLatin1("%1.ogg").arg(videoBase)));
0451             else
0452                 content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("<object data=\"%1\"><img src=\"%2\"/></object>"
0453                                                                                                "<a href=\"%3\"><img src=\"download.png\"/></a>")
0454                                                                                .arg(videoFile)
0455                                                                                .arg(createImage(current, 256))
0456                                                                                .arg(videoFile));
0457         else
0458             content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("<a href=\"**NEXTPAGE**\"><img src=\"%2\"/></a>"
0459                                                                                            "<a href=\"%1\"><img src=\"download.png\"/></a>")
0460                                                                            .arg(videoFile, createImage(current, 256)));
0461     } else
0462         content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"),
0463                         QString::fromLatin1("<a href=\"**NEXTPAGE**\"><img src=\"%1\" alt=\"%1\"/></a>")
0464                             .arg(createImage(current, width)));
0465 
0466     // -------------------------------------------------- Links
0467     QString link;
0468 
0469     // prev link
0470     if (!prev.isNull())
0471         link = i18n("<a href=\"%1\">prev</a>", namePage(width, height, prev));
0472     else
0473         link = i18n("prev");
0474     content.replace(QString::fromLatin1("**PREV**"), link);
0475 
0476     // PENDING(blackie) These next 5 line also exists exactly like that in HTMLGenerator::Generator::generateIndexPage. Please refactor.
0477     // prevfile
0478     if (!prev.isNull())
0479         link = namePage(width, height, prev);
0480     else
0481         link = i18n("prev");
0482     content.replace(QString::fromLatin1("**PREVFILE**"), link);
0483 
0484     // index link
0485     link = i18n("<a href=\"index-%1.html\">index</a>", ImageSizeCheckBox::text(width, height, true));
0486     content.replace(QString::fromLatin1("**INDEX**"), link);
0487 
0488     // indexfile
0489     link = QString::fromLatin1("index-%1.html").arg(ImageSizeCheckBox::text(width, height, true));
0490     content.replace(QString::fromLatin1("**INDEXFILE**"), link);
0491 
0492     // Next Link
0493     if (!next.isNull())
0494         link = i18n("<a href=\"%1\">next</a>", namePage(width, height, next));
0495     else
0496         link = i18n("next");
0497     content.replace(QString::fromLatin1("**NEXT**"), link);
0498 
0499     // Nextfile
0500     if (!next.isNull())
0501         link = namePage(width, height, next);
0502     else
0503         link = i18n("next");
0504     content.replace(QString::fromLatin1("**NEXTFILE**"), link);
0505 
0506     if (!next.isNull())
0507         link = namePage(width, height, next);
0508     else
0509         link = QString::fromLatin1("index-%1.html").arg(ImageSizeCheckBox::text(width, height, true));
0510 
0511     content.replace(QString::fromLatin1("**NEXTPAGE**"), link);
0512 
0513     // -------------------------------------------------- Resolutions
0514     QString resolutions;
0515     const QList<ImageSizeCheckBox *> &actRes = m_setup.activeResolutions();
0516     if (actRes.count() > 1) {
0517         for (QList<ImageSizeCheckBox *>::ConstIterator sizeIt = actRes.begin();
0518              sizeIt != actRes.end(); ++sizeIt) {
0519             int w = (*sizeIt)->width();
0520             int h = (*sizeIt)->height();
0521             QString page = namePage(w, h, currentFile);
0522             QString text = (*sizeIt)->text(false);
0523             resolutions += QString::fromLatin1(" ");
0524 
0525             if (width == w && height == h)
0526                 resolutions += text;
0527             else
0528                 resolutions += QString::fromLatin1("<a href=\"%1\">%2</a>").arg(page).arg(text);
0529         }
0530     }
0531     content.replace(QString::fromLatin1("**RESOLUTIONS**"), resolutions);
0532 
0533     // -------------------------------------------------- Copyright
0534     QString copyright;
0535 
0536     if (!m_setup.copyright().isEmpty())
0537         copyright = QString::fromLatin1("&#169; %1").arg(m_setup.copyright());
0538     else
0539         copyright = QString::fromLatin1("&nbsp;");
0540     content.replace(QString::fromLatin1("**COPYRIGHT**"), QString::fromLatin1("%1").arg(copyright));
0541 
0542     // -------------------------------------------------- Description
0543     QString description = populateDescription(DB::ImageDB::instance()->categoryCollection()->categories(), info);
0544 
0545     if (!description.isEmpty())
0546         content.replace(QString::fromLatin1("**DESCRIPTION**"), QString::fromLatin1("<ul>\n%1\n</ul>").arg(description));
0547     else
0548         content.replace(QString::fromLatin1("**DESCRIPTION**"), QString::fromLatin1(""));
0549 
0550     // -------------------------------------------------- write to file
0551     QString fileName = m_tempDir.filePath(namePage(width, height, currentFile));
0552     bool ok = writeToFile(fileName, content);
0553     if (!ok)
0554         return false;
0555 
0556     return true;
0557 }
0558 
0559 QString HTMLGenerator::Generator::namePage(int width, int height, const DB::FileName &fileName)
0560 {
0561     QString name = m_filenameMapper.uniqNameFor(fileName);
0562     QString base = QFileInfo(name).completeBaseName();
0563     return QString::fromLatin1("%1-%2.html").arg(base, ImageSizeCheckBox::text(width, height, true));
0564 }
0565 
0566 QString HTMLGenerator::Generator::nameImage(const DB::FileName &fileName, int size)
0567 {
0568     QString name = m_filenameMapper.uniqNameFor(fileName);
0569     QString base = QFileInfo(name).completeBaseName();
0570     if (size == maxImageSize() && !KPABase::isVideo(fileName)) {
0571         if (name.endsWith(QString::fromLatin1(".jpg"), Qt::CaseSensitive) || name.endsWith(QString::fromLatin1(".jpeg"), Qt::CaseSensitive))
0572             return name;
0573         else
0574             return base + QString::fromLatin1(".jpg");
0575     } else if (size == maxImageSize() && KPABase::isVideo(fileName)) {
0576         return name;
0577     } else
0578         return QString::fromLatin1("%1-%2.jpg").arg(base).arg(size);
0579 }
0580 
0581 QString HTMLGenerator::Generator::createImage(const DB::FileName &fileName, int size)
0582 {
0583     const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName);
0584     if (m_generatedFiles.contains(qMakePair(fileName, size))) {
0585         m_waitCounter--;
0586     } else {
0587         ImageManager::ImageRequest *request = new ImageManager::ImageRequest(fileName, QSize(size, size),
0588                                                                              info->angle(), this);
0589         request->setPriority(ImageManager::BatchTask);
0590         ImageManager::AsyncLoader::instance()->load(request);
0591         m_generatedFiles.insert(qMakePair(fileName, size));
0592     }
0593 
0594     return nameImage(fileName, size);
0595 }
0596 
0597 QString HTMLGenerator::Generator::createVideo(const DB::FileName &fileName)
0598 {
0599     setValue(m_total - m_waitCounter);
0600     qApp->processEvents();
0601 
0602     QString baseName = nameImage(fileName, maxImageSize());
0603     QString destName = m_tempDir.filePath(baseName);
0604     if (!m_copiedVideos.contains(fileName)) {
0605         if (m_setup.html5VideoGenerate()) {
0606             // TODO: shouldn't we use avconv library directly instead of KRun
0607             // TODO: should check that the avconv (ffmpeg takes the same parameters on older systems) and ffmpeg2theora exist
0608             // TODO: Figure out avconv parameters to get rid of ffmpeg2theora
0609             auto *uiParent = MainWindow::Window::theMainWindow();
0610             const auto avCmd = QString::fromLatin1("%1 -y -i %2  -vcodec libx264 -b 250k -bt 50k -acodec libfaac -ab 56k -ac 2 -s %3 %4")
0611                                    .arg(m_avconv)
0612                                    .arg(fileName.absolute())
0613                                    .arg(QString::fromLatin1("320x240"))
0614                                    .arg(destName.replace(QRegExp(QString::fromLatin1("\\..*")), QString::fromLatin1(".mp4")));
0615             const auto f2tCmd = QString::fromLatin1("ffmpeg2theora -v 7 -o %1 -x %2 %3")
0616                                     .arg(destName.replace(QRegExp(QString::fromLatin1("\\..*")), QString::fromLatin1(".ogg")))
0617                                     .arg(QString::fromLatin1("320"))
0618                                     .arg(fileName.absolute());
0619 #if KIO_VERSION <= QT_VERSION_CHECK(5, 69, 0)
0620             KRun::runCommand(avCmd, uiParent);
0621             KRun::runCommand(f2tCmd, uiParent);
0622 #else
0623             KIO::CommandLauncherJob *avJob = new KIO::CommandLauncherJob(avCmd);
0624             avJob->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, uiParent));
0625             avJob->start();
0626             KIO::CommandLauncherJob *f2tJob = new KIO::CommandLauncherJob(f2tCmd);
0627             f2tJob->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, uiParent));
0628             f2tJob->start();
0629 #endif
0630         } else
0631             Utilities::copyOrOverwrite(fileName.absolute(), destName);
0632         m_copiedVideos.insert(fileName);
0633     }
0634     return baseName;
0635 }
0636 
0637 QString HTMLGenerator::Generator::kimFileName(bool relative)
0638 {
0639     if (relative)
0640         return QString::fromLatin1("%2.kim").arg(m_setup.outputDir());
0641     else
0642         return m_tempDir.filePath(QString::fromLatin1("%2.kim").arg(m_setup.outputDir()));
0643 }
0644 
0645 bool HTMLGenerator::Generator::writeToFile(const QString &fileName, const QString &str)
0646 {
0647     QFile file(fileName);
0648     if (!file.open(QIODevice::WriteOnly)) {
0649         KMessageBox::error(this, i18n("Could not create file '%1'.", fileName),
0650                            i18n("Could Not Create File"));
0651         return false;
0652     }
0653 
0654     QByteArray data = str.toUtf8();
0655     file.write(data);
0656     file.close();
0657     return true;
0658 }
0659 
0660 bool HTMLGenerator::Generator::linkIndexFile()
0661 {
0662     ImageSizeCheckBox *resolution = m_setup.activeResolutions()[0];
0663     QString fromFile = QString::fromLatin1("index-%1.html")
0664                            .arg(resolution->text(true));
0665     fromFile = m_tempDir.filePath(fromFile);
0666     QString destFile = m_tempDir.filePath(QString::fromLatin1("index.html"));
0667     bool ok = Utilities::copyOrOverwrite(fromFile, destFile);
0668     if (!ok) {
0669         KMessageBox::error(this, i18n("<p>Unable to copy %1 to %2</p>", fromFile, destFile));
0670 
0671         return false;
0672     }
0673     return ok;
0674 }
0675 
0676 void HTMLGenerator::Generator::slotCancelGenerate()
0677 {
0678     ImageManager::AsyncLoader::instance()->stop(this);
0679     m_waitCounter = 0;
0680     if (m_hasEnteredLoop)
0681         m_eventLoop->exit();
0682 }
0683 
0684 void HTMLGenerator::Generator::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image)
0685 {
0686     const DB::FileName fileName = request->databaseFileName();
0687     const QSize imgSize = request->size();
0688     const bool loadedOK = request->loadedOK();
0689 
0690     setValue(m_total - m_waitCounter);
0691 
0692     m_waitCounter--;
0693 
0694     int size = imgSize.width();
0695     QString file = m_tempDir.filePath(nameImage(fileName, size));
0696 
0697     bool success = loadedOK && image.save(file, "JPEG");
0698     if (!success) {
0699         // We better stop the imageloading. In case this is a full disk, we will just get all images loaded, while this
0700         // error box is showing, resulting in a bunch of error messages, and memory running out due to all the hanging
0701         // pixmapLoaded methods.
0702         slotCancelGenerate();
0703         KMessageBox::error(this, i18n("Unable to write image '%1'.", file));
0704     }
0705 
0706     if (!KPABase::isVideo(fileName)) {
0707         try {
0708             auto imageInfo = DB::ImageDB::instance()->info(fileName);
0709             Exif::writeExifInfoToFile(fileName, file, imageInfo->description());
0710         } catch (...) {
0711         }
0712     }
0713 
0714     if (m_waitCounter == 0 && m_hasEnteredLoop) {
0715         m_eventLoop->exit();
0716     }
0717 }
0718 
0719 int HTMLGenerator::Generator::calculateSteps()
0720 {
0721     int count = m_setup.activeResolutions().count();
0722     return m_setup.imageList().size() * (1 + count); // 1 thumbnail + 1 real image
0723 }
0724 
0725 void HTMLGenerator::Generator::getThemeInfo(QString *baseDir, QString *name, QString *author)
0726 {
0727     *baseDir = m_setup.themePath();
0728     KConfig themeconfig(QString::fromLatin1("%1/kphotoalbum.theme").arg(*baseDir), KConfig::SimpleConfig);
0729     KConfigGroup config = themeconfig.group("theme");
0730 
0731     *name = config.readEntry("Name");
0732     *author = config.readEntry("Author");
0733 }
0734 
0735 int HTMLGenerator::Generator::maxImageSize()
0736 {
0737     int res = 0;
0738     for (QList<ImageSizeCheckBox *>::ConstIterator sizeIt = m_setup.activeResolutions().begin();
0739          sizeIt != m_setup.activeResolutions().end(); ++sizeIt) {
0740         res = qMax(res, (*sizeIt)->width());
0741     }
0742     return res;
0743 }
0744 
0745 void HTMLGenerator::Generator::minImageSize(int &width, int &height)
0746 {
0747     width = height = 0;
0748     for (QList<ImageSizeCheckBox *>::ConstIterator sizeIt = m_setup.activeResolutions().begin();
0749          sizeIt != m_setup.activeResolutions().end(); ++sizeIt) {
0750         if ((width == 0) && ((*sizeIt)->width() > 0)) {
0751             width = (*sizeIt)->width();
0752             height = (*sizeIt)->height();
0753         } else if ((*sizeIt)->width() > 0) {
0754             width = qMin(width, (*sizeIt)->width());
0755             height = qMin(height, (*sizeIt)->height());
0756         }
0757     }
0758 }
0759 
0760 void HTMLGenerator::Generator::showBrowser()
0761 {
0762     if (m_setup.generateKimFile())
0763         ImportExport::Export::showUsageDialog();
0764 
0765     if (!m_setup.baseURL().isEmpty())
0766         new KRun(QUrl::fromUserInput(QString::fromLatin1("%1/%2/index.html").arg(m_setup.baseURL(), m_setup.outputDir())),
0767                  MainWindow::Window::theMainWindow());
0768 
0769     m_eventLoop->exit();
0770 }
0771 
0772 QString HTMLGenerator::Generator::populateDescription(QList<DB::CategoryPtr> categories, const DB::ImageInfoPtr info)
0773 {
0774     QString description;
0775 
0776     if (m_setup.includeCategory(QString::fromLatin1("**DATE**")))
0777         description += QString::fromLatin1("<li> <b>%1</b> %2</li>").arg(i18n("Date"), info->date().toString());
0778 
0779     for (QList<DB::CategoryPtr>::Iterator it = categories.begin(); it != categories.end(); ++it) {
0780         if ((*it)->isSpecialCategory()) {
0781             continue;
0782         }
0783 
0784         const auto name = (*it)->name();
0785         const auto itemsOfCategory = info->itemsOfCategory(name);
0786         if (!itemsOfCategory.empty() && m_setup.includeCategory(name)) {
0787             const QStringList itemsList(itemsOfCategory.begin(), itemsOfCategory.end());
0788             description += QStringLiteral("  <li> <b>%1:</b> %2</li>").arg(name, itemsList.join(QLatin1String(", ")));
0789         }
0790     }
0791 
0792     if (!info->description().isEmpty() && m_setup.includeCategory(QString::fromLatin1("**DESCRIPTION**"))) {
0793         description += QString::fromLatin1("  <li> <b>Description:</b> %1</li>").arg(info->description());
0794     }
0795 
0796     return description;
0797 }
0798 
0799 // vi:expandtab:tabstop=4 shiftwidth=4:
0800 
0801 #include "moc_Generator.cpp"