kde-extraapps/gwenview/lib/thumbnailprovider/thumbnailprovider.cpp
Ivailo Monev e2d987fe3b gwenview: fix build case with QT_NO_IMAGE_TEXT defined
Signed-off-by: Ivailo Monev <xakepa10@laimg.moc>
2019-05-15 00:16:31 +00:00

586 lines
19 KiB
C++

// 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 "moc_thumbnailprovider.cpp"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
// Qt
#include <QDir>
#include <QFile>
#include <QImage>
#include <QPixmap>
#include <QCryptographicHash>
// KDE
#include <KApplication>
#include <KDebug>
#include <kde_file.h>
#include <KFileItem>
#include <KIO/JobUiDelegate>
#include <KIO/PreviewJob>
#include <KStandardDirs>
#include <KTemporaryFile>
// Local
#include "mimetypeutils.h"
#include "thumbnailwriter.h"
#include "thumbnailgenerator.h"
#include "urlutils.h"
namespace Gwenview
{
#undef ENABLE_LOG
#undef LOG
//#define ENABLE_LOG
#ifdef ENABLE_LOG
#define LOG(x) kDebug() << x
#else
#define LOG(x) ;
#endif
K_GLOBAL_STATIC(ThumbnailWriter, sThumbnailWriter)
static QString generateOriginalUri(const KUrl& url_)
{
KUrl url = url_;
// Don't include the password if any
url.setPass(QString());
return url.url();
}
static QString generateThumbnailPath(const QString& uri, ThumbnailGroup::Enum group)
{
QByteArray md5 = QCryptographicHash::hash(QFile::encodeName(uri), QCryptographicHash::Md5);
QString baseDir = ThumbnailProvider::thumbnailBaseDir(group);
return baseDir + QString(QFile::encodeName(md5.toHex())) + ".png";
}
//------------------------------------------------------------------------
//
// ThumbnailProvider static methods
//
//------------------------------------------------------------------------
static QString sThumbnailBaseDir;
QString ThumbnailProvider::thumbnailBaseDir()
{
if (sThumbnailBaseDir.isEmpty()) {
const QByteArray customDir = qgetenv("GV_THUMBNAIL_DIR");
if (customDir.isEmpty()) {
sThumbnailBaseDir = QDir::homePath() + "/.thumbnails/";
} else {
sThumbnailBaseDir = QString::fromLocal8Bit(customDir.constData()) + '/';
}
}
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 += "normal/";
break;
case ThumbnailGroup::Large:
dir += "large/";
break;
}
return dir;
}
void ThumbnailProvider::deleteImageThumbnail(const KUrl& url)
{
QString uri = generateOriginalUri(url);
QFile::remove(generateThumbnailPath(uri, ThumbnailGroup::Normal));
QFile::remove(generateThumbnailPath(uri, ThumbnailGroup::Large));
}
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;
}
#ifndef QT_NO_IMAGE_TEXT
thumb.setText("Thumb::URI", newUri);
#endif
thumb.save(newPath, "png");
QFile::remove(QFile::encodeName(oldPath));
}
void ThumbnailProvider::moveThumbnail(const KUrl& oldUrl, const KUrl& newUrl)
{
QString oldUri = generateOriginalUri(oldUrl);
QString newUri = generateOriginalUri(newUrl);
moveThumbnailHelper(oldUri, newUri, ThumbnailGroup::Normal);
moveThumbnailHelper(oldUri, newUri, ThumbnailGroup::Large);
}
//------------------------------------------------------------------------
//
// ThumbnailProvider implementation
//
//------------------------------------------------------------------------
ThumbnailProvider::ThumbnailProvider()
: KIO::Job()
, mState(STATE_NEXTTHUMB)
, mOriginalTime(0)
{
LOG(this);
// Make sure we have a place to store our thumbnails
QString thumbnailDirNormal = ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Normal);
QString thumbnailDirLarge = ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Large);
KStandardDirs::makeDir(thumbnailDirNormal, 0700);
KStandardDirs::makeDir(thumbnailDirLarge, 0700);
// Look for images and store the items in our todo list
mCurrentItem = KFileItem();
mThumbnailGroup = ThumbnailGroup::Large;
createNewThumbnailGenerator();
}
ThumbnailProvider::~ThumbnailProvider()
{
LOG(this);
abortSubjob();
mThumbnailGenerator->cancel();
disconnect(mThumbnailGenerator, 0, this, 0);
disconnect(mThumbnailGenerator, 0, sThumbnailWriter, 0);
connect(mThumbnailGenerator, SIGNAL(finished()), mThumbnailGenerator, SLOT(deleteLater()));
if (mPreviousThumbnailGenerator) {
disconnect(mPreviousThumbnailGenerator, 0, sThumbnailWriter, 0);
}
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->isRunning() && !mPreviousThumbnailGenerator) {
mPreviousThumbnailGenerator = mThumbnailGenerator;
mPreviousThumbnailGenerator->cancel();
disconnect(mPreviousThumbnailGenerator, 0, this, 0);
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<QString> itemSet;
Q_FOREACH(const KFileItem & item, mItems) {
itemSet.insert(item.url().url());
}
Q_FOREACH(const KFileItem & item, items) {
if (!itemSet.contains(item.url().url())) {
mItems.append(item);
}
}
} else {
mItems = items;
}
if (mCurrentItem.isNull()) {
determineNextIcon();
}
}
void ThumbnailProvider::removeItems(const KFileItemList& itemList)
{
if (mItems.isEmpty()) {
return;
}
Q_FOREACH(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();
finished();
return;
}
mCurrentItem = mItems.takeFirst();
LOG("mCurrentItem.url=" << mCurrentItem.url());
// First, stat the orig file
mState = STATE_STATORIG;
mCurrentUrl = mCurrentItem.url();
mCurrentUrl.cleanPath();
mOriginalFileSize = mCurrentItem.size();
// Do direct stat instead of using KIO if the file is local (faster)
bool directStatOk = false;
if (UrlUtils::urlIsFastLocalFile(mCurrentUrl)) {
KDE_struct_stat buff;
if (KDE::stat(mCurrentUrl.toLocalFile(), &buff) == 0) {
directStatOk = true;
mOriginalTime = buff.st_mtime;
QMetaObject::invokeMethod(this, "checkThumbnail", Qt::QueuedConnection);
}
}
if (!directStatOk) {
KIO::Job* job = KIO::stat(mCurrentUrl, KIO::HideProgressInfo);
job->ui()->setWindow(KApplication::kApplication()->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
{
QImage image = sThumbnailWriter->value(mThumbnailPath);
if (!image.isNull()) {
return image;
}
image = QImage(mThumbnailPath);
if (image.isNull() && mThumbnailGroup == ThumbnailGroup::Normal) {
// If there is a large-sized thumbnail, generate the normal-sized version from it
QString largeThumbnailPath = generateThumbnailPath(mOriginalUri, ThumbnailGroup::Large);
QImage largeImage(largeThumbnailPath);
if (largeImage.isNull()) {
return image;
}
int size = ThumbnailGroup::pixelSize(ThumbnailGroup::Normal);
image = largeImage.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
#ifndef QT_NO_IMAGE_TEXT
Q_FOREACH(const QString& key, largeImage.textKeys()) {
QString text = largeImage.text(key);
image.setText(key, text);
}
#endif
sThumbnailWriter->queueThumbnail(mThumbnailPath, image);
}
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.directory().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();
#ifndef QT_NO_IMAGE_TEXT
KIO::filesize_t fileSize = thumb.text(QLatin1String("Thumb::Size")).toULongLong();
if (!thumb.isNull()) {
if (thumb.text(QLatin1String("Thumb::URI")) == mOriginalUri &&
thumb.text(QLatin1String("Thumb::MTime")).toInt() == mOriginalTime &&
(fileSize == 0 || fileSize == mOriginalFileSize)) {
int width = 0, height = 0;
QSize size;
bool ok;
width = thumb.text(QLatin1String("Thumb::Image::Width")).toInt(&ok);
if (ok) height = thumb.text(QLatin1String("Thumb::Image::Height")).toInt(&ok);
if (ok) {
size = QSize(width, height);
} else {
kWarning() << "Thumbnail for" << mOriginalUri << "does not contain correct image size information";
KFileMetaInfo fmi(mCurrentUrl);
if (fmi.isValid()) {
KFileMetaInfoItem item = fmi.item("Dimensions");
if (item.isValid()) {
size = item.value().toSize();
} else {
kWarning() << "KFileMetaInfoItem for" << mOriginalUri << "did not get image size information";
}
} else {
kWarning() << "Could not get a valid KFileMetaInfo instance for" << mOriginalUri;
}
}
emitThumbnailLoaded(thumb, size);
determineNextIcon();
return;
}
}
#endif
// 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;
KTemporaryFile tempFile;
tempFile.setAutoRemove(false);
if (!tempFile.open()) {
kWarning() << "Couldn't create temp file to download " << mCurrentUrl.prettyUrl();
emitThumbnailLoadingFailed();
determineNextIcon();
return;
}
mTempPath = tempFile.fileName();
KUrl url;
url.setPath(mTempPath);
KIO::Job* job = KIO::file_copy(mCurrentUrl, url, -1, KIO::Overwrite | KIO::HideProgressInfo);
job->ui()->setWindow(KApplication::kApplication()->activeWindow());
LOG("Download remote file" << mCurrentUrl.prettyUrl() << "to" << url.pathOrUrl());
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);
//job->ui()->setWindow(KApplication::kApplication()->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->isRunning() &&
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;
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);
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());
emit thumbnailLoadingFailed(mCurrentItem);
}
bool ThumbnailProvider::isThumbnailWriterEmpty()
{
return sThumbnailWriter->isEmpty();
}
} // namespace