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"