587 lines
19 KiB
C++
Raw Normal View History

2024-06-29 11:52:32 +06:00
// vim: set tabstop=4 shiftwidth=4 expandtab:
/* Gwenview - A simple image viewer for KDE
Copyright 2000-2007 Aurélien Gâteau <agateau@kde.org>
This class is based on the ImagePreviewJob class from Konqueror.
*/
/* This file is part of the KDE project
Copyright (C) 2000 David Faure <faure@kde.org>
2000 Carsten Pfeiffer <pfeiffer@kde.org>
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 <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
// Qt
#include <QApplication>
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QStandardPaths>
#include <QTemporaryFile>
// KF
#include <KIO/FileCopyJob>
#include <KIO/PreviewJob>
#include <KIO/StatJob>
#include <KJobWidgets>
// 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<KFileItem> 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<KIO::StatJob *>(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<ThumbnailGroup::Enum>(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"