// vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview - A simple image viewer for KDE Copyright 2000-2007 Aurélien Gâteau This class is based on the ImagePreviewJob class from Konqueror. */ /* This file is part of the KDE project Copyright (C) 2000 David Faure 2000 Carsten Pfeiffer This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "thumbnailprovider.h" #include #include #include // Qt #include #include #include #include #include #include // KF #include #include #include #include // Local #include "gwenview_lib_debug.h" #include "mimetypeutils.h" #include "thumbnailgenerator.h" #include "thumbnailwriter.h" #include "urlutils.h" namespace Gwenview { #undef ENABLE_LOG #undef LOG // #define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) qCDebug(GWENVIEW_LIB_LOG) << x #else #define LOG(x) ; #endif Q_GLOBAL_STATIC(ThumbnailWriter, sThumbnailWriter) static const ThumbnailGroup::Enum s_thumbnailGroups[] = { ThumbnailGroup::Normal, ThumbnailGroup::Large, ThumbnailGroup::XLarge, ThumbnailGroup::XXLarge, }; static QString generateOriginalUri(const QUrl &url_) { QUrl url = url_; return url.adjusted(QUrl::RemovePassword).url(); } static QString generateThumbnailPath(const QString &uri, ThumbnailGroup::Enum group) { QString baseDir = ThumbnailProvider::thumbnailBaseDir(group); QCryptographicHash md5(QCryptographicHash::Md5); md5.addData(QFile::encodeName(uri)); return baseDir + QFile::encodeName(QString::fromLatin1(md5.result().toHex())) + QStringLiteral(".png"); } //------------------------------------------------------------------------ // // ThumbnailProvider static methods // //------------------------------------------------------------------------ static QString sThumbnailBaseDir; QString ThumbnailProvider::thumbnailBaseDir() { if (sThumbnailBaseDir.isEmpty()) { const QByteArray customDir = qgetenv("GV_THUMBNAIL_DIR"); if (customDir.isEmpty()) { sThumbnailBaseDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/thumbnails/"); } else { sThumbnailBaseDir = QFile::decodeName(customDir) + QLatin1Char('/'); } } return sThumbnailBaseDir; } void ThumbnailProvider::setThumbnailBaseDir(const QString &dir) { sThumbnailBaseDir = dir; } QString ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Enum group) { QString dir = thumbnailBaseDir(); switch (group) { case ThumbnailGroup::Normal: dir += QStringLiteral("normal/"); break; case ThumbnailGroup::Large: dir += QStringLiteral("large/"); break; case ThumbnailGroup::XLarge: dir += QStringLiteral("x-large/"); break; case ThumbnailGroup::XXLarge: dir += QStringLiteral("xx-large/"); break; default: dir += QLatin1String("x-gwenview/"); // Should never be hit, but just in case } return dir; } void ThumbnailProvider::deleteImageThumbnail(const QUrl &url) { QString uri = generateOriginalUri(url); for (auto group : s_thumbnailGroups) { QFile::remove(generateThumbnailPath(uri, group)); } } static void moveThumbnailHelper(const QString &oldUri, const QString &newUri, ThumbnailGroup::Enum group) { QString oldPath = generateThumbnailPath(oldUri, group); QString newPath = generateThumbnailPath(newUri, group); QImage thumb; if (!thumb.load(oldPath)) { return; } thumb.setText(QStringLiteral("Thumb::URI"), newUri); thumb.save(newPath, "png"); QFile::remove(QFile::encodeName(oldPath)); } void ThumbnailProvider::moveThumbnail(const QUrl &oldUrl, const QUrl &newUrl) { QString oldUri = generateOriginalUri(oldUrl); QString newUri = generateOriginalUri(newUrl); for (auto group : s_thumbnailGroups) { moveThumbnailHelper(oldUri, newUri, group); } } //------------------------------------------------------------------------ // // ThumbnailProvider implementation // //------------------------------------------------------------------------ ThumbnailProvider::ThumbnailProvider() : KIO::Job() , mState(STATE_NEXTTHUMB) , mOriginalTime(0) { LOG(this); // Make sure we have a place to store our thumbnails for (auto group : s_thumbnailGroups) { const QString thumbnailDir = ThumbnailProvider::thumbnailBaseDir(group); QDir().mkpath(thumbnailDir); QFile::setPermissions(thumbnailDir, QFileDevice::WriteOwner | QFileDevice::ReadOwner | QFileDevice::ExeOwner); } // Look for images and store the items in our todo list mCurrentItem = KFileItem(); mThumbnailGroup = ThumbnailGroup::XXLarge; createNewThumbnailGenerator(); } ThumbnailProvider::~ThumbnailProvider() { LOG(this); disconnect(mThumbnailGenerator, nullptr, this, nullptr); disconnect(mThumbnailGenerator, nullptr, sThumbnailWriter, nullptr); abortSubjob(); mThumbnailGenerator->cancel(); if (mPreviousThumbnailGenerator) { disconnect(mPreviousThumbnailGenerator, nullptr, sThumbnailWriter, nullptr); } sThumbnailWriter->requestInterruption(); sThumbnailWriter->wait(); } void ThumbnailProvider::stop() { // Clear mItems and create a new ThumbnailGenerator if mThumbnailGenerator is running, // but also make sure that at most two ThumbnailGenerators are running. // startCreatingThumbnail() will take care that these two threads won't work on the same item. mItems.clear(); abortSubjob(); if (!mThumbnailGenerator->isStopped() && !mPreviousThumbnailGenerator) { mPreviousThumbnailGenerator = mThumbnailGenerator; mPreviousThumbnailGenerator->cancel(); disconnect(mPreviousThumbnailGenerator, nullptr, this, nullptr); connect(mPreviousThumbnailGenerator, SIGNAL(finished()), mPreviousThumbnailGenerator, SLOT(deleteLater())); createNewThumbnailGenerator(); mCurrentItem = KFileItem(); } } const KFileItemList &ThumbnailProvider::pendingItems() const { return mItems; } void ThumbnailProvider::setThumbnailGroup(ThumbnailGroup::Enum group) { mThumbnailGroup = group; } void ThumbnailProvider::appendItems(const KFileItemList &items) { if (!mItems.isEmpty()) { QSet itemSet{mItems.begin(), mItems.end()}; for (const KFileItem &item : items) { if (!itemSet.contains(item)) { mItems.append(item); } } } else { mItems = items; } if (mCurrentItem.isNull()) { determineNextIcon(); } } void ThumbnailProvider::removeItems(const KFileItemList &itemList) { if (mItems.isEmpty()) { return; } for (const KFileItem &item : itemList) { // If we are removing the next item, update to be the item after or the // first if we removed the last item mItems.removeAll(item); if (item == mCurrentItem) { abortSubjob(); } } // No more current item, carry on to the next remaining item if (mCurrentItem.isNull()) { determineNextIcon(); } } void ThumbnailProvider::removePendingItems() { mItems.clear(); } bool ThumbnailProvider::isRunning() const { return !mCurrentItem.isNull(); } //-Internal-------------------------------------------------------------- void ThumbnailProvider::createNewThumbnailGenerator() { mThumbnailGenerator = new ThumbnailGenerator; connect(mThumbnailGenerator, SIGNAL(done(QImage, QSize)), SLOT(thumbnailReady(QImage, QSize)), Qt::QueuedConnection); connect(mThumbnailGenerator, SIGNAL(thumbnailReadyToBeCached(QString, QImage)), sThumbnailWriter, SLOT(queueThumbnail(QString, QImage)), Qt::QueuedConnection); } void ThumbnailProvider::abortSubjob() { if (hasSubjobs()) { LOG("Killing subjob"); KJob *job = subjobs().first(); job->kill(); removeSubjob(job); mCurrentItem = KFileItem(); } } void ThumbnailProvider::determineNextIcon() { LOG(this); mState = STATE_NEXTTHUMB; // No more items ? if (mItems.isEmpty()) { LOG("No more items. Nothing to do"); mCurrentItem = KFileItem(); Q_EMIT finished(); return; } mCurrentItem = mItems.takeFirst(); LOG("mCurrentItem.url=" << mCurrentItem.url()); // First, stat the orig file mState = STATE_STATORIG; mCurrentUrl = mCurrentItem.url().adjusted(QUrl::NormalizePathSegments); mOriginalFileSize = mCurrentItem.size(); // Do direct stat instead of using KIO if the file is local (faster) if (UrlUtils::urlIsFastLocalFile(mCurrentUrl)) { QFileInfo fileInfo(mCurrentUrl.toLocalFile()); mOriginalTime = fileInfo.lastModified().toSecsSinceEpoch(); QMetaObject::invokeMethod(this, &ThumbnailProvider::checkThumbnail, Qt::QueuedConnection); } else { KIO::Job *job = KIO::stat(mCurrentUrl, KIO::HideProgressInfo); KJobWidgets::setWindow(job, qApp->activeWindow()); LOG("KIO::stat orig" << mCurrentUrl.url()); addSubjob(job); } LOG("/determineNextIcon" << this); } void ThumbnailProvider::slotResult(KJob *job) { LOG(mState); removeSubjob(job); Q_ASSERT(subjobs().isEmpty()); // We should have only one job at a time switch (mState) { case STATE_NEXTTHUMB: Q_ASSERT(false); determineNextIcon(); return; case STATE_STATORIG: { // Could not stat original, drop this one and move on to the next one if (job->error()) { emitThumbnailLoadingFailed(); determineNextIcon(); return; } // Get modification time of the original file KIO::UDSEntry entry = static_cast(job)->statResult(); mOriginalTime = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); checkThumbnail(); return; } case STATE_DOWNLOADORIG: if (job->error()) { emitThumbnailLoadingFailed(); LOG("Delete temp file" << mTempPath); QFile::remove(mTempPath); mTempPath.clear(); determineNextIcon(); } else { startCreatingThumbnail(mTempPath); } return; case STATE_PREVIEWJOB: determineNextIcon(); return; } } void ThumbnailProvider::thumbnailReady(const QImage &_img, const QSize &_size) { QImage img = _img; QSize size = _size; if (!img.isNull()) { emitThumbnailLoaded(img, size); } else { emitThumbnailLoadingFailed(); } if (!mTempPath.isEmpty()) { LOG("Delete temp file" << mTempPath); QFile::remove(mTempPath); mTempPath.clear(); } determineNextIcon(); } QImage ThumbnailProvider::loadThumbnailFromCache() const { if (mThumbnailGroup > ThumbnailGroup::XXLarge) { return {}; } QImage image = sThumbnailWriter->value(mThumbnailPath); if (!image.isNull()) { return image; } if (!QFileInfo::exists(mThumbnailPath)) { return {}; } image = QImage(mThumbnailPath); int largeThumbnailGroup = mThumbnailGroup; while (image.isNull() && ++largeThumbnailGroup <= ThumbnailGroup::XXLarge) { // If there is a large-sized thumbnail, generate the small-sized version from it const QString largeThumbnailPath = generateThumbnailPath(mOriginalUri, static_cast(largeThumbnailGroup)); const QImage largeImage(largeThumbnailPath); if (!largeImage.isNull()) { const int size = ThumbnailGroup::pixelSize(mThumbnailGroup); image = largeImage.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation); const QStringList textKeys = largeImage.textKeys(); for (const QString &key : textKeys) { QString text = largeImage.text(key); image.setText(key, text); } sThumbnailWriter->queueThumbnail(mThumbnailPath, image); break; } } return image; } void ThumbnailProvider::checkThumbnail() { if (mCurrentItem.isNull()) { // This can happen if current item has been removed by removeItems() determineNextIcon(); return; } // If we are in the thumbnail dir, just load the file if (mCurrentUrl.isLocalFile() && mCurrentUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().startsWith(thumbnailBaseDir())) { QImage image(mCurrentUrl.toLocalFile()); emitThumbnailLoaded(image, image.size()); determineNextIcon(); return; } mOriginalUri = generateOriginalUri(mCurrentUrl); mThumbnailPath = generateThumbnailPath(mOriginalUri, mThumbnailGroup); LOG("Stat thumb" << mThumbnailPath); QImage thumb = loadThumbnailFromCache(); KIO::filesize_t fileSize = thumb.text(QStringLiteral("Thumb::Size")).toULongLong(); if (!thumb.isNull()) { if (thumb.text(QStringLiteral("Thumb::URI")) == mOriginalUri && thumb.text(QStringLiteral("Thumb::MTime")).toInt() == mOriginalTime && (fileSize == 0 || fileSize == mOriginalFileSize)) { int width = 0, height = 0; QSize size; bool ok; width = thumb.text(QStringLiteral("Thumb::Image::Width")).toInt(&ok); if (ok) height = thumb.text(QStringLiteral("Thumb::Image::Height")).toInt(&ok); if (ok) { size = QSize(width, height); } else { LOG("Thumbnail for" << mOriginalUri << "does not contain correct image size information"); // Don't try to determine the size of a video, it probably won't work and // will cause high I/O usage with big files (bug #307007). if (MimeTypeUtils::urlKind(mCurrentUrl) == MimeTypeUtils::KIND_VIDEO) { emitThumbnailLoaded(thumb, QSize()); determineNextIcon(); return; } } emitThumbnailLoaded(thumb, size); determineNextIcon(); return; } } // Thumbnail not found or not valid if (MimeTypeUtils::fileItemKind(mCurrentItem) == MimeTypeUtils::KIND_RASTER_IMAGE) { if (mCurrentUrl.isLocalFile()) { // Original is a local file, create the thumbnail startCreatingThumbnail(mCurrentUrl.toLocalFile()); } else { // Original is remote, download it mState = STATE_DOWNLOADORIG; QTemporaryFile tempFile; tempFile.setAutoRemove(false); if (!tempFile.open()) { qCWarning(GWENVIEW_LIB_LOG) << "Couldn't create temp file to download " << mCurrentUrl.toDisplayString(); emitThumbnailLoadingFailed(); determineNextIcon(); return; } mTempPath = tempFile.fileName(); QUrl url = QUrl::fromLocalFile(mTempPath); KIO::Job *job = KIO::file_copy(mCurrentUrl, url, -1, KIO::Overwrite | KIO::HideProgressInfo); KJobWidgets::setWindow(job, qApp->activeWindow()); LOG("Download remote file" << mCurrentUrl.toDisplayString() << "to" << url.toDisplayString()); addSubjob(job); } } else { // Not a raster image, use a KPreviewJob LOG("Starting a KPreviewJob for" << mCurrentItem.url()); mState = STATE_PREVIEWJOB; KFileItemList list; list.append(mCurrentItem); const int pixelSize = ThumbnailGroup::pixelSize(mThumbnailGroup); if (mPreviewPlugins.isEmpty()) { mPreviewPlugins = KIO::PreviewJob::availablePlugins(); } KIO::Job *job = KIO::filePreview(list, QSize(pixelSize, pixelSize), &mPreviewPlugins); // KJobWidgets::setWindow(job, qApp->activeWindow()); connect(job, SIGNAL(gotPreview(KFileItem, QPixmap)), this, SLOT(slotGotPreview(KFileItem, QPixmap))); connect(job, SIGNAL(failed(KFileItem)), this, SLOT(emitThumbnailLoadingFailed())); addSubjob(job); } } void ThumbnailProvider::startCreatingThumbnail(const QString &pixPath) { LOG("Creating thumbnail from" << pixPath); // If mPreviousThumbnailGenerator is already working on our current item // its thumbnail will be passed to sThumbnailWriter when ready. So we // connect mPreviousThumbnailGenerator's signal "finished" to determineNextIcon // which will load the thumbnail from sThumbnailWriter or from disk // (because we re-add mCurrentItem to mItems). if (mPreviousThumbnailGenerator && !mPreviousThumbnailGenerator->isStopped() && mOriginalUri == mPreviousThumbnailGenerator->originalUri() && mOriginalTime == mPreviousThumbnailGenerator->originalTime() && mOriginalFileSize == mPreviousThumbnailGenerator->originalFileSize() && mCurrentItem.mimetype() == mPreviousThumbnailGenerator->originalMimeType()) { connect(mPreviousThumbnailGenerator, SIGNAL(finished()), SLOT(determineNextIcon())); mItems.prepend(mCurrentItem); return; } mThumbnailGenerator->load(mOriginalUri, mOriginalTime, mOriginalFileSize, mCurrentItem.mimetype(), pixPath, mThumbnailPath, mThumbnailGroup); } void ThumbnailProvider::slotGotPreview(const KFileItem &item, const QPixmap &pixmap) { if (mCurrentItem.isNull()) { // This can happen if current item has been removed by removeItems() return; } LOG(mCurrentItem.url()); QSize size; Q_EMIT thumbnailLoaded(item, pixmap, size, mOriginalFileSize); } void ThumbnailProvider::emitThumbnailLoaded(const QImage &img, const QSize &size) { if (mCurrentItem.isNull()) { // This can happen if current item has been removed by removeItems() return; } LOG(mCurrentItem.url()); QPixmap thumb = QPixmap::fromImage(img); Q_EMIT thumbnailLoaded(mCurrentItem, thumb, size, mOriginalFileSize); } void ThumbnailProvider::emitThumbnailLoadingFailed() { if (mCurrentItem.isNull()) { // This can happen if current item has been removed by removeItems() return; } LOG(mCurrentItem.url()); Q_EMIT thumbnailLoadingFailed(mCurrentItem); } bool ThumbnailProvider::isThumbnailWriterEmpty() { return sThumbnailWriter->isEmpty(); } } // namespace #include "moc_thumbnailprovider.cpp"