1 // SPDX-FileCopyrightText: 2003-2020 Jesper K. Pedersen <blackie@kde.org>
2 // SPDX-FileCopyrightText: 2021 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
3 //
4 // SPDX-License-Identifier: GPL-2.0-or-later
5 
6 #ifndef KPATHUMBNAILS_THUMBNAILCACHE_H
7 #define KPATHUMBNAILS_THUMBNAILCACHE_H
8 #include "CacheFileInfo.h"
9 
10 #include <kpabase/FileNameList.h>
11 
12 #include <QDir>
13 #include <QFile>
14 #include <QHash>
15 #include <QImage>
16 #include <QMutex>
17 
18 template <class Key, class T>
19 class QCache;
20 
21 namespace ImageManager
22 {
23 
24 class ThumbnailMapping;
25 
26 /**
27  * @brief The ThumbnailCache implements thumbnail storage optimized for speed.
28  *
29  * ## On-disk storage
30  * The problem with the FreeDesktop.org thumbnail storage is that there is one file per image.
31  * This means that showing a full page of thumbnails, containing dozens of images requires many
32  * file operations.
33  *
34  * Our storage scheme introduces a single index file (\c thumbnailindex) that contains
35  * the index of known thumbnails and their location in the thumbnail storage files (\c thumb-N).
36  * The thumbnail storage files contain raw JPEG thumbnail data.
37  *
38  * This layout creates far less files on the filesystem,
39  * the files can be memory-mapped, and because similar sets of images are often
40  * shown together, data locality is used to our advantage.
41  *
42  * ## Caveats
43  * Note that thumbnails are only ever added, never deleted from the thumbnail files.
44  * Old images remain in the thumbnail files - they are just removed from the index file.
45  *
46  * ## Further reading
47  * - https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html
48  */
49 class ThumbnailCache : public QObject
50 {
51     Q_OBJECT
52 
53 public:
54     /**
55      * @brief ThumbnailCache
56      * Provide access to a KPhotoAlbum-style thumbnail cache in the given directory.
57      * @param baseDirectory the directory in which the \c thumbnailindex file resides.
58      */
59     ThumbnailCache(const QString &baseDirectory);
60     ~ThumbnailCache() override;
61     /**
62      * @brief Insert a thumbnail for the given file.
63      * @param name the image file name
64      * @param image the thumbnail data
65      */
66     void insert(const DB::FileName &name, const QImage &image);
67     /**
68      * @brief insert inserts raw JPEG-encoded data into the thumbnail database.
69      * It is recommended that you use insert(const DB::FileName&, const QImage&) if possible.
70      * @param name the image file name
71      * @param thumbnailData the JPEG encoded image data
72      */
73     void insert(const DB::FileName &name, const QByteArray &thumbnailData);
74     /**
75      * @brief lookup and return the thumbnail for the given file.
76      * Note: this method requires a GuiApplication to exist.
77      * @param name the image file name
78      * @return a QPixmap containing the thumbnail, or a null QPixmap if no thumbnail was found.
79      */
80     QPixmap lookup(const DB::FileName &name) const;
81     /**
82      * @brief lookupRawData
83      * @param name the image file name
84      * @return the raw JPEG thumbnail data or a null QByteArray.
85      */
86     QByteArray lookupRawData(const DB::FileName &name) const;
87     /**
88      * @brief Check if the ThumbnailCache contains a thumbnail for the given file.
89      * @param name the image file name
90      * @return \c true if the thumbnail exists, \c false otherwise.
91      */
92     bool contains(const DB::FileName &name) const;
93     /**
94      * @brief "Forget" the thumbnail for an image.
95      * @param name the image file name
96      */
97     void removeThumbnail(const DB::FileName &name);
98     /**
99      * @brief "Forget" the thumbnails for the given images.
100      * Like removeThumbnail(), but for a list of images
101      * @param names a list of image file names
102      */
103     void removeThumbnails(const DB::FileNameList &names);
104 
105     /**
106      * @brief thumbnailSize
107      * Usually, this is the size of the thumbnails in the cache.
108      * If the index file was converted from an older file version (4),
109      * the size is read from the configuration file.
110      * @return the current thumbnail size.
111      */
112     int thumbnailSize() const;
113 
114     /**
115      * @brief Returns the file format version of the thumbnailindex file currently on disk.
116      *
117      * Usually, this is equal to the current version, but if an old ThumbnailCache
118      * that is still compatible with this version of KPhotoAlbum is loaded and was not yet stored,
119      * it may differ.
120      * @return 4 or 5 if the cache has been written to disk, or -1 for a fresh, unsaved cache.
121      */
122     int actualFileVersion() const;
123 
124     /**
125      * @brief Version of the tumbnailindex file when saved.
126      * @return The file format version of the thumbnailindex file.
127      */
128     static int preferredFileVersion();
129 
130     /**
131      * @brief Check all thumbnails for consistency with thumbnailSize().
132      * Only the thumbnails which are saved to disk are checked.
133      * If you have changed changed the cache you need to save it to guarantee correct results.
134      * @return all thumbnails that do not match the expected image dimensions.
135      */
136     DB::FileNameList findIncorrectlySizedThumbnails() const;
137 
138     /**
139      * @brief size
140      * @return the number of (saved) thumbnails in the cache
141      */
142     int size() const;
143 
144     /**
145      * @brief Compacts the on-disk storage for the cache by discarding stale data from its files.
146      * To ensure consistency, this method also saves the cache.
147      */
148     void vacuum();
149 
150 public slots:
151     /**
152      * @brief Save the thumbnail cache to disk.
153      * Note: this method emits an internal signal which calls the actual save implementation.
154      * Therefore, saving may not be finished when this method returns.
155      */
156     void save() const;
157     /**
158      * @brief Invalidate the ThumbnailCache and remove the thumbnail files and index.
159      */
160     void flush();
161 
162     /**
163      * @brief setThumbnailSize sets the thumbnail size recorded in the thumbnail index file.
164      * If the value changes, the thumbnail cache is invalidated.
165      * Except minimal sanity checks, no bounds for thumbnailSize are enforced.
166      * @param thumbnailSize
167      */
168     void setThumbnailSize(int thumbnailSize);
169 signals:
170     /**
171      * @brief doSave is emitted when save() is called.
172      * This signal is more or less an internal signal.
173      */
174     void doSave() const;
175 
176     /**
177      * @brief cacheInvalidated is emitted when the thumbnails are no longer valid.
178      * This usually happens when the thumbnail size changed.
179      * This signal is *not* emitted when the cache was flushed by explicit request.
180      * @see cacheFlushed()
181      */
182     void cacheInvalidated();
183 
184     /**
185      * @brief cacheFlushed is emitted if the cache was flushed by explicit request.
186      * @see flush()
187      * @see cacheInvalidated()
188      */
189     void cacheFlushed();
190 
191     /**
192      * @brief saveComplete is emitted after the thumbnail cache was successfully saved.
193      * At the time the signal is emitted, the cache is not dirty (i.e. a full save was performed).
194      */
195     void saveComplete() const;
196 
197 private:
198     /**
199      * @brief load the \c thumbnailindex file if possible.
200      * This function populates the thumbnail hash, but does not
201      * load any actual thumbnail data.
202      * If the file does not exist, or if it is not compatible,
203      * then it is discarded.
204      */
205     void load();
206     QString fileNameForIndex(int index) const;
207     /**
208      * @brief thumbnailPath
209      * @param utf8FileName the name of the file (does not have to exist), UTF-8 encoded
210      * @return the file path for the named file in the thumbnail directory
211      */
212     QString thumbnailPath(const char *utf8FileName) const;
213     /**
214      * @brief thumbnailPath
215      * @param fileName the name of the file (does not have to exist)
216      * @return the file path for the named file in the thumbnail directory
217      */
218     QString thumbnailPath(const QString &fileName) const;
219 
220     // mutable because saveIncremental is const
221     mutable int m_fileVersion = -1;
222     int m_thumbnailSize = -1;
223     const QDir m_baseDir;
224     QHash<DB::FileName, CacheFileInfo> m_hash;
225     mutable QHash<DB::FileName, CacheFileInfo> m_unsavedHash;
226     /* Protects accesses to the data (hash and unsaved hash) */
227     mutable QMutex m_dataLock;
228     /* Prevents multiple saves from happening simultaneously */
229     mutable QMutex m_saveLock;
230     /* Protects writing thumbnails to disk */
231     mutable QMutex m_thumbnailWriterLock;
232     int m_currentFile;
233     int m_currentOffset;
234     mutable QTimer *m_timer;
235     mutable bool m_needsFullSave;
236     mutable bool m_isDirty;
237     /**
238      * @brief saveFull stores the full contents of the index file.
239      * I.e. the whole file is rewritten.
240      */
241     void saveFull() const;
242     /**
243      * @brief saveIncremental appends all unsaved hash entries to the index file.
244      */
245     void saveIncremental() const;
246     /**
247      * @brief saveInternal checks whether a full save is requested or needed and calls the incremental or full save accordingly.
248      */
249     void saveInternal() const;
250     /**
251      * @brief saveImpl calls saveInternal and resets the save timer.
252      */
253     void saveImpl() const;
254 
255     /**
256      * Holds an in-memory cache of thumbnail files.
257      */
258     mutable QCache<int, ThumbnailMapping> *m_memcache;
259     mutable QFile *m_currentWriter;
260 };
261 
262 /**
263  * @brief defaultThumbnailDirectory
264  * @return the default thumbnail (sub-)directory name, e.g. ".thumbnails"
265  */
266 QString defaultThumbnailDirectory();
267 }
268 
269 #endif /* KPATHUMBNAILS_THUMBNAILCACHE_H */
270 
271 // vi:expandtab:tabstop=4 shiftwidth=4:
272