1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2006-01-11
7  * Description : shared image loading and caching
8  *
9  * Copyright (C) 2005-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
10  *
11  * This program is free software; you can redistribute it
12  * and/or modify it under the terms of the GNU General
13  * Public License as published by the Free Software Foundation;
14  * either version 2, or (at your option)
15  * any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * ============================================================ */
23 
24 #include "loadingcache.h"
25 
26 // Qt includes
27 
28 #include <QCache>
29 #include <QMap>
30 
31 // Local includes
32 
33 #include "digikam_debug.h"
34 #include "iccsettings.h"
35 #include "kmemoryinfo.h"
36 #include "metaengine.h"
37 #include "thumbnailsize.h"
38 
39 namespace Digikam
40 {
41 
LoadingProcessListener()42 LoadingProcessListener::LoadingProcessListener()
43 {
44 }
45 
~LoadingProcessListener()46 LoadingProcessListener::~LoadingProcessListener()
47 {
48 }
49 
50 // -----------------------------------------------------------------------------------
51 
LoadingProcess()52 LoadingProcess::LoadingProcess()
53 {
54 }
55 
~LoadingProcess()56 LoadingProcess::~LoadingProcess()
57 {
58 }
59 
60 
61 // -----------------------------------------------------------------------------------
62 
63 class Q_DECL_HIDDEN LoadingCache::Private
64 {
65 public:
66 
Private(LoadingCache * const q)67     explicit Private(LoadingCache* const q)
68       : watch(nullptr),
69         q    (q)
70     {
71     }
72 
73     void mapImageFilePath(const QString& filePath, const QString& cacheKey);
74     void mapThumbnailFilePath(const QString& filePath, const QString& cacheKey);
75     void cleanUpImageFilePathHash();
76     void cleanUpThumbnailFilePathHash();
77     LoadingCacheFileWatch* fileWatch() const;
78 
79 public:
80 
81     QCache<QString, DImg>           imageCache;
82     QCache<QString, QImage>         thumbnailImageCache;
83     QCache<QString, QPixmap>        thumbnailPixmapCache;
84     QMultiMap<QString, QString>     imageFilePathMap;
85     QMultiMap<QString, QString>     thumbnailFilePathMap;
86     QHash<LoadingProcess*, QString> loadingDict;
87 
88     /// Note: Don't make the mutex recursive, we need to use a wait condition on it
89     QMutex                          mutex;
90 
91     QWaitCondition                  condVar;
92 
93     LoadingCacheFileWatch*          watch;
94     LoadingCache*                   q;
95 };
96 
fileWatch() const97 LoadingCacheFileWatch* LoadingCache::Private::fileWatch() const
98 {
99     // install default watch if no watch is set yet
100 
101     if (!watch)
102     {
103         q->setFileWatch(new LoadingCacheFileWatch);
104     }
105 
106     return watch;
107 }
108 
mapImageFilePath(const QString & filePath,const QString & cacheKey)109 void LoadingCache::Private::mapImageFilePath(const QString& filePath, const QString& cacheKey)
110 {
111     if (imageFilePathMap.size() > (5 * imageCache.size()))
112     {
113         cleanUpImageFilePathHash();
114     }
115 
116     imageFilePathMap.insert(filePath, cacheKey);
117 }
118 
mapThumbnailFilePath(const QString & filePath,const QString & cacheKey)119 void LoadingCache::Private::mapThumbnailFilePath(const QString& filePath, const QString& cacheKey)
120 {
121     if (thumbnailFilePathMap.size() > (5 * (thumbnailImageCache.size() + thumbnailPixmapCache.size())))
122     {
123         cleanUpThumbnailFilePathHash();
124     }
125 
126     thumbnailFilePathMap.insert(filePath, cacheKey);
127 }
128 
cleanUpImageFilePathHash()129 void LoadingCache::Private::cleanUpImageFilePathHash()
130 {
131     // Remove all entries from hash whose value is no longer a key in the cache
132 
133     QList<QString> keys = imageCache.keys();
134     QMultiMap<QString, QString>::iterator it;
135 
136     for (it = imageFilePathMap.begin() ; it != imageFilePathMap.end() ; )
137     {
138         if (!keys.contains(it.value()))
139         {
140             it = imageFilePathMap.erase(it);
141         }
142         else
143         {
144             ++it;
145         }
146     }
147 }
148 
cleanUpThumbnailFilePathHash()149 void LoadingCache::Private::cleanUpThumbnailFilePathHash()
150 {
151     QList<QString> keys;
152 
153     keys += thumbnailImageCache.keys();
154     keys += thumbnailPixmapCache.keys();
155 
156     QMultiMap<QString, QString>::iterator it;
157 
158     for (it = thumbnailFilePathMap.begin() ; it != thumbnailFilePathMap.end() ; )
159     {
160         if (!keys.contains(it.value()))
161         {
162             it = thumbnailFilePathMap.erase(it);
163         }
164         else
165         {
166             ++it;
167         }
168     }
169 }
170 
171 LoadingCache* LoadingCache::m_instance = nullptr;
172 
cache()173 LoadingCache* LoadingCache::cache()
174 {
175     if (!m_instance)
176     {
177         m_instance = new LoadingCache;
178     }
179 
180     return m_instance;
181 }
182 
cleanUp()183 void LoadingCache::cleanUp()
184 {
185     delete m_instance;
186 }
187 
LoadingCache()188 LoadingCache::LoadingCache()
189     : d(new Private(this))
190 {
191     KMemoryInfo memory = KMemoryInfo::currentInfo();
192     setCacheSize(qBound(60, int(memory.megabytes(KMemoryInfo::TotalRam)*0.05), 400));
193     setThumbnailCacheSize(5, 100); // the pixmap number should not be based on system memory, it's graphics memory
194 
195     // good place to call it here as LoadingCache is a singleton
196 
197     qRegisterMetaType<LoadingDescription>("LoadingDescription");
198     qRegisterMetaType<DImg>("DImg");
199     qRegisterMetaType<MetaEngineData>("MetaEngineData");
200 
201     connect(IccSettings::instance(), SIGNAL(signalICCSettingsChanged(ICCSettingsContainer,ICCSettingsContainer)),
202             this, SLOT(iccSettingsChanged(ICCSettingsContainer,ICCSettingsContainer)));
203 }
204 
~LoadingCache()205 LoadingCache::~LoadingCache()
206 {
207     delete d->watch;
208     delete d;
209     m_instance = nullptr;
210 }
211 
retrieveImage(const QString & cacheKey) const212 DImg* LoadingCache::retrieveImage(const QString& cacheKey) const
213 {
214     QString filePath(d->imageFilePathMap.key(cacheKey));
215     d->fileWatch()->checkFileWatch(filePath);
216 
217     return d->imageCache[cacheKey];
218 }
219 
putImage(const QString & cacheKey,const DImg & img,const QString & filePath) const220 bool LoadingCache::putImage(const QString& cacheKey, const DImg& img, const QString& filePath) const
221 {
222     bool isInserted = false;
223 
224     if (isCacheable(img))
225     {
226         int cost   = img.numBytes() / 1024;
227         isInserted = d->imageCache.insert(cacheKey, new DImg(img), cost);
228 
229         if (isInserted && !filePath.isEmpty())
230         {
231             d->mapImageFilePath(filePath, cacheKey);
232             d->fileWatch()->addedImage(filePath);
233         }
234     }
235 
236     return isInserted;
237 }
238 
removeImage(const QString & cacheKey)239 void LoadingCache::removeImage(const QString& cacheKey)
240 {
241     d->imageCache.remove(cacheKey);
242 }
243 
removeImages()244 void LoadingCache::removeImages()
245 {
246     d->imageCache.clear();
247 }
248 
isCacheable(const DImg & img) const249 bool LoadingCache::isCacheable(const DImg& img) const
250 {
251     // return whether image fits in cache
252 
253     return ((quint64)d->imageCache.maxCost() >= (img.numBytes() / 1024));
254 }
255 
addLoadingProcess(LoadingProcess * const process)256 void LoadingCache::addLoadingProcess(LoadingProcess* const process)
257 {
258     d->loadingDict.insert(process, process->cacheKey());
259 }
260 
retrieveLoadingProcess(const QString & cacheKey) const261 LoadingProcess* LoadingCache::retrieveLoadingProcess(const QString& cacheKey) const
262 {
263     return d->loadingDict.key(cacheKey, nullptr);
264 }
265 
removeLoadingProcess(LoadingProcess * const process)266 void LoadingCache::removeLoadingProcess(LoadingProcess* const process)
267 {
268     d->loadingDict.remove(process);
269 }
270 
notifyNewLoadingProcess(LoadingProcess * const process,const LoadingDescription & description)271 void LoadingCache::notifyNewLoadingProcess(LoadingProcess* const process, const LoadingDescription& description)
272 {
273     for (QHash<LoadingProcess*, QString>::const_iterator it = d->loadingDict.constBegin() ;
274          it != d->loadingDict.constEnd() ; ++it)
275     {
276         it.key()->notifyNewLoadingProcess(process, description);
277     }
278 }
279 
setCacheSize(int megabytes)280 void LoadingCache::setCacheSize(int megabytes)
281 {
282     qCDebug(DIGIKAM_GENERAL_LOG) << "Allowing a cache size of" << megabytes << "MB";
283     d->imageCache.setMaxCost(megabytes * 1024);
284 }
285 
286 // --- Thumbnails ----
287 
retrieveThumbnail(const QString & cacheKey) const288 const QImage* LoadingCache::retrieveThumbnail(const QString& cacheKey) const
289 {
290     return d->thumbnailImageCache[cacheKey];
291 }
292 
retrieveThumbnailPixmap(const QString & cacheKey) const293 const QPixmap* LoadingCache::retrieveThumbnailPixmap(const QString& cacheKey) const
294 {
295     return d->thumbnailPixmapCache[cacheKey];
296 }
297 
hasThumbnailPixmap(const QString & cacheKey) const298 bool LoadingCache::hasThumbnailPixmap(const QString& cacheKey) const
299 {
300     return d->thumbnailPixmapCache.contains(cacheKey);
301 }
302 
putThumbnail(const QString & cacheKey,const QImage & thumb,const QString & filePath)303 void LoadingCache::putThumbnail(const QString& cacheKey, const QImage& thumb, const QString& filePath)
304 {
305 
306 #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
307 
308     int cost = thumb.sizeInBytes();
309 
310 #else
311 
312     int cost = thumb.byteCount();
313 
314 #endif
315 
316     if (d->thumbnailImageCache.insert(cacheKey, new QImage(thumb), cost))
317     {
318         d->mapThumbnailFilePath(filePath, cacheKey);
319     }
320 }
321 
putThumbnail(const QString & cacheKey,const QPixmap & thumb,const QString & filePath)322 void LoadingCache::putThumbnail(const QString& cacheKey, const QPixmap& thumb, const QString& filePath)
323 {
324     int cost = thumb.width() * thumb.height() * thumb.depth() / 8;
325 
326     if (d->thumbnailPixmapCache.insert(cacheKey, new QPixmap(thumb), cost))
327     {
328         d->mapThumbnailFilePath(filePath, cacheKey);
329     }
330 }
331 
removeThumbnail(const QString & cacheKey)332 void LoadingCache::removeThumbnail(const QString& cacheKey)
333 {
334     d->thumbnailImageCache.remove(cacheKey);
335     d->thumbnailPixmapCache.remove(cacheKey);
336 }
337 
removeThumbnails()338 void LoadingCache::removeThumbnails()
339 {
340     d->thumbnailImageCache.clear();
341     d->thumbnailPixmapCache.clear();
342 }
343 
setThumbnailCacheSize(int numberOfQImages,int numberOfQPixmaps)344 void LoadingCache::setThumbnailCacheSize(int numberOfQImages, int numberOfQPixmaps)
345 {
346     d->thumbnailImageCache.setMaxCost(numberOfQImages *
347                                       ThumbnailSize::maxThumbsSize() *
348                                       ThumbnailSize::maxThumbsSize() * 4);
349 
350     d->thumbnailPixmapCache.setMaxCost(numberOfQPixmaps *
351                                        ThumbnailSize::maxThumbsSize() *
352                                        ThumbnailSize::maxThumbsSize() * QPixmap::defaultDepth() / 8);
353 }
354 
setFileWatch(LoadingCacheFileWatch * const watch)355 void LoadingCache::setFileWatch(LoadingCacheFileWatch* const watch)
356 {
357     delete d->watch;
358     d->watch          = watch;
359     d->watch->m_cache = this;
360 }
361 
imageFilePathsInCache() const362 QStringList LoadingCache::imageFilePathsInCache() const
363 {
364     d->cleanUpImageFilePathHash();
365 
366     return d->imageFilePathMap.uniqueKeys();
367 }
368 
thumbnailFilePathsInCache() const369 QStringList LoadingCache::thumbnailFilePathsInCache() const
370 {
371     d->cleanUpThumbnailFilePathHash();
372 
373     return d->thumbnailFilePathMap.uniqueKeys();
374 }
375 
notifyFileChanged(const QString & filePath,bool notify)376 void LoadingCache::notifyFileChanged(const QString& filePath, bool notify)
377 {
378     QList<QString> keys = d->imageFilePathMap.values(filePath);
379 
380     foreach (const QString& cacheKey, keys)
381     {
382         d->imageCache.remove(cacheKey);
383     }
384 
385     keys = d->thumbnailFilePathMap.values(filePath);
386 
387     foreach (const QString& cacheKey, keys)
388     {
389         d->thumbnailImageCache.remove(cacheKey);
390         d->thumbnailPixmapCache.remove(cacheKey);
391     }
392 
393     if (notify)
394     {
395         emit fileChanged(filePath);
396     }
397 }
398 
iccSettingsChanged(const ICCSettingsContainer & current,const ICCSettingsContainer & previous)399 void LoadingCache::iccSettingsChanged(const ICCSettingsContainer& current, const ICCSettingsContainer& previous)
400 {
401     if ((current.enableCM           != previous.enableCM)           ||
402         (current.useManagedPreviews != previous.useManagedPreviews) ||
403         (current.monitorProfile     != previous.monitorProfile))
404     {
405         LoadingCache::CacheLock lock(this);
406         removeImages();
407         removeThumbnails();
408     }
409 }
410 
411 //---------------------------------------------------------------------------------------------------
412 
LoadingCacheFileWatch()413 LoadingCacheFileWatch::LoadingCacheFileWatch()
414     : m_cache(nullptr)
415 {
416 }
417 
~LoadingCacheFileWatch()418 LoadingCacheFileWatch::~LoadingCacheFileWatch()
419 {
420     if (m_cache)
421     {
422         LoadingCache::CacheLock lock(m_cache);
423 
424         if (m_cache->d->watch == this)
425         {
426             m_cache->d->watch = nullptr;
427         }
428     }
429 }
430 
addedImage(const QString & filePath)431 void LoadingCacheFileWatch::addedImage(const QString& filePath)
432 {
433     if (!m_cache || filePath.isEmpty())
434     {
435         return;
436     }
437 
438     QStringList cachePaths = m_cache->imageFilePathsInCache();
439 
440     if (m_watchMap.size() > cachePaths.size())
441     {
442         foreach (const QString& path, m_watchMap.keys())
443         {
444             if (!cachePaths.contains(path))
445             {
446                 m_watchMap.remove(path);
447             }
448         }
449     }
450 
451     QFileInfo info(filePath);
452 
453     m_watchMap.insert(filePath, qMakePair(info.size(),
454                                           info.lastModified()));
455 }
456 
checkFileWatch(const QString & filePath)457 void LoadingCacheFileWatch::checkFileWatch(const QString& filePath)
458 {
459     if (!m_cache || filePath.isEmpty())
460     {
461         return;
462     }
463 
464     if (m_watchMap.contains(filePath))
465     {
466         QFileInfo info(filePath);
467         QPair<qint64, QDateTime> pair = m_watchMap.value(filePath);
468 
469         if ((info.size()         != pair.first) ||
470             (info.lastModified() != pair.second))
471         {
472             qCDebug(DIGIKAM_GENERAL_LOG) << "LoadingCache file dirty:" << filePath;
473 
474             m_cache->notifyFileChanged(filePath);
475             m_watchMap.remove(filePath);
476         }
477     }
478 }
479 
notifyFileChanged(const QString & filePath)480 void LoadingCacheFileWatch::notifyFileChanged(const QString& filePath)
481 {
482     if (m_cache)
483     {
484         LoadingCache::CacheLock lock(m_cache);
485         m_cache->notifyFileChanged(filePath);
486     }
487 }
488 
489 //---------------------------------------------------------------------------------------------------
490 
CacheLock(LoadingCache * const cache)491 LoadingCache::CacheLock::CacheLock(LoadingCache* const cache)
492     : m_cache(cache)
493 {
494     m_cache->d->mutex.lock();
495 }
496 
~CacheLock()497 LoadingCache::CacheLock::~CacheLock()
498 {
499     m_cache->d->mutex.unlock();
500 }
501 
wakeAll()502 void LoadingCache::CacheLock::wakeAll()
503 {
504     // obviously the mutex is locked when this function is called
505 
506     m_cache->d->condVar.wakeAll();
507 }
508 
timedWait()509 void LoadingCache::CacheLock::timedWait()
510 {
511     // same as above, the mutex is certainly locked
512 
513     m_cache->d->condVar.wait(&m_cache->d->mutex, 1000);
514 }
515 
516 } // namespace Digikam
517