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