1
// vim: set tabstop=4 shiftwidth=4 noexpandtab:
2
/* Gwenview - A simple image viewer for KDE
3
Copyright 2000-2007 AurĆ©lien GĆ¢teau <agateau@kde.org>
4
This class is based on the ImagePreviewJob class from Konqueror.
6
/* This file is part of the KDE project
7
Copyright (C) 2000 David Faure <faure@kde.org>
8
2000 Carsten Pfeiffer <pfeiffer@kde.org>
10
This program is free software; you can redistribute it and/or modify
11
it under the terms of the GNU General Public License as published by
12
the Free Software Foundation; either version 2 of the License, or
13
(at your option) any later version.
15
This program is distributed in the hope that it will be useful,
16
but WITHOUT ANY WARRANTY; without even the implied warranty of
17
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
GNU General Public License for more details.
20
You should have received a copy of the GNU General Public License
21
along with this program; if not, write to the Free Software
22
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24
#include "thumbnailloadjob.moc"
26
#include <sys/types.h>
35
#include <QImageReader>
41
#include <kapplication.h>
45
#include <kfileitem.h>
46
#include <kio/jobuidelegate.h>
47
#include <kio/previewjob.h>
48
#include <kstandarddirs.h>
49
#include <ktemporaryfile.h>
52
#include "mimetypeutils.h"
53
#include "jpegcontent.h"
54
#include "imageutils.h"
63
#define LOG(x) kDebug() << x
69
static QString generateOriginalUri(const KUrl& url_) {
71
// Don't include the password if any
72
url.setPass(QString::null); //krazy:exclude=nullstrassign for old broken gcc
77
static QString generateThumbnailPath(const QString& uri, ThumbnailGroup::Enum group) {
78
KMD5 md5( QFile::encodeName(uri) );
79
QString baseDir=ThumbnailLoadJob::thumbnailBaseDir(group);
80
return baseDir + QString(QFile::encodeName( md5.hexDigest())) + ".png";
83
//------------------------------------------------------------------------
87
//------------------------------------------------------------------------
88
K_GLOBAL_STATIC(ThumbnailCache, sThumbnailCache)
91
static void storeThumbnailToDiskCache(const QString& path, const QImage& image) {
94
tmp.setPrefix(path + ".gwenview.tmp");
95
tmp.setSuffix(".png");
97
kWarning() << "Could not create a temporary file.";
101
if (!image.save(tmp.fileName(), "png")) {
102
kWarning() << "Could not save thumbnail";
106
KDE_rename(QFile::encodeName(tmp.fileName()), QFile::encodeName(path));
110
void ThumbnailCache::queueThumbnail(const QString& path, const QImage& image) {
112
QMutexLocker locker(&mMutex);
113
mCache.insert(path, image);
116
void ThumbnailCache::run() {
117
QMutexLocker locker(&mMutex);
118
while (!mCache.isEmpty()) {
119
Cache::ConstIterator it = mCache.constBegin();
120
const QString path = it.key();
121
const QImage image = it.value();
123
// This part of the thread is the most time consuming but it does not
124
// depend on mCache so we can unlock here. This way other thumbnails
125
// can be added or queried
127
storeThumbnailToDiskCache(path, image);
134
QImage ThumbnailCache::value(const QString& path) const {
135
QMutexLocker locker(&mMutex);
136
return mCache.value(path);
139
bool ThumbnailCache::isEmpty() const {
140
QMutexLocker locker(&mMutex);
141
return mCache.isEmpty();
145
//------------------------------------------------------------------------
149
//------------------------------------------------------------------------
150
ThumbnailThread::ThumbnailThread()
153
void ThumbnailThread::load(
154
const QString& originalUri, time_t originalTime, int originalSize, const QString& originalMimeType,
155
const QString& pixPath,
156
const QString& thumbnailPath,
157
ThumbnailGroup::Enum group)
159
QMutexLocker lock( &mMutex );
160
assert( mPixPath.isNull());
162
mOriginalUri = originalUri;
163
mOriginalTime = originalTime;
164
mOriginalSize = originalSize;
165
mOriginalMimeType = originalMimeType;
167
mThumbnailPath = thumbnailPath;
168
mThumbnailGroup = group;
169
if(!isRunning()) start();
173
bool ThumbnailThread::testCancel() {
174
QMutexLocker lock( &mMutex );
178
void ThumbnailThread::cancel() {
179
QMutexLocker lock( &mMutex );
184
void ThumbnailThread::run() {
186
while( !testCancel()) {
188
QMutexLocker lock(&mMutex);
189
// empty mPixPath means nothing to do
190
LOG("Waiting for mPixPath");
191
if (mPixPath.isNull()) {
192
LOG("mPixPath.isNull");
200
QMutexLocker lock(&mMutex);
201
Q_ASSERT(!mPixPath.isNull());
202
LOG("Loading" << mPixPath);
204
bool ok = loadThumbnail(&needCaching);
205
if (ok && needCaching) {
208
mPixPath.clear(); // done, ready for next
214
QSize size(mOriginalWidth, mOriginalHeight);
215
LOG("emitting done signal, size=" << size);
216
QMutexLocker lock(&mMutex);
221
LOG("Ending thread");
224
bool ThumbnailThread::loadThumbnail(bool* needCaching) {
227
int pixelSize = ThumbnailGroup::pixelSize(mThumbnailGroup);
228
Orientation orientation = NORMAL;
230
QImageReader reader(mPixPath);
231
// If it's a Jpeg, try to load an embedded thumbnail, if available
232
if (reader.format() == "jpeg") {
234
content.load(mPixPath);
235
QImage thumbnail = content.thumbnail();
236
orientation = content.orientation();
238
if (qMax(thumbnail.width(), thumbnail.height()) >= pixelSize) {
240
if (orientation != NORMAL && orientation != NOT_AVAILABLE) {
241
QMatrix matrix = ImageUtils::transformMatrix(orientation);
242
mImage = mImage.transformed(matrix);
244
mOriginalWidth = content.size().width();
245
mOriginalHeight = content.size().height();
250
// Generate thumbnail from full image
251
QSize originalSize = reader.size();
252
if (originalSize.isValid() && reader.supportsOption(QImageIOHandler::ScaledSize)) {
254
const int maxSize = qMax(originalSize.width(), originalSize.height());
255
for (scale=1; pixelSize*scale*2 <= maxSize && scale <= 8; scale *= 2) {}
256
const QSize scaledSize = originalSize / scale;
257
if (!scaledSize.isEmpty()) {
258
reader.setScaledSize(scaledSize);
262
QImage originalImage;
263
// format() is empty after QImageReader::read() is called
264
QByteArray format = reader.format();
265
if (!reader.read(&originalImage)) {
266
kWarning() << "Could not generate thumbnail for file" << mOriginalUri;
269
if (!originalSize.isValid()) {
270
originalSize = originalImage.size();
272
mOriginalWidth = originalSize.width();
273
mOriginalHeight = originalSize.height();
275
if (qMax(mOriginalWidth, mOriginalHeight)<=pixelSize) {
276
mImage = originalImage;
277
*needCaching = format != "png";
279
mImage = originalImage.scaled(pixelSize, pixelSize, Qt::KeepAspectRatio);
282
// Rotate if necessary
283
if (orientation != NORMAL && orientation != NOT_AVAILABLE) {
284
QMatrix matrix = ImageUtils::transformMatrix(orientation);
285
mImage = mImage.transformed(matrix);
287
switch (orientation) {
292
qSwap(mOriginalWidth, mOriginalHeight);
302
void ThumbnailThread::cacheThumbnail() {
303
mImage.setText("Thumb::Uri" , 0, mOriginalUri);
304
mImage.setText("Thumb::MTime" , 0, QString::number(mOriginalTime));
305
mImage.setText("Thumb::Size" , 0, QString::number(mOriginalSize));
306
mImage.setText("Thumb::Mimetype" , 0, mOriginalMimeType);
307
mImage.setText("Thumb::Image::Width" , 0, QString::number(mOriginalWidth));
308
mImage.setText("Thumb::Image::Height", 0, QString::number(mOriginalHeight));
309
mImage.setText("Software" , 0, "Gwenview");
311
emit thumbnailReadyToBeCached(mThumbnailPath, mImage);
315
//------------------------------------------------------------------------
317
// ThumbnailLoadJob static methods
319
//------------------------------------------------------------------------
320
static QString sThumbnailBaseDir;
321
QString ThumbnailLoadJob::thumbnailBaseDir() {
322
if (sThumbnailBaseDir.isEmpty()) {
323
sThumbnailBaseDir = QDir::homePath() + "/.thumbnails/";
325
return sThumbnailBaseDir;
329
void ThumbnailLoadJob::setThumbnailBaseDir(const QString& dir) {
330
sThumbnailBaseDir = dir;
334
QString ThumbnailLoadJob::thumbnailBaseDir(ThumbnailGroup::Enum group) {
335
QString dir = thumbnailBaseDir();
337
case ThumbnailGroup::Normal:
340
case ThumbnailGroup::Large:
348
void ThumbnailLoadJob::deleteImageThumbnail(const KUrl& url) {
349
QString uri=generateOriginalUri(url);
350
QFile::remove(generateThumbnailPath(uri, ThumbnailGroup::Normal));
351
QFile::remove(generateThumbnailPath(uri, ThumbnailGroup::Large));
355
static void moveThumbnailHelper(const QString& oldUri, const QString& newUri, ThumbnailGroup::Enum group) {
356
QString oldPath = generateThumbnailPath(oldUri, group);
357
QString newPath = generateThumbnailPath(newUri, group);
359
if (!thumb.load(oldPath)) {
362
thumb.setText("Thumb::Uri", 0, newUri);
363
thumb.save(newPath, "png");
364
QFile::remove(QFile::encodeName(oldPath));
367
void ThumbnailLoadJob::moveThumbnail(const KUrl& oldUrl, const KUrl& newUrl) {
368
QString oldUri = generateOriginalUri(oldUrl);
369
QString newUri = generateOriginalUri(newUrl);
370
moveThumbnailHelper(oldUri, newUri, ThumbnailGroup::Normal);
371
moveThumbnailHelper(oldUri, newUri, ThumbnailGroup::Large);
375
//------------------------------------------------------------------------
377
// ThumbnailLoadJob implementation
379
//------------------------------------------------------------------------
380
ThumbnailLoadJob::ThumbnailLoadJob(const KFileItemList& items, ThumbnailGroup::Enum group)
382
, mState( STATE_NEXTTHUMB )
383
, mThumbnailGroup(group)
387
// Make sure we have a place to store our thumbnails
388
QString thumbnailDir = ThumbnailLoadJob::thumbnailBaseDir(mThumbnailGroup);
389
KStandardDirs::makeDir(thumbnailDir, 0700);
391
// Look for images and store the items in our todo list
392
Q_ASSERT(!items.empty());
394
mCurrentItem = KFileItem();
396
connect(&mThumbnailThread, SIGNAL(done(const QImage&, const QSize&)),
397
SLOT(thumbnailReady(const QImage&, const QSize&)),
398
Qt::QueuedConnection);
400
connect(&mThumbnailThread, SIGNAL(thumbnailReadyToBeCached(const QString&, const QImage&)),
401
sThumbnailCache, SLOT(queueThumbnail(const QString&, const QImage&)),
402
Qt::QueuedConnection);
406
ThumbnailLoadJob::~ThumbnailLoadJob() {
409
LOG("Killing subjob");
410
KJob* job = subjobs().first();
414
mThumbnailThread.cancel();
415
mThumbnailThread.wait();
416
if (!sThumbnailCache->isRunning()) {
417
sThumbnailCache->start();
422
void ThumbnailLoadJob::start() {
423
if (mItems.isEmpty()) {
424
LOG("Nothing to do");
434
const KFileItemList& ThumbnailLoadJob::pendingItems() const {
439
void ThumbnailLoadJob::setThumbnailGroup(ThumbnailGroup::Enum group) {
440
mThumbnailGroup = group;
444
//-Internal--------------------------------------------------------------
445
void ThumbnailLoadJob::appendItem(const KFileItem& item) {
446
if (!mItems.contains(item)) {
452
void ThumbnailLoadJob::removeItems(const KFileItemList& itemList) {
453
Q_FOREACH(const KFileItem& item, itemList) {
454
// If we are removing the next item, update to be the item after or the
455
// first if we removed the last item
456
mItems.removeAll( item );
458
if (item == mCurrentItem) {
459
// Abort current item
460
mCurrentItem = KFileItem();
462
KJob* job = subjobs().first();
469
// No more current item, carry on to the next remaining item
470
if (mCurrentItem.isNull()) {
476
void ThumbnailLoadJob::determineNextIcon() {
478
mState = STATE_NEXTTHUMB;
481
if (mItems.isEmpty()) {
483
LOG("emitting result");
489
mCurrentItem = mItems.first();
491
LOG("mCurrentItem.url=" << mCurrentItem.url());
493
// First, stat the orig file
494
mState = STATE_STATORIG;
495
mCurrentUrl = mCurrentItem.url();
496
mCurrentUrl.cleanPath();
498
// Do direct stat instead of using KIO if the file is local (faster)
499
bool directStatOk = false;
500
if (UrlUtils::urlIsFastLocalFile(mCurrentUrl)) {
501
KDE_struct_stat buff;
502
if ( KDE::stat( mCurrentUrl.toLocalFile(), &buff ) == 0 ) {
504
mOriginalTime = buff.st_mtime;
505
QTimer::singleShot( 0, this, SLOT( checkThumbnail()));
509
KIO::Job* job = KIO::stat(mCurrentUrl, KIO::HideProgressInfo);
510
job->ui()->setWindow(KApplication::kApplication()->activeWindow());
511
LOG( "KIO::stat orig" << mCurrentUrl.url() );
514
LOG("/determineNextIcon" << this);
518
void ThumbnailLoadJob::slotResult(KJob * job) {
521
Q_ASSERT(subjobs().isEmpty()); // We should have only one job at a time
524
case STATE_NEXTTHUMB:
529
case STATE_STATORIG: {
530
// Could not stat original, drop this one and move on to the next one
532
emitThumbnailLoadingFailed();
537
// Get modification time of the original file
538
KIO::UDSEntry entry = static_cast<KIO::StatJob*>(job)->statResult();
539
mOriginalTime = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
544
case STATE_DOWNLOADORIG:
546
emitThumbnailLoadingFailed();
547
LOG("Delete temp file" << mTempPath);
548
QFile::remove(mTempPath);
552
startCreatingThumbnail(mTempPath);
556
case STATE_PREVIEWJOB:
563
void ThumbnailLoadJob::thumbnailReady( const QImage& _img, const QSize& _size) {
566
if ( !img.isNull()) {
567
emitThumbnailLoaded(img, size);
569
emitThumbnailLoadingFailed();
571
if( !mTempPath.isEmpty()) {
572
LOG("Delete temp file" << mTempPath);
573
QFile::remove(mTempPath);
579
QImage ThumbnailLoadJob::loadThumbnailFromCache() const {
580
QImage image = sThumbnailCache->value(mThumbnailPath);
581
if (!image.isNull()) {
584
return QImage(mThumbnailPath);
587
void ThumbnailLoadJob::checkThumbnail() {
588
// If we are in the thumbnail dir, just load the file
589
if (mCurrentUrl.isLocalFile()
590
&& mCurrentUrl.directory().startsWith(thumbnailBaseDir()) )
592
QImage image(mCurrentUrl.toLocalFile());
593
emitThumbnailLoaded(image, image.size());
599
mOriginalUri=generateOriginalUri(mCurrentUrl);
600
mThumbnailPath=generateThumbnailPath(mOriginalUri, mThumbnailGroup);
602
LOG("Stat thumb" << mThumbnailPath);
604
QImage thumb = loadThumbnailFromCache();
605
if (!thumb.isNull()) {
606
if (thumb.text("Thumb::Uri", 0) == mOriginalUri &&
607
thumb.text("Thumb::MTime", 0).toInt() == mOriginalTime )
609
int width=0, height=0;
613
width=thumb.text("Thumb::Image::Width", 0).toInt(&ok);
614
if (ok) height=thumb.text("Thumb::Image::Height", 0).toInt(&ok);
616
size=QSize(width, height);
618
LOG("Thumbnail for" << mOriginalUri << "does not contain correct image size information");
619
KFileMetaInfo fmi(mCurrentUrl);
621
KFileMetaInfoItem item=fmi.item("Dimensions");
622
if (item.isValid()) {
623
size=item.value().toSize();
625
LOG("KFileMetaInfoItem for" << mOriginalUri << "did not get image size information");
628
LOG("Could not get a valid KFileMetaInfo instance for" << mOriginalUri);
631
emitThumbnailLoaded(thumb, size);
637
// Thumbnail not found or not valid
638
if (MimeTypeUtils::fileItemKind(mCurrentItem) == MimeTypeUtils::KIND_RASTER_IMAGE) {
639
if (mCurrentUrl.isLocalFile()) {
640
// Original is a local file, create the thumbnail
641
startCreatingThumbnail(mCurrentUrl.toLocalFile());
643
// Original is remote, download it
644
mState=STATE_DOWNLOADORIG;
646
KTemporaryFile tempFile;
647
tempFile.setAutoRemove(false);
648
if (!tempFile.open()) {
649
kWarning() << "Couldn't create temp file to download " << mCurrentUrl.prettyUrl();
650
emitThumbnailLoadingFailed();
654
mTempPath = tempFile.fileName();
657
url.setPath(mTempPath);
658
KIO::Job* job=KIO::file_copy(mCurrentUrl, url,-1, KIO::Overwrite | KIO::HideProgressInfo);
659
job->ui()->setWindow(KApplication::kApplication()->activeWindow());
660
LOG("Download remote file" << mCurrentUrl.prettyUrl() << "to" << url.pathOrUrl());
664
// Not a raster image, use a KPreviewJob
665
LOG("Starting a KPreviewJob for" << mCurrentItem.url());
666
mState=STATE_PREVIEWJOB;
668
list.append(mCurrentItem);
669
const int pixelSize = ThumbnailGroup::pixelSize(mThumbnailGroup);
670
KIO::Job* job = KIO::filePreview(list, QSize(pixelSize, pixelSize));
671
//job->ui()->setWindow(KApplication::kApplication()->activeWindow());
672
connect(job, SIGNAL(gotPreview(const KFileItem&, const QPixmap&)),
673
this, SLOT(slotGotPreview(const KFileItem&, const QPixmap&)) );
674
connect(job, SIGNAL(failed(const KFileItem&)),
675
this, SLOT(emitThumbnailLoadingFailed()) );
680
void ThumbnailLoadJob::startCreatingThumbnail(const QString& pixPath) {
681
LOG("Creating thumbnail from" << pixPath);
682
mThumbnailThread.load( mOriginalUri, mOriginalTime, mCurrentItem.size(),
683
mCurrentItem.mimetype(), pixPath, mThumbnailPath, mThumbnailGroup);
687
void ThumbnailLoadJob::slotGotPreview(const KFileItem& item, const QPixmap& pixmap) {
688
LOG(mCurrentItem.url());
690
emit thumbnailLoaded(item, pixmap, size);
694
void ThumbnailLoadJob::emitThumbnailLoaded(const QImage& img, const QSize& size) {
695
LOG(mCurrentItem.url());
696
QPixmap thumb = QPixmap::fromImage(img);
697
emit thumbnailLoaded(mCurrentItem, thumb, size);
701
void ThumbnailLoadJob::emitThumbnailLoadingFailed() {
702
LOG(mCurrentItem.url());
703
emit thumbnailLoadingFailed(mCurrentItem);
707
bool ThumbnailLoadJob::isPendingThumbnailCacheEmpty() {
708
return sThumbnailCache->isEmpty();