1 /* This file is part of the KDE project
2
3 SPDX-FileCopyrightText: 2001, 2003 Lukas Tinkl <lukas@kde.org>
4 SPDX-FileCopyrightText: Andreas Schlapbach <schlpbch@iam.unibe.ch>
5
6 SPDX-License-Identifier: LGPL-2.0-only
7 */
8
9 #include "imgalleryplugin.h"
10
11 #include <QTextStream>
12 #include <QFile>
13 #include <QDateTime>
14 #include <QPixmap>
15 #include <QImage>
16 #include <QTextCodec>
17 #include <QApplication>
18 #include <QDesktopServices>
19 #include <QImageReader>
20 #include <QMimeDatabase>
21 #include <QMimeType>
22 #include <QPushButton>
23 #include <QProgressDialog>
24
25 #include <klocalizedstring.h>
26 #include <kmessagebox.h>
27 #include <kpluginfactory.h>
28 #include <kactioncollection.h>
29
30 #include <kparts/part.h>
31
32 #include "imgallerydialog.h"
33 #include "imgallery_debug.h"
34
K_PLUGIN_FACTORY(KImGalleryPluginFactory,registerPlugin<KImGalleryPlugin> ();)35 K_PLUGIN_FACTORY(KImGalleryPluginFactory, registerPlugin<KImGalleryPlugin>();)
36
37 // Eliminate lots of deprecation warnings with Qt 5.15.
38 // Using a macro to redefine well known symbols is not good practice, but
39 // the alternative is lots of QT_VERSION_CHECK conditionals everywhere.
40 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
41 #define endl Qt::endl
42 #endif
43
44 static QString directory(const QUrl &url) {
45 return url.adjusted(QUrl::StripTrailingSlash).adjusted(QUrl::RemoveFilename).toLocalFile();
46 }
47
KImGalleryPlugin(QObject * parent,const QVariantList &)48 KImGalleryPlugin::KImGalleryPlugin(QObject *parent, const QVariantList &)
49 : KParts::Plugin(parent), m_commentMap(nullptr)
50 {
51 QAction *a = actionCollection()->addAction(QStringLiteral("create_img_gallery"));
52 a->setText(i18n("&Create Image Gallery..."));
53 a->setIcon(QIcon::fromTheme(QStringLiteral("imagegallery")));
54 actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::Key_I));
55 connect(a, &QAction::triggered, this, &KImGalleryPlugin::slotExecute);
56 }
57
slotExecute()58 void KImGalleryPlugin::slotExecute()
59 {
60 m_progressDlg = nullptr;
61 if (!parent()) {
62 KMessageBox::sorry(nullptr, i18n("Could not create the plugin, please report a bug."));
63 return;
64 }
65 m_part = qobject_cast<KParts::ReadOnlyPart *>(parent());
66
67 if (!m_part || !m_part->url().isLocalFile()) { //TODO support remote URLs too?
68 KMessageBox::sorry(m_part->widget(), i18n("Creating an image gallery works only on local folders."));
69 return;
70 }
71
72 QString path = m_part->url().adjusted(QUrl::StripTrailingSlash).toLocalFile() + '/';
73 m_configDlg = new KIGPDialog(m_part->widget(), path);
74
75 if (m_configDlg->exec() == QDialog::Accepted) {
76 qCDebug(IMAGEGALLERY_LOG) << "dialog is ok";
77 m_configDlg->writeConfig();
78 m_copyFiles = m_configDlg->copyOriginalFiles();
79 m_recurseSubDirectories = m_configDlg->recurseSubDirectories();
80 m_useCommentFile = m_configDlg->useCommentFile();
81 m_imagesPerRow = m_configDlg->getImagesPerRow();
82
83 QUrl url(m_configDlg->getImageUrl());
84 if (!url.isEmpty() && url.isValid()) {
85 m_progressDlg = new QProgressDialog(m_part->widget());
86 connect(m_progressDlg, &QProgressDialog::canceled, this, &KImGalleryPlugin::slotCancelled);
87
88 m_progressDlg->setLabelText(i18n("Creating thumbnails"));
89 QPushButton *button = new QPushButton(m_progressDlg);
90 KGuiItem::assign(button, KStandardGuiItem::cancel());
91 m_progressDlg->setCancelButton(button);
92 m_cancelled = false;
93 m_progressDlg->show();
94 if (createHtml(url, m_part->url().path(), m_configDlg->recursionLevel() > 0 ? m_configDlg->recursionLevel() + 1 : 0, m_configDlg->getImageFormat())) {
95 QDesktopServices::openUrl(url); // Open a browser to show the result
96 } else {
97 deleteCancelledGallery(url, m_part->url().path(), m_configDlg->recursionLevel() > 0 ? m_configDlg->recursionLevel() + 1 : 0, m_configDlg->getImageFormat());
98 }
99 }
100 }
101 delete m_progressDlg;
102 }
103
createDirectory(const QDir & dir,const QString & imgGalleryDir,const QString & dirName)104 bool KImGalleryPlugin::createDirectory(const QDir &dir, const QString &imgGalleryDir, const QString &dirName)
105 {
106 QDir thumb_dir(dir);
107
108 if (!thumb_dir.exists()) {
109 thumb_dir.setPath(imgGalleryDir);
110 if (!(thumb_dir.mkdir(dirName/*, false*/))) {
111 KMessageBox::sorry(m_part->widget(), i18n("Could not create folder: %1", thumb_dir.path()));
112 return false;
113 } else {
114 thumb_dir.setPath(imgGalleryDir + '/' + dirName + '/');
115 return true;
116 }
117 } else {
118 return true;
119 }
120 }
121
createHead(QTextStream & stream)122 void KImGalleryPlugin::createHead(QTextStream &stream)
123 {
124 const QString chsetName = QTextCodec::codecForLocale()->name();
125
126 stream << "<?xml version=\"1.0\" encoding=\"" + chsetName + "\" ?>" << endl;
127 stream << "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" << endl;
128 stream << "<html xmlns=\"http://www.w3.org/1999/xhtml\">" << endl;
129 stream << "<head>" << endl;
130 stream << "<title>" << m_configDlg->getTitle().toHtmlEscaped() << "</title>" << endl;
131 stream << "<meta http-equiv=\"content-type\" content=\"text/html; charset=" << chsetName << "\"/>" << endl;
132 stream << "<meta name=\"GENERATOR\" content=\"KDE Konqueror KImgallery plugin version " KDE_VERSION_STRING "\"/>" << endl;
133 createCSSSection(stream);
134 stream << "</head>" << endl;
135 }
136
createCSSSection(QTextStream & stream)137 void KImGalleryPlugin::createCSSSection(QTextStream &stream)
138 {
139 const QString backgroundColor = m_configDlg->getBackgroundColor().name();
140 const QString foregroundColor = m_configDlg->getForegroundColor().name();
141 //adding a touch of style
142 stream << "<style type='text/css'>\n";
143 stream << "BODY {color: " << foregroundColor << "; background: " << backgroundColor << ";" << endl;
144 stream << " font-family: " << m_configDlg->getFontName() << ", sans-serif;" << endl;
145 stream << " font-size: " << m_configDlg->getFontSize() << "pt; margin: 8%; }" << endl;
146 stream << "H1 {color: " << foregroundColor << ";}" << endl;
147 stream << "TABLE {text-align: center; margin-left: auto; margin-right: auto;}" << endl;
148 stream << "TD { color: " << foregroundColor << "; padding: 1em}" << endl;
149 stream << "IMG { border: 1px solid " << foregroundColor << "; }" << endl;
150 stream << "</style>" << endl;
151 }
152
extension(const QString & imageFormat)153 QString KImGalleryPlugin::extension(const QString &imageFormat)
154 {
155 if (imageFormat == QLatin1String("PNG")) {
156 return QStringLiteral(".png");
157 }
158 if (imageFormat == QLatin1String("JPEG")) {
159 return QStringLiteral(".jpg");
160 }
161 Q_ASSERT(false);
162 return QString();
163 }
164
createBody(QTextStream & stream,const QString & sourceDirName,const QStringList & subDirList,const QDir & imageDir,const QUrl & url,const QString & imageFormat)165 void KImGalleryPlugin::createBody(QTextStream &stream, const QString &sourceDirName, const QStringList &subDirList,
166 const QDir &imageDir, const QUrl &url, const QString &imageFormat)
167 {
168 int numOfImages = imageDir.count();
169 const QString imgGalleryDir = directory(url);
170 const QString today(QLocale().toString(QDate::currentDate()));
171
172 stream << "<body>\n<h1>" << m_configDlg->getTitle().toHtmlEscaped() << "</h1><p>" << endl;
173 stream << i18n("<i>Number of images</i>: %1", numOfImages) << "<br/>" << endl;
174 stream << i18n("<i>Created on</i>: %1", today) << "</p>" << endl;
175
176 stream << "<hr/>" << endl;
177
178 if (m_recurseSubDirectories && subDirList.count() > 2) { //subDirList.count() is always >= 2 because of the "." and ".." directories
179 stream << i18n("<i>Subfolders</i>:") << "<br>" << endl;
180 for (QStringList::ConstIterator it = subDirList.constBegin(); it != subDirList.constEnd(); it++) {
181 if (*it == QLatin1String(".") || *it == QLatin1String("..")) {
182 continue; //disregard the "." and ".." directories
183 }
184 stream << "<a href=\"" << *it << "/" << url.fileName()
185 << "\">" << *it << "</a><br>" << endl;
186 }
187 stream << "<hr/>" << endl;
188 }
189
190 stream << "<table>" << endl;
191
192 //table with images
193 int imgIndex;
194 QFileInfo imginfo;
195 QPixmap imgProp;
196 for (imgIndex = 0; !m_cancelled && (imgIndex < numOfImages);) {
197 stream << "<tr>" << endl;
198
199 for (int col = 0; !m_cancelled && (col < m_imagesPerRow) && (imgIndex < numOfImages); col++) {
200 const QString imgName = imageDir[imgIndex];
201
202 if (m_copyFiles) {
203 stream << "<td align='center'>\n<a href=\"images/" << imgName << "\">";
204 } else {
205 stream << "<td align='center'>\n<a href=\"" << imgName << "\">";
206 }
207
208 if (createThumb(imgName, sourceDirName, imgGalleryDir, imageFormat)) {
209 const QString imgPath("thumbs/" + imgName + extension(imageFormat));
210 stream << "<img src=\"" << imgPath << "\" width=\"" << m_imgWidth << "\" ";
211 stream << "height=\"" << m_imgHeight << "\" alt=\"" << imgPath << "\"/>";
212 m_progressDlg->setLabelText(i18n("Created thumbnail for: \n%1", imgName));
213 } else {
214 qCDebug(IMAGEGALLERY_LOG) << "Creating thumbnail for " << imgName << " failed";
215 m_progressDlg->setLabelText(i18n("Creating thumbnail for: \n%1\n failed", imgName));
216 }
217 stream << "</a>" << endl;
218
219 if (m_configDlg->printImageName()) {
220 stream << "<div>" << imgName << "</div>" << endl;
221 }
222
223 if (m_configDlg->printImageProperty()) {
224 imgProp.load(imageDir.absoluteFilePath(imgName));
225 stream << "<div>" << imgProp.width() << " x " << imgProp.height() << "</div>" << endl;
226 }
227
228 if (m_configDlg->printImageSize()) {
229 imginfo.setFile(imageDir, imgName);
230 stream << "<div>(" << (imginfo.size() / 1024) << " " << i18n("KiB") << ")" << "</div>" << endl;
231 }
232
233 if (m_useCommentFile) {
234 QString imgComment = (*m_commentMap)[imgName];
235 stream << "<div>" << imgComment.toHtmlEscaped() << "</div>" << endl;
236 }
237 stream << "</td>" << endl;
238
239 m_progressDlg->setMaximum(numOfImages);
240 m_progressDlg->setValue(imgIndex);
241 qApp->processEvents();
242 imgIndex++;
243 }
244 stream << "</tr>" << endl;
245 }
246 //close the HTML
247 stream << "</table>\n</body>\n</html>" << endl;
248 }
249
createHtml(const QUrl & url,const QString & sourceDirName,int recursionLevel,const QString & imageFormat)250 bool KImGalleryPlugin::createHtml(const QUrl &url, const QString &sourceDirName, int recursionLevel, const QString &imageFormat)
251 {
252 if (m_cancelled) {
253 return false;
254 }
255
256 if (!parent() || !parent()->inherits("DolphinPart")) {
257 return false;
258 }
259
260 QStringList subDirList;
261 if (m_recurseSubDirectories && (recursionLevel >= 0)) { //recursionLevel == 0 means endless
262 QDir toplevel_dir = QDir(sourceDirName);
263 toplevel_dir.setFilter(QDir::Dirs | QDir::Readable | QDir::Writable);
264 subDirList = toplevel_dir.entryList();
265
266 for (QStringList::ConstIterator it = subDirList.constBegin(); it != subDirList.constEnd() && !m_cancelled; it++) {
267 const QString currentDir = *it;
268 if (currentDir == QLatin1String(".") || currentDir == QLatin1String("..")) {
269 continue; //disregard the "." and ".." directories
270 }
271 QDir subDir = QDir(directory(url) + '/' + currentDir);
272 if (!subDir.exists()) {
273 subDir.setPath(directory(url));
274 if (!(subDir.mkdir(currentDir/*, false*/))) {
275 KMessageBox::sorry(m_part->widget(), i18n("Could not create folder: %1", subDir.path()));
276 continue;
277 } else {
278 subDir.setPath(directory(url) + '/' + currentDir);
279 }
280 }
281 if (!createHtml(QUrl::fromLocalFile(subDir.path() + '/' + url.fileName()), sourceDirName + '/' + currentDir,
282 recursionLevel > 1 ? recursionLevel - 1 : 0, imageFormat)) {
283 return false;
284 }
285 }
286 }
287
288 if (m_useCommentFile) {
289 loadCommentFile();
290 }
291
292 qCDebug(IMAGEGALLERY_LOG) << "sourceDirName: " << sourceDirName;
293
294 QMimeDatabase db;
295 QStringList imageNameFilters;
296 const QList<QByteArray> &mimeNames = QImageReader::supportedMimeTypes();
297 for (const QByteArray &mimeName : mimeNames)
298 {
299 const QMimeType mimeType = db.mimeTypeForName(mimeName);
300 imageNameFilters.append(mimeType.globPatterns());
301 }
302
303 QDir imageDir(sourceDirName, QString(),
304 QDir::Name | QDir::IgnoreCase, QDir::Files | QDir::Readable);
305 imageDir.setNameFilters(imageNameFilters);
306
307 const QString imgGalleryDir = directory(url);
308 qCDebug(IMAGEGALLERY_LOG) << "imgGalleryDir: " << imgGalleryDir;
309
310 // Create the "thumbs" subdirectory if necessary
311 QDir thumb_dir(imgGalleryDir + QLatin1String("/thumbs/"));
312 if (createDirectory(thumb_dir, imgGalleryDir, QStringLiteral("thumbs")) == false) {
313 return false;
314 }
315
316 // Create the "images" subdirectory if necessary
317 QDir images_dir(imgGalleryDir + QLatin1String("/images/"));
318 if (m_copyFiles) {
319 if (createDirectory(images_dir, imgGalleryDir, QStringLiteral("images")) == false) {
320 return false;
321 }
322 }
323
324 QFile file(url.path());
325 qCDebug(IMAGEGALLERY_LOG) << "url.path(): " << url.path() << ", thumb_dir: " << thumb_dir.path()
326 << ", imageDir: " << imageDir.path();
327
328 if (imageDir.exists() && file.open(QIODevice::WriteOnly)) {
329 QTextStream stream(&file);
330 stream.setCodec(QTextCodec::codecForLocale());
331
332 createHead(stream);
333 createBody(stream, sourceDirName, subDirList, imageDir, url, imageFormat); //ugly
334
335 file.close();
336
337 return !m_cancelled;
338
339 } else {
340 QString path = url.toLocalFile();
341 if (!path.endsWith("/")) {
342 path += '/';
343 }
344 KMessageBox::sorry(m_part->widget(), i18n("Could not open file: %1", path));
345 return false;
346 }
347 }
348
deleteCancelledGallery(const QUrl & url,const QString & sourceDirName,int recursionLevel,const QString & imageFormat)349 void KImGalleryPlugin::deleteCancelledGallery(const QUrl &url, const QString &sourceDirName, int recursionLevel, const QString &imageFormat)
350 {
351 if (m_recurseSubDirectories && (recursionLevel >= 0)) {
352 QStringList subDirList;
353 QDir toplevel_dir = QDir(sourceDirName);
354 toplevel_dir.setFilter(QDir::Dirs);
355 subDirList = toplevel_dir.entryList();
356
357 for (QStringList::ConstIterator it = subDirList.constBegin(); it != subDirList.constEnd(); it++) {
358 if (*it == QLatin1String(".") || *it == QLatin1String("..") || *it == QLatin1String("thumbs") || (m_copyFiles && *it == QLatin1String("images"))) {
359 continue; //disregard the "." and ".." directories
360 }
361 deleteCancelledGallery(QUrl::fromLocalFile(directory(url) + '/' + *it + '/' + url.fileName()),
362 sourceDirName + '/' + *it,
363 recursionLevel > 1 ? recursionLevel - 1 : 0, imageFormat);
364 }
365 }
366
367 const QString imgGalleryDir = directory(url);
368 QDir thumb_dir(imgGalleryDir + QLatin1String("/thumbs/"));
369 QDir images_dir(imgGalleryDir + QLatin1String("/images/"));
370 QDir imageDir(sourceDirName, QStringLiteral("*.png *.PNG *.gif *.GIF *.jpg *.JPG *.jpeg *.JPEG *.bmp *.BMP"),
371 QDir::Name | QDir::IgnoreCase, QDir::Files | QDir::Readable);
372 QFile file(url.path());
373
374 // Remove the image file ..
375 file.remove();
376 // ..all the thumbnails ..
377 for (uint i = 0; i < imageDir.count(); i++) {
378 const QString imgName = imageDir[i];
379 const QString imgNameFormat = imgName + extension(imageFormat);
380 bool isRemoved = thumb_dir.remove(imgNameFormat);
381 qCDebug(IMAGEGALLERY_LOG) << "removing: " << thumb_dir.path() << "/" << imgNameFormat << "; " << isRemoved;
382 }
383 // ..and the thumb directory
384 thumb_dir.rmdir(thumb_dir.path());
385
386 // ..and the images directory if images were to be copied
387 if (m_copyFiles) {
388 for (uint i = 0; i < imageDir.count(); i++) {
389 const QString imgName = imageDir[i];
390 bool isRemoved = images_dir.remove(imgName);
391 qCDebug(IMAGEGALLERY_LOG) << "removing: " << images_dir.path() << "/" << imgName << "; " << isRemoved;
392 }
393 images_dir.rmdir(images_dir.path());
394 }
395 }
396
loadCommentFile()397 void KImGalleryPlugin::loadCommentFile()
398 {
399 QFile file(m_configDlg->getCommentFile());
400 if (file.open(QIODevice::ReadOnly)) {
401 qCDebug(IMAGEGALLERY_LOG) << "File opened.";
402
403 QTextStream *m_textStream = new QTextStream(&file);
404 m_textStream->setCodec(QTextCodec::codecForLocale());
405
406 delete m_commentMap;
407 m_commentMap = new CommentMap;
408
409 QString picName, picComment, curLine, curLineStripped;
410 while (!m_textStream->atEnd()) {
411 curLine = m_textStream->readLine();
412 curLineStripped = curLine.trimmed();
413 // Lines starting with '#' are comment
414 if (!(curLineStripped.isEmpty()) && !curLineStripped.startsWith(QLatin1String("#"))) {
415 if (curLineStripped.endsWith(QLatin1String(":"))) {
416 picComment.clear();
417 picName = curLineStripped.left(curLineStripped.length() - 1);
418 qCDebug(IMAGEGALLERY_LOG) << "picName: " << picName;
419 } else {
420 do {
421 //qCDebug(IMAGEGALLERY_LOG) << "picComment";
422 picComment += curLine + '\n';
423 curLine = m_textStream->readLine();
424 } while (!m_textStream->atEnd() && !(curLine.trimmed().isEmpty()) &&
425 !curLine.trimmed().startsWith(QLatin1String("#")));
426 //qCDebug(IMAGEGALLERY_LOG) << "Pic comment: " << picComment;
427 m_commentMap->insert(picName, picComment);
428 }
429 }
430 }
431 CommentMap::ConstIterator it;
432 for (it = m_commentMap->constBegin(); it != m_commentMap->constEnd(); ++it) {
433 qCDebug(IMAGEGALLERY_LOG) << "picName: " << it.key() << ", picComment: " << it.value();
434 }
435 file.close();
436 qCDebug(IMAGEGALLERY_LOG) << "File closed.";
437 delete m_textStream;
438 } else {
439 KMessageBox::sorry(m_part->widget(), i18n("Could not open file: %1", m_configDlg->getCommentFile()));
440 m_useCommentFile = false;
441 }
442 }
443
createThumb(const QString & imgName,const QString & sourceDirName,const QString & imgGalleryDir,const QString & imageFormat)444 bool KImGalleryPlugin::createThumb(const QString &imgName, const QString &sourceDirName,
445 const QString &imgGalleryDir, const QString &imageFormat)
446 {
447 QImage img;
448 const QString pixPath = sourceDirName + QLatin1String("/") + imgName;
449
450 if (m_copyFiles) {
451 QFile::copy(pixPath, imgGalleryDir + QLatin1String("/images/") + imgName);
452 }
453
454 const QString imgNameFormat = imgName + extension(imageFormat);
455 const QString thumbDir = imgGalleryDir + QLatin1String("/thumbs/");
456 int extent = m_configDlg->getThumbnailSize();
457
458 // this code is stolen from kdebase/kioslave/thumbnail/imagecreator.cpp
459 // (c) 2000 gis and malte
460
461 m_imgWidth = 120; // Setting the size of the images is
462 m_imgHeight = 90; // required to generate faster 'loading' pages
463 if (img.load(pixPath)) {
464 int w = img.width(), h = img.height();
465 // scale to pixie size
466 // qCDebug(IMAGEGALLERY_LOG) << "w: " << w << " h: " << h;
467 // Resizing if to big
468 if (w > extent || h > extent) {
469 if (w > h) {
470 h = (int)((double)(h * extent) / w);
471 if (h == 0) {
472 h = 1;
473 }
474 w = extent;
475 Q_ASSERT(h <= extent);
476 } else {
477 w = (int)((double)(w * extent) / h);
478 if (w == 0) {
479 w = 1;
480 }
481 h = extent;
482 Q_ASSERT(w <= extent);
483 }
484 const QImage scaleImg(img.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
485 if (scaleImg.width() != w || scaleImg.height() != h) {
486 qCDebug(IMAGEGALLERY_LOG) << "Resizing failed. Aborting.";
487 return false;
488 }
489 img = scaleImg;
490 if (m_configDlg->colorDepthSet() == true) {
491 QImage::Format format;
492 switch (m_configDlg->getColorDepth()) {
493 case 1:
494 format = QImage::Format_Mono;
495 break;
496 case 8:
497 format = QImage::Format_Indexed8;
498 break;
499 case 16:
500 format = QImage::Format_RGB16;
501 break;
502 case 32:
503 default:
504 format = QImage::Format_RGB32;
505 break;
506 }
507
508 const QImage depthImg(img.convertToFormat(format));
509 img = depthImg;
510 }
511 }
512 qCDebug(IMAGEGALLERY_LOG) << "Saving thumbnail to: " << thumbDir + imgNameFormat;
513 if (!img.save(thumbDir + imgNameFormat, imageFormat.toLatin1())) {
514 qCDebug(IMAGEGALLERY_LOG) << "Saving failed. Aborting.";
515 return false;
516 }
517 m_imgWidth = w;
518 m_imgHeight = h;
519 return true;
520 }
521 return false;
522 }
523
slotCancelled()524 void KImGalleryPlugin::slotCancelled()
525 {
526 m_cancelled = true;
527 }
528
529 #include "imgalleryplugin.moc"
530