You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gwenview/src/gvcore/thumbnailloadjob.cpp

764 lines
21 KiB

// vim: set tabstop=4 shiftwidth=4 noexpandtab:
/* Gwenview - A simple image viewer for TDE
Copyright 2000-2004 Aur<75>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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "thumbnailloadjob.moc"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <assert.h>
// TQt
#include <tqdir.h>
#include <tqfile.h>
#include <tqimage.h>
#include <tqpixmap.h>
#include <tqtimer.h>
// KDE
#include <tdeapplication.h>
#include <kdebug.h>
#include <tdefileitem.h>
#include <kiconloader.h>
#include <tdeio/previewjob.h>
#include <klargefile.h>
#include <kmdcodec.h>
#include <kstandarddirs.h>
#include <tdetempfile.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 TQString generateOriginalURI(KURL url) {
// Don't include the password if any
url.setPass(TQString());
return url.url();
}
static TQString generateThumbnailPath(const TQString& uri, int size) {
KMD5 md5( TQFile::encodeName(uri).data() );
TQString baseDir=ThumbnailLoadJob::thumbnailBaseDir(size);
return baseDir + TQString(TQFile::encodeName( md5.hexDigest())) + ".png";
}
//------------------------------------------------------------------------
//
// ThumbnailThread
//
//------------------------------------------------------------------------
void ThumbnailThread::load(
const TQString& originalURI, time_t originalTime, int originalSize, const TQString& originalMimeType,
const TQString& pixPath,
const TQString& thumbnailPath,
int size, bool storeThumbnail)
{
TQMutexLocker 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() {
TQMutexLocker lock( &mMutex );
while( !testCancel()) {
// empty mPixPath means nothing to do
while( mPixPath.isNull()) {
mCond.cancellableWait( &mMutex );
if( testCancel()) return;
}
loadThumbnail();
mPixPath = TQString(); // done, ready for next
TQSize size(mOriginalWidth, mOriginalHeight);
emitCancellableSignal( this, TQT_SIGNAL( done( const TQImage&, const TQSize&)), mImage, size);
}
}
void ThumbnailThread::loadThumbnail() {
mImage = TQImage();
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 TQt
if (!loaded) {
TQImage originalImage;
if (originalImage.load(mPixPath)) {
mOriginalWidth=originalImage.width();
mOriginalHeight=originalImage.height();
int thumbSize=mThumbnailSize<=ThumbnailSize::NORMAL ? ThumbnailSize::NORMAL : ThumbnailSize::LARGE;
if( testCancel()) return;
if (TQMAX(mOriginalWidth, mOriginalHeight)<=thumbSize ) {
mImage=originalImage;
needCaching = false;
} else {
mImage=ImageUtils::scale(originalImage,thumbSize,thumbSize,ImageUtils::SMOOTH_FAST,TQ_ScaleMin);
}
loaded = true;
}
}
if( testCancel()) return;
if( mStoreThumbnailsInCache && needCaching ) {
mImage.setText("Thumb::URI", 0, mOriginalURI);
mImage.setText("Thumb::MTime", 0, TQString::number(mOriginalTime));
mImage.setText("Thumb::Size", 0, TQString::number(mOriginalSize));
mImage.setText("Thumb::Mimetype", 0, mOriginalMimeType);
mImage.setText("Thumb::Image::Width", 0, TQString::number(mOriginalWidth));
mImage.setText("Thumb::Image::Height", 0, TQString::number(mOriginalHeight));
mImage.setText("Software", 0, "Gwenview");
TQString thumbnailDir = ThumbnailLoadJob::thumbnailBaseDir(mThumbnailSize);
TDEStandardDirs::makeDir(thumbnailDir, 0700);
KTempFile tmp(thumbnailDir + "/gwenview", ".png");
tmp.setAutoDelete(true);
if (tmp.status()!=0) {
TQString 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(TQFile::encodeName(tmp.name()), TQFile::encodeName(mThumbnailPath));
}
}
bool ThumbnailThread::isJPEG() {
TQString format=TQImageIO::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(TQFile::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 = TQMAX(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 TQImage
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, tqRgb(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;
TQRgb *out = (TQRgb*)( mImage.scanLine(j) );
for (uint i=cinfo.output_width; i--; ) {
in-=3;
out[i] = tqRgb(in[0], in[1], in[2]);
}
}
}
int newMax = TQMAX(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
//
//------------------------------------------------------------------------
TQString ThumbnailLoadJob::thumbnailBaseDir() {
static TQString dir;
if (!dir.isEmpty()) return dir;
dir=TQDir::homeDirPath() + "/.thumbnails/";
return dir;
}
TQString ThumbnailLoadJob::thumbnailBaseDir(int size) {
TQString dir = thumbnailBaseDir();
if (size<=ThumbnailSize::NORMAL) {
dir+="normal/";
} else {
dir+="large/";
}
return dir;
}
void ThumbnailLoadJob::deleteImageThumbnail(const KURL& url) {
TQString uri=generateOriginalURI(url);
TQFile::remove(generateThumbnailPath(uri, ThumbnailSize::NORMAL));
TQFile::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 TQValueVector<const KFileItem*>* items, int size)
: TDEIO::Job(false), mState( STATE_NEXTTHUMB ),
mCurrentVisibleIndex( -1 ), mFirstVisibleIndex( -1 ), mLastVisibleIndex( -1 ),
mThumbnailSize(size), mSuspended( false )
{
LOG("");
mBrokenPixmap=TDEGlobal::iconLoader()->loadIcon("file_broken",
TDEIcon::NoGroup, ThumbnailSize::MIN);
// Look for images and store the items in our todo list
Q_ASSERT(!items->empty());
mAllItems=*items;
mProcessedState.resize( mAllItems.count());
tqFill( mProcessedState.begin(), mProcessedState.end(), false );
mCurrentItem = NULL;
connect(&mThumbnailThread, TQT_SIGNAL(done(const TQImage&, const TQSize&)),
TQT_SLOT(thumbnailReady(const TQImage&, const TQSize&)) );
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 TDEIO if the file is local (faster)
if( mCurrentURL.isLocalFile()
&& !TDEIO::probably_slow_mounted( mCurrentURL.path())) {
KDE_struct_stat buff;
if ( KDE_stat( TQFile::encodeName(mCurrentURL.path()), &buff ) == 0 ) {
mOriginalTime = buff.st_mtime;
TQTimer::singleShot( 0, this, TQT_SLOT( checkThumbnail()));
}
}
if( mOriginalTime == 0 ) { // TDEIO must be used
TDEIO::Job* job = TDEIO::stat(mCurrentURL,false);
job->setWindow(TDEApplication::kApplication()->mainWidget());
LOG( "TDEIO::stat orig " << mCurrentURL.url() );
addSubjob(job);
}
}
void ThumbnailLoadJob::slotResult(TDEIO::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
TDEIO::UDSEntry entry = static_cast<TDEIO::StatJob*>(job)->statResult();
TDEIO::UDSEntry::ConstIterator it= entry.begin();
mOriginalTime = 0;
for (; it!=entry.end(); ++it) {
if ((*it).m_uds == TDEIO::UDS_MODIFICATION_TIME) {
mOriginalTime = (time_t)((*it).m_long);
break;
}
}
checkThumbnail();
return;
}
case STATE_DOWNLOADORIG:
if (job->error()) {
emitThumbnailLoadingFailed();
LOG("Delete temp file " << mTempPath);
TQFile::remove(mTempPath);
mTempPath = TQString();
determineNextIcon();
} else {
startCreatingThumbnail(mTempPath);
}
return;
case STATE_PREVIEWJOB:
determineNextIcon();
return;
}
}
void ThumbnailLoadJob::thumbnailReady( const TQImage& im, const TQSize& _size) {
TQImage img = TSDeepCopy( im );
TQSize size = _size;
if ( !img.isNull()) {
emitThumbnailLoaded(img, size);
} else {
emitThumbnailLoadingFailed();
}
if( !mTempPath.isEmpty()) {
LOG("Delete temp file " << mTempPath);
TQFile::remove(mTempPath);
mTempPath = TQString();
}
determineNextIcon();
}
void ThumbnailLoadJob::checkThumbnail() {
// If we are in the thumbnail dir, just load the file
if (mCurrentURL.isLocalFile()
&& mCurrentURL.directory(false).startsWith(thumbnailBaseDir()) )
{
TQImage image(mCurrentURL.path());
emitThumbnailLoaded(image, image.size());
determineNextIcon();
return;
}
TQSize imagesize;
if( mOriginalTime == time_t( Cache::instance()->timestamp( mCurrentURL ).toTime_t())) {
TQPixmap 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);
TQImage thumb;
if ( thumb.load(mThumbnailPath) ) {
if (thumb.text("Thumb::URI", 0) == mOriginalURI &&
thumb.text("Thumb::MTime", 0).toInt() == mOriginalTime )
{
int width=0, height=0;
TQSize 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=TQSize(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);
TDEIO::Job* job=TDEIO::file_copy(mCurrentURL, url,-1,true,false,false);
job->setWindow(TDEApplication::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);
TDEIO::Job* job=TDEIO::filePreview(list, mThumbnailSize);
job->setWindow(TDEApplication::kApplication()->mainWidget());
connect(job, TQT_SIGNAL(gotPreview(const KFileItem*, const TQPixmap&)),
this, TQT_SLOT(slotGotPreview(const KFileItem*, const TQPixmap&)) );
connect(job, TQT_SIGNAL(failed(const KFileItem*)),
this, TQT_SLOT(emitThumbnailLoadingFailed()) );
addSubjob(job);
return;
}
}
void ThumbnailLoadJob::startCreatingThumbnail(const TQString& 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 TQPixmap& pixmap) {
LOG("");
TQSize size;
emit thumbnailLoaded(item, pixmap, size);
}
void ThumbnailLoadJob::emitThumbnailLoaded(const TQImage& img, TQSize size) {
int biggestDimension=TQMAX(img.width(), img.height());
TQImage thumbImg;
if (biggestDimension>mThumbnailSize) {
// Scale down thumbnail if necessary
thumbImg=ImageUtils::scale(img,mThumbnailSize, mThumbnailSize, ImageUtils::SMOOTH_FAST,TQ_ScaleMin);
} else {
thumbImg=img;
}
TQDateTime tm;
tm.setTime_t( mOriginalTime );
TQPixmap thumb( thumbImg ); // store as TQPixmap in cache (faster to retrieve, no conversion needed)
Cache::instance()->addThumbnail( mCurrentURL, thumb, size, tm );
emit thumbnailLoaded(mCurrentItem, thumb, size);
}
void ThumbnailLoadJob::emitThumbnailLoadingFailed() {
TQSize size;
emit thumbnailLoaded(mCurrentItem, mBrokenPixmap, size);
}
} // namespace