File indexing completed on 2024-05-05 04:22:01

0001 // SPDX-FileCopyrightText: 2004 Andrew Coles <andrew.i.coles@googlemail.com>
0002 // SPDX-FileCopyrightText: 2004-2007 Laurent Montel <montel@kde.org>
0003 // SPDX-FileCopyrightText: 2004-2005 Stephan Binner <binner@kde.org>
0004 // SPDX-FileCopyrightText: 2004-2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0005 // SPDX-FileCopyrightText: 2005-2007 Dirk Mueller <mueller@kde.org>
0006 // SPDX-FileCopyrightText: 2007-2011 Jan Kundrát <jkt@flaska.net>
0007 // SPDX-FileCopyrightText: 2008 Henner Zeller <h.zeller@acm.org>
0008 // SPDX-FileCopyrightText: 2008-2010 Tuomas Suutari <tuomas@nepnep.net>
0009 // SPDX-FileCopyrightText: 2009 Hassan Ibraheem <hasan.ibraheem@gmail.com>
0010 // SPDX-FileCopyrightText: 2012-2013 Miika Turkia <miika.turkia@gmail.com>
0011 // SPDX-FileCopyrightText: 2012-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0012 // SPDX-FileCopyrightText: 2013 Pino Toscano <pino@kde.org>
0013 // SPDX-FileCopyrightText: 2016-2019 Tobias Leupold <tl@stonemx.de>
0014 // SPDX-FileCopyrightText: 2018 Antoni Bella Pérez <antonibella5@yahoo.com>
0015 // SPDX-FileCopyrightText: 2018 Yuri Chornoivan <yurchor@ukr.net>
0016 //
0017 // SPDX-License-Identifier: GPL-2.0-or-later
0018 
0019 #include "Export.h"
0020 
0021 #include "XMLHandler.h"
0022 
0023 #include <DB/ImageDB.h>
0024 #include <DB/ImageInfo.h>
0025 #include <ImageManager/AsyncLoader.h>
0026 #include <ImageManager/RawImageDecoder.h>
0027 #include <Utilities/FileUtil.h>
0028 #include <kpabase/FileExtensions.h>
0029 #include <kpabase/FileNameList.h>
0030 #include <kpabase/FileNameUtil.h>
0031 
0032 #include <KConfigGroup>
0033 #include <KHelpClient>
0034 #include <KLocalizedString>
0035 #include <KMessageBox>
0036 #include <KZip>
0037 #include <QApplication>
0038 #include <QBuffer>
0039 #include <QCheckBox>
0040 #include <QDialogButtonBox>
0041 #include <QFileDialog>
0042 #include <QFileInfo>
0043 #include <QGroupBox>
0044 #include <QLayout>
0045 #include <QProgressDialog>
0046 #include <QPushButton>
0047 #include <QRadioButton>
0048 #include <QSpinBox>
0049 #include <QVBoxLayout>
0050 
0051 using namespace ImportExport;
0052 
0053 namespace
0054 {
0055 bool isRAW(const DB::FileName &fileName)
0056 {
0057     return KPABase::isUsableRawImage(fileName);
0058 }
0059 } // namespace
0060 
0061 void Export::imageExport(const DB::FileNameList &list)
0062 {
0063     ExportConfig config;
0064     if (config.exec() == QDialog::Rejected)
0065         return;
0066 
0067     int maxSize = -1;
0068     if (config.mp_enforeMaxSize->isChecked())
0069         maxSize = config.mp_maxSize->value();
0070 
0071     // Ask for zip file name
0072     QString zipFile = QFileDialog::getSaveFileName(
0073         nullptr, /* parent */
0074         i18n("Save an export file"), /* caption */
0075         QString(), /* directory */
0076         i18n("KPhotoAlbum import files") + QLatin1String("(*.kim)") /*filter*/
0077     );
0078     if (zipFile.isNull())
0079         return;
0080 
0081     bool ok;
0082     Export *exp = new Export(list, zipFile, config.mp_compress->isChecked(), maxSize, config.imageFileLocation(), QString(), config.mp_generateThumbnails->isChecked(), &ok);
0083     delete exp; // It will not return before done - we still need a class to connect slots etc.
0084 
0085     if (ok)
0086         showUsageDialog();
0087 }
0088 
0089 // PENDING(blackie) add warning if images are to be copied into a non empty directory.
0090 ExportConfig::ExportConfig()
0091 {
0092     setWindowTitle(i18nc("@title:window", "Export Configuration / Copy Images"));
0093     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help);
0094     QWidget *mainWidget = new QWidget(this);
0095     QVBoxLayout *mainLayout = new QVBoxLayout;
0096     setLayout(mainLayout);
0097     mainLayout->addWidget(mainWidget);
0098 
0099     QWidget *top = new QWidget;
0100     mainLayout->addWidget(top);
0101 
0102     QVBoxLayout *lay1 = new QVBoxLayout(top);
0103 
0104     // Include images
0105     QGroupBox *grp = new QGroupBox(i18n("How to Handle Images"));
0106     lay1->addWidget(grp);
0107 
0108     QVBoxLayout *boxLay = new QVBoxLayout(grp);
0109     m_include = new QRadioButton(i18n("Include in .kim file"), grp);
0110     m_manually = new QRadioButton(i18n("Do not copy files, only generate .kim file"), grp);
0111     m_auto = new QRadioButton(i18n("Automatically copy next to .kim file"), grp);
0112     m_link = new QRadioButton(i18n("Hard link next to .kim file"), grp);
0113     m_symlink = new QRadioButton(i18n("Symbolic link next to .kim file"), grp);
0114     m_auto->setChecked(true);
0115 
0116     boxLay->addWidget(m_include);
0117     boxLay->addWidget(m_manually);
0118     boxLay->addWidget(m_auto);
0119     boxLay->addWidget(m_link);
0120     boxLay->addWidget(m_symlink);
0121 
0122     // Compress
0123     mp_compress = new QCheckBox(i18n("Compress export file"), top);
0124     lay1->addWidget(mp_compress);
0125 
0126     // Generate thumbnails
0127     mp_generateThumbnails = new QCheckBox(i18n("Generate thumbnails"), top);
0128     mp_generateThumbnails->setChecked(false);
0129     lay1->addWidget(mp_generateThumbnails);
0130 
0131     // Enforece max size
0132     QHBoxLayout *hlay = new QHBoxLayout;
0133     lay1->addLayout(hlay);
0134 
0135     mp_enforeMaxSize = new QCheckBox(i18n("Limit maximum image dimensions to: "));
0136     hlay->addWidget(mp_enforeMaxSize);
0137 
0138     mp_maxSize = new QSpinBox;
0139     mp_maxSize->setRange(100, 4000);
0140 
0141     hlay->addWidget(mp_maxSize);
0142     mp_maxSize->setValue(800);
0143 
0144     connect(mp_enforeMaxSize, &QCheckBox::toggled, mp_maxSize, &QSpinBox::setEnabled);
0145     mp_maxSize->setEnabled(false);
0146 
0147     QString txt = i18n("<p>If your images are stored in a non-compressed file format then you may check this; "
0148                        "otherwise, this just wastes time during import and export operations.</p>"
0149                        "<p>In other words, do not check this if your images are stored in jpg, png or gif; but do check this "
0150                        "if your images are stored in tiff.</p>");
0151     mp_compress->setWhatsThis(txt);
0152 
0153     txt = i18n("<p>Generate thumbnail images</p>");
0154     mp_generateThumbnails->setWhatsThis(txt);
0155 
0156     txt = i18n("<p>With this option you may limit the maximum dimensions (width and height) of your images. "
0157                "Doing so will make the resulting export file smaller, but will of course also make the quality "
0158                "worse if someone wants to see the exported images with larger dimensions.</p>");
0159 
0160     mp_enforeMaxSize->setWhatsThis(txt);
0161     mp_maxSize->setWhatsThis(txt);
0162 
0163     txt = i18n("<p>When exporting images, bear in mind that there are two things the "
0164                "person importing these images again will need:<br/>"
0165                "1) meta information (image content, date etc.)<br/>"
0166                "2) the images themselves.</p>"
0167 
0168                "<p>The images themselves can either be placed next to the .kim file, "
0169                "or copied into the .kim file. Copying the images into the .kim file works well "
0170                "for a recipient who wants all, or most of those images, for example "
0171                "when emailing a whole group of images. However, when you place the "
0172                "images on the Web, a lot of people will see them but most likely only "
0173                "download a few of them. It works better in this kind of case, to "
0174                "separate the images and the .kim file, by place them next to each "
0175                "other, so the user can access the images s/he wants.</p>");
0176 
0177     grp->setWhatsThis(txt);
0178     m_include->setWhatsThis(txt);
0179     m_manually->setWhatsThis(txt);
0180     m_link->setWhatsThis(txt);
0181     m_symlink->setWhatsThis(txt);
0182     m_auto->setWhatsThis(txt);
0183 
0184     QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
0185     okButton->setDefault(true);
0186     okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0187     connect(buttonBox, &QDialogButtonBox::accepted, this, &ExportConfig::accept);
0188     connect(buttonBox, &QDialogButtonBox::rejected, this, &ExportConfig::reject);
0189     mainLayout->addWidget(buttonBox);
0190 
0191     QPushButton *helpButton = buttonBox->button(QDialogButtonBox::Help);
0192     connect(helpButton, &QPushButton::clicked, this, &ExportConfig::showHelp);
0193 }
0194 
0195 ImageFileLocation ExportConfig::imageFileLocation() const
0196 {
0197     if (m_include->isChecked())
0198         return Inline;
0199     else if (m_manually->isChecked())
0200         return ManualCopy;
0201     else if (m_link->isChecked())
0202         return Link;
0203     else if (m_symlink->isChecked())
0204         return Symlink;
0205     else
0206         return AutoCopy;
0207 }
0208 
0209 void ExportConfig::showHelp()
0210 {
0211     KHelpClient::invokeHelp(QStringLiteral("chp-importExport"));
0212 }
0213 
0214 Export::~Export()
0215 {
0216     delete m_zip;
0217     delete m_eventLoop;
0218 }
0219 
0220 Export::Export(
0221     const DB::FileNameList &list,
0222     const QString &zipFile,
0223     bool compress,
0224     int maxSize,
0225     ImageFileLocation location,
0226     const QString &baseUrl,
0227     bool doGenerateThumbnails,
0228     bool *ok)
0229     : m_internalOk(true)
0230     , m_ok(ok)
0231     , m_maxSize(maxSize)
0232     , m_location(location)
0233     , m_eventLoop(new QEventLoop)
0234 {
0235     if (ok == nullptr)
0236         ok = &m_internalOk;
0237     *ok = true;
0238     m_destdir = QFileInfo(zipFile).path();
0239     m_zip = new KZip(zipFile);
0240     m_zip->setCompression(compress ? KZip::DeflateCompression : KZip::NoCompression);
0241     if (!m_zip->open(QIODevice::WriteOnly)) {
0242         KMessageBox::error(nullptr, i18n("Error creating zip file"));
0243         *ok = false;
0244         return;
0245     }
0246 
0247     // Create progress dialog
0248     int total = 1;
0249     if (location != ManualCopy)
0250         total += list.size();
0251     if (doGenerateThumbnails)
0252         total += list.size();
0253 
0254     m_steps = 0;
0255     m_progressDialog = new QProgressDialog(MainWindow::Window::theMainWindow());
0256     m_progressDialog->setCancelButtonText(i18n("&Cancel"));
0257     m_progressDialog->setMaximum(total);
0258 
0259     m_progressDialog->setValue(0);
0260     m_progressDialog->show();
0261 
0262     // Copy image files and generate thumbnails
0263     if (location != ManualCopy) {
0264         m_copyingFiles = true;
0265         copyImages(list);
0266     }
0267 
0268     if (*m_ok && doGenerateThumbnails) {
0269         m_copyingFiles = false;
0270         generateThumbnails(list);
0271     }
0272 
0273     if (*m_ok) {
0274         // Create the index.xml file
0275         m_progressDialog->setLabelText(i18n("Creating index file"));
0276         QByteArray indexml = XMLHandler().createIndexXML(list, baseUrl, m_location, &m_filenameMapper);
0277         m_zip->writeFile(QStringLiteral("index.xml"), indexml.data());
0278 
0279         m_steps++;
0280         m_progressDialog->setValue(m_steps);
0281         m_zip->close();
0282     }
0283 }
0284 
0285 void Export::generateThumbnails(const DB::FileNameList &list)
0286 {
0287     m_progressDialog->setLabelText(i18n("Creating thumbnails"));
0288     m_loopEntered = false;
0289     m_subdir = QLatin1String("Thumbnails/");
0290     m_filesRemaining = list.size(); // Used to break the event loop.
0291     for (const DB::FileName &fileName : list) {
0292         const auto info = DB::ImageDB::instance()->info(fileName);
0293         ImageManager::ImageRequest *request = new ImageManager::ImageRequest(fileName, QSize(128, 128), info->angle(), this);
0294         request->setPriority(ImageManager::BatchTask);
0295         ImageManager::AsyncLoader::instance()->load(request);
0296     }
0297     if (m_filesRemaining > 0) {
0298         m_loopEntered = true;
0299         m_eventLoop->exec();
0300     }
0301 }
0302 
0303 void Export::copyImages(const DB::FileNameList &list)
0304 {
0305     Q_ASSERT(m_location != ManualCopy);
0306 
0307     m_loopEntered = false;
0308     m_subdir = QLatin1String("Images/");
0309 
0310     m_progressDialog->setLabelText(i18n("Copying image files"));
0311 
0312     m_filesRemaining = 0;
0313     for (const DB::FileName &fileName : list) {
0314         QString file = fileName.absolute();
0315         QString zippedName = m_filenameMapper.uniqNameFor(fileName);
0316 
0317         if (m_maxSize == -1 || KPABase::isVideo(fileName) || isRAW(fileName)) {
0318             const QFileInfo fileInfo(file);
0319             if (fileInfo.isSymLink()) {
0320                 file = fileInfo.symLinkTarget();
0321             }
0322             if (m_location == Inline)
0323                 m_zip->addLocalFile(file, QStringLiteral("Images/") + zippedName);
0324             else if (m_location == AutoCopy)
0325                 Utilities::copyOrOverwrite(file, m_destdir + QLatin1String("/") + zippedName);
0326             else if (m_location == Link)
0327                 Utilities::makeHardLink(file, m_destdir + QLatin1String("/") + zippedName);
0328             else if (m_location == Symlink)
0329                 Utilities::makeSymbolicLink(file, m_destdir + QLatin1String("/") + zippedName);
0330 
0331             m_steps++;
0332             m_progressDialog->setValue(m_steps);
0333         } else {
0334             m_filesRemaining++;
0335             ImageManager::ImageRequest *request = new ImageManager::ImageRequest(DB::FileName::fromAbsolutePath(file), QSize(m_maxSize, m_maxSize), 0, this);
0336             request->setPriority(ImageManager::BatchTask);
0337             ImageManager::AsyncLoader::instance()->load(request);
0338         }
0339 
0340         // Test if the cancel button was pressed.
0341         qApp->processEvents(QEventLoop::AllEvents);
0342 
0343         if (m_progressDialog->wasCanceled()) {
0344             *m_ok = false;
0345             return;
0346         }
0347     }
0348     if (m_filesRemaining > 0) {
0349         m_loopEntered = true;
0350         m_eventLoop->exec();
0351     }
0352 }
0353 
0354 void Export::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image)
0355 {
0356     const DB::FileName fileName = request->databaseFileName();
0357     if (!request->loadedOK())
0358         return;
0359 
0360     const QString ext = (KPABase::isVideo(fileName) || isRAW(fileName)) ? QStringLiteral("jpg") : QFileInfo(m_filenameMapper.uniqNameFor(fileName)).completeSuffix();
0361 
0362     // Add the file to the zip archive
0363     QString zipFileName = QStringLiteral("%1/%2.%3").arg(Utilities::stripEndingForwardSlash(m_subdir), QFileInfo(m_filenameMapper.uniqNameFor(fileName)).baseName(), ext);
0364     QByteArray data;
0365     QBuffer buffer(&data);
0366     buffer.open(QIODevice::WriteOnly);
0367     image.save(&buffer, QFileInfo(zipFileName).suffix().toLower().toLatin1().constData());
0368 
0369     if (m_location == Inline || !m_copyingFiles)
0370         m_zip->writeFile(zipFileName, data.constData());
0371     else {
0372         QString file = m_destdir + QLatin1String("/") + m_filenameMapper.uniqNameFor(fileName);
0373         QFile out(file);
0374         if (!out.open(QIODevice::WriteOnly)) {
0375             KMessageBox::error(nullptr, i18n("Error writing file %1", file));
0376             *m_ok = false;
0377         }
0378         out.write(data.constData(), data.size());
0379         out.close();
0380     }
0381 
0382     qApp->processEvents(QEventLoop::AllEvents);
0383 
0384     bool canceled = (!*m_ok || m_progressDialog->wasCanceled());
0385 
0386     if (canceled) {
0387         *m_ok = false;
0388         m_eventLoop->exit();
0389         ImageManager::AsyncLoader::instance()->stop(this);
0390         return;
0391     }
0392 
0393     m_steps++;
0394     m_filesRemaining--;
0395     m_progressDialog->setValue(m_steps);
0396 
0397     if (m_filesRemaining == 0 && m_loopEntered)
0398         m_eventLoop->exit();
0399 }
0400 
0401 void Export::showUsageDialog()
0402 {
0403     QString txt = i18n("<p>Other KPhotoAlbum users may now load the import file into their database, by choosing <b>import</b> in "
0404                        "the file menu.</p>"
0405                        "<p>If they find it on a web site, and the web server is correctly configured, all they need to do is simply "
0406                        "to click it from within konqueror. To enable this, your web server needs to be configured for KPhotoAlbum. You do so by adding "
0407                        "the following line to <b>/etc/httpd/mime.types</b> or similar:"
0408                        "<pre>application/vnd.kde.kphotoalbum-import kim</pre>"
0409                        "This will make your web server tell konqueror that it is a KPhotoAlbum file when clicking on the link, "
0410                        "otherwise the web server will just tell konqueror that it is a plain text file.</p>");
0411 
0412     KMessageBox::information(nullptr, txt, i18n("How to Use the Export File"), QStringLiteral("export_how_to_use_the_export_file"));
0413 }
0414 
0415 // vi:expandtab:tabstop=4 shiftwidth=4:
0416 
0417 #include "moc_Export.cpp"