// vim: set tabstop=4 shiftwidth=4 noexpandtab /* Gwenview - A simple image viewer for KDE Copyright 2000-2004 Aurélien Gâ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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "cache.h" // TQt // KDE #include #include #include #include #include #include #include "cache.moc" namespace Gwenview { // Local #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) kdDebug() << k_funcinfo << x << endl #else #define LOG(x) ; #endif //#define DEBUG_CACHE const char CONFIG_CACHE_MAXSIZE[]="maxSize"; struct ImageData : public KShared { ImageData( const KURL& url, const TQDateTime& _timestamp ) : timestamp(_timestamp) , age(0) , fast_url( url.isLocalFile() && !KIO::probably_slow_mounted( url.path())) , priority( false ) { } void addFile( const TQByteArray& file ); void addImage( const ImageFrames& frames, const TQCString& format ); void addThumbnail( const TQPixmap& thumbnail, TQSize imagesize ); long long cost() const; int size() const; TQByteArray file; ImageFrames frames; TQPixmap thumbnail; TQSize imagesize; TQCString format; TQDateTime timestamp; mutable int age; bool fast_url; bool priority; int fileSize() const; int imageSize() const; int thumbnailSize() const; bool reduceSize(); bool isEmpty() const; typedef KSharedPtr Ptr; }; typedef TQMap ImageMap; struct Cache::Private { ImageMap mImages; int mMaxSize; int mThumbnailSize; TQValueList< KURL > mPriorityURLs; /** * This function tries to returns a valid ImageData for url and timestamp. * If it can't find one, it will create a new one and return it. */ ImageData::Ptr getOrCreateImageData(const KURL& url, const TQDateTime& timestamp) { if (mImages.contains(url)) { ImageData::Ptr data = mImages[url]; if (data->timestamp == timestamp) return data; } ImageData::Ptr data = new ImageData(url, timestamp); mImages[url] = data; if (mPriorityURLs.contains(url)) data->priority = true; return data; } }; Cache::Cache() { d = new Private; d->mMaxSize = DEFAULT_MAXSIZE; // don't remember size for every thumbnail, but have one global and dump all if needed d->mThumbnailSize = 0; } Cache::~Cache() { d->mImages.clear(); delete d; } static Cache* sCache; static KStaticDeleter sCacheDeleter; Cache* Cache::instance() { if (!sCache) { sCacheDeleter.setObject(sCache, new Cache()); } return sCache; } // Priority URLs are used e.g. when prefetching for the slideshow - after an image is prefetched, // the loader tries to put the image in the cache. When the slideshow advances, the next loader // just gets the image from the cache. However, the prefetching may be useless if the image // actually doesn't stay long enough in the cache, e.g. because of being too big for the cache. // Marking an URL as a priority one will make sure it stays in the cache and that the cache // will be even enlarged as necessary if needed. void Cache::setPriorityURL( const KURL& url, bool set ) { if( set ) { d->mPriorityURLs.append( url ); if( d->mImages.contains( url )) { d->mImages[ url ]->priority = true; } } else { d->mPriorityURLs.remove( url ); if( d->mImages.contains( url )) { d->mImages[ url ]->priority = false; } checkMaxSize(); } } void Cache::addFile( const KURL& url, const TQByteArray& file, const TQDateTime& timestamp ) { LOG(url.prettyURL()); updateAge(); d->getOrCreateImageData(url, timestamp)->addFile(file); checkMaxSize(); } void Cache::addImage( const KURL& url, const ImageFrames& frames, const TQCString& format, const TQDateTime& timestamp ) { LOG(url.prettyURL()); updateAge(); d->getOrCreateImageData(url, timestamp)->addImage(frames, format); checkMaxSize(); } void Cache::addThumbnail( const KURL& url, const TQPixmap& thumbnail, TQSize imagesize, const TQDateTime& timestamp ) { // Thumbnails are many and often - things would age too quickly. Therefore // when adding thumbnails updateAge() is called from the outside only once for all of them. // updateAge(); d->getOrCreateImageData(url, timestamp)->addThumbnail(thumbnail, imagesize); checkMaxSize(); } void Cache::invalidate( const KURL& url ) { d->mImages.remove( url ); } TQDateTime Cache::timestamp( const KURL& url ) const { LOG(url.prettyURL()); if( d->mImages.contains( url )) return d->mImages[ url ]->timestamp; return TQDateTime(); } TQByteArray Cache::file( const KURL& url ) const { LOG(url.prettyURL()); if( d->mImages.contains( url )) { const ImageData::Ptr data = d->mImages[ url ]; if( data->file.isNull()) return TQByteArray(); data->age = 0; return data->file; } return TQByteArray(); } void Cache::getFrames( const KURL& url, ImageFrames* frames, TQCString* format ) const { LOG(url.prettyURL()); Q_ASSERT(frames); Q_ASSERT(format); frames->clear(); *format = TQCString(); if( d->mImages.contains( url )) { const ImageData::Ptr data = d->mImages[ url ]; if( data->frames.isEmpty()) return; *frames = data->frames; *format = data->format; data->age = 0; } } TQPixmap Cache::thumbnail( const KURL& url, TQSize& imagesize ) const { if( d->mImages.contains( url )) { const ImageData::Ptr data = d->mImages[ url ]; if( data->thumbnail.isNull()) return TQPixmap(); imagesize = data->imagesize; // data.age = 0; return data->thumbnail; } return TQPixmap(); } void Cache::updateAge() { for( ImageMap::Iterator it = d->mImages.begin(); it != d->mImages.end(); ++it ) { (*it)->age++; } } void Cache::checkThumbnailSize( int size ) { if( size != d->mThumbnailSize ) { // simply remove all thumbnails, should happen rarely for( ImageMap::Iterator it = d->mImages.begin(); it != d->mImages.end(); ) { if( !(*it)->thumbnail.isNull()) { ImageMap::Iterator it2 = it; ++it; d->mImages.remove( it2 ); } else { ++it; } } d->mThumbnailSize = size; } } #ifdef DEBUG_CACHE static KURL _cache_url; // hack only for debugging for item to show also its key #endif void Cache::checkMaxSize() { for(;;) { int size = 0; ImageMap::Iterator max; long long max_cost = -1; #ifdef DEBUG_CACHE int with_file = 0; int with_thumb = 0; int with_image = 0; #endif for( ImageMap::Iterator it = d->mImages.begin(); it != d->mImages.end(); ++it ) { size += (*it)->size(); #ifdef DEBUG_CACHE if( !(*it).file.isNull()) ++with_file; if( !(*it).thumbnail.isNull()) ++with_thumb; if( !(*it).frames.isEmpty()) ++with_image; #endif long long cost = (*it)->cost(); if( cost > max_cost && ! (*it)->priority ) { max_cost = cost; max = it; } } if( size <= d->mMaxSize || max_cost == -1 ) { #if 0 #ifdef DEBUG_CACHE kdDebug() << "Cache: Statistics (" << d->mImages.size() << "/" << with_file << "/" << with_thumb << "/" << with_image << ")" << endl; #endif #endif break; } #ifdef DEBUG_CACHE _cache_url = max.key(); #endif if( !(*max)->reduceSize() || (*max)->isEmpty()) d->mImages.remove( max ); } } void Cache::readConfig(KConfig* config,const TQString& group) { KConfigGroupSaver saver( config, group ); d->mMaxSize = config->readNumEntry( CONFIG_CACHE_MAXSIZE, d->mMaxSize ); checkMaxSize(); } void ImageData::addFile( const TQByteArray& f ) { file = f; file.detach(); // explicit sharing age = 0; } void ImageData::addImage( const ImageFrames& fs, const TQCString& f ) { frames = fs; format = f; age = 0; } void ImageData::addThumbnail( const TQPixmap& thumb, TQSize imgsize ) { thumbnail = thumb; imagesize = imgsize; // age = 0; } int ImageData::size() const { return TQMAX( fileSize() + imageSize() + thumbnailSize(), 100 ); // some minimal size per item } int ImageData::fileSize() const { return !file.isNull() ? file.size() : 0; } int ImageData::thumbnailSize() const { return !thumbnail.isNull() ? thumbnail.height() * thumbnail.width() * thumbnail.depth() / 8 : 0; } int ImageData::imageSize() const { int ret = 0; for( ImageFrames::ConstIterator it = frames.begin(); it != frames.end(); ++it ) { ret += (*it).image.height() * (*it).image.width() * (*it).image.depth() / 8; } return ret; } bool ImageData::reduceSize() { if( !file.isNull() && fast_url && !frames.isEmpty()) { file = TQByteArray(); #ifdef DEBUG_CACHE kdDebug() << "Cache: Dumping fast file: " << _cache_url.prettyURL() << ":" << cost() << endl; #endif return true; } if( !thumbnail.isNull()) { #ifdef DEBUG_CACHE kdDebug() << "Cache: Dumping thumbnail: " << _cache_url.prettyURL() << ":" << cost() << endl; #endif thumbnail = TQPixmap(); return true; } if( !file.isNull() && !frames.isEmpty()) { // possibly slow file to fetch - dump the image data unless the image // is JPEG (which needs raw data anyway) or the raw data much larger than the image if( format == "JPEG" || fileSize() < imageSize() / 10 ) { frames.clear(); #ifdef DEBUG_CACHE kdDebug() << "Cache: Dumping images: " << _cache_url.prettyURL() << ":" << cost() << endl; #endif } else { file = TQByteArray(); #ifdef DEBUG_CACHE kdDebug() << "Cache: Dumping file: " << _cache_url.prettyURL() << ":" << cost() << endl; #endif } return true; } #ifdef DEBUG_CACHE kdDebug() << "Cache: Dumping completely: " << _cache_url.prettyURL() << ":" << cost() << endl; #endif return false; // reducing here would mean clearing everything } bool ImageData::isEmpty() const { return file.isNull() && frames.isEmpty() && thumbnail.isNull(); } long long ImageData::cost() const { long long s = size(); if( fast_url && !file.isNull()) { s *= ( format == "JPEG" ? 10 : 100 ); // heavy penalty for storing local files } else if( !thumbnail.isNull()) { s *= 10 * 10; // thumbnails are small, and try to get rid of them soon } static const int mod[] = { 50, 30, 20, 16, 12, 10 }; if( age <= 5 ) { return s * 10 / mod[ age ]; } else { return s * ( age - 5 ); } } } // namespace