diff options
Diffstat (limited to 'src/gvcore/thumbnailloadjob.cpp')
-rw-r--r-- | src/gvcore/thumbnailloadjob.cpp | 763 |
1 files changed, 763 insertions, 0 deletions
diff --git a/src/gvcore/thumbnailloadjob.cpp b/src/gvcore/thumbnailloadjob.cpp new file mode 100644 index 0000000..92290ac --- /dev/null +++ b/src/gvcore/thumbnailloadjob.cpp @@ -0,0 +1,763 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* Gwenview - A simple image viewer for KDE + Copyright 2000-2004 Aurélien Gâteau + This class is based on the ImagePreviewJob class from Konqueror. + Original copyright follows. +*/ +/* 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ +#include "thumbnailloadjob.moc" + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <assert.h> + +// Qt +#include <qdir.h> +#include <qfile.h> +#include <qimage.h> +#include <qpixmap.h> +#include <qtimer.h> + +// KDE +#include <kapplication.h> +#include <kdebug.h> +#include <kfileitem.h> +#include <kiconloader.h> +#include <kio/previewjob.h> +#include <klargefile.h> +#include <kmdcodec.h> +#include <kstandarddirs.h> +#include <ktempfile.h> + +// libjpeg +#include <setjmp.h> +#define XMD_H +extern "C" { +#include <jpeglib.h> +} + +// Local +#include "cache.h" +#include "mimetypeutils.h" +#include "miscconfig.h" +#include "imageutils/jpegcontent.h" +#include "imageutils/imageutils.h" +#include "thumbnailsize.h" +#include "fileviewconfig.h" +namespace Gwenview { + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + + +static QString generateOriginalURI(KURL url) { + // Don't include the password if any + url.setPass(QString::null); + return url.url(); +} + + +static QString generateThumbnailPath(const QString& uri, int size) { + KMD5 md5( QFile::encodeName(uri) ); + QString baseDir=ThumbnailLoadJob::thumbnailBaseDir(size); + return baseDir + QString(QFile::encodeName( md5.hexDigest())) + ".png"; +} + +//------------------------------------------------------------------------ +// +// ThumbnailThread +// +//------------------------------------------------------------------------ +void ThumbnailThread::load( + const QString& originalURI, time_t originalTime, int originalSize, const QString& originalMimeType, + const QString& pixPath, + const QString& thumbnailPath, + int size, bool storeThumbnail) +{ + QMutexLocker lock( &mMutex ); + assert( mPixPath.isNull()); + + mOriginalURI = TSDeepCopy(originalURI); + mOriginalTime = originalTime; + mOriginalSize = originalSize; + mOriginalMimeType = TSDeepCopy(originalMimeType); + mPixPath = TSDeepCopy(pixPath); + mThumbnailPath = TSDeepCopy(thumbnailPath); + mThumbnailSize = size; + mStoreThumbnailsInCache = storeThumbnail; + if(!running()) start(); + mCond.wakeOne(); +} + +void ThumbnailThread::run() { + QMutexLocker lock( &mMutex ); + while( !testCancel()) { + // empty mPixPath means nothing to do + while( mPixPath.isNull()) { + mCond.cancellableWait( &mMutex ); + if( testCancel()) return; + } + loadThumbnail(); + mPixPath = QString(); // done, ready for next + QSize size(mOriginalWidth, mOriginalHeight); + emitCancellableSignal( this, SIGNAL( done( const QImage&, const QSize&)), mImage, size); + } +} + +void ThumbnailThread::loadThumbnail() { + mImage = QImage(); + bool loaded=false; + bool needCaching=true; + + // If it's a JPEG, try to load a small image directly from the file + if (isJPEG()) { + ImageUtils::JPEGContent content; + content.load(mPixPath); + mOriginalWidth = content.size().width(); + mOriginalHeight = content.size().height(); + mImage = content.thumbnail(); + + if( !mImage.isNull() + && ( mImage.width() >= mThumbnailSize // don't use small thumbnails + || mImage.height() >= mThumbnailSize )) { + loaded = true; + needCaching = false; + } + if(!loaded) { + loaded=loadJPEG(); + } + if (loaded && MiscConfig::autoRotateImages()) { + // Rotate if necessary + ImageUtils::Orientation orientation = content.orientation(); + mImage=ImageUtils::transform(mImage,orientation); + } + } + // File is not a JPEG, or JPEG optimized load failed, load file using Qt + if (!loaded) { + QImage originalImage; + if (originalImage.load(mPixPath)) { + mOriginalWidth=originalImage.width(); + mOriginalHeight=originalImage.height(); + int thumbSize=mThumbnailSize<=ThumbnailSize::NORMAL ? ThumbnailSize::NORMAL : ThumbnailSize::LARGE; + + if( testCancel()) return; + + if (QMAX(mOriginalWidth, mOriginalHeight)<=thumbSize ) { + mImage=originalImage; + needCaching = false; + } else { + mImage=ImageUtils::scale(originalImage,thumbSize,thumbSize,ImageUtils::SMOOTH_FAST,QImage::ScaleMin); + } + loaded = true; + } + } + + if( testCancel()) return; + + if( mStoreThumbnailsInCache && needCaching ) { + mImage.setText("Thumb::URI", 0, mOriginalURI); + mImage.setText("Thumb::MTime", 0, QString::number(mOriginalTime)); + mImage.setText("Thumb::Size", 0, QString::number(mOriginalSize)); + mImage.setText("Thumb::Mimetype", 0, mOriginalMimeType); + mImage.setText("Thumb::Image::Width", 0, QString::number(mOriginalWidth)); + mImage.setText("Thumb::Image::Height", 0, QString::number(mOriginalHeight)); + mImage.setText("Software", 0, "Gwenview"); + + QString thumbnailDir = ThumbnailLoadJob::thumbnailBaseDir(mThumbnailSize); + KStandardDirs::makeDir(thumbnailDir, 0700); + + KTempFile tmp(thumbnailDir + "/gwenview", ".png"); + tmp.setAutoDelete(true); + if (tmp.status()!=0) { + QString reason( strerror(tmp.status()) ); + kdWarning() << "Could not create a temporary file.\nReason: " << reason << endl; + return; + } + + if (!mImage.save(tmp.name(), "PNG")) { + kdWarning() << "Could not save thumbnail for file " << mOriginalURI << endl; + return; + } + + rename(QFile::encodeName(tmp.name()), QFile::encodeName(mThumbnailPath)); + } +} + + +bool ThumbnailThread::isJPEG() { + QString format=QImageIO::imageFormat(mPixPath); + return format=="JPEG"; +} + + + +struct JPEGFatalError : public jpeg_error_mgr { + jmp_buf mJmpBuffer; + + static void handler(j_common_ptr cinfo) { + JPEGFatalError* error=static_cast<JPEGFatalError*>(cinfo->err); + (error->output_message)(cinfo); + longjmp(error->mJmpBuffer,1); + } +}; + +bool ThumbnailThread::loadJPEG() { + struct jpeg_decompress_struct cinfo; + + // Open file + FILE* inputFile=fopen(QFile::encodeName( mPixPath ).data(), "rb"); + if(!inputFile) return false; + + // Error handling + struct JPEGFatalError jerr; + cinfo.err = jpeg_std_error(&jerr); + cinfo.err->error_exit = JPEGFatalError::handler; + if (setjmp(jerr.mJmpBuffer)) { + jpeg_destroy_decompress(&cinfo); + fclose(inputFile); + return false; + } + + // Init decompression + jpeg_create_decompress(&cinfo); + jpeg_stdio_src(&cinfo, inputFile); + jpeg_read_header(&cinfo, TRUE); + + // Get image size and check if we need a thumbnail + int size= mThumbnailSize <= ThumbnailSize::NORMAL ? ThumbnailSize::NORMAL : ThumbnailSize::LARGE; + int imgSize = QMAX(cinfo.image_width, cinfo.image_height); + + if (imgSize<=size) { + fclose(inputFile); + return mImage.load(mPixPath); + } + + // Compute scale value + int scale=1; + while(size*scale*2<=imgSize) { + scale*=2; + } + if(scale>8) scale=8; + + cinfo.scale_num=1; + cinfo.scale_denom=scale; + + // Create QImage + jpeg_start_decompress(&cinfo); + + switch(cinfo.output_components) { + case 3: + case 4: + mImage.create( cinfo.output_width, cinfo.output_height, 32 ); + break; + case 1: // B&W image + mImage.create( cinfo.output_width, cinfo.output_height, 8, 256 ); + for (int i=0; i<256; i++) + mImage.setColor(i, qRgb(i,i,i)); + break; + default: + jpeg_destroy_decompress(&cinfo); + fclose(inputFile); + return false; + } + + uchar** lines = mImage.jumpTable(); + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, lines + cinfo.output_scanline, cinfo.output_height); + } + jpeg_finish_decompress(&cinfo); + +// Expand 24->32 bpp + if ( cinfo.output_components == 3 ) { + for (uint j=0; j<cinfo.output_height; j++) { + uchar *in = mImage.scanLine(j) + cinfo.output_width*3; + QRgb *out = (QRgb*)( mImage.scanLine(j) ); + + for (uint i=cinfo.output_width; i--; ) { + in-=3; + out[i] = qRgb(in[0], in[1], in[2]); + } + } + } + + int newMax = QMAX(cinfo.output_width, cinfo.output_height); + int newx = size*cinfo.output_width / newMax; + int newy = size*cinfo.output_height / newMax; + + mImage=ImageUtils::scale(mImage,newx, newy,ImageUtils::SMOOTH_FAST); + + jpeg_destroy_decompress(&cinfo); + fclose(inputFile); + + return true; +} + + +//------------------------------------------------------------------------ +// +// ThumbnailLoadJob static methods +// +//------------------------------------------------------------------------ +QString ThumbnailLoadJob::thumbnailBaseDir() { + static QString dir; + if (!dir.isEmpty()) return dir; + dir=QDir::homeDirPath() + "/.thumbnails/"; + return dir; +} + + +QString ThumbnailLoadJob::thumbnailBaseDir(int size) { + QString dir = thumbnailBaseDir(); + if (size<=ThumbnailSize::NORMAL) { + dir+="normal/"; + } else { + dir+="large/"; + } + return dir; +} + + +void ThumbnailLoadJob::deleteImageThumbnail(const KURL& url) { + QString uri=generateOriginalURI(url); + QFile::remove(generateThumbnailPath(uri, ThumbnailSize::NORMAL)); + QFile::remove(generateThumbnailPath(uri, ThumbnailSize::LARGE)); +} + + +//------------------------------------------------------------------------ +// +// ThumbnailLoadJob implementation +// +//------------------------------------------------------------------------ + + +/* + + This class tries to first generate the most important thumbnails, i.e. + first the currently selected one, then the ones that are visible, and then + the rest, the closer the the currently selected one the sooner + + mAllItems contains all thumbnails + mItems contains pending thumbnails, in the priority order + mCurrentItem is currently processed thumbnail, already removed from mItems + mProcessedState needs to match mAllItems, and contains information about every + thumbnail whether it has been already processed + + thumbnailIndex() returns index of a thumbnail in mAllItems, or -1 + updateItemsOrder() builds mItems from mAllItems +*/ + +ThumbnailLoadJob::ThumbnailLoadJob(const QValueVector<const KFileItem*>* items, int size) +: KIO::Job(false), mState( STATE_NEXTTHUMB ), + mCurrentVisibleIndex( -1 ), mFirstVisibleIndex( -1 ), mLastVisibleIndex( -1 ), + mThumbnailSize(size), mSuspended( false ) +{ + LOG(""); + + mBrokenPixmap=KGlobal::iconLoader()->loadIcon("file_broken", + KIcon::NoGroup, ThumbnailSize::MIN); + + // Look for images and store the items in our todo list + Q_ASSERT(!items->empty()); + mAllItems=*items; + mProcessedState.resize( mAllItems.count()); + qFill( mProcessedState.begin(), mProcessedState.end(), false ); + mCurrentItem = NULL; + + connect(&mThumbnailThread, SIGNAL(done(const QImage&, const QSize&)), + SLOT(thumbnailReady(const QImage&, const QSize&)) ); + Cache::instance()->updateAge(); // see addThumbnail in Cache +} + + +ThumbnailLoadJob::~ThumbnailLoadJob() { + LOG(""); + mThumbnailThread.cancel(); + mThumbnailThread.wait(); +} + + +void ThumbnailLoadJob::start() { + // build mItems from mAllItems if not done yet + if (mLastVisibleIndex == -1 ) { + setPriorityItems( NULL, NULL, NULL ); + } + if (mItems.isEmpty()) { + LOG("Nothing to do"); + emit result(this); + delete this; + return; + } + + determineNextIcon(); +} + +void ThumbnailLoadJob::suspend() { + mSuspended = true; +} + +void ThumbnailLoadJob::resume() { + if( !mSuspended ) return; + mSuspended = false; + if( mState == STATE_NEXTTHUMB ) // don't load next while already loading + determineNextIcon(); +} + +//-Internal-------------------------------------------------------------- +void ThumbnailLoadJob::appendItem(const KFileItem* item) { + int index = thumbnailIndex( item ); + if( index >= 0 ) { + mProcessedState[ index ] = false; + return; + } + mAllItems.append(item); + mProcessedState.append( false ); + updateItemsOrder(); +} + + +void ThumbnailLoadJob::itemRemoved(const KFileItem* item) { + Q_ASSERT(item); + + // If we are removing the next item, update to be the item after or the + // first if we removed the last item + mItems.remove( item ); + int index = thumbnailIndex( item ); + if( index >= 0 ) { + mAllItems.erase( mAllItems.begin() + index ); + mProcessedState.erase( mProcessedState.begin() + index ); + } + + if (item == mCurrentItem) { + // Abort + mCurrentItem = NULL; + if (subjobs.first()) { + subjobs.first()->kill(); + subjobs.removeFirst(); + } + determineNextIcon(); + } +} + + +void ThumbnailLoadJob::setPriorityItems(const KFileItem* current, const KFileItem* first, const KFileItem* last) { + if( mAllItems.isEmpty()) { + mCurrentVisibleIndex = mFirstVisibleIndex = mLastVisibleIndex = 0; + return; + } + mFirstVisibleIndex = -1; + mLastVisibleIndex = - 1; + mCurrentVisibleIndex = -1; + if( first != NULL ) mFirstVisibleIndex = thumbnailIndex( first ); + if( last != NULL ) mLastVisibleIndex = thumbnailIndex( last ); + if( current != NULL ) mCurrentVisibleIndex = thumbnailIndex( current ); + if( mFirstVisibleIndex == -1 ) mFirstVisibleIndex = 0; + if( mLastVisibleIndex == -1 ) mLastVisibleIndex = mAllItems.count() - 1; + if( mCurrentVisibleIndex == -1 ) mCurrentVisibleIndex = mFirstVisibleIndex; + updateItemsOrder(); +} + +void ThumbnailLoadJob::updateItemsOrder() { + mItems.clear(); + int forward = mCurrentVisibleIndex + 1; + int backward = mCurrentVisibleIndex; + int first = mFirstVisibleIndex; + int last = mLastVisibleIndex; + updateItemsOrderHelper( forward, backward, first, last ); + if( first != 0 || last != int( mAllItems.count()) - 1 ) { + // add non-visible items + updateItemsOrderHelper( last + 1, first - 1, 0, mAllItems.count() - 1); + } +} + +void ThumbnailLoadJob::updateItemsOrderHelper( int forward, int backward, int first, int last ) { + // start from the current item, add one following it, and one preceding it, for all visible items + while( forward <= last || backward >= first ) { + // start with backward - that's the curent item for the first time + while( backward >= first ) { + if( !mProcessedState[ backward ] ) { + mItems.append( mAllItems[ backward ] ); + --backward; + break; + } + --backward; + } + while( forward <= last ) { + if( !mProcessedState[ forward ] ) { + mItems.append( mAllItems[ forward ] ); + ++forward; + break; + } + ++forward; + } + } +} + +void ThumbnailLoadJob::determineNextIcon() { + mState = STATE_NEXTTHUMB; + if( mSuspended ) { + return; + } + + // No more items ? + if (mItems.isEmpty()) { + // Done + LOG("emitting result"); + emit result(this); + delete this; + return; + } + + mCurrentItem=mItems.first(); + mItems.pop_front(); + Q_ASSERT( !mProcessedState[ thumbnailIndex( mCurrentItem )] ); + mProcessedState[ thumbnailIndex( mCurrentItem )] = true; + + // First, stat the orig file + mState = STATE_STATORIG; + mOriginalTime = 0; + mCurrentURL = mCurrentItem->url(); + mCurrentURL.cleanPath(); + + // Do direct stat instead of using KIO if the file is local (faster) + if( mCurrentURL.isLocalFile() + && !KIO::probably_slow_mounted( mCurrentURL.path())) { + KDE_struct_stat buff; + if ( KDE_stat( QFile::encodeName(mCurrentURL.path()), &buff ) == 0 ) { + mOriginalTime = buff.st_mtime; + QTimer::singleShot( 0, this, SLOT( checkThumbnail())); + } + } + if( mOriginalTime == 0 ) { // KIO must be used + KIO::Job* job = KIO::stat(mCurrentURL,false); + job->setWindow(KApplication::kApplication()->mainWidget()); + LOG( "KIO::stat orig " << mCurrentURL.url() ); + addSubjob(job); + } +} + + +void ThumbnailLoadJob::slotResult(KIO::Job * job) { + LOG(mState); + subjobs.remove(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(); + KIO::UDSEntry::ConstIterator it= entry.begin(); + mOriginalTime = 0; + for (; it!=entry.end(); ++it) { + if ((*it).m_uds == KIO::UDS_MODIFICATION_TIME) { + mOriginalTime = (time_t)((*it).m_long); + break; + } + } + checkThumbnail(); + return; + } + + case STATE_DOWNLOADORIG: + if (job->error()) { + emitThumbnailLoadingFailed(); + LOG("Delete temp file " << mTempPath); + QFile::remove(mTempPath); + mTempPath = QString::null; + determineNextIcon(); + } else { + startCreatingThumbnail(mTempPath); + } + return; + + case STATE_PREVIEWJOB: + determineNextIcon(); + return; + } +} + + +void ThumbnailLoadJob::thumbnailReady( const QImage& im, const QSize& _size) { + QImage img = TSDeepCopy( im ); + QSize size = _size; + if ( !img.isNull()) { + emitThumbnailLoaded(img, size); + } else { + emitThumbnailLoadingFailed(); + } + if( !mTempPath.isEmpty()) { + LOG("Delete temp file " << mTempPath); + QFile::remove(mTempPath); + mTempPath = QString::null; + } + determineNextIcon(); +} + +void ThumbnailLoadJob::checkThumbnail() { + // If we are in the thumbnail dir, just load the file + if (mCurrentURL.isLocalFile() + && mCurrentURL.directory(false).startsWith(thumbnailBaseDir()) ) + { + QImage image(mCurrentURL.path()); + emitThumbnailLoaded(image, image.size()); + determineNextIcon(); + return; + } + QSize imagesize; + if( mOriginalTime == time_t( Cache::instance()->timestamp( mCurrentURL ).toTime_t())) { + QPixmap cached = Cache::instance()->thumbnail( mCurrentURL, imagesize ); + if( !cached.isNull()) { + emit thumbnailLoaded(mCurrentItem, cached, imagesize); + determineNextIcon(); + return; + } + } + + mOriginalURI=generateOriginalURI(mCurrentURL); + mThumbnailPath=generateThumbnailPath(mOriginalURI, mThumbnailSize); + + LOG("Stat thumb " << mThumbnailPath); + + QImage thumb; + if ( thumb.load(mThumbnailPath) ) { + if (thumb.text("Thumb::URI", 0) == mOriginalURI && + thumb.text("Thumb::MTime", 0).toInt() == mOriginalTime ) + { + int width=0, height=0; + QSize size; + bool ok; + + width=thumb.text("Thumb::Image::Width", 0).toInt(&ok); + if (ok) height=thumb.text("Thumb::Image::Height", 0).toInt(&ok); + if (ok) { + size=QSize(width, height); + } else { + LOG("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 { + LOG("KFileMetaInfoItem for " << mOriginalURI << " did not get image size information"); + } + } else { + LOG("Could not get a valid KFileMetaInfo instance for " << mOriginalURI); + } + } + emitThumbnailLoaded(thumb, size); + determineNextIcon(); + return; + } + } + + // Thumbnail not found or not valid + if ( MimeTypeUtils::rasterImageMimeTypes().contains(mCurrentItem->mimetype()) ) { + // This is a raster image + if (mCurrentURL.isLocalFile()) { + // Original is a local file, create the thumbnail + startCreatingThumbnail(mCurrentURL.path()); + } else { + // Original is remote, download it + mState=STATE_DOWNLOADORIG; + KTempFile tmpFile; + mTempPath=tmpFile.name(); + KURL url; + url.setPath(mTempPath); + KIO::Job* job=KIO::file_copy(mCurrentURL, url,-1,true,false,false); + job->setWindow(KApplication::kApplication()->mainWidget()); + LOG("Download remote file " << mCurrentURL.prettyURL()); + addSubjob(job); + } + } else { + // Not a raster image, use a KPreviewJob + mState=STATE_PREVIEWJOB; + KFileItemList list; + list.append(mCurrentItem); + KIO::Job* job=KIO::filePreview(list, mThumbnailSize); + job->setWindow(KApplication::kApplication()->mainWidget()); + connect(job, SIGNAL(gotPreview(const KFileItem*, const QPixmap&)), + this, SLOT(slotGotPreview(const KFileItem*, const QPixmap&)) ); + connect(job, SIGNAL(failed(const KFileItem*)), + this, SLOT(emitThumbnailLoadingFailed()) ); + addSubjob(job); + return; + } +} + +void ThumbnailLoadJob::startCreatingThumbnail(const QString& pixPath) { + LOG("Creating thumbnail from " << pixPath); + mThumbnailThread.load( mOriginalURI, mOriginalTime, mCurrentItem->size(), + mCurrentItem->mimetype(), pixPath, mThumbnailPath, mThumbnailSize, + FileViewConfig::storeThumbnailsInCache()); +} + + +void ThumbnailLoadJob::slotGotPreview(const KFileItem* item, const QPixmap& pixmap) { + LOG(""); + QSize size; + emit thumbnailLoaded(item, pixmap, size); +} + + +void ThumbnailLoadJob::emitThumbnailLoaded(const QImage& img, QSize size) { + int biggestDimension=QMAX(img.width(), img.height()); + + QImage thumbImg; + if (biggestDimension>mThumbnailSize) { + // Scale down thumbnail if necessary + thumbImg=ImageUtils::scale(img,mThumbnailSize, mThumbnailSize, ImageUtils::SMOOTH_FAST,QImage::ScaleMin); + } else { + thumbImg=img; + } + QDateTime tm; + tm.setTime_t( mOriginalTime ); + QPixmap thumb( thumbImg ); // store as QPixmap in cache (faster to retrieve, no conversion needed) + Cache::instance()->addThumbnail( mCurrentURL, thumb, size, tm ); + emit thumbnailLoaded(mCurrentItem, thumb, size); +} + + +void ThumbnailLoadJob::emitThumbnailLoadingFailed() { + QSize size; + emit thumbnailLoaded(mCurrentItem, mBrokenPixmap, size); +} + + +} // namespace |