File indexing completed on 2024-05-12 04:19:49

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*  Gwenview - A simple image viewer for KDE
0003     Copyright 2000-2007 Aurélien Gâteau <agateau@kde.org>
0004     This class is based on the ImagePreviewJob class from Konqueror.
0005 */
0006 /*  This file is part of the KDE project
0007     Copyright (C) 2000 David Faure <faure@kde.org>
0008                   2000 Carsten Pfeiffer <pfeiffer@kde.org>
0009 
0010     This program is free software; you can redistribute it and/or modify
0011     it under the terms of the GNU General Public License as published by
0012     the Free Software Foundation; either version 2 of the License, or
0013     (at your option) any later version.
0014 
0015     This program is distributed in the hope that it will be useful,
0016     but WITHOUT ANY WARRANTY; without even the implied warranty of
0017     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0018     GNU General Public License for more details.
0019 
0020     You should have received a copy of the GNU General Public License
0021     along with this program; if not, write to the Free Software
0022     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
0023 */
0024 #include "thumbnailprovider.h"
0025 
0026 #include <sys/stat.h>
0027 #include <sys/types.h>
0028 #include <unistd.h>
0029 
0030 // Qt
0031 #include <QApplication>
0032 #include <QCryptographicHash>
0033 #include <QDir>
0034 #include <QFile>
0035 #include <QStandardPaths>
0036 #include <QTemporaryFile>
0037 
0038 // KF
0039 #include <KIO/FileCopyJob>
0040 #include <KIO/PreviewJob>
0041 #include <KIO/StatJob>
0042 #include <KJobWidgets>
0043 
0044 // Local
0045 #include "gwenview_lib_debug.h"
0046 #include "mimetypeutils.h"
0047 #include "thumbnailgenerator.h"
0048 #include "thumbnailwriter.h"
0049 #include "urlutils.h"
0050 
0051 namespace Gwenview
0052 {
0053 #undef ENABLE_LOG
0054 #undef LOG
0055 // #define ENABLE_LOG
0056 #ifdef ENABLE_LOG
0057 #define LOG(x) qCDebug(GWENVIEW_LIB_LOG) << x
0058 #else
0059 #define LOG(x) ;
0060 #endif
0061 
0062 Q_GLOBAL_STATIC(ThumbnailWriter, sThumbnailWriter)
0063 
0064 static const ThumbnailGroup::Enum s_thumbnailGroups[] = {
0065     ThumbnailGroup::Normal,
0066     ThumbnailGroup::Large,
0067     ThumbnailGroup::XLarge,
0068     ThumbnailGroup::XXLarge,
0069 };
0070 
0071 static QString generateOriginalUri(const QUrl &url_)
0072 {
0073     QUrl url = url_;
0074     return url.adjusted(QUrl::RemovePassword).url();
0075 }
0076 
0077 static QString generateThumbnailPath(const QString &uri, ThumbnailGroup::Enum group)
0078 {
0079     QString baseDir = ThumbnailProvider::thumbnailBaseDir(group);
0080     QCryptographicHash md5(QCryptographicHash::Md5);
0081     md5.addData(QFile::encodeName(uri));
0082     return baseDir + QFile::encodeName(QString::fromLatin1(md5.result().toHex())) + QStringLiteral(".png");
0083 }
0084 
0085 //------------------------------------------------------------------------
0086 //
0087 // ThumbnailProvider static methods
0088 //
0089 //------------------------------------------------------------------------
0090 static QString sThumbnailBaseDir;
0091 QString ThumbnailProvider::thumbnailBaseDir()
0092 {
0093     if (sThumbnailBaseDir.isEmpty()) {
0094         const QByteArray customDir = qgetenv("GV_THUMBNAIL_DIR");
0095         if (customDir.isEmpty()) {
0096             sThumbnailBaseDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/thumbnails/");
0097         } else {
0098             sThumbnailBaseDir = QFile::decodeName(customDir) + QLatin1Char('/');
0099         }
0100     }
0101     return sThumbnailBaseDir;
0102 }
0103 
0104 void ThumbnailProvider::setThumbnailBaseDir(const QString &dir)
0105 {
0106     sThumbnailBaseDir = dir;
0107 }
0108 
0109 QString ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Enum group)
0110 {
0111     QString dir = thumbnailBaseDir();
0112     switch (group) {
0113     case ThumbnailGroup::Normal:
0114         dir += QStringLiteral("normal/");
0115         break;
0116     case ThumbnailGroup::Large:
0117         dir += QStringLiteral("large/");
0118         break;
0119     case ThumbnailGroup::XLarge:
0120         dir += QStringLiteral("x-large/");
0121         break;
0122     case ThumbnailGroup::XXLarge:
0123         dir += QStringLiteral("xx-large/");
0124         break;
0125     default:
0126         dir += QLatin1String("x-gwenview/"); // Should never be hit, but just in case
0127     }
0128     return dir;
0129 }
0130 
0131 void ThumbnailProvider::deleteImageThumbnail(const QUrl &url)
0132 {
0133     QString uri = generateOriginalUri(url);
0134     for (auto group : s_thumbnailGroups) {
0135         QFile::remove(generateThumbnailPath(uri, group));
0136     }
0137 }
0138 
0139 static void moveThumbnailHelper(const QString &oldUri, const QString &newUri, ThumbnailGroup::Enum group)
0140 {
0141     QString oldPath = generateThumbnailPath(oldUri, group);
0142     QString newPath = generateThumbnailPath(newUri, group);
0143     QImage thumb;
0144     if (!thumb.load(oldPath)) {
0145         return;
0146     }
0147     thumb.setText(QStringLiteral("Thumb::URI"), newUri);
0148     thumb.save(newPath, "png");
0149     QFile::remove(QFile::encodeName(oldPath));
0150 }
0151 
0152 void ThumbnailProvider::moveThumbnail(const QUrl &oldUrl, const QUrl &newUrl)
0153 {
0154     QString oldUri = generateOriginalUri(oldUrl);
0155     QString newUri = generateOriginalUri(newUrl);
0156     for (auto group : s_thumbnailGroups) {
0157         moveThumbnailHelper(oldUri, newUri, group);
0158     }
0159 }
0160 
0161 //------------------------------------------------------------------------
0162 //
0163 // ThumbnailProvider implementation
0164 //
0165 //------------------------------------------------------------------------
0166 ThumbnailProvider::ThumbnailProvider()
0167     : KIO::Job()
0168     , mState(STATE_NEXTTHUMB)
0169     , mOriginalTime(0)
0170 {
0171     LOG(this);
0172 
0173     // Make sure we have a place to store our thumbnails
0174     for (auto group : s_thumbnailGroups) {
0175         const QString thumbnailDir = ThumbnailProvider::thumbnailBaseDir(group);
0176         QDir().mkpath(thumbnailDir);
0177         QFile::setPermissions(thumbnailDir, QFileDevice::WriteOwner | QFileDevice::ReadOwner | QFileDevice::ExeOwner);
0178     }
0179 
0180     // Look for images and store the items in our todo list
0181     mCurrentItem = KFileItem();
0182     mThumbnailGroup = ThumbnailGroup::XXLarge;
0183     createNewThumbnailGenerator();
0184 }
0185 
0186 ThumbnailProvider::~ThumbnailProvider()
0187 {
0188     LOG(this);
0189     disconnect(mThumbnailGenerator, nullptr, this, nullptr);
0190     disconnect(mThumbnailGenerator, nullptr, sThumbnailWriter, nullptr);
0191     abortSubjob();
0192     mThumbnailGenerator->cancel();
0193     if (mPreviousThumbnailGenerator) {
0194         disconnect(mPreviousThumbnailGenerator, nullptr, sThumbnailWriter, nullptr);
0195     }
0196     sThumbnailWriter->requestInterruption();
0197     sThumbnailWriter->wait();
0198 }
0199 
0200 void ThumbnailProvider::stop()
0201 {
0202     // Clear mItems and create a new ThumbnailGenerator if mThumbnailGenerator is running,
0203     // but also make sure that at most two ThumbnailGenerators are running.
0204     // startCreatingThumbnail() will take care that these two threads won't work on the same item.
0205     mItems.clear();
0206     abortSubjob();
0207     if (!mThumbnailGenerator->isStopped() && !mPreviousThumbnailGenerator) {
0208         mPreviousThumbnailGenerator = mThumbnailGenerator;
0209         mPreviousThumbnailGenerator->cancel();
0210         disconnect(mPreviousThumbnailGenerator, nullptr, this, nullptr);
0211         connect(mPreviousThumbnailGenerator, SIGNAL(finished()), mPreviousThumbnailGenerator, SLOT(deleteLater()));
0212         createNewThumbnailGenerator();
0213         mCurrentItem = KFileItem();
0214     }
0215 }
0216 
0217 const KFileItemList &ThumbnailProvider::pendingItems() const
0218 {
0219     return mItems;
0220 }
0221 
0222 void ThumbnailProvider::setThumbnailGroup(ThumbnailGroup::Enum group)
0223 {
0224     mThumbnailGroup = group;
0225 }
0226 
0227 void ThumbnailProvider::appendItems(const KFileItemList &items)
0228 {
0229     if (!mItems.isEmpty()) {
0230         QSet<KFileItem> itemSet{mItems.begin(), mItems.end()};
0231 
0232         for (const KFileItem &item : items) {
0233             if (!itemSet.contains(item)) {
0234                 mItems.append(item);
0235             }
0236         }
0237     } else {
0238         mItems = items;
0239     }
0240 
0241     if (mCurrentItem.isNull()) {
0242         determineNextIcon();
0243     }
0244 }
0245 
0246 void ThumbnailProvider::removeItems(const KFileItemList &itemList)
0247 {
0248     if (mItems.isEmpty()) {
0249         return;
0250     }
0251     for (const KFileItem &item : itemList) {
0252         // If we are removing the next item, update to be the item after or the
0253         // first if we removed the last item
0254         mItems.removeAll(item);
0255 
0256         if (item == mCurrentItem) {
0257             abortSubjob();
0258         }
0259     }
0260 
0261     // No more current item, carry on to the next remaining item
0262     if (mCurrentItem.isNull()) {
0263         determineNextIcon();
0264     }
0265 }
0266 
0267 void ThumbnailProvider::removePendingItems()
0268 {
0269     mItems.clear();
0270 }
0271 
0272 bool ThumbnailProvider::isRunning() const
0273 {
0274     return !mCurrentItem.isNull();
0275 }
0276 
0277 //-Internal--------------------------------------------------------------
0278 void ThumbnailProvider::createNewThumbnailGenerator()
0279 {
0280     mThumbnailGenerator = new ThumbnailGenerator;
0281     connect(mThumbnailGenerator, SIGNAL(done(QImage, QSize)), SLOT(thumbnailReady(QImage, QSize)), Qt::QueuedConnection);
0282 
0283     connect(mThumbnailGenerator,
0284             SIGNAL(thumbnailReadyToBeCached(QString, QImage)),
0285             sThumbnailWriter,
0286             SLOT(queueThumbnail(QString, QImage)),
0287             Qt::QueuedConnection);
0288 }
0289 
0290 void ThumbnailProvider::abortSubjob()
0291 {
0292     if (hasSubjobs()) {
0293         LOG("Killing subjob");
0294         KJob *job = subjobs().first();
0295         job->kill();
0296         removeSubjob(job);
0297         mCurrentItem = KFileItem();
0298     }
0299 }
0300 
0301 void ThumbnailProvider::determineNextIcon()
0302 {
0303     LOG(this);
0304     mState = STATE_NEXTTHUMB;
0305 
0306     // No more items ?
0307     if (mItems.isEmpty()) {
0308         LOG("No more items. Nothing to do");
0309         mCurrentItem = KFileItem();
0310         Q_EMIT finished();
0311         return;
0312     }
0313 
0314     mCurrentItem = mItems.takeFirst();
0315     LOG("mCurrentItem.url=" << mCurrentItem.url());
0316 
0317     // First, stat the orig file
0318     mState = STATE_STATORIG;
0319     mCurrentUrl = mCurrentItem.url().adjusted(QUrl::NormalizePathSegments);
0320     mOriginalFileSize = mCurrentItem.size();
0321 
0322     // Do direct stat instead of using KIO if the file is local (faster)
0323     if (UrlUtils::urlIsFastLocalFile(mCurrentUrl)) {
0324         QFileInfo fileInfo(mCurrentUrl.toLocalFile());
0325         mOriginalTime = fileInfo.lastModified().toSecsSinceEpoch();
0326         QMetaObject::invokeMethod(this, &ThumbnailProvider::checkThumbnail, Qt::QueuedConnection);
0327     } else {
0328         KIO::Job *job = KIO::stat(mCurrentUrl, KIO::HideProgressInfo);
0329         KJobWidgets::setWindow(job, qApp->activeWindow());
0330         LOG("KIO::stat orig" << mCurrentUrl.url());
0331         addSubjob(job);
0332     }
0333     LOG("/determineNextIcon" << this);
0334 }
0335 
0336 void ThumbnailProvider::slotResult(KJob *job)
0337 {
0338     LOG(mState);
0339     removeSubjob(job);
0340     Q_ASSERT(subjobs().isEmpty()); // We should have only one job at a time
0341 
0342     switch (mState) {
0343     case STATE_NEXTTHUMB:
0344         Q_ASSERT(false);
0345         determineNextIcon();
0346         return;
0347 
0348     case STATE_STATORIG: {
0349         // Could not stat original, drop this one and move on to the next one
0350         if (job->error()) {
0351             emitThumbnailLoadingFailed();
0352             determineNextIcon();
0353             return;
0354         }
0355 
0356         // Get modification time of the original file
0357         KIO::UDSEntry entry = static_cast<KIO::StatJob *>(job)->statResult();
0358         mOriginalTime = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
0359         checkThumbnail();
0360         return;
0361     }
0362 
0363     case STATE_DOWNLOADORIG:
0364         if (job->error()) {
0365             emitThumbnailLoadingFailed();
0366             LOG("Delete temp file" << mTempPath);
0367             QFile::remove(mTempPath);
0368             mTempPath.clear();
0369             determineNextIcon();
0370         } else {
0371             startCreatingThumbnail(mTempPath);
0372         }
0373         return;
0374 
0375     case STATE_PREVIEWJOB:
0376         determineNextIcon();
0377         return;
0378     }
0379 }
0380 
0381 void ThumbnailProvider::thumbnailReady(const QImage &_img, const QSize &_size)
0382 {
0383     QImage img = _img;
0384     QSize size = _size;
0385     if (!img.isNull()) {
0386         emitThumbnailLoaded(img, size);
0387     } else {
0388         emitThumbnailLoadingFailed();
0389     }
0390     if (!mTempPath.isEmpty()) {
0391         LOG("Delete temp file" << mTempPath);
0392         QFile::remove(mTempPath);
0393         mTempPath.clear();
0394     }
0395     determineNextIcon();
0396 }
0397 
0398 QImage ThumbnailProvider::loadThumbnailFromCache() const
0399 {
0400     if (mThumbnailGroup > ThumbnailGroup::XXLarge) {
0401         return {};
0402     }
0403 
0404     QImage image = sThumbnailWriter->value(mThumbnailPath);
0405     if (!image.isNull()) {
0406         return image;
0407     }
0408 
0409     if (!QFileInfo::exists(mThumbnailPath)) {
0410         return {};
0411     }
0412 
0413     image = QImage(mThumbnailPath);
0414     int largeThumbnailGroup = mThumbnailGroup;
0415     while (image.isNull() && ++largeThumbnailGroup <= ThumbnailGroup::XXLarge) {
0416         // If there is a large-sized thumbnail, generate the small-sized version from it
0417         const QString largeThumbnailPath = generateThumbnailPath(mOriginalUri, static_cast<ThumbnailGroup::Enum>(largeThumbnailGroup));
0418         const QImage largeImage(largeThumbnailPath);
0419         if (!largeImage.isNull()) {
0420             const int size = ThumbnailGroup::pixelSize(mThumbnailGroup);
0421             image = largeImage.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0422             const QStringList textKeys = largeImage.textKeys();
0423             for (const QString &key : textKeys) {
0424                 QString text = largeImage.text(key);
0425                 image.setText(key, text);
0426             }
0427             sThumbnailWriter->queueThumbnail(mThumbnailPath, image);
0428             break;
0429         }
0430     }
0431 
0432     return image;
0433 }
0434 
0435 void ThumbnailProvider::checkThumbnail()
0436 {
0437     if (mCurrentItem.isNull()) {
0438         // This can happen if current item has been removed by removeItems()
0439         determineNextIcon();
0440         return;
0441     }
0442 
0443     // If we are in the thumbnail dir, just load the file
0444     if (mCurrentUrl.isLocalFile() && mCurrentUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().startsWith(thumbnailBaseDir())) {
0445         QImage image(mCurrentUrl.toLocalFile());
0446         emitThumbnailLoaded(image, image.size());
0447         determineNextIcon();
0448         return;
0449     }
0450 
0451     mOriginalUri = generateOriginalUri(mCurrentUrl);
0452     mThumbnailPath = generateThumbnailPath(mOriginalUri, mThumbnailGroup);
0453 
0454     LOG("Stat thumb" << mThumbnailPath);
0455 
0456     QImage thumb = loadThumbnailFromCache();
0457     KIO::filesize_t fileSize = thumb.text(QStringLiteral("Thumb::Size")).toULongLong();
0458     if (!thumb.isNull()) {
0459         if (thumb.text(QStringLiteral("Thumb::URI")) == mOriginalUri && thumb.text(QStringLiteral("Thumb::MTime")).toInt() == mOriginalTime
0460             && (fileSize == 0 || fileSize == mOriginalFileSize)) {
0461             int width = 0, height = 0;
0462             QSize size;
0463             bool ok;
0464 
0465             width = thumb.text(QStringLiteral("Thumb::Image::Width")).toInt(&ok);
0466             if (ok)
0467                 height = thumb.text(QStringLiteral("Thumb::Image::Height")).toInt(&ok);
0468             if (ok) {
0469                 size = QSize(width, height);
0470             } else {
0471                 LOG("Thumbnail for" << mOriginalUri << "does not contain correct image size information");
0472                 // Don't try to determine the size of a video, it probably won't work and
0473                 // will cause high I/O usage with big files (bug #307007).
0474                 if (MimeTypeUtils::urlKind(mCurrentUrl) == MimeTypeUtils::KIND_VIDEO) {
0475                     emitThumbnailLoaded(thumb, QSize());
0476                     determineNextIcon();
0477                     return;
0478                 }
0479             }
0480             emitThumbnailLoaded(thumb, size);
0481             determineNextIcon();
0482             return;
0483         }
0484     }
0485 
0486     // Thumbnail not found or not valid
0487     if (MimeTypeUtils::fileItemKind(mCurrentItem) == MimeTypeUtils::KIND_RASTER_IMAGE) {
0488         if (mCurrentUrl.isLocalFile()) {
0489             // Original is a local file, create the thumbnail
0490             startCreatingThumbnail(mCurrentUrl.toLocalFile());
0491         } else {
0492             // Original is remote, download it
0493             mState = STATE_DOWNLOADORIG;
0494 
0495             QTemporaryFile tempFile;
0496             tempFile.setAutoRemove(false);
0497             if (!tempFile.open()) {
0498                 qCWarning(GWENVIEW_LIB_LOG) << "Couldn't create temp file to download " << mCurrentUrl.toDisplayString();
0499                 emitThumbnailLoadingFailed();
0500                 determineNextIcon();
0501                 return;
0502             }
0503             mTempPath = tempFile.fileName();
0504 
0505             QUrl url = QUrl::fromLocalFile(mTempPath);
0506             KIO::Job *job = KIO::file_copy(mCurrentUrl, url, -1, KIO::Overwrite | KIO::HideProgressInfo);
0507             KJobWidgets::setWindow(job, qApp->activeWindow());
0508             LOG("Download remote file" << mCurrentUrl.toDisplayString() << "to" << url.toDisplayString());
0509             addSubjob(job);
0510         }
0511     } else {
0512         // Not a raster image, use a KPreviewJob
0513         LOG("Starting a KPreviewJob for" << mCurrentItem.url());
0514         mState = STATE_PREVIEWJOB;
0515         KFileItemList list;
0516         list.append(mCurrentItem);
0517         const int pixelSize = ThumbnailGroup::pixelSize(mThumbnailGroup);
0518         if (mPreviewPlugins.isEmpty()) {
0519             mPreviewPlugins = KIO::PreviewJob::availablePlugins();
0520         }
0521         KIO::Job *job = KIO::filePreview(list, QSize(pixelSize, pixelSize), &mPreviewPlugins);
0522         // KJobWidgets::setWindow(job, qApp->activeWindow());
0523         connect(job, SIGNAL(gotPreview(KFileItem, QPixmap)), this, SLOT(slotGotPreview(KFileItem, QPixmap)));
0524         connect(job, SIGNAL(failed(KFileItem)), this, SLOT(emitThumbnailLoadingFailed()));
0525         addSubjob(job);
0526     }
0527 }
0528 
0529 void ThumbnailProvider::startCreatingThumbnail(const QString &pixPath)
0530 {
0531     LOG("Creating thumbnail from" << pixPath);
0532     // If mPreviousThumbnailGenerator is already working on our current item
0533     // its thumbnail will be passed to sThumbnailWriter when ready. So we
0534     // connect mPreviousThumbnailGenerator's signal "finished" to determineNextIcon
0535     // which will load the thumbnail from sThumbnailWriter or from disk
0536     // (because we re-add mCurrentItem to mItems).
0537     if (mPreviousThumbnailGenerator && !mPreviousThumbnailGenerator->isStopped() && mOriginalUri == mPreviousThumbnailGenerator->originalUri()
0538         && mOriginalTime == mPreviousThumbnailGenerator->originalTime() && mOriginalFileSize == mPreviousThumbnailGenerator->originalFileSize()
0539         && mCurrentItem.mimetype() == mPreviousThumbnailGenerator->originalMimeType()) {
0540         connect(mPreviousThumbnailGenerator, SIGNAL(finished()), SLOT(determineNextIcon()));
0541         mItems.prepend(mCurrentItem);
0542         return;
0543     }
0544     mThumbnailGenerator->load(mOriginalUri, mOriginalTime, mOriginalFileSize, mCurrentItem.mimetype(), pixPath, mThumbnailPath, mThumbnailGroup);
0545 }
0546 
0547 void ThumbnailProvider::slotGotPreview(const KFileItem &item, const QPixmap &pixmap)
0548 {
0549     if (mCurrentItem.isNull()) {
0550         // This can happen if current item has been removed by removeItems()
0551         return;
0552     }
0553     LOG(mCurrentItem.url());
0554     QSize size;
0555     Q_EMIT thumbnailLoaded(item, pixmap, size, mOriginalFileSize);
0556 }
0557 
0558 void ThumbnailProvider::emitThumbnailLoaded(const QImage &img, const QSize &size)
0559 {
0560     if (mCurrentItem.isNull()) {
0561         // This can happen if current item has been removed by removeItems()
0562         return;
0563     }
0564     LOG(mCurrentItem.url());
0565     QPixmap thumb = QPixmap::fromImage(img);
0566     Q_EMIT thumbnailLoaded(mCurrentItem, thumb, size, mOriginalFileSize);
0567 }
0568 
0569 void ThumbnailProvider::emitThumbnailLoadingFailed()
0570 {
0571     if (mCurrentItem.isNull()) {
0572         // This can happen if current item has been removed by removeItems()
0573         return;
0574     }
0575     LOG(mCurrentItem.url());
0576     Q_EMIT thumbnailLoadingFailed(mCurrentItem);
0577 }
0578 
0579 bool ThumbnailProvider::isThumbnailWriterEmpty()
0580 {
0581     return sThumbnailWriter->isEmpty();
0582 }
0583 
0584 } // namespace
0585 
0586 #include "moc_thumbnailprovider.cpp"