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