|
|
|
|
// vim: set tabstop=4 shiftwidth=4 noexpandtab
|
|
|
|
|
/*
|
|
|
|
|
Gwenview - A simple image viewer for KDE
|
|
|
|
|
Copyright 2000-2004 Aur<EFBFBD>lien G<EFBFBD>teau
|
|
|
|
|
|
|
|
|
|
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 "imageloader.h"
|
|
|
|
|
|
|
|
|
|
#include <assert.h>
|
|
|
|
|
|
|
|
|
|
// Qt
|
|
|
|
|
#include <qtimer.h>
|
|
|
|
|
#include <qwmatrix.h>
|
|
|
|
|
|
|
|
|
|
// KDE
|
|
|
|
|
#include <kapplication.h>
|
|
|
|
|
#include <kimageio.h>
|
|
|
|
|
#include <kmimetype.h>
|
|
|
|
|
|
|
|
|
|
// Local
|
|
|
|
|
#include "cache.h"
|
|
|
|
|
#include "miscconfig.h"
|
|
|
|
|
#include "imageutils/imageutils.h"
|
|
|
|
|
#include "imageutils/jpegcontent.h"
|
|
|
|
|
|
|
|
|
|
#include "imageloader.moc"
|
|
|
|
|
namespace Gwenview {
|
|
|
|
|
|
|
|
|
|
const unsigned int DECODE_CHUNK_SIZE=4096;
|
|
|
|
|
|
|
|
|
|
/** Interval between image updates, in milli seconds */
|
|
|
|
|
const int IMAGE_UPDATE_INTERVAL=100;
|
|
|
|
|
|
|
|
|
|
#undef ENABLE_LOG
|
|
|
|
|
#undef LOG
|
|
|
|
|
#undef LOG2
|
|
|
|
|
|
|
|
|
|
#define ENABLE_LOG 0
|
|
|
|
|
|
|
|
|
|
#if ENABLE_LOG >= 1
|
|
|
|
|
#define LOG(x) kdDebug() << k_funcinfo << x << endl
|
|
|
|
|
#else
|
|
|
|
|
#define LOG(x) ;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#if ENABLE_LOG >= 2
|
|
|
|
|
#define LOG2(x) kdDebug() << k_funcinfo << x << endl
|
|
|
|
|
#else
|
|
|
|
|
#define LOG2(x) ;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
static QMap< KURL, ImageLoader* > sLoaders;
|
|
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// CancellableBuffer
|
|
|
|
|
// This class acts like QBuffer, but will simulates a truncated file if the
|
|
|
|
|
// TSThread which was passed to its constructor has been asked for cancellation
|
|
|
|
|
//
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
class CancellableBuffer : public QBuffer {
|
|
|
|
|
public:
|
|
|
|
|
CancellableBuffer(QByteArray buffer, TSThread* thread)
|
|
|
|
|
: QBuffer(buffer), mThread(thread) {}
|
|
|
|
|
|
|
|
|
|
bool atEnd() const {
|
|
|
|
|
if (mThread->testCancel()) {
|
|
|
|
|
LOG("cancel detected");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return QBuffer::atEnd();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Q_LONG readBlock(char * data, Q_ULONG maxlen) {
|
|
|
|
|
if (mThread->testCancel()) {
|
|
|
|
|
LOG("cancel detected");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return QBuffer::readBlock(data, maxlen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Q_LONG readLine(char * data, Q_ULONG maxlen) {
|
|
|
|
|
if (mThread->testCancel()) {
|
|
|
|
|
LOG("cancel detected");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return QBuffer::readLine(data, maxlen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray readAll() {
|
|
|
|
|
if (mThread->testCancel()) {
|
|
|
|
|
LOG("cancel detected");
|
|
|
|
|
return QByteArray();
|
|
|
|
|
}
|
|
|
|
|
return QBuffer::readAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int getch() {
|
|
|
|
|
if (mThread->testCancel()) {
|
|
|
|
|
LOG("cancel detected");
|
|
|
|
|
setStatus(IO_ReadError);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
return QBuffer::getch();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
TSThread* mThread;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// DecoderThread
|
|
|
|
|
//
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
void DecoderThread::run() {
|
|
|
|
|
QMutexLocker locker(&mMutex);
|
|
|
|
|
LOG("");
|
|
|
|
|
|
|
|
|
|
// This block makes sure imageIO won't access the image after the signal
|
|
|
|
|
// has been posted
|
|
|
|
|
{
|
|
|
|
|
QImageIO imageIO;
|
|
|
|
|
|
|
|
|
|
CancellableBuffer buffer(mRawData, this);
|
|
|
|
|
buffer.open(IO_ReadOnly);
|
|
|
|
|
imageIO.setIODevice(&buffer);
|
|
|
|
|
bool ok=imageIO.read();
|
|
|
|
|
if (testCancel()) {
|
|
|
|
|
LOG("cancelled");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ok) {
|
|
|
|
|
LOG("failed");
|
|
|
|
|
postSignal( this, SIGNAL(failed()) );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG("succeeded");
|
|
|
|
|
mImage=imageIO.image();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG("succeeded, emitting signal");
|
|
|
|
|
postSignal( this, SIGNAL(succeeded()) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void DecoderThread::setRawData(const QByteArray& data) {
|
|
|
|
|
QMutexLocker locker(&mMutex);
|
|
|
|
|
mRawData=data.copy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QImage DecoderThread::popLoadedImage() {
|
|
|
|
|
QMutexLocker locker(&mMutex);
|
|
|
|
|
QImage img=mImage;
|
|
|
|
|
mImage=QImage();
|
|
|
|
|
return img;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// ImageLoaderPrivate
|
|
|
|
|
//
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
struct OwnerData {
|
|
|
|
|
const QObject* owner;
|
|
|
|
|
BusyLevel priority;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
enum GetState {
|
|
|
|
|
GET_PENDING_STAT, // Stat has not been started
|
|
|
|
|
GET_STATING, // Stat has been started
|
|
|
|
|
GET_PENDING_GET, // Stat is done, get has not been started
|
|
|
|
|
GET_GETTING, // Get has been started
|
|
|
|
|
GET_DONE, // All data has been received
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
enum DecodeState {
|
|
|
|
|
DECODE_WAITING, // No data to decode yet
|
|
|
|
|
DECODE_PENDING_THREADED_DECODING, // Waiting for all data to start threaded decoding
|
|
|
|
|
DECODE_THREADED_DECODING, // Threaded decoder is running
|
|
|
|
|
DECODE_INCREMENTAL_DECODING, // Incremental decoder is running
|
|
|
|
|
DECODE_INCREMENTAL_DECODING_DONE, // Incremental decoder is done
|
|
|
|
|
DECODE_CACHED, // Image has been obtained from cache, but raw data was missing. Wait for get to finish.
|
|
|
|
|
DECODE_DONE, // All done
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class ImageLoaderPrivate {
|
|
|
|
|
public:
|
|
|
|
|
ImageLoaderPrivate(ImageLoader* impl)
|
|
|
|
|
: mDecodedSize(0)
|
|
|
|
|
, mGetState(GET_PENDING_STAT)
|
|
|
|
|
, mDecodeState(DECODE_WAITING)
|
|
|
|
|
, mDecoder(impl)
|
|
|
|
|
, mSuspended(false)
|
|
|
|
|
, mNextFrameDelay(0)
|
|
|
|
|
, mWasFrameData(false)
|
|
|
|
|
, mOrientation(ImageUtils::NOT_AVAILABLE)
|
|
|
|
|
, mURLKind(MimeTypeUtils::KIND_UNKNOWN)
|
|
|
|
|
{}
|
|
|
|
|
|
|
|
|
|
// How many of the raw data we have already decoded
|
|
|
|
|
unsigned int mDecodedSize;
|
|
|
|
|
|
|
|
|
|
GetState mGetState;
|
|
|
|
|
DecodeState mDecodeState;
|
|
|
|
|
|
|
|
|
|
KURL mURL;
|
|
|
|
|
|
|
|
|
|
// The file timestamp
|
|
|
|
|
QDateTime mTimestamp;
|
|
|
|
|
|
|
|
|
|
// The raw data we get
|
|
|
|
|
QByteArray mRawData;
|
|
|
|
|
|
|
|
|
|
// The async decoder and it's waking timer
|
|
|
|
|
QImageDecoder mDecoder;
|
|
|
|
|
QTimer mDecoderTimer;
|
|
|
|
|
|
|
|
|
|
// The decoder thread
|
|
|
|
|
DecoderThread mDecoderThread;
|
|
|
|
|
|
|
|
|
|
// A rect of recently loaded pixels that the rest of the application has
|
|
|
|
|
// not been notified about with the imageChanged() signal
|
|
|
|
|
QRect mLoadChangedRect;
|
|
|
|
|
|
|
|
|
|
// The time since we last emitted the imageChanged() signal
|
|
|
|
|
QTime mTimeSinceLastUpdate;
|
|
|
|
|
|
|
|
|
|
// Whether the loading should be suspended
|
|
|
|
|
bool mSuspended;
|
|
|
|
|
|
|
|
|
|
// Delay used for next frame after it's finished decoding.
|
|
|
|
|
int mNextFrameDelay;
|
|
|
|
|
|
|
|
|
|
bool mWasFrameData;
|
|
|
|
|
|
|
|
|
|
QImage mProcessedImage; // image frame currently being decoded
|
|
|
|
|
|
|
|
|
|
QRegion mLoadedRegion; // loaded parts of mProcessedImage
|
|
|
|
|
|
|
|
|
|
ImageFrames mFrames;
|
|
|
|
|
|
|
|
|
|
QCString mImageFormat;
|
|
|
|
|
|
|
|
|
|
ImageUtils::Orientation mOrientation;
|
|
|
|
|
|
|
|
|
|
QString mMimeType;
|
|
|
|
|
MimeTypeUtils::Kind mURLKind;
|
|
|
|
|
|
|
|
|
|
QValueVector< OwnerData > mOwners; // loaders may be shared
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void determineImageFormat() {
|
|
|
|
|
Q_ASSERT(mRawData.size()>0);
|
|
|
|
|
QBuffer buffer(mRawData);
|
|
|
|
|
buffer.open(IO_ReadOnly);
|
|
|
|
|
mImageFormat = QImageIO::imageFormat(&buffer);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// ImageLoader
|
|
|
|
|
//
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
ImageLoader::ImageLoader() {
|
|
|
|
|
LOG("");
|
|
|
|
|
d = new ImageLoaderPrivate(this);
|
|
|
|
|
connect( BusyLevelManager::instance(), SIGNAL( busyLevelChanged(BusyLevel)),
|
|
|
|
|
this, SLOT( slotBusyLevelChanged(BusyLevel)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ImageLoader::~ImageLoader() {
|
|
|
|
|
LOG("");
|
|
|
|
|
if (d->mDecoderThread.running()) {
|
|
|
|
|
d->mDecoderThread.cancel();
|
|
|
|
|
d->mDecoderThread.wait();
|
|
|
|
|
}
|
|
|
|
|
delete d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ImageLoader::setURL( const KURL& url ) {
|
|
|
|
|
assert( d->mURL.isEmpty());
|
|
|
|
|
d->mURL = url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::startLoading() {
|
|
|
|
|
d->mTimestamp = Cache::instance()->timestamp( d->mURL );
|
|
|
|
|
slotBusyLevelChanged( BusyLevelManager::instance()->busyLevel());
|
|
|
|
|
|
|
|
|
|
connect(&d->mDecoderTimer, SIGNAL(timeout()), this, SLOT(decodeChunk()) );
|
|
|
|
|
|
|
|
|
|
connect(&d->mDecoderThread, SIGNAL(succeeded()),
|
|
|
|
|
this, SLOT(slotDecoderThreadSucceeded()) );
|
|
|
|
|
connect(&d->mDecoderThread, SIGNAL(failed()),
|
|
|
|
|
this, SLOT(slotDecoderThreadFailed()) );
|
|
|
|
|
|
|
|
|
|
checkPendingStat();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::checkPendingStat() {
|
|
|
|
|
if( d->mSuspended || d->mGetState != GET_PENDING_STAT ) return;
|
|
|
|
|
|
|
|
|
|
KIO::Job* job=KIO::stat( d->mURL, false );
|
|
|
|
|
job->setWindow(KApplication::kApplication()->mainWidget());
|
|
|
|
|
connect(job, SIGNAL(result(KIO::Job*)),
|
|
|
|
|
this, SLOT(slotStatResult(KIO::Job*)) );
|
|
|
|
|
d->mGetState = GET_STATING;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::slotStatResult(KIO::Job* job) {
|
|
|
|
|
LOG("error code: " << job->error());
|
|
|
|
|
|
|
|
|
|
// Get modification time of the original file
|
|
|
|
|
KIO::UDSEntry entry = static_cast<KIO::StatJob*>(job)->statResult();
|
|
|
|
|
KIO::UDSEntry::ConstIterator it= entry.begin();
|
|
|
|
|
QDateTime urlTimestamp;
|
|
|
|
|
for (; it!=entry.end(); it++) {
|
|
|
|
|
if ((*it).m_uds == KIO::UDS_MODIFICATION_TIME) {
|
|
|
|
|
urlTimestamp.setTime_t( (*it).m_long );
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if( d->mTimestamp.isValid() && urlTimestamp == d->mTimestamp ) {
|
|
|
|
|
// We have the image in cache
|
|
|
|
|
LOG(d->mURL << ", We have the image in cache");
|
|
|
|
|
d->mRawData = Cache::instance()->file( d->mURL );
|
|
|
|
|
Cache::instance()->getFrames(d->mURL, &d->mFrames, &d->mImageFormat);
|
|
|
|
|
|
|
|
|
|
if( !d->mFrames.isEmpty()) {
|
|
|
|
|
LOG("The image in cache can be used");
|
|
|
|
|
d->mProcessedImage = d->mFrames[0].image;
|
|
|
|
|
emit sizeLoaded(d->mProcessedImage.width(), d->mProcessedImage.height());
|
|
|
|
|
emit imageChanged(d->mProcessedImage.rect());
|
|
|
|
|
|
|
|
|
|
if (d->mRawData.isNull() && d->mImageFormat=="JPEG") {
|
|
|
|
|
// Raw data is needed for JPEG, wait for it to be downloaded
|
|
|
|
|
LOG("Wait for raw data to be downloaded");
|
|
|
|
|
d->mDecodeState = DECODE_CACHED;
|
|
|
|
|
} else {
|
|
|
|
|
// We don't care about raw data
|
|
|
|
|
finish(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Image in cache is broken
|
|
|
|
|
LOG("The image in cache cannot be used");
|
|
|
|
|
if( !d->mRawData.isNull()) {
|
|
|
|
|
LOG("Using cached raw data");
|
|
|
|
|
// Raw data is ok, skip get step and decode it
|
|
|
|
|
d->mGetState = GET_DONE;
|
|
|
|
|
d->mTimeSinceLastUpdate.start();
|
|
|
|
|
d->mDecoderTimer.start(0, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d->mTimestamp = urlTimestamp;
|
|
|
|
|
d->mRawData.resize(0);
|
|
|
|
|
d->mGetState = GET_PENDING_GET;
|
|
|
|
|
checkPendingGet();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::checkPendingGet() {
|
|
|
|
|
if( d->mSuspended || d->mGetState != GET_PENDING_GET ) return;
|
|
|
|
|
|
|
|
|
|
// Start loading the image
|
|
|
|
|
KIO::Job* getJob=KIO::get( d->mURL, false, false);
|
|
|
|
|
getJob->setWindow(KApplication::kApplication()->mainWidget());
|
|
|
|
|
|
|
|
|
|
connect(getJob, SIGNAL(data(KIO::Job*, const QByteArray&)),
|
|
|
|
|
this, SLOT(slotDataReceived(KIO::Job*, const QByteArray&)) );
|
|
|
|
|
|
|
|
|
|
connect(getJob, SIGNAL(result(KIO::Job*)),
|
|
|
|
|
this, SLOT(slotGetResult(KIO::Job*)) );
|
|
|
|
|
|
|
|
|
|
d->mTimeSinceLastUpdate.start();
|
|
|
|
|
d->mGetState = GET_GETTING;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ImageLoader::slotGetResult(KIO::Job* job) {
|
|
|
|
|
LOG("error code: " << job->error());
|
|
|
|
|
if( job->error() != 0 ) {
|
|
|
|
|
// failed
|
|
|
|
|
finish( false );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d->mGetState = GET_DONE;
|
|
|
|
|
|
|
|
|
|
// Store raw data in cache
|
|
|
|
|
// Note: Cache will give high cost to non-JPEG raw data.
|
|
|
|
|
Cache::instance()->addFile( d->mURL, d->mRawData, d->mTimestamp );
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch (d->mDecodeState) {
|
|
|
|
|
case DECODE_CACHED:
|
|
|
|
|
// image was in cache, but not raw data
|
|
|
|
|
finish( true );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case DECODE_PENDING_THREADED_DECODING:
|
|
|
|
|
// Start the decoder thread if needed
|
|
|
|
|
startThread();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Finish decoding if needed
|
|
|
|
|
if (!d->mDecoderTimer.isActive()) d->mDecoderTimer.start(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// There is no way in KImageIO to get the mimeType from the image format.
|
|
|
|
|
// This function assumes KImageIO::types and KImageIO::mimeTypes return items
|
|
|
|
|
// in the same order (which they do, according to the source code).
|
|
|
|
|
static QString mimeTypeFromFormat(const char* format) {
|
|
|
|
|
QStringList formats = KImageIO::types(KImageIO::Reading);
|
|
|
|
|
QStringList mimeTypes = KImageIO::mimeTypes(KImageIO::Reading);
|
|
|
|
|
int pos = formats.findIndex(QString::fromAscii(format));
|
|
|
|
|
Q_ASSERT(pos != -1);
|
|
|
|
|
return mimeTypes[pos];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::slotDataReceived(KIO::Job* job, const QByteArray& chunk) {
|
|
|
|
|
LOG2("size: " << chunk.size());
|
|
|
|
|
if (chunk.size()<=0) return;
|
|
|
|
|
|
|
|
|
|
int oldSize=d->mRawData.size();
|
|
|
|
|
d->mRawData.resize(oldSize + chunk.size());
|
|
|
|
|
memcpy(d->mRawData.data()+oldSize, chunk.data(), chunk.size() );
|
|
|
|
|
|
|
|
|
|
if (oldSize==0) {
|
|
|
|
|
// Try to determine the data type
|
|
|
|
|
QBuffer buffer(d->mRawData);
|
|
|
|
|
buffer.open(IO_ReadOnly);
|
|
|
|
|
const char* format = QImageIO::imageFormat(&buffer);
|
|
|
|
|
if (format) {
|
|
|
|
|
// This is a raster image, get the mime type now
|
|
|
|
|
d->mURLKind = MimeTypeUtils::KIND_RASTER_IMAGE;
|
|
|
|
|
d->mMimeType = mimeTypeFromFormat(format);
|
|
|
|
|
} else {
|
|
|
|
|
KMimeType::Ptr ptr = KMimeType::findByContent(d->mRawData);
|
|
|
|
|
d->mMimeType = ptr->name();
|
|
|
|
|
d->mURLKind = MimeTypeUtils::mimeTypeKind(d->mMimeType);
|
|
|
|
|
}
|
|
|
|
|
if (d->mURLKind!=MimeTypeUtils::KIND_RASTER_IMAGE) {
|
|
|
|
|
Q_ASSERT(!d->mDecoderTimer.isActive());
|
|
|
|
|
job->kill(true /* quietly */);
|
|
|
|
|
LOG("emit urlKindDetermined(!raster)");
|
|
|
|
|
emit urlKindDetermined();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
LOG("emit urlKindDetermined(raster)");
|
|
|
|
|
emit urlKindDetermined();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decode the received data
|
|
|
|
|
if( !d->mDecoderTimer.isActive() &&
|
|
|
|
|
(d->mDecodeState==DECODE_WAITING || d->mDecodeState==DECODE_INCREMENTAL_DECODING)
|
|
|
|
|
) {
|
|
|
|
|
d->mDecoderTimer.start(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ImageLoader::decodeChunk() {
|
|
|
|
|
if( d->mSuspended ) {
|
|
|
|
|
LOG("suspended");
|
|
|
|
|
d->mDecoderTimer.stop();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int chunkSize = QMIN(DECODE_CHUNK_SIZE, int(d->mRawData.size())-d->mDecodedSize);
|
|
|
|
|
int decodedSize = 0;
|
|
|
|
|
if (chunkSize>0) {
|
|
|
|
|
decodedSize = d->mDecoder.decode(
|
|
|
|
|
(const uchar*)(d->mRawData.data()+d->mDecodedSize),
|
|
|
|
|
chunkSize);
|
|
|
|
|
|
|
|
|
|
if (decodedSize<0) {
|
|
|
|
|
// We can't use incremental decoding, switch to threaded decoding
|
|
|
|
|
d->mDecoderTimer.stop();
|
|
|
|
|
if (d->mGetState == GET_DONE) {
|
|
|
|
|
startThread();
|
|
|
|
|
} else {
|
|
|
|
|
d->mDecodeState = DECODE_PENDING_THREADED_DECODING;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We just decoded some data
|
|
|
|
|
if (d->mDecodeState == DECODE_WAITING) {
|
|
|
|
|
d->mDecodeState = DECODE_INCREMENTAL_DECODING;
|
|
|
|
|
}
|
|
|
|
|
d->mDecodedSize+=decodedSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (decodedSize == 0) {
|
|
|
|
|
// We decoded as much as possible from the buffer, wait to receive
|
|
|
|
|
// more data before coming again in decodeChunk
|
|
|
|
|
d->mDecoderTimer.stop();
|
|
|
|
|
|
|
|
|
|
if (d->mGetState == GET_DONE) {
|
|
|
|
|
// All available data has been received.
|
|
|
|
|
if (d->mDecodeState == DECODE_INCREMENTAL_DECODING) {
|
|
|
|
|
// Decoder is not finished, the image must be truncated,
|
|
|
|
|
// let's simulate its end
|
|
|
|
|
kdWarning() << "ImageLoader::decodeChunk(): image '" << d->mURL.prettyURL() << "' is truncated.\n";
|
|
|
|
|
|
|
|
|
|
if (d->mProcessedImage.isNull()) {
|
|
|
|
|
d->mProcessedImage = d->mDecoder.image();
|
|
|
|
|
}
|
|
|
|
|
emit imageChanged(d->mProcessedImage.rect());
|
|
|
|
|
end();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ImageLoader::startThread() {
|
|
|
|
|
LOG("starting decoder thread");
|
|
|
|
|
d->mDecodeState = DECODE_THREADED_DECODING;
|
|
|
|
|
d->mDecoderThread.setRawData(d->mRawData);
|
|
|
|
|
d->mDecoderThread.start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ImageLoader::slotDecoderThreadFailed() {
|
|
|
|
|
LOG("");
|
|
|
|
|
// Image can't be loaded
|
|
|
|
|
finish( false );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ImageLoader::slotDecoderThreadSucceeded() {
|
|
|
|
|
LOG("");
|
|
|
|
|
d->mProcessedImage = d->mDecoderThread.popLoadedImage();
|
|
|
|
|
d->mFrames.append( ImageFrame( d->mProcessedImage, 0 ));
|
|
|
|
|
emit sizeLoaded(d->mProcessedImage.width(), d->mProcessedImage.height());
|
|
|
|
|
emit imageChanged(d->mProcessedImage.rect());
|
|
|
|
|
finish(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cache image and emit imageLoaded
|
|
|
|
|
*/
|
|
|
|
|
void ImageLoader::finish( bool ok ) {
|
|
|
|
|
LOG("");
|
|
|
|
|
|
|
|
|
|
d->mDecodeState = DECODE_DONE;
|
|
|
|
|
|
|
|
|
|
if (!ok) {
|
|
|
|
|
d->mFrames.clear();
|
|
|
|
|
d->mRawData = QByteArray();
|
|
|
|
|
d->mImageFormat = QCString();
|
|
|
|
|
d->mProcessedImage = QImage();
|
|
|
|
|
emit imageLoaded( false );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (d->mImageFormat.isEmpty()) {
|
|
|
|
|
d->determineImageFormat();
|
|
|
|
|
}
|
|
|
|
|
Q_ASSERT(d->mFrames.count() > 0);
|
|
|
|
|
Cache::instance()->addImage( d->mURL, d->mFrames, d->mImageFormat, d->mTimestamp );
|
|
|
|
|
emit imageLoaded( true );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BusyLevel ImageLoader::priority() const {
|
|
|
|
|
BusyLevel mylevel = BUSY_NONE;
|
|
|
|
|
for( QValueVector< OwnerData >::ConstIterator it = d->mOwners.begin();
|
|
|
|
|
it != d->mOwners.end();
|
|
|
|
|
++it ) {
|
|
|
|
|
mylevel = QMAX( mylevel, (*it).priority );
|
|
|
|
|
}
|
|
|
|
|
return mylevel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::slotBusyLevelChanged( BusyLevel level ) {
|
|
|
|
|
// this loader may be needed for normal loading (BUSY_LOADING), or
|
|
|
|
|
// only for prefetching
|
|
|
|
|
BusyLevel mylevel = priority();
|
|
|
|
|
if( level > mylevel ) {
|
|
|
|
|
suspendLoading();
|
|
|
|
|
} else {
|
|
|
|
|
resumeLoading();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::suspendLoading() {
|
|
|
|
|
d->mDecoderTimer.stop();
|
|
|
|
|
d->mSuspended = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::resumeLoading() {
|
|
|
|
|
d->mSuspended = false;
|
|
|
|
|
d->mDecoderTimer.start(0, false);
|
|
|
|
|
checkPendingGet();
|
|
|
|
|
checkPendingStat();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// QImageConsumer
|
|
|
|
|
//
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
void ImageLoader::end() {
|
|
|
|
|
LOG("");
|
|
|
|
|
|
|
|
|
|
// Notify about the last loaded rectangle
|
|
|
|
|
LOG("mLoadChangedRect " << d->mLoadChangedRect);
|
|
|
|
|
if (!d->mLoadChangedRect.isEmpty()) {
|
|
|
|
|
emit imageChanged( d->mLoadChangedRect );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d->mDecoderTimer.stop();
|
|
|
|
|
d->mDecodeState = DECODE_INCREMENTAL_DECODING_DONE;
|
|
|
|
|
|
|
|
|
|
// We are done
|
|
|
|
|
if( d->mFrames.count() == 0 ) {
|
|
|
|
|
d->mFrames.append( ImageFrame( d->mProcessedImage, 0 ));
|
|
|
|
|
}
|
|
|
|
|
// The image has been totally decoded, we delay the call to finish because
|
|
|
|
|
// when we return from this function we will be in decodeChunk(), after the
|
|
|
|
|
// call to decode(), so we don't want to switch to a new impl yet, since
|
|
|
|
|
// this means deleting "this".
|
|
|
|
|
QTimer::singleShot(0, this, SLOT(callFinish()) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ImageLoader::callFinish() {
|
|
|
|
|
finish(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ImageLoader::changed(const QRect& constRect) {
|
|
|
|
|
LOG2("");
|
|
|
|
|
QRect rect = constRect;
|
|
|
|
|
|
|
|
|
|
if (d->mLoadedRegion.isEmpty()) {
|
|
|
|
|
// This is the first time we get called. Init mProcessedImage and emit
|
|
|
|
|
// sizeLoaded.
|
|
|
|
|
LOG("mLoadedRegion is empty");
|
|
|
|
|
|
|
|
|
|
// By default, mProcessedImage should use the image from mDecoder
|
|
|
|
|
d->mProcessedImage = d->mDecoder.image();
|
|
|
|
|
|
|
|
|
|
if (d->mImageFormat.isEmpty()) {
|
|
|
|
|
d->determineImageFormat();
|
|
|
|
|
}
|
|
|
|
|
Q_ASSERT(!d->mImageFormat.isEmpty());
|
|
|
|
|
if (d->mImageFormat == "JPEG") {
|
|
|
|
|
// This is a JPEG, extract orientation and adjust mProcessedImage
|
|
|
|
|
// if necessary according to misc options
|
|
|
|
|
ImageUtils::JPEGContent content;
|
|
|
|
|
|
|
|
|
|
if (content.loadFromData(d->mRawData)) {
|
|
|
|
|
d->mOrientation = content.orientation();
|
|
|
|
|
if (MiscConfig::autoRotateImages() &&
|
|
|
|
|
d->mOrientation != ImageUtils::NOT_AVAILABLE && d->mOrientation != ImageUtils::NORMAL) {
|
|
|
|
|
QSize size = content.size();
|
|
|
|
|
d->mProcessedImage = QImage(size, d->mDecoder.image().depth());
|
|
|
|
|
}
|
|
|
|
|
d->mProcessedImage.setDotsPerMeterX(content.dotsPerMeterX());
|
|
|
|
|
d->mProcessedImage.setDotsPerMeterY(content.dotsPerMeterY());
|
|
|
|
|
} else {
|
|
|
|
|
kdWarning() << "ImageLoader::changed(): JPEGContent could not load '" << d->mURL.prettyURL() << "'\n";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG("emit sizeLoaded " << d->mProcessedImage.size());
|
|
|
|
|
emit sizeLoaded(d->mProcessedImage.width(), d->mProcessedImage.height());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply orientation if necessary and if wanted by user settings (misc options)
|
|
|
|
|
if (MiscConfig::autoRotateImages() &&
|
|
|
|
|
d->mOrientation != ImageUtils::NOT_AVAILABLE && d->mOrientation != ImageUtils::NORMAL) {
|
|
|
|
|
// We can only rotate whole images, so copy the loaded rect in a temp
|
|
|
|
|
// image, rotate the temp image and copy it to mProcessedImage
|
|
|
|
|
|
|
|
|
|
// Copy loaded rect
|
|
|
|
|
QImage temp(rect.size(), d->mProcessedImage.depth());
|
|
|
|
|
bitBlt(&temp, 0, 0,
|
|
|
|
|
&d->mDecoder.image(), rect.left(), rect.top(), rect.width(), rect.height());
|
|
|
|
|
|
|
|
|
|
// Rotate
|
|
|
|
|
temp = ImageUtils::transform(temp, d->mOrientation);
|
|
|
|
|
|
|
|
|
|
// Compute destination rect
|
|
|
|
|
QWMatrix matrix = ImageUtils::transformMatrix(d->mOrientation);
|
|
|
|
|
|
|
|
|
|
QRect imageRect = d->mDecoder.image().rect();
|
|
|
|
|
imageRect = matrix.mapRect(imageRect);
|
|
|
|
|
|
|
|
|
|
rect = matrix.mapRect(rect);
|
|
|
|
|
rect.moveBy(-imageRect.left(), -imageRect.top());
|
|
|
|
|
|
|
|
|
|
// copy temp to mProcessedImage
|
|
|
|
|
bitBlt(&d->mProcessedImage, rect.left(), rect.top(),
|
|
|
|
|
&temp, 0, 0, temp.width(), temp.height());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update state tracking vars
|
|
|
|
|
d->mWasFrameData = true;
|
|
|
|
|
d->mLoadChangedRect |= rect;
|
|
|
|
|
d->mLoadedRegion |= rect;
|
|
|
|
|
if( d->mTimeSinceLastUpdate.elapsed() > IMAGE_UPDATE_INTERVAL ) {
|
|
|
|
|
LOG("emitting imageChanged " << d->mLoadChangedRect);
|
|
|
|
|
d->mTimeSinceLastUpdate.start();
|
|
|
|
|
emit imageChanged(d->mLoadChangedRect);
|
|
|
|
|
d->mLoadChangedRect = QRect();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::frameDone() {
|
|
|
|
|
frameDone( QPoint( 0, 0 ), d->mDecoder.image().rect());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::frameDone(const QPoint& offset, const QRect& rect) {
|
|
|
|
|
LOG("");
|
|
|
|
|
// Another case where the image loading in Qt's is a bit borken.
|
|
|
|
|
// It's possible to get several notes about a frame being done for one frame (with MNG).
|
|
|
|
|
if( !d->mWasFrameData ) {
|
|
|
|
|
// To make it even more fun, with MNG the sequence is actually
|
|
|
|
|
// setFramePeriod( 0 )
|
|
|
|
|
// frameDone()
|
|
|
|
|
// setFramePeriod( delay )
|
|
|
|
|
// frameDone()
|
|
|
|
|
// Therefore ignore the second frameDone(), but fix the delay that should be
|
|
|
|
|
// after the frame.
|
|
|
|
|
if( d->mFrames.count() > 0 ) {
|
|
|
|
|
d->mFrames.last().delay = d->mNextFrameDelay;
|
|
|
|
|
d->mNextFrameDelay = 0;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
d->mWasFrameData = false;
|
|
|
|
|
if( !d->mLoadChangedRect.isEmpty()) {
|
|
|
|
|
emit imageChanged(d->mLoadChangedRect);
|
|
|
|
|
d->mLoadChangedRect = QRect();
|
|
|
|
|
d->mTimeSinceLastUpdate.start();
|
|
|
|
|
}
|
|
|
|
|
d->mLoadedRegion = QRegion();
|
|
|
|
|
|
|
|
|
|
QImage image;
|
|
|
|
|
if (d->mProcessedImage.isNull()) {
|
|
|
|
|
image = d->mDecoder.image().copy();
|
|
|
|
|
} else {
|
|
|
|
|
image = d->mProcessedImage.copy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if( offset != QPoint( 0, 0 ) || rect != image.rect()) {
|
|
|
|
|
// Blit last frame below 'image'
|
|
|
|
|
if( !d->mFrames.isEmpty()) {
|
|
|
|
|
QImage im = d->mFrames.last().image.copy();
|
|
|
|
|
bitBlt( &im, offset.x(), offset.y(), &image, rect.x(), rect.y(), rect.width(), rect.height());
|
|
|
|
|
image = im;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
d->mFrames.append( ImageFrame( image, d->mNextFrameDelay ));
|
|
|
|
|
d->mNextFrameDelay = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::setLooping(int) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::setFramePeriod(int milliseconds) {
|
|
|
|
|
if( milliseconds < 0 ) milliseconds = 0; // -1 means showing immediately
|
|
|
|
|
if( d->mNextFrameDelay == 0 || milliseconds != 0 ) {
|
|
|
|
|
d->mNextFrameDelay = milliseconds;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::setSize(int, int) {
|
|
|
|
|
// Do nothing, size is handled when ::changed() is called for the first
|
|
|
|
|
// time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QImage ImageLoader::processedImage() const {
|
|
|
|
|
return d->mProcessedImage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ImageFrames ImageLoader::frames() const {
|
|
|
|
|
return d->mFrames;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QCString ImageLoader::imageFormat() const {
|
|
|
|
|
return d->mImageFormat;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QByteArray ImageLoader::rawData() const {
|
|
|
|
|
return d->mRawData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QString ImageLoader::mimeType() const {
|
|
|
|
|
return d->mMimeType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MimeTypeUtils::Kind ImageLoader::urlKind() const {
|
|
|
|
|
return d->mURLKind;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
KURL ImageLoader::url() const {
|
|
|
|
|
return d->mURL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QRegion ImageLoader::loadedRegion() const {
|
|
|
|
|
return d->mLoadedRegion;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool ImageLoader::completed() const {
|
|
|
|
|
return d->mDecodeState == DECODE_DONE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ImageLoader::ref( const QObject* owner, BusyLevel priority ) {
|
|
|
|
|
OwnerData data;
|
|
|
|
|
data.owner = owner;
|
|
|
|
|
data.priority = priority;
|
|
|
|
|
d->mOwners.append( data );
|
|
|
|
|
connect( owner, SIGNAL( destroyed()), SLOT( ownerDestroyed()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::deref( const QObject* owner ) {
|
|
|
|
|
for( QValueVector< OwnerData >::Iterator it = d->mOwners.begin();
|
|
|
|
|
it != d->mOwners.end();
|
|
|
|
|
++it ) {
|
|
|
|
|
if( (*it).owner == owner ) {
|
|
|
|
|
d->mOwners.erase( it );
|
|
|
|
|
if( d->mOwners.count() == 0 ) {
|
|
|
|
|
sLoaders.remove( d->mURL );
|
|
|
|
|
delete this;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
assert( false );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::release( const QObject* owner ) {
|
|
|
|
|
disconnect( owner );
|
|
|
|
|
deref( owner );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ImageLoader::ownerDestroyed() {
|
|
|
|
|
deref( sender());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Managing loaders
|
|
|
|
|
//
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
ImageLoader* ImageLoader::loader( const KURL& url, const QObject* owner, BusyLevel priority ) {
|
|
|
|
|
if( sLoaders.contains( url )) {
|
|
|
|
|
ImageLoader* loader = sLoaders[ url ];
|
|
|
|
|
loader->ref( owner, priority );
|
|
|
|
|
// resume if this owner has high priority
|
|
|
|
|
loader->slotBusyLevelChanged( BusyLevelManager::instance()->busyLevel());
|
|
|
|
|
return loader;
|
|
|
|
|
}
|
|
|
|
|
ImageLoader* loader = new ImageLoader;
|
|
|
|
|
loader->ref( owner, priority );
|
|
|
|
|
sLoaders[ url ] = loader;
|
|
|
|
|
loader->setURL( url );
|
|
|
|
|
// Code using a loader first calls loader() to get ImageLoader* and only after that it can
|
|
|
|
|
// connect to its signals etc., so don't start loading immediately.
|
|
|
|
|
// This also helps with preloading jobs, since BUSY_LOADING busy level is not entered immediately
|
|
|
|
|
// when a new picture is selected, so preloading jobs without this delay could start working
|
|
|
|
|
// immediately.
|
|
|
|
|
QTimer::singleShot( priority >= BUSY_LOADING ? 0 : 10, loader, SLOT( startLoading()));
|
|
|
|
|
return loader;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|