1 /* SPDX-FileCopyrightText: 2003-2020 The KPhotoAlbum Development Team
2 
3    SPDX-License-Identifier: GPL-2.0-or-later
4 */
5 
6 #include "Export.h"
7 
8 #include "XMLHandler.h"
9 
10 #include <DB/ImageDB.h>
11 #include <DB/ImageInfo.h>
12 #include <ImageManager/AsyncLoader.h>
13 #include <ImageManager/RawImageDecoder.h>
14 #include <Utilities/FileUtil.h>
15 #include <Utilities/VideoUtil.h>
16 #include <kpabase/FileNameList.h>
17 #include <kpabase/FileNameUtil.h>
18 
19 #include <KConfigGroup>
20 #include <KHelpClient>
21 #include <KLocalizedString>
22 #include <KMessageBox>
23 #include <KZip>
24 #include <QApplication>
25 #include <QBuffer>
26 #include <QCheckBox>
27 #include <QDialogButtonBox>
28 #include <QFileDialog>
29 #include <QFileInfo>
30 #include <QGroupBox>
31 #include <QLayout>
32 #include <QProgressDialog>
33 #include <QPushButton>
34 #include <QRadioButton>
35 #include <QSpinBox>
36 #include <QVBoxLayout>
37 
38 using namespace ImportExport;
39 
40 namespace
41 {
isRAW(const DB::FileName & fileName)42 bool isRAW(const DB::FileName &fileName)
43 {
44     return ImageManager::RAWImageDecoder::isRAW(fileName);
45 }
46 } //namespace
47 
imageExport(const DB::FileNameList & list)48 void Export::imageExport(const DB::FileNameList &list)
49 {
50     ExportConfig config;
51     if (config.exec() == QDialog::Rejected)
52         return;
53 
54     int maxSize = -1;
55     if (config.mp_enforeMaxSize->isChecked())
56         maxSize = config.mp_maxSize->value();
57 
58     // Ask for zip file name
59     QString zipFile = QFileDialog::getSaveFileName(
60         nullptr, /* parent */
61         i18n("Save an export file"), /* caption */
62         QString(), /* directory */
63         i18n("KPhotoAlbum import files") + QLatin1String("(*.kim)") /*filter*/
64     );
65     if (zipFile.isNull())
66         return;
67 
68     bool ok;
69     Export *exp = new Export(list, zipFile, config.mp_compress->isChecked(), maxSize, config.imageFileLocation(), QString(), config.mp_generateThumbnails->isChecked(), &ok);
70     delete exp; // It will not return before done - we still need a class to connect slots etc.
71 
72     if (ok)
73         showUsageDialog();
74 }
75 
76 // PENDING(blackie) add warning if images are to be copied into a non empty directory.
ExportConfig()77 ExportConfig::ExportConfig()
78 {
79     setWindowTitle(i18nc("@title:window", "Export Configuration / Copy Images"));
80     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help);
81     QWidget *mainWidget = new QWidget(this);
82     QVBoxLayout *mainLayout = new QVBoxLayout;
83     setLayout(mainLayout);
84     mainLayout->addWidget(mainWidget);
85 
86     QWidget *top = new QWidget;
87     mainLayout->addWidget(top);
88 
89     QVBoxLayout *lay1 = new QVBoxLayout(top);
90 
91     // Include images
92     QGroupBox *grp = new QGroupBox(i18n("How to Handle Images"));
93     lay1->addWidget(grp);
94 
95     QVBoxLayout *boxLay = new QVBoxLayout(grp);
96     m_include = new QRadioButton(i18n("Include in .kim file"), grp);
97     m_manually = new QRadioButton(i18n("Do not copy files, only generate .kim file"), grp);
98     m_auto = new QRadioButton(i18n("Automatically copy next to .kim file"), grp);
99     m_link = new QRadioButton(i18n("Hard link next to .kim file"), grp);
100     m_symlink = new QRadioButton(i18n("Symbolic link next to .kim file"), grp);
101     m_auto->setChecked(true);
102 
103     boxLay->addWidget(m_include);
104     boxLay->addWidget(m_manually);
105     boxLay->addWidget(m_auto);
106     boxLay->addWidget(m_link);
107     boxLay->addWidget(m_symlink);
108 
109     // Compress
110     mp_compress = new QCheckBox(i18n("Compress export file"), top);
111     lay1->addWidget(mp_compress);
112 
113     // Generate thumbnails
114     mp_generateThumbnails = new QCheckBox(i18n("Generate thumbnails"), top);
115     mp_generateThumbnails->setChecked(false);
116     lay1->addWidget(mp_generateThumbnails);
117 
118     // Enforece max size
119     QHBoxLayout *hlay = new QHBoxLayout;
120     lay1->addLayout(hlay);
121 
122     mp_enforeMaxSize = new QCheckBox(i18n("Limit maximum image dimensions to: "));
123     hlay->addWidget(mp_enforeMaxSize);
124 
125     mp_maxSize = new QSpinBox;
126     mp_maxSize->setRange(100, 4000);
127 
128     hlay->addWidget(mp_maxSize);
129     mp_maxSize->setValue(800);
130 
131     connect(mp_enforeMaxSize, &QCheckBox::toggled, mp_maxSize, &QSpinBox::setEnabled);
132     mp_maxSize->setEnabled(false);
133 
134     QString txt = i18n("<p>If your images are stored in a non-compressed file format then you may check this; "
135                        "otherwise, this just wastes time during import and export operations.</p>"
136                        "<p>In other words, do not check this if your images are stored in jpg, png or gif; but do check this "
137                        "if your images are stored in tiff.</p>");
138     mp_compress->setWhatsThis(txt);
139 
140     txt = i18n("<p>Generate thumbnail images</p>");
141     mp_generateThumbnails->setWhatsThis(txt);
142 
143     txt = i18n("<p>With this option you may limit the maximum dimensions (width and height) of your images. "
144                "Doing so will make the resulting export file smaller, but will of course also make the quality "
145                "worse if someone wants to see the exported images with larger dimensions.</p>");
146 
147     mp_enforeMaxSize->setWhatsThis(txt);
148     mp_maxSize->setWhatsThis(txt);
149 
150     txt = i18n("<p>When exporting images, bear in mind that there are two things the "
151                "person importing these images again will need:<br/>"
152                "1) meta information (image content, date etc.)<br/>"
153                "2) the images themselves.</p>"
154 
155                "<p>The images themselves can either be placed next to the .kim file, "
156                "or copied into the .kim file. Copying the images into the .kim file works well "
157                "for a recipient who wants all, or most of those images, for example "
158                "when emailing a whole group of images. However, when you place the "
159                "images on the Web, a lot of people will see them but most likely only "
160                "download a few of them. It works better in this kind of case, to "
161                "separate the images and the .kim file, by place them next to each "
162                "other, so the user can access the images s/he wants.</p>");
163 
164     grp->setWhatsThis(txt);
165     m_include->setWhatsThis(txt);
166     m_manually->setWhatsThis(txt);
167     m_link->setWhatsThis(txt);
168     m_symlink->setWhatsThis(txt);
169     m_auto->setWhatsThis(txt);
170 
171     QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
172     okButton->setDefault(true);
173     okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
174     connect(buttonBox, &QDialogButtonBox::accepted, this, &ExportConfig::accept);
175     connect(buttonBox, &QDialogButtonBox::rejected, this, &ExportConfig::reject);
176     mainLayout->addWidget(buttonBox);
177 
178     QPushButton *helpButton = buttonBox->button(QDialogButtonBox::Help);
179     connect(helpButton, &QPushButton::clicked, this, &ExportConfig::showHelp);
180 }
181 
imageFileLocation() const182 ImageFileLocation ExportConfig::imageFileLocation() const
183 {
184     if (m_include->isChecked())
185         return Inline;
186     else if (m_manually->isChecked())
187         return ManualCopy;
188     else if (m_link->isChecked())
189         return Link;
190     else if (m_symlink->isChecked())
191         return Symlink;
192     else
193         return AutoCopy;
194 }
195 
showHelp()196 void ExportConfig::showHelp()
197 {
198     KHelpClient::invokeHelp(QStringLiteral("chp-importExport"));
199 }
200 
~Export()201 Export::~Export()
202 {
203     delete m_zip;
204     delete m_eventLoop;
205 }
206 
Export(const DB::FileNameList & list,const QString & zipFile,bool compress,int maxSize,ImageFileLocation location,const QString & baseUrl,bool doGenerateThumbnails,bool * ok)207 Export::Export(
208     const DB::FileNameList &list,
209     const QString &zipFile,
210     bool compress,
211     int maxSize,
212     ImageFileLocation location,
213     const QString &baseUrl,
214     bool doGenerateThumbnails,
215     bool *ok)
216     : m_internalOk(true)
217     , m_ok(ok)
218     , m_maxSize(maxSize)
219     , m_location(location)
220     , m_eventLoop(new QEventLoop)
221 {
222     if (ok == nullptr)
223         ok = &m_internalOk;
224     *ok = true;
225     m_destdir = QFileInfo(zipFile).path();
226     m_zip = new KZip(zipFile);
227     m_zip->setCompression(compress ? KZip::DeflateCompression : KZip::NoCompression);
228     if (!m_zip->open(QIODevice::WriteOnly)) {
229         KMessageBox::error(nullptr, i18n("Error creating zip file"));
230         *ok = false;
231         return;
232     }
233 
234     // Create progress dialog
235     int total = 1;
236     if (location != ManualCopy)
237         total += list.size();
238     if (doGenerateThumbnails)
239         total += list.size();
240 
241     m_steps = 0;
242     m_progressDialog = new QProgressDialog(MainWindow::Window::theMainWindow());
243     m_progressDialog->setCancelButtonText(i18n("&Cancel"));
244     m_progressDialog->setMaximum(total);
245 
246     m_progressDialog->setValue(0);
247     m_progressDialog->show();
248 
249     // Copy image files and generate thumbnails
250     if (location != ManualCopy) {
251         m_copyingFiles = true;
252         copyImages(list);
253     }
254 
255     if (*m_ok && doGenerateThumbnails) {
256         m_copyingFiles = false;
257         generateThumbnails(list);
258     }
259 
260     if (*m_ok) {
261         // Create the index.xml file
262         m_progressDialog->setLabelText(i18n("Creating index file"));
263         QByteArray indexml = XMLHandler().createIndexXML(list, baseUrl, m_location, &m_filenameMapper);
264         m_zip->writeFile(QStringLiteral("index.xml"), indexml.data());
265 
266         m_steps++;
267         m_progressDialog->setValue(m_steps);
268         m_zip->close();
269     }
270 }
271 
generateThumbnails(const DB::FileNameList & list)272 void Export::generateThumbnails(const DB::FileNameList &list)
273 {
274     m_progressDialog->setLabelText(i18n("Creating thumbnails"));
275     m_loopEntered = false;
276     m_subdir = QLatin1String("Thumbnails/");
277     m_filesRemaining = list.size(); // Used to break the event loop.
278     for (const DB::FileName &fileName : list) {
279         const auto info = DB::ImageDB::instance()->info(fileName);
280         ImageManager::ImageRequest *request = new ImageManager::ImageRequest(fileName, QSize(128, 128), info->angle(), this);
281         request->setPriority(ImageManager::BatchTask);
282         ImageManager::AsyncLoader::instance()->load(request);
283     }
284     if (m_filesRemaining > 0) {
285         m_loopEntered = true;
286         m_eventLoop->exec();
287     }
288 }
289 
copyImages(const DB::FileNameList & list)290 void Export::copyImages(const DB::FileNameList &list)
291 {
292     Q_ASSERT(m_location != ManualCopy);
293 
294     m_loopEntered = false;
295     m_subdir = QLatin1String("Images/");
296 
297     m_progressDialog->setLabelText(i18n("Copying image files"));
298 
299     m_filesRemaining = 0;
300     for (const DB::FileName &fileName : list) {
301         QString file = fileName.absolute();
302         QString zippedName = m_filenameMapper.uniqNameFor(fileName);
303 
304         if (m_maxSize == -1 || Utilities::isVideo(fileName) || isRAW(fileName)) {
305             const QFileInfo fileInfo(file);
306             if (fileInfo.isSymLink()) {
307                 file = fileInfo.symLinkTarget();
308             }
309             if (m_location == Inline)
310                 m_zip->addLocalFile(file, QStringLiteral("Images/") + zippedName);
311             else if (m_location == AutoCopy)
312                 Utilities::copyOrOverwrite(file, m_destdir + QLatin1String("/") + zippedName);
313             else if (m_location == Link)
314                 Utilities::makeHardLink(file, m_destdir + QLatin1String("/") + zippedName);
315             else if (m_location == Symlink)
316                 Utilities::makeSymbolicLink(file, m_destdir + QLatin1String("/") + zippedName);
317 
318             m_steps++;
319             m_progressDialog->setValue(m_steps);
320         } else {
321             m_filesRemaining++;
322             ImageManager::ImageRequest *request = new ImageManager::ImageRequest(DB::FileName::fromAbsolutePath(file), QSize(m_maxSize, m_maxSize), 0, this);
323             request->setPriority(ImageManager::BatchTask);
324             ImageManager::AsyncLoader::instance()->load(request);
325         }
326 
327         // Test if the cancel button was pressed.
328         qApp->processEvents(QEventLoop::AllEvents);
329 
330         if (m_progressDialog->wasCanceled()) {
331             *m_ok = false;
332             return;
333         }
334     }
335     if (m_filesRemaining > 0) {
336         m_loopEntered = true;
337         m_eventLoop->exec();
338     }
339 }
340 
pixmapLoaded(ImageManager::ImageRequest * request,const QImage & image)341 void Export::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image)
342 {
343     const DB::FileName fileName = request->databaseFileName();
344     if (!request->loadedOK())
345         return;
346 
347     const QString ext = (Utilities::isVideo(fileName) || isRAW(fileName)) ? QStringLiteral("jpg") : QFileInfo(m_filenameMapper.uniqNameFor(fileName)).completeSuffix();
348 
349     // Add the file to the zip archive
350     QString zipFileName = QStringLiteral("%1/%2.%3").arg(Utilities::stripEndingForwardSlash(m_subdir)).arg(QFileInfo(m_filenameMapper.uniqNameFor(fileName)).baseName()).arg(ext);
351     QByteArray data;
352     QBuffer buffer(&data);
353     buffer.open(QIODevice::WriteOnly);
354     image.save(&buffer, QFileInfo(zipFileName).suffix().toLower().toLatin1().constData());
355 
356     if (m_location == Inline || !m_copyingFiles)
357         m_zip->writeFile(zipFileName, data.constData());
358     else {
359         QString file = m_destdir + QLatin1String("/") + m_filenameMapper.uniqNameFor(fileName);
360         QFile out(file);
361         if (!out.open(QIODevice::WriteOnly)) {
362             KMessageBox::error(nullptr, i18n("Error writing file %1", file));
363             *m_ok = false;
364         }
365         out.write(data.constData(), data.size());
366         out.close();
367     }
368 
369     qApp->processEvents(QEventLoop::AllEvents);
370 
371     bool canceled = (!*m_ok || m_progressDialog->wasCanceled());
372 
373     if (canceled) {
374         *m_ok = false;
375         m_eventLoop->exit();
376         ImageManager::AsyncLoader::instance()->stop(this);
377         return;
378     }
379 
380     m_steps++;
381     m_filesRemaining--;
382     m_progressDialog->setValue(m_steps);
383 
384     if (m_filesRemaining == 0 && m_loopEntered)
385         m_eventLoop->exit();
386 }
387 
showUsageDialog()388 void Export::showUsageDialog()
389 {
390     QString txt = i18n("<p>Other KPhotoAlbum users may now load the import file into their database, by choosing <b>import</b> in "
391                        "the file menu.</p>"
392                        "<p>If they find it on a web site, and the web server is correctly configured, all they need to do is simply "
393                        "to click it from within konqueror. To enable this, your web server needs to be configured for KPhotoAlbum. You do so by adding "
394                        "the following line to <b>/etc/httpd/mime.types</b> or similar:"
395                        "<pre>application/vnd.kde.kphotoalbum-import kim</pre>"
396                        "This will make your web server tell konqueror that it is a KPhotoAlbum file when clicking on the link, "
397                        "otherwise the web server will just tell konqueror that it is a plain text file.</p>");
398 
399     KMessageBox::information(nullptr, txt, i18n("How to Use the Export File"), QStringLiteral("export_how_to_use_the_export_file"));
400 }
401 
402 // vi:expandtab:tabstop=4 shiftwidth=4:
403