/* * * This file is part of the KDE project. * Copyright (C) 2007 Rivo Laks * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kpixmapcache.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(HAVE_MADVISE) #include #endif //#define DISABLE_PIXMAPCACHE #ifdef Q_OS_SOLARIS #ifndef _XPG_4_2 extern "C" int madvise(caddr_t addr, size_t len, int advice); #endif #endif #define KPIXMAPCACHE_VERSION 0x000208 namespace { class KPCLockFile { public: KPCLockFile(const QString& filename) { mValid = false; mLockFile = new KLockFile(filename); // Try to lock the file up to 5 times, waiting 5 ms between retries KLockFile::LockResult result; for (int i = 0; i < 5; i++) { result = mLockFile->lock(KLockFile::NoBlockFlag); if (result == KLockFile::LockOK) { mValid = true; break; } usleep(5*1000); } // Output error msg if locking failed if (!mValid) { kError() << "Failed to lock file" << filename << ", last result =" << result; } } ~KPCLockFile() { unlock(); delete mLockFile; } void unlock() { if (mValid) { mLockFile->unlock(); mValid = false; } } bool isValid() const { return mValid; } private: bool mValid; KLockFile* mLockFile; }; // Contained in the header so we will know if we created this or not. Older // versions of kdelibs had the version on the byte right after "CACHE ". // "DEUX" will be read as a quint32 by such versions, and will always be // greater than the last such version (0x000207), whether a big-endian or // little-endian system is used. Therefore older systems will correctly // recognize that this is from a newer kdelibs. (This is an issue since old // and new kdelibs do not read the version from the exact same spot.) static const char KPC_MAGIC[] = "KDE PIXMAP CACHE DEUX"; struct KPixmapCacheDataHeader { // -1 from sizeof so we don't write out the trailing null. If you change // the list of members change them in the KPixmapCacheIndexHeader as well! char magic[sizeof(KPC_MAGIC) - 1]; quint32 cacheVersion; quint32 size; }; struct KPixmapCacheIndexHeader { // -1 from sizeof so we don't write out the trailing null. // The follow are also in KPixmapCacheDataHeader char magic[sizeof(KPC_MAGIC) - 1]; quint32 cacheVersion; quint32 size; // These belong only to this header type. quint32 cacheId; time_t timestamp; }; class KPCMemoryDevice : public QIODevice { public: KPCMemoryDevice(char* start, quint32* size, quint32 available); virtual ~KPCMemoryDevice(); virtual qint64 size() const { return *mSize; } void setSize(quint32 s) { *mSize = s; } virtual bool seek(qint64 pos); protected: virtual qint64 readData(char* data, qint64 maxSize); virtual qint64 writeData(const char* data, qint64 maxSize); private: char* mMemory; KPixmapCacheIndexHeader *mHeader; // alias of mMemory quint32* mSize; quint32 mInitialSize; qint64 mAvailable; quint32 mPos; }; KPCMemoryDevice::KPCMemoryDevice(char* start, quint32* size, quint32 available) : QIODevice() { mMemory = start; mHeader = reinterpret_cast(start); mSize = size; mAvailable = available; mPos = 0; this->open(QIODevice::ReadWrite); // Load up-to-date size from the memory *mSize = mHeader->size; mInitialSize = *mSize; } KPCMemoryDevice::~KPCMemoryDevice() { if (*mSize != mInitialSize) { // Update file size mHeader->size = *mSize; } } bool KPCMemoryDevice::seek(qint64 pos) { if (pos < 0 || pos > *mSize) { return false; } mPos = pos; return QIODevice::seek(pos); } qint64 KPCMemoryDevice::readData(char* data, qint64 len) { len = qMin(len, qint64(*mSize) - mPos); if (len <= 0) { return 0; } memcpy(data, mMemory + mPos, len); mPos += len; return len; } qint64 KPCMemoryDevice::writeData(const char* data, qint64 len) { if (mPos + len > mAvailable) { kError() << "Overflow of" << mPos+len - mAvailable; return -1; } memcpy(mMemory + mPos, (uchar*)data, len); mPos += len; *mSize = qMax(*mSize, mPos); return len; } } // namespace class KPixmapCache::Private { public: Private(KPixmapCache* q); ~Private(); // Return device used to read from index or data file. The device is either // QFile or KPCMemoryDevice (if mmap is used) QIODevice* indexDevice(); QIODevice* dataDevice(); // Unmmaps any currently mmapped files and then tries to (re)mmap the cache // files. If mmapping is disabled then it does nothing bool mmapFiles(); void unmmapFiles(); // Marks the shared mmapped files as invalid so that all processes will // reload the files void invalidateMmapFiles(); // List of all KPixmapCache::Private instances in this process. static QList mCaches; static unsigned kpcNumber; // Used to setup for qpcKey int findOffset(const QString& key); int binarySearchKey(QDataStream& stream, const QString& key, int start); void writeIndexEntry(QDataStream& stream, const QString& key, int dataoffset); bool checkLockFile(); bool checkFileVersion(const QString& filename); bool loadIndexHeader(); bool loadDataHeader(); bool removeEntries(int newsize); bool scheduleRemoveEntries(int newsize); void init(); bool loadData(int offset, QPixmap& pix); int writeData(const QString& key, const QPixmap& pix); void writeIndex(const QString& key, int offset); // Prepends key's hash to the key. This makes comparisons and key // lookups faster as the beginnings of the keys are more random QString indexKey(const QString& key); // Returns a QString suitable for use in the static QPixmapCache, which // differentiates each KPC object in the process. QString qpcKey(const QString& key) const; KPixmapCache* q; QString mThisString; // Used by qpcKey quint32 mHeaderSize; // full size of the index header, including custom (subclass') header data quint32 mIndexRootOffset; // offset of the first entry in index file QString mName; QString mIndexFile; QString mDataFile; QString mLockFileName; QMutex mMutex; quint32 mTimestamp; quint32 mCacheId; // Unique id, will change when cache is recreated int mCacheLimit; RemoveStrategy mRemoveStrategy:4; bool mUseQPixmapCache:4; bool mInited:8; // Whether init() has been called (it's called on-demand) bool mEnabled:8; // whether it's possible to use the cache bool mValid:8; // whether cache has been inited and is ready to be used // Holds info about mmapped file struct MmapInfo { MmapInfo() { file = 0; indexHeader = 0; } QFile* file; // If this is not null, then the file is mmapped // This points to the mmap'ed file area. KPixmapCacheIndexHeader *indexHeader; quint32 size; // Number of currently used bytes quint32 available; // Number of available bytes (including those reserved for mmap) }; MmapInfo mIndexMmapInfo; MmapInfo mDataMmapInfo; // Mmaps given file, growing it to newsize bytes. bool mmapFile(const QString& filename, MmapInfo* info, int newsize); void unmmapFile(MmapInfo* info); // Used by removeEntries() class KPixmapCacheEntry { public: KPixmapCacheEntry(int indexoffset_, const QString& key_, int dataoffset_, int pos_, quint32 timesused_, quint32 lastused_) : indexoffset(indexoffset_), key(key_), dataoffset(dataoffset_), pos(pos_), timesused(timesused_), lastused(lastused_) { } int indexoffset; QString key; int dataoffset; int pos; quint32 timesused; quint32 lastused; }; // Various comparison functions for different removal strategies static bool compareEntriesByAge(const KPixmapCacheEntry& a, const KPixmapCacheEntry& b) { return a.pos > b.pos; } static bool compareEntriesByTimesUsed(const KPixmapCacheEntry& a, const KPixmapCacheEntry& b) { return a.timesused > b.timesused; } static bool compareEntriesByLastUsed(const KPixmapCacheEntry& a, const KPixmapCacheEntry& b) { return a.lastused > b.lastused; } }; // List of KPixmapCache::Private instances. QList KPixmapCache::Private::mCaches; unsigned KPixmapCache::Private::kpcNumber = 0; KPixmapCache::Private::Private(KPixmapCache* _q) { q = _q; mCaches.append(this); mThisString = QString("%1").arg(kpcNumber++); } KPixmapCache::Private::~Private() { mCaches.removeAll(this); } bool KPixmapCache::Private::mmapFiles() { unmmapFiles(); // Noop if nothing has been mmapped if (!q->isValid()) { return false; } //TODO: 100MB limit if we have no cache limit, is that sensible? int cacheLimit = mCacheLimit > 0 ? mCacheLimit : 100 * 1024; if (!mmapFile(mIndexFile, &mIndexMmapInfo, (int)(cacheLimit * 0.4 + 100) * 1024)) { q->setValid(false); return false; } if (!mmapFile(mDataFile, &mDataMmapInfo, (int)(cacheLimit * 1.5 + 500) * 1024)) { unmmapFile(&mIndexMmapInfo); q->setValid(false); return false; } return true; } void KPixmapCache::Private::unmmapFiles() { unmmapFile(&mIndexMmapInfo); unmmapFile(&mDataMmapInfo); } void KPixmapCache::Private::invalidateMmapFiles() { if (!q->isValid()) return; // Set cache id to 0, this will force a reload the next time the files are used if (mIndexMmapInfo.file) { kDebug(264) << "Invalidating cache"; mIndexMmapInfo.indexHeader->cacheId = 0; } } bool KPixmapCache::Private::mmapFile(const QString& filename, MmapInfo* info, int newsize) { info->file = new QFile(filename); if (!info->file->open(QIODevice::ReadWrite)) { kDebug(264) << "Couldn't open" << filename; delete info->file; info->file = 0; return false; } if (!info->size) { info->size = info->file->size(); } info->available = newsize; // Only resize if greater than current file size, otherwise we may cause SIGBUS // errors from mmap(). if (info->file->size() < info->available && !info->file->resize(info->available)) { kError(264) << "Couldn't resize" << filename << "to" << newsize; delete info->file; info->file = 0; return false; } //void* indexMem = mmap(0, info->available, PROT_READ | PROT_WRITE, MAP_SHARED, info->file->handle(), 0); void *indexMem = info->file->map(0, info->available); if (indexMem == 0) { kError() << "mmap failed for" << filename; delete info->file; info->file = 0; return false; } info->indexHeader = reinterpret_cast(indexMem); #ifdef HAVE_MADVISE posix_madvise(indexMem, info->size, POSIX_MADV_WILLNEED); #endif info->file->close(); // Update our stored file size. Other objects that have this mmaped will have to // invalidate their map if size is different. if(0 == info->indexHeader->size) { // This size includes index header and and custom headers tacked on // by subclasses. info->indexHeader->size = mHeaderSize; info->size = info->indexHeader->size; } return true; } void KPixmapCache::Private::unmmapFile(MmapInfo* info) { if (info->file) { info->file->unmap(reinterpret_cast(info->indexHeader)); info->indexHeader = 0; info->available = 0; info->size = 0; delete info->file; info->file = 0; } } QIODevice* KPixmapCache::Private::indexDevice() { QIODevice* device = 0; if (mIndexMmapInfo.file) { // Make sure the file still exists QFileInfo fi(mIndexFile); if (!fi.exists() || fi.size() != mIndexMmapInfo.available) { kDebug(264) << "File size has changed, re-initializing."; q->recreateCacheFiles(); // Recreates memory maps as well. } fi.refresh(); if(fi.exists() && fi.size() == mIndexMmapInfo.available) { // Create the device device = new KPCMemoryDevice( reinterpret_cast(mIndexMmapInfo.indexHeader), &mIndexMmapInfo.size, mIndexMmapInfo.available); } // Is it possible to have a valid cache with no file? If not it would be easier // to do return 0 in the else portion of the prior test. if(!q->isValid()) { delete device; return 0; } } if (!device) { QFile* file = new QFile(mIndexFile); if (!file->exists() || (size_t) file->size() < sizeof(KPixmapCacheIndexHeader)) { q->recreateCacheFiles(); } if (!q->isValid() || !file->open(QIODevice::ReadWrite)) { kDebug(264) << "Couldn't open index file" << mIndexFile; delete file; return 0; } device = file; } // Make sure the device is up-to-date KPixmapCacheIndexHeader indexHeader; int numRead = device->read(reinterpret_cast(&indexHeader), sizeof indexHeader); if (sizeof indexHeader != numRead) { kError(264) << "Unable to read header from pixmap cache index."; delete device; return 0; } if (indexHeader.cacheId != mCacheId) { kDebug(264) << "Cache has changed, reloading"; delete device; init(); if (!q->isValid()) { return 0; } else { return indexDevice(); // Careful, this is a recursive call. } } return device; } QIODevice* KPixmapCache::Private::dataDevice() { if (mDataMmapInfo.file) { // Make sure the file still exists QFileInfo fi(mDataFile); if (!fi.exists() || fi.size() != mDataMmapInfo.available) { kDebug(264) << "File size has changed, re-initializing."; q->recreateCacheFiles(); // Recreates memory maps as well. // Index file has also been recreated so we cannot continue with // modifying the data file because it would make things inconsistent. return 0; } fi.refresh(); if (fi.exists() && fi.size() == mDataMmapInfo.available) { // Create the device return new KPCMemoryDevice( reinterpret_cast(mDataMmapInfo.indexHeader), &mDataMmapInfo.size, mDataMmapInfo.available); } else return 0; } QFile* file = new QFile(mDataFile); if (!file->exists() || (size_t) file->size() < sizeof(KPixmapCacheDataHeader)) { q->recreateCacheFiles(); // Index file has also been recreated so we cannot continue with // modifying the data file because it would make things inconsistent. delete file; return 0; } if (!file->open(QIODevice::ReadWrite)) { kDebug(264) << "Couldn't open data file" << mDataFile; delete file; return 0; } return file; } int KPixmapCache::Private::binarySearchKey(QDataStream& stream, const QString& key, int start) { stream.device()->seek(start); QString fkey; qint32 foffset; quint32 timesused, lastused; qint32 leftchild, rightchild; stream >> fkey >> foffset >> timesused >> lastused >> leftchild >> rightchild; if (fkey.isEmpty()) { return start; } if (key < fkey) { if (leftchild) { return binarySearchKey(stream, key, leftchild); } } else if (key == fkey) { return start; } else if (rightchild) { return binarySearchKey(stream, key, rightchild); } return start; } int KPixmapCache::Private::findOffset(const QString& key) { // Open device and datastream on it QIODevice* device = indexDevice(); if (!device) { return -1; } device->seek(mIndexRootOffset); QDataStream stream(device); // If we're already at the end of the stream then the root node doesn't // exist yet and there are no entries. Otherwise, do a binary search // starting from the root node. if (!stream.atEnd()) { // One exception is that the root node may exist but be invalid, // which can happen when the cache data is discarded. This is // represented by an empty fkey QString fkey; stream >> fkey; if (fkey.isEmpty()) { delete device; return -1; } int nodeoffset = binarySearchKey(stream, key, mIndexRootOffset); // Load the found entry and check if it's the one we're looking for. device->seek(nodeoffset); stream >> fkey; if (fkey == key) { // Read offset and statistics qint32 foffset; quint32 timesused, lastused; stream >> foffset >> timesused; // Update statistics timesused++; lastused = ::time(0); stream.device()->seek(stream.device()->pos() - sizeof(quint32)); stream << timesused << lastused; delete device; return foffset; } } // Nothing was found delete device; return -1; } bool KPixmapCache::Private::checkLockFile() { // For KLockFile we need to ensure the lock file doesn't exist. if (QFile::exists(mLockFileName)) { if (!QFile::remove(mLockFileName)) { kError() << "Couldn't remove lockfile" << mLockFileName; return false; } } return true; } bool KPixmapCache::Private::checkFileVersion(const QString& filename) { if (!mEnabled) { return false; } if (QFile::exists(filename)) { // File already exists, make sure it can be opened QFile f(filename); if (!f.open(QIODevice::ReadOnly)) { kError() << "Couldn't open file" << filename; return false; } // The index header is the same as the beginning of the data header (on purpose), // so use index header for either one. KPixmapCacheIndexHeader indexHeader; // Ensure we have a valid cache. if(sizeof indexHeader != f.read(reinterpret_cast(&indexHeader), sizeof indexHeader) || qstrncmp(indexHeader.magic, KPC_MAGIC, sizeof(indexHeader.magic)) != 0) { kDebug(264) << "File" << filename << "is not KPixmapCache file, or is"; kDebug(264) << "version <= 0x000207, will recreate..."; return q->recreateCacheFiles(); } if(indexHeader.cacheVersion == KPIXMAPCACHE_VERSION) return true; // Don't recreate the cache if it has newer version to avoid // problems when upgrading kdelibs. if(indexHeader.cacheVersion > KPIXMAPCACHE_VERSION) { kDebug(264) << "File" << filename << "has newer version, disabling cache"; return false; } kDebug(264) << "File" << filename << "is outdated, will recreate..."; } return q->recreateCacheFiles(); } bool KPixmapCache::Private::loadDataHeader() { // Open file and datastream on it QFile file(mDataFile); if (!file.open(QIODevice::ReadOnly)) { return false; } KPixmapCacheDataHeader dataHeader; if(sizeof dataHeader != file.read(reinterpret_cast(&dataHeader), sizeof dataHeader)) { kDebug(264) << "Unable to read from data file" << mDataFile; return false; } mDataMmapInfo.size = dataHeader.size; return true; } bool KPixmapCache::Private::loadIndexHeader() { // Open file and datastream on it QFile file(mIndexFile); if (!file.open(QIODevice::ReadOnly)) { return false; } KPixmapCacheIndexHeader indexHeader; if(sizeof indexHeader != file.read(reinterpret_cast(&indexHeader), sizeof indexHeader)) { kWarning(264) << "Failed to read index file's header"; q->recreateCacheFiles(); return false; } mCacheId = indexHeader.cacheId; mTimestamp = indexHeader.timestamp; mIndexMmapInfo.size = indexHeader.size; QDataStream stream(&file); // Give custom implementations chance to load their headers if (!q->loadCustomIndexHeader(stream)) { return false; } mHeaderSize = file.pos(); mIndexRootOffset = file.pos(); return true; } QString KPixmapCache::Private::indexKey(const QString& key) { const QByteArray latin1 = key.toLatin1(); return QString("%1%2").arg((ushort)qChecksum(latin1.data(), latin1.size()), 4, 16, QLatin1Char('0')).arg(key); } QString KPixmapCache::Private::qpcKey(const QString& key) const { return mThisString + key; } void KPixmapCache::Private::writeIndexEntry(QDataStream& stream, const QString& key, int dataoffset) { // New entry will be written to the end of the file qint32 offset = stream.device()->size(); // Find parent index node for this node. int parentoffset = binarySearchKey(stream, key, mIndexRootOffset); if (parentoffset != stream.device()->size()) { // Check if this entry has the same key QString fkey; stream.device()->seek(parentoffset); stream >> fkey; // The key would be empty if the cache had been discarded. if (key == fkey || fkey.isEmpty()) { // We're overwriting an existing entry offset = parentoffset; } } stream.device()->seek(offset); // Write the data stream << key << (qint32)dataoffset; // Statistics (# of uses and last used timestamp) stream << (quint32)1 << (quint32)::time(0); // Write (empty) children offsets stream << (qint32)0 << (qint32)0; // If we created the root node or overwrote existing entry then the two // offsets are equal and we're done. Otherwise set parent's child offset // to correct value. if (parentoffset != offset) { stream.device()->seek(parentoffset); QString fkey; qint32 foffset, tmp; quint32 timesused, lastused; stream >> fkey >> foffset >> timesused >> lastused; if (key < fkey) { // New entry will be parent's left child stream << offset; } else { // New entry will be parent's right child stream >> tmp; stream << offset; } } } bool KPixmapCache::Private::removeEntries(int newsize) { KPCLockFile lock(mLockFileName); if (!lock.isValid()) { kDebug(264) << "Couldn't lock cache" << mName; return false; } QMutexLocker mutexlocker(&mMutex); // Open old (current) files QFile indexfile(mIndexFile); if (!indexfile.open(QIODevice::ReadOnly)) { kDebug(264) << "Couldn't open old index file"; return false; } QDataStream istream(&indexfile); QFile datafile(mDataFile); if (!datafile.open(QIODevice::ReadOnly)) { kDebug(264) << "Couldn't open old data file"; return false; } if (datafile.size() <= newsize*1024) { kDebug(264) << "Cache size is already within limit (" << datafile.size() << " <= " << newsize*1024 << ")"; return true; } QDataStream dstream(&datafile); // Open new files QFile newindexfile(mIndexFile + ".new"); if (!newindexfile.open(QIODevice::ReadWrite)) { kDebug(264) << "Couldn't open new index file"; return false; } QDataStream newistream(&newindexfile); QFile newdatafile(mDataFile + ".new"); if (!newdatafile.open(QIODevice::WriteOnly)) { kDebug(264) << "Couldn't open new data file"; return false; } QDataStream newdstream(&newdatafile); // Copy index file header char* header = new char[mHeaderSize]; if (istream.readRawData(header, mHeaderSize) != (int)mHeaderSize) { kDebug(264) << "Couldn't read index header"; delete [] header; return false; } // Set file size to 0 for mmap stuff reinterpret_cast(header)->size = 0; newistream.writeRawData(header, mHeaderSize); // Copy data file header int dataheaderlen = sizeof(KPixmapCacheDataHeader); // mHeaderSize is always bigger than dataheaderlen, so we needn't create // new buffer if (dstream.readRawData(header, dataheaderlen) != dataheaderlen) { kDebug(264) << "Couldn't read data header"; delete [] header; return false; } // Set file size to 0 for mmap stuff reinterpret_cast(header)->size = 0; newdstream.writeRawData(header, dataheaderlen); delete [] header; // Load all entries QList entries; // Do BFS to find all entries QQueue open; open.enqueue(mIndexRootOffset); while (!open.isEmpty()) { int indexoffset = open.dequeue(); indexfile.seek(indexoffset); QString fkey; qint32 foffset; quint32 timesused, lastused; qint32 leftchild, rightchild; istream >> fkey >> foffset >> timesused >> lastused >> leftchild >> rightchild; entries.append(KPixmapCacheEntry(indexoffset, fkey, foffset, entries.count(), timesused, lastused)); if (leftchild) { open.enqueue(leftchild); } if (rightchild) { open.enqueue(rightchild); } } // Sort the entries according to RemoveStrategy. This moves the best // entries to the beginning of the list if (q->removeEntryStrategy() == RemoveOldest) { qSort(entries.begin(), entries.end(), compareEntriesByAge); } else if (q->removeEntryStrategy() == RemoveSeldomUsed) { qSort(entries.begin(), entries.end(), compareEntriesByTimesUsed); } else { qSort(entries.begin(), entries.end(), compareEntriesByLastUsed); } // Write some entries to the new files int entrieswritten = 0; for (entrieswritten = 0; entrieswritten < entries.count(); entrieswritten++) { const KPixmapCacheEntry& entry = entries[entrieswritten]; // Load data datafile.seek(entry.dataoffset); int entrysize = -datafile.pos(); // We have some duplication here but this way we avoid uncompressing // the data and constructing QPixmap which we don't really need. QString fkey; dstream >> fkey; qint32 format, w, h, bpl; dstream >> format >> w >> h >> bpl; QByteArray imgdatacompressed; dstream >> imgdatacompressed; // Load custom data as well. This will be stored by the subclass itself. if (!q->loadCustomData(dstream)) { return false; } // Find out size of this entry entrysize += datafile.pos(); // Make sure we'll stay within size limit if (newdatafile.size() + entrysize > newsize*1024) { break; } // Now write the same data to the new file int newdataoffset = newdatafile.pos(); newdstream << fkey; newdstream << format << w << h << bpl; newdstream << imgdatacompressed; q->writeCustomData(newdstream); // Finally, add the index entry writeIndexEntry(newistream, entry.key, newdataoffset); } // Remove old files and rename the new ones indexfile.remove(); datafile.remove(); newindexfile.rename(mIndexFile); newdatafile.rename(mDataFile); invalidateMmapFiles(); kDebug(264) << "Wrote back" << entrieswritten << "of" << entries.count() << "entries"; return true; } KPixmapCache::KPixmapCache(const QString& name) :d(new Private(this)) { d->mName = name; d->mUseQPixmapCache = true; d->mCacheLimit = 3 * 1024; d->mRemoveStrategy = RemoveLeastRecentlyUsed; // We cannot call init() here because the subclasses haven't been // constructed yet and so their virtual methods cannot be used. d->mInited = false; } KPixmapCache::~KPixmapCache() { d->unmmapFiles(); delete d; } void KPixmapCache::Private::init() { mInited = true; #ifdef DISABLE_PIXMAPCACHE mValid = mEnabled = false; #else mValid = false; // Find locations of the files mIndexFile = KGlobal::dirs()->locateLocal("cache", "kpc/" + mName + ".index"); mDataFile = KGlobal::dirs()->locateLocal("cache", "kpc/" + mName + ".data"); mLockFileName = KGlobal::dirs()->locateLocal("cache", "kpc/" + mName + ".lock"); mEnabled = true; mEnabled &= checkLockFile(); mEnabled &= checkFileVersion(mDataFile); mEnabled &= checkFileVersion(mIndexFile); if (!mEnabled) { kDebug(264) << "Pixmap cache" << mName << "is disabled"; } else { // Cache is enabled, but check if it's ready for use loadDataHeader(); q->setValid(loadIndexHeader()); // Init mmap stuff if mmap is used mmapFiles(); } #endif } void KPixmapCache::ensureInited() const { if (!d->mInited) { const_cast(this)->d->init(); } } bool KPixmapCache::loadCustomIndexHeader(QDataStream&) { return true; } void KPixmapCache::writeCustomIndexHeader(QDataStream&) { } bool KPixmapCache::isEnabled() const { ensureInited(); return d->mEnabled; } bool KPixmapCache::isValid() const { ensureInited(); return d->mEnabled && d->mValid; } void KPixmapCache::setValid(bool valid) { ensureInited(); d->mValid = valid; } unsigned int KPixmapCache::timestamp() const { ensureInited(); return d->mTimestamp; } void KPixmapCache::setTimestamp(unsigned int ts) { ensureInited(); d->mTimestamp = ts; // Write to file KPCLockFile lock(d->mLockFileName); if (!lock.isValid()) { // FIXME: now what? return; } QIODevice* device = d->indexDevice(); if (!device) { return; } KPixmapCacheIndexHeader header; device->seek(0); if(sizeof header != device->read(reinterpret_cast(&header), sizeof header)) { delete device; return; } header.timestamp = ts; device->seek(0); device->write(reinterpret_cast(&header), sizeof header); delete device; } int KPixmapCache::size() const { ensureInited(); if (d->mDataMmapInfo.file) { return d->mDataMmapInfo.size / 1024; } return QFileInfo(d->mDataFile).size() / 1024; } void KPixmapCache::setUseQPixmapCache(bool use) { d->mUseQPixmapCache = use; } bool KPixmapCache::useQPixmapCache() const { return d->mUseQPixmapCache; } int KPixmapCache::cacheLimit() const { return d->mCacheLimit; } void KPixmapCache::setCacheLimit(int kbytes) { //FIXME: KDE5: this should be uint! if (kbytes < 0) { return; } d->mCacheLimit = kbytes; // if we are initialized, let's make sure that we are actually within // our limits. if (d->mInited && d->mCacheLimit && size() > d->mCacheLimit) { if (size() > (int)(d->mCacheLimit)) { // Can't wait any longer, do it immediately d->removeEntries(d->mCacheLimit * 0.65); } } } KPixmapCache::RemoveStrategy KPixmapCache::removeEntryStrategy() const { return d->mRemoveStrategy; } void KPixmapCache::setRemoveEntryStrategy(KPixmapCache::RemoveStrategy strategy) { d->mRemoveStrategy = strategy; } bool KPixmapCache::recreateCacheFiles() { if (!isEnabled()) { return false; } KPCLockFile lock(d->mLockFileName); // Hope we got the lock... d->invalidateMmapFiles(); d->mEnabled = false; // Create index file KSaveFile indexfile(d->mIndexFile); if (!indexfile.open(QIODevice::WriteOnly)) { kError() << "Couldn't create index file" << d->mIndexFile; return false; } d->mCacheId = ::time(0); d->mTimestamp = ::time(0); // We can't know the full size until custom headers written. // mmapFiles() will take care of correcting the size. KPixmapCacheIndexHeader indexHeader = { {0}, KPIXMAPCACHE_VERSION, 0, d->mCacheId, d->mTimestamp }; memcpy(indexHeader.magic, KPC_MAGIC, sizeof(indexHeader.magic)); indexfile.write(reinterpret_cast(&indexHeader), sizeof indexHeader); // Create data file KSaveFile datafile(d->mDataFile); if (!datafile.open(QIODevice::WriteOnly)) { kError() << "Couldn't create data file" << d->mDataFile; return false; } KPixmapCacheDataHeader dataHeader = { {0}, KPIXMAPCACHE_VERSION, sizeof dataHeader }; memcpy(dataHeader.magic, KPC_MAGIC, sizeof(dataHeader.magic)); datafile.write(reinterpret_cast(&dataHeader), sizeof dataHeader); setValid(true); QDataStream istream(&indexfile); writeCustomIndexHeader(istream); d->mHeaderSize = indexfile.pos(); d->mIndexRootOffset = d->mHeaderSize; // Close the files and mmap them (if mmapping is used) indexfile.close(); datafile.close(); indexfile.finalize(); datafile.finalize(); d->mEnabled = true; d->mmapFiles(); return true; } void KPixmapCache::deleteCache(const QString& name) { QString indexFile = KGlobal::dirs()->locateLocal("cache", "kpc/" + name + ".index"); QString dataFile = KGlobal::dirs()->locateLocal("cache", "kpc/" + name + ".data"); QFile::remove(indexFile); QFile::remove(dataFile); } void KPixmapCache::discard() { // To "discard" the cache we simply have to make sure that every that // was in there before is no longer present when we search for them. // Easiest way to do *that* is to simply delete the index. KPCLockFile lock(d->mLockFileName); if(!lock.isValid()) { kError(264) << "Unable to lock pixmap cache when trying to discard it"; return; } QIODevice *device = d->indexDevice(); if (!device) { kError(264) << "Unable to access index when trying to discard cache"; return; } device->seek(d->mIndexRootOffset); QDataStream stream(device); // Stream an empty QString as the hash key to signify that the cache // has been discarded. stream << QString(); if (d->mUseQPixmapCache) { // TODO: This is broken, it removes every cached QPixmap in the whole // process, not just this KPixmapCache. QPixmapCache::clear(); } } void KPixmapCache::removeEntries(int newsize) { if (!newsize) { newsize = d->mCacheLimit; if (!newsize) { // nothing to do! return; } } d->removeEntries(newsize); } bool KPixmapCache::find(const QString& key, QPixmap& pix) { ensureInited(); if (!isValid()) { return false; } //kDebug(264) << "key:" << key << ", use QPC:" << d->mUseQPixmapCache; // First try the QPixmapCache if (d->mUseQPixmapCache && QPixmapCache::find(d->qpcKey(key), &pix)) { //kDebug(264) << "Found from QPC"; return true; } KPCLockFile lock(d->mLockFileName); if (!lock.isValid()) { return false; } // Try to find the offset QString indexkey = d->indexKey(key); int offset = d->findOffset(indexkey); //kDebug(264) << "found offset" << offset; if (offset == -1) { return false; } // Load the data bool ret = d->loadData(offset, pix); if (ret && d->mUseQPixmapCache) { // This pixmap wasn't in QPC, put it there QPixmapCache::insert(d->qpcKey(key), pix); } return ret; } bool KPixmapCache::Private::loadData(int offset, QPixmap& pix) { // Open device and datastream on it QIODevice* device = dataDevice(); if (!device) { return false; } //kDebug(264) << "Seeking to pos" << offset << "/" << file.size(); if (!device->seek(offset)) { kError() << "Couldn't seek to pos" << offset; delete device; return false; } QDataStream stream(device); // Load QString fkey; stream >> fkey; // Load image info and compressed data qint32 format, w, h, bpl; stream >> format >> w >> h >> bpl; QByteArray imgdatacompressed; stream >> imgdatacompressed; // Uncompress the data and create the image // TODO: make sure this actually works. QImage ctor we use here seems to // want 32-bit aligned data. QByteArray uses malloc() to allocate it's // data, which _probably_ returns 32-bit aligned data. QByteArray imgdata = qUncompress(imgdatacompressed); if (!imgdata.isEmpty()) { QImage img((const uchar*)imgdata.constData(), w, h, bpl, (QImage::Format)format); img.bits(); // make deep copy since we don't want to keep imgdata around pix = QPixmap::fromImage(img); } else { pix = QPixmap(w, h); } if (!q->loadCustomData(stream)) { delete device; return false; } delete device; if (stream.status() != QDataStream::Ok) { kError() << "stream is bad :-( status=" << stream.status(); return false; } //kDebug(264) << "pixmap successfully loaded"; return true; } bool KPixmapCache::loadCustomData(QDataStream&) { return true; } void KPixmapCache::insert(const QString& key, const QPixmap& pix) { ensureInited(); if (!isValid()) { return; } //kDebug(264) << "key:" << key << ", size:" << pix.width() << "x" << pix.height(); // Insert to QPixmapCache as well if (d->mUseQPixmapCache) { QPixmapCache::insert(d->qpcKey(key), pix); } KPCLockFile lock(d->mLockFileName); if (!lock.isValid()) { return; } // Insert to cache QString indexkey = d->indexKey(key); int offset = d->writeData(key, pix); //kDebug(264) << "data is at offset" << offset; if (offset == -1) { return; } d->writeIndex(indexkey, offset); // Make sure the cache size stays within limits if (d->mCacheLimit && size() > d->mCacheLimit) { lock.unlock(); if (size() > (int)(d->mCacheLimit)) { // Can't wait any longer, do it immediately d->removeEntries(d->mCacheLimit * 0.65); } } } int KPixmapCache::Private::writeData(const QString& key, const QPixmap& pix) { // Open device and datastream on it QIODevice* device = dataDevice(); if (!device) { return -1; } int offset = device->size(); device->seek(offset); QDataStream stream(device); // Write the data stream << key; // Write image info and compressed data QImage img = pix.toImage(); QByteArray imgdatacompressed = qCompress(img.bits(), img.byteCount()); stream << (qint32)img.format() << (qint32)img.width() << (qint32)img.height() << (qint32)img.bytesPerLine(); stream << imgdatacompressed; q->writeCustomData(stream); delete device; return offset; } bool KPixmapCache::writeCustomData(QDataStream&) { return true; } void KPixmapCache::Private::writeIndex(const QString& key, int dataoffset) { // Open device and datastream on it QIODevice* device = indexDevice(); if (!device) { return; } QDataStream stream(device); writeIndexEntry(stream, key, dataoffset); delete device; } QPixmap KPixmapCache::loadFromFile(const QString& filename) { QFileInfo fi(filename); if (!fi.exists()) { return QPixmap(); } else if (fi.lastModified().toTime_t() > timestamp()) { // Cache is obsolete, will be regenerated discard(); } QPixmap pix; QString key("file:" + filename); if (!find(key, pix)) { // It wasn't in the cache, so load it... pix = QPixmap(filename); if (pix.isNull()) { return pix; } // ... and put it there insert(key, pix); } return pix; } QPixmap KPixmapCache::loadFromSvg(const QString& filename, const QSize& size) { QFileInfo fi(filename); if (!fi.exists()) { return QPixmap(); } else if (fi.lastModified().toTime_t() > timestamp()) { // Cache is obsolete, will be regenerated discard(); } QPixmap pix; QString key = QString("file:%1_%2_%3").arg(filename).arg(size.width()).arg(size.height()); if (!find(key, pix)) { // It wasn't in the cache, so load it... QSvgRenderer svg; if (!svg.load(filename)) { return pix; // null pixmap } else { QSize pixSize = size.isValid() ? size : svg.defaultSize(); pix = QPixmap(pixSize); pix.fill(Qt::transparent); QPainter p(&pix); svg.render(&p, QRectF(QPointF(), pixSize)); } // ... and put it there insert(key, pix); } return pix; }