1 // vim: set tabstop=4 shiftwidth=4 expandtab:
2 /* Gwenview - A simple image viewer for KDE
3 Copyright 2000-2007 Aurélien Gâteau <agateau@kde.org>
4 This class is based on the ImagePreviewJob class from Konqueror.
5 */
6 /* This file is part of the KDE project
7 Copyright (C) 2000 David Faure <faure@kde.org>
8 2000 Carsten Pfeiffer <pfeiffer@kde.org>
9
10 This program is free software; you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation; either version 2 of the License, or
13 (at your option) any later version.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 */
24 #include "thumbnailprovider.h"
25
26 #include <sys/stat.h>
27 #include <sys/types.h>
28 #include <unistd.h>
29
30 // Qt
31 #include <QApplication>
32 #include <QCryptographicHash>
33 #include <QDir>
34 #include <QFile>
35 #include <QStandardPaths>
36 #include <QTemporaryFile>
37
38 // KF
39 #include <KIO/JobUiDelegate>
40 #include <KIO/PreviewJob>
41 #include <KJobWidgets>
42
43 // Local
44 #include "gwenview_lib_debug.h"
45 #include "mimetypeutils.h"
46 #include "thumbnailgenerator.h"
47 #include "thumbnailwriter.h"
48 #include "urlutils.h"
49
50 namespace Gwenview
51 {
52 #undef ENABLE_LOG
53 #undef LOG
54 //#define ENABLE_LOG
55 #ifdef ENABLE_LOG
56 #define LOG(x) qCDebug(GWENVIEW_LIB_LOG) << x
57 #else
58 #define LOG(x) ;
59 #endif
60
Q_GLOBAL_STATIC(ThumbnailWriter,sThumbnailWriter)61 Q_GLOBAL_STATIC(ThumbnailWriter, sThumbnailWriter)
62
63 static QString generateOriginalUri(const QUrl &url_)
64 {
65 QUrl url = url_;
66 return url.adjusted(QUrl::RemovePassword).url();
67 }
68
generateThumbnailPath(const QString & uri,ThumbnailGroup::Enum group)69 static QString generateThumbnailPath(const QString &uri, ThumbnailGroup::Enum group)
70 {
71 QString baseDir = ThumbnailProvider::thumbnailBaseDir(group);
72 QCryptographicHash md5(QCryptographicHash::Md5);
73 md5.addData(QFile::encodeName(uri));
74 return baseDir + QFile::encodeName(QString::fromLatin1(md5.result().toHex())) + QStringLiteral(".png");
75 }
76
77 //------------------------------------------------------------------------
78 //
79 // ThumbnailProvider static methods
80 //
81 //------------------------------------------------------------------------
82 static QString sThumbnailBaseDir;
thumbnailBaseDir()83 QString ThumbnailProvider::thumbnailBaseDir()
84 {
85 if (sThumbnailBaseDir.isEmpty()) {
86 const QByteArray customDir = qgetenv("GV_THUMBNAIL_DIR");
87 if (customDir.isEmpty()) {
88 sThumbnailBaseDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/thumbnails/");
89 } else {
90 sThumbnailBaseDir = QFile::decodeName(customDir) + QLatin1Char('/');
91 }
92 }
93 return sThumbnailBaseDir;
94 }
95
setThumbnailBaseDir(const QString & dir)96 void ThumbnailProvider::setThumbnailBaseDir(const QString &dir)
97 {
98 sThumbnailBaseDir = dir;
99 }
100
thumbnailBaseDir(ThumbnailGroup::Enum group)101 QString ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Enum group)
102 {
103 QString dir = thumbnailBaseDir();
104 switch (group) {
105 case ThumbnailGroup::Normal:
106 dir += QStringLiteral("normal/");
107 break;
108 case ThumbnailGroup::Large:
109 dir += QStringLiteral("large/");
110 break;
111 case ThumbnailGroup::Large2x:
112 default:
113 dir += QLatin1String("x-gwenview/"); // Should never be hit, but just in case
114 }
115 return dir;
116 }
117
deleteImageThumbnail(const QUrl & url)118 void ThumbnailProvider::deleteImageThumbnail(const QUrl &url)
119 {
120 QString uri = generateOriginalUri(url);
121 QFile::remove(generateThumbnailPath(uri, ThumbnailGroup::Normal));
122 QFile::remove(generateThumbnailPath(uri, ThumbnailGroup::Large));
123 }
124
moveThumbnailHelper(const QString & oldUri,const QString & newUri,ThumbnailGroup::Enum group)125 static void moveThumbnailHelper(const QString &oldUri, const QString &newUri, ThumbnailGroup::Enum group)
126 {
127 QString oldPath = generateThumbnailPath(oldUri, group);
128 QString newPath = generateThumbnailPath(newUri, group);
129 QImage thumb;
130 if (!thumb.load(oldPath)) {
131 return;
132 }
133 thumb.setText(QStringLiteral("Thumb::URI"), newUri);
134 thumb.save(newPath, "png");
135 QFile::remove(QFile::encodeName(oldPath));
136 }
137
moveThumbnail(const QUrl & oldUrl,const QUrl & newUrl)138 void ThumbnailProvider::moveThumbnail(const QUrl &oldUrl, const QUrl &newUrl)
139 {
140 QString oldUri = generateOriginalUri(oldUrl);
141 QString newUri = generateOriginalUri(newUrl);
142 moveThumbnailHelper(oldUri, newUri, ThumbnailGroup::Normal);
143 moveThumbnailHelper(oldUri, newUri, ThumbnailGroup::Large);
144 }
145
146 //------------------------------------------------------------------------
147 //
148 // ThumbnailProvider implementation
149 //
150 //------------------------------------------------------------------------
ThumbnailProvider()151 ThumbnailProvider::ThumbnailProvider()
152 : KIO::Job()
153 , mState(STATE_NEXTTHUMB)
154 , mOriginalTime(0)
155 {
156 LOG(this);
157
158 // Make sure we have a place to store our thumbnails
159 QString thumbnailDirNormal = ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Normal);
160 QString thumbnailDirLarge = ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Large);
161 QDir().mkpath(thumbnailDirNormal);
162 QDir().mkpath(thumbnailDirLarge);
163 QFile::setPermissions(thumbnailDirNormal, QFileDevice::WriteOwner | QFileDevice::ReadOwner | QFileDevice::ExeOwner);
164 QFile::setPermissions(thumbnailDirLarge, QFileDevice::WriteOwner | QFileDevice::ReadOwner | QFileDevice::ExeOwner);
165
166 // Look for images and store the items in our todo list
167 mCurrentItem = KFileItem();
168 mThumbnailGroup = ThumbnailGroup::Large;
169 createNewThumbnailGenerator();
170 }
171
~ThumbnailProvider()172 ThumbnailProvider::~ThumbnailProvider()
173 {
174 LOG(this);
175 disconnect(mThumbnailGenerator, nullptr, this, nullptr);
176 disconnect(mThumbnailGenerator, nullptr, sThumbnailWriter, nullptr);
177 abortSubjob();
178 mThumbnailGenerator->cancel();
179 if (mPreviousThumbnailGenerator) {
180 disconnect(mPreviousThumbnailGenerator, nullptr, sThumbnailWriter, nullptr);
181 }
182 sThumbnailWriter->requestInterruption();
183 sThumbnailWriter->wait();
184 }
185
stop()186 void ThumbnailProvider::stop()
187 {
188 // Clear mItems and create a new ThumbnailGenerator if mThumbnailGenerator is running,
189 // but also make sure that at most two ThumbnailGenerators are running.
190 // startCreatingThumbnail() will take care that these two threads won't work on the same item.
191 mItems.clear();
192 abortSubjob();
193 if (!mThumbnailGenerator->isStopped() && !mPreviousThumbnailGenerator) {
194 mPreviousThumbnailGenerator = mThumbnailGenerator;
195 mPreviousThumbnailGenerator->cancel();
196 disconnect(mPreviousThumbnailGenerator, nullptr, this, nullptr);
197 connect(mPreviousThumbnailGenerator, SIGNAL(finished()), mPreviousThumbnailGenerator, SLOT(deleteLater()));
198 createNewThumbnailGenerator();
199 mCurrentItem = KFileItem();
200 }
201 }
202
pendingItems() const203 const KFileItemList &ThumbnailProvider::pendingItems() const
204 {
205 return mItems;
206 }
207
setThumbnailGroup(ThumbnailGroup::Enum group)208 void ThumbnailProvider::setThumbnailGroup(ThumbnailGroup::Enum group)
209 {
210 mThumbnailGroup = group;
211 }
212
appendItems(const KFileItemList & items)213 void ThumbnailProvider::appendItems(const KFileItemList &items)
214 {
215 if (!mItems.isEmpty()) {
216 QSet<KFileItem> itemSet{mItems.begin(), mItems.end()};
217
218 for (const KFileItem &item : items) {
219 if (!itemSet.contains(item)) {
220 mItems.append(item);
221 }
222 }
223 } else {
224 mItems = items;
225 }
226
227 if (mCurrentItem.isNull()) {
228 determineNextIcon();
229 }
230 }
231
removeItems(const KFileItemList & itemList)232 void ThumbnailProvider::removeItems(const KFileItemList &itemList)
233 {
234 if (mItems.isEmpty()) {
235 return;
236 }
237 for (const KFileItem &item : itemList) {
238 // If we are removing the next item, update to be the item after or the
239 // first if we removed the last item
240 mItems.removeAll(item);
241
242 if (item == mCurrentItem) {
243 abortSubjob();
244 }
245 }
246
247 // No more current item, carry on to the next remaining item
248 if (mCurrentItem.isNull()) {
249 determineNextIcon();
250 }
251 }
252
removePendingItems()253 void ThumbnailProvider::removePendingItems()
254 {
255 mItems.clear();
256 }
257
isRunning() const258 bool ThumbnailProvider::isRunning() const
259 {
260 return !mCurrentItem.isNull();
261 }
262
263 //-Internal--------------------------------------------------------------
createNewThumbnailGenerator()264 void ThumbnailProvider::createNewThumbnailGenerator()
265 {
266 mThumbnailGenerator = new ThumbnailGenerator;
267 connect(mThumbnailGenerator, SIGNAL(done(QImage, QSize)), SLOT(thumbnailReady(QImage, QSize)), Qt::QueuedConnection);
268
269 connect(mThumbnailGenerator,
270 SIGNAL(thumbnailReadyToBeCached(QString, QImage)),
271 sThumbnailWriter,
272 SLOT(queueThumbnail(QString, QImage)),
273 Qt::QueuedConnection);
274 }
275
abortSubjob()276 void ThumbnailProvider::abortSubjob()
277 {
278 if (hasSubjobs()) {
279 LOG("Killing subjob");
280 KJob *job = subjobs().first();
281 job->kill();
282 removeSubjob(job);
283 mCurrentItem = KFileItem();
284 }
285 }
286
determineNextIcon()287 void ThumbnailProvider::determineNextIcon()
288 {
289 LOG(this);
290 mState = STATE_NEXTTHUMB;
291
292 // No more items ?
293 if (mItems.isEmpty()) {
294 LOG("No more items. Nothing to do");
295 mCurrentItem = KFileItem();
296 Q_EMIT finished();
297 return;
298 }
299
300 mCurrentItem = mItems.takeFirst();
301 LOG("mCurrentItem.url=" << mCurrentItem.url());
302
303 // First, stat the orig file
304 mState = STATE_STATORIG;
305 mCurrentUrl = mCurrentItem.url().adjusted(QUrl::NormalizePathSegments);
306 mOriginalFileSize = mCurrentItem.size();
307
308 // Do direct stat instead of using KIO if the file is local (faster)
309 if (UrlUtils::urlIsFastLocalFile(mCurrentUrl)) {
310 QFileInfo fileInfo(mCurrentUrl.toLocalFile());
311 mOriginalTime = fileInfo.lastModified().toSecsSinceEpoch();
312 QMetaObject::invokeMethod(this, &ThumbnailProvider::checkThumbnail, Qt::QueuedConnection);
313 } else {
314 KIO::Job *job = KIO::stat(mCurrentUrl, KIO::HideProgressInfo);
315 KJobWidgets::setWindow(job, qApp->activeWindow());
316 LOG("KIO::stat orig" << mCurrentUrl.url());
317 addSubjob(job);
318 }
319 LOG("/determineNextIcon" << this);
320 }
321
slotResult(KJob * job)322 void ThumbnailProvider::slotResult(KJob *job)
323 {
324 LOG(mState);
325 removeSubjob(job);
326 Q_ASSERT(subjobs().isEmpty()); // We should have only one job at a time
327
328 switch (mState) {
329 case STATE_NEXTTHUMB:
330 Q_ASSERT(false);
331 determineNextIcon();
332 return;
333
334 case STATE_STATORIG: {
335 // Could not stat original, drop this one and move on to the next one
336 if (job->error()) {
337 emitThumbnailLoadingFailed();
338 determineNextIcon();
339 return;
340 }
341
342 // Get modification time of the original file
343 KIO::UDSEntry entry = static_cast<KIO::StatJob *>(job)->statResult();
344 mOriginalTime = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
345 checkThumbnail();
346 return;
347 }
348
349 case STATE_DOWNLOADORIG:
350 if (job->error()) {
351 emitThumbnailLoadingFailed();
352 LOG("Delete temp file" << mTempPath);
353 QFile::remove(mTempPath);
354 mTempPath.clear();
355 determineNextIcon();
356 } else {
357 startCreatingThumbnail(mTempPath);
358 }
359 return;
360
361 case STATE_PREVIEWJOB:
362 determineNextIcon();
363 return;
364 }
365 }
366
thumbnailReady(const QImage & _img,const QSize & _size)367 void ThumbnailProvider::thumbnailReady(const QImage &_img, const QSize &_size)
368 {
369 QImage img = _img;
370 QSize size = _size;
371 if (!img.isNull()) {
372 emitThumbnailLoaded(img, size);
373 } else {
374 emitThumbnailLoadingFailed();
375 }
376 if (!mTempPath.isEmpty()) {
377 LOG("Delete temp file" << mTempPath);
378 QFile::remove(mTempPath);
379 mTempPath.clear();
380 }
381 determineNextIcon();
382 }
383
loadThumbnailFromCache() const384 QImage ThumbnailProvider::loadThumbnailFromCache() const
385 {
386 if (mThumbnailGroup > ThumbnailGroup::Large) {
387 return QImage();
388 }
389
390 QImage image = sThumbnailWriter->value(mThumbnailPath);
391 if (!image.isNull()) {
392 return image;
393 }
394
395 image = QImage(mThumbnailPath);
396 if (image.isNull() && mThumbnailGroup == ThumbnailGroup::Normal) {
397 // If there is a large-sized thumbnail, generate the normal-sized version from it
398 QString largeThumbnailPath = generateThumbnailPath(mOriginalUri, ThumbnailGroup::Large);
399 QImage largeImage(largeThumbnailPath);
400 if (largeImage.isNull()) {
401 return image;
402 }
403 int size = ThumbnailGroup::pixelSize(ThumbnailGroup::Normal);
404 image = largeImage.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
405 const QStringList textKeys = largeImage.textKeys();
406 for (const QString &key : textKeys) {
407 QString text = largeImage.text(key);
408 image.setText(key, text);
409 }
410 sThumbnailWriter->queueThumbnail(mThumbnailPath, image);
411 }
412
413 return image;
414 }
415
checkThumbnail()416 void ThumbnailProvider::checkThumbnail()
417 {
418 if (mCurrentItem.isNull()) {
419 // This can happen if current item has been removed by removeItems()
420 determineNextIcon();
421 return;
422 }
423
424 // If we are in the thumbnail dir, just load the file
425 if (mCurrentUrl.isLocalFile() && mCurrentUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().startsWith(thumbnailBaseDir())) {
426 QImage image(mCurrentUrl.toLocalFile());
427 emitThumbnailLoaded(image, image.size());
428 determineNextIcon();
429 return;
430 }
431
432 mOriginalUri = generateOriginalUri(mCurrentUrl);
433 mThumbnailPath = generateThumbnailPath(mOriginalUri, mThumbnailGroup);
434
435 LOG("Stat thumb" << mThumbnailPath);
436
437 QImage thumb = loadThumbnailFromCache();
438 KIO::filesize_t fileSize = thumb.text(QStringLiteral("Thumb::Size")).toULongLong();
439 if (!thumb.isNull()) {
440 if (thumb.text(QStringLiteral("Thumb::URI")) == mOriginalUri && thumb.text(QStringLiteral("Thumb::MTime")).toInt() == mOriginalTime
441 && (fileSize == 0 || fileSize == mOriginalFileSize)) {
442 int width = 0, height = 0;
443 QSize size;
444 bool ok;
445
446 width = thumb.text(QStringLiteral("Thumb::Image::Width")).toInt(&ok);
447 if (ok)
448 height = thumb.text(QStringLiteral("Thumb::Image::Height")).toInt(&ok);
449 if (ok) {
450 size = QSize(width, height);
451 } else {
452 LOG("Thumbnail for" << mOriginalUri << "does not contain correct image size information");
453 // Don't try to determine the size of a video, it probably won't work and
454 // will cause high I/O usage with big files (bug #307007).
455 if (MimeTypeUtils::urlKind(mCurrentUrl) == MimeTypeUtils::KIND_VIDEO) {
456 emitThumbnailLoaded(thumb, QSize());
457 determineNextIcon();
458 return;
459 }
460 }
461 emitThumbnailLoaded(thumb, size);
462 determineNextIcon();
463 return;
464 }
465 }
466
467 // Thumbnail not found or not valid
468 if (MimeTypeUtils::fileItemKind(mCurrentItem) == MimeTypeUtils::KIND_RASTER_IMAGE) {
469 if (mCurrentUrl.isLocalFile()) {
470 // Original is a local file, create the thumbnail
471 startCreatingThumbnail(mCurrentUrl.toLocalFile());
472 } else {
473 // Original is remote, download it
474 mState = STATE_DOWNLOADORIG;
475
476 QTemporaryFile tempFile;
477 tempFile.setAutoRemove(false);
478 if (!tempFile.open()) {
479 qCWarning(GWENVIEW_LIB_LOG) << "Couldn't create temp file to download " << mCurrentUrl.toDisplayString();
480 emitThumbnailLoadingFailed();
481 determineNextIcon();
482 return;
483 }
484 mTempPath = tempFile.fileName();
485
486 QUrl url = QUrl::fromLocalFile(mTempPath);
487 KIO::Job *job = KIO::file_copy(mCurrentUrl, url, -1, KIO::Overwrite | KIO::HideProgressInfo);
488 KJobWidgets::setWindow(job, qApp->activeWindow());
489 LOG("Download remote file" << mCurrentUrl.toDisplayString() << "to" << url.toDisplayString());
490 addSubjob(job);
491 }
492 } else {
493 // Not a raster image, use a KPreviewJob
494 LOG("Starting a KPreviewJob for" << mCurrentItem.url());
495 mState = STATE_PREVIEWJOB;
496 KFileItemList list;
497 list.append(mCurrentItem);
498 const int pixelSize = ThumbnailGroup::pixelSize(mThumbnailGroup);
499 if (mPreviewPlugins.isEmpty()) {
500 mPreviewPlugins = KIO::PreviewJob::availablePlugins();
501 }
502 KIO::Job *job = KIO::filePreview(list, QSize(pixelSize, pixelSize), &mPreviewPlugins);
503 // KJobWidgets::setWindow(job, qApp->activeWindow());
504 connect(job, SIGNAL(gotPreview(KFileItem, QPixmap)), this, SLOT(slotGotPreview(KFileItem, QPixmap)));
505 connect(job, SIGNAL(failed(KFileItem)), this, SLOT(emitThumbnailLoadingFailed()));
506 addSubjob(job);
507 }
508 }
509
startCreatingThumbnail(const QString & pixPath)510 void ThumbnailProvider::startCreatingThumbnail(const QString &pixPath)
511 {
512 LOG("Creating thumbnail from" << pixPath);
513 // If mPreviousThumbnailGenerator is already working on our current item
514 // its thumbnail will be passed to sThumbnailWriter when ready. So we
515 // connect mPreviousThumbnailGenerator's signal "finished" to determineNextIcon
516 // which will load the thumbnail from sThumbnailWriter or from disk
517 // (because we re-add mCurrentItem to mItems).
518 if (mPreviousThumbnailGenerator && !mPreviousThumbnailGenerator->isStopped() && mOriginalUri == mPreviousThumbnailGenerator->originalUri()
519 && mOriginalTime == mPreviousThumbnailGenerator->originalTime() && mOriginalFileSize == mPreviousThumbnailGenerator->originalFileSize()
520 && mCurrentItem.mimetype() == mPreviousThumbnailGenerator->originalMimeType()) {
521 connect(mPreviousThumbnailGenerator, SIGNAL(finished()), SLOT(determineNextIcon()));
522 mItems.prepend(mCurrentItem);
523 return;
524 }
525 mThumbnailGenerator->load(mOriginalUri, mOriginalTime, mOriginalFileSize, mCurrentItem.mimetype(), pixPath, mThumbnailPath, mThumbnailGroup);
526 }
527
slotGotPreview(const KFileItem & item,const QPixmap & pixmap)528 void ThumbnailProvider::slotGotPreview(const KFileItem &item, const QPixmap &pixmap)
529 {
530 if (mCurrentItem.isNull()) {
531 // This can happen if current item has been removed by removeItems()
532 return;
533 }
534 LOG(mCurrentItem.url());
535 QSize size;
536 Q_EMIT thumbnailLoaded(item, pixmap, size, mOriginalFileSize);
537 }
538
emitThumbnailLoaded(const QImage & img,const QSize & size)539 void ThumbnailProvider::emitThumbnailLoaded(const QImage &img, const QSize &size)
540 {
541 if (mCurrentItem.isNull()) {
542 // This can happen if current item has been removed by removeItems()
543 return;
544 }
545 LOG(mCurrentItem.url());
546 QPixmap thumb = QPixmap::fromImage(img);
547 Q_EMIT thumbnailLoaded(mCurrentItem, thumb, size, mOriginalFileSize);
548 }
549
emitThumbnailLoadingFailed()550 void ThumbnailProvider::emitThumbnailLoadingFailed()
551 {
552 if (mCurrentItem.isNull()) {
553 // This can happen if current item has been removed by removeItems()
554 return;
555 }
556 LOG(mCurrentItem.url());
557 Q_EMIT thumbnailLoadingFailed(mCurrentItem);
558 }
559
isThumbnailWriterEmpty()560 bool ThumbnailProvider::isThumbnailWriterEmpty()
561 {
562 return sThumbnailWriter->isEmpty();
563 }
564
565 } // namespace
566