1
// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
3
* Copyright (C) 2012 Victor Eduardo <victoreduardm@gmail.com>
5
* This library is free software; you can redistribute it and/or
6
* modify it under the terms of the GNU Lesser General Public
7
* License as published by the Free Software Foundation; either
8
* version 2.1 of the License, or (at your option) any later version.
10
* See the file COPYING for the full license text.
14
* A thread-safe place to store cached pixbufs
16
* Pixbuf images are permanently stored at the directory passed to the constructor,
17
* and use JPEG buffers by default, since they are much lighter in terms of
18
* resource usage and rendering time, while offering acceptable quality. A
19
* different image format can be specified at construct-time too.
21
* When saving changes to the cache directory, a MD5-based name is used, which
22
* is in turn computed from the image's key. This often works better than escaping
23
* (and later unescaping) paths, and lets us have tighter control over the image
24
* format used, since it's encoded along with the image name.
26
* Keep in mind that both cache_image_*() and decache_image() do blocking I/O,
27
* and therefore it's recommended to make extensive use of has_image() prior to
28
* calling these in order to avoid unnecessary disk access.
30
* This class is meant to be kept as generic as possible. It currently does not
31
* depend on Huddle's internal API (except for PixbufUtils, which is also considered
32
* to be generic). It should be easy to re-use it on another application.
34
public class Huddle.PixbufCache {
37
* Supported image's content types.
39
public const string[] IMAGE_TYPES = {
46
public Gee.Map<string, Gdk.Pixbuf> images {
47
owned get { return image_map.read_only_view; }
50
public Gdk.PixbufFormat image_format { get; private set; }
52
private const string DEFAULT_FORMAT_NAME = "jpeg";
53
private File image_dir;
54
private Gee.HashMap<string, Gdk.Pixbuf> image_map;
57
* Creates a new {@link Huddle.PixbufCache} object.
58
* It also creates the cache directory if it doesn't exist, and thus expect blocking I/O.
60
* @param image_dir a {@link GLib.File} representing the cache directory.
61
* @param image_format a string specifying the image format, or null to use the default
62
* format (JPEG). Valid image formats are those supported by {@link Gdk.Pixbuf.save}.
64
public PixbufCache (File image_dir, Gdk.PixbufFormat? image_format = null) {
65
if (image_format == null) {
66
foreach (var format in Gdk.Pixbuf.get_formats ()) {
67
if (format.get_name () == DEFAULT_FORMAT_NAME) {
68
this.image_format = format;
73
this.image_format = image_format;
76
// We need to be able to write images to disk for permanent storage
77
assert (this.image_format != null && this.image_format.is_writable ());
79
this.image_dir = image_dir;
82
image_dir.make_directory_with_parents (null);
84
if (!(err is IOError.EXISTS))
85
warning ("Could not create image cache directory: %s", err.message);
88
image_map = new Gee.HashMap<string, Gdk.Pixbuf> ();
92
* This method is called right before storing a pixbuf in the internal hashmap.
93
* Its purpose is to allow client code to make modifications to the passed image
94
* (e.g. adding a drop shadow, etc.) Changes are *not* reflected on disk.
96
* You can also use this method to prevent the storage of certain images in
97
* the cache. To do so it just needs to set the new pixbuf to null. In such case,
98
* the call to {@link Huddle.PixbufCache.cache_image} will have no effect, since
99
* null pixbufs are not added to the internal map, nor saved to disk.
101
public delegate Gdk.Pixbuf? FilterFunction (string key, Gdk.Pixbuf orig_pixbuf);
102
public unowned FilterFunction? filter_func;
105
* Verifies whether the key has a corresponding image in the cache.
107
public bool has_image (string key) {
108
return image_map.has_key (key);
112
* Returns the location of an image on disk. This call does no blocking I/O.
113
* Use it to consistently read cached image files.
115
* This method only computes a path based on the passed key, and thus it doesn't
116
* know whether the file pointed by the returned path exists or not. It is
117
* recommended to use {@link Huddle.PixbufCache.has_image} to check for that.
119
public string get_cached_image_path (string key) {
120
string filename = Checksum.compute_for_string (ChecksumType.MD5, key + image_format.get_name ());
121
return image_dir.get_child (filename).get_path ();
125
* Removes the image corresponding to the key from the table. It also deletes
126
* the associated file from the cache directory.
128
public Gdk.Pixbuf? decache_image (string key) {
132
image_map.unset (key, out val);
133
delete_file (get_cached_image_path (key));
140
* Associates an image to a key.
142
* The image is stored on an internal table and on disk, and can be later
143
* retrieved through {@link Huddle.PixbufCache.get_image}.
145
* This method can also be used to update image buffers when they have changed,
146
* since the old image is overwritten (in both primary memory and disk.)
148
public async void cache_image_async (string key, Gdk.Pixbuf image) {
149
yield cache_image_internal_async (key, image, true);
153
* This method does the same as {@link Huddle.PixbufCache.cache_image}, with the only
154
* difference that it first fetches the image from the given file.
156
public async void cache_image_from_file_async (string key, File image_file, Cancellable? c = null) {
157
var image = yield load_image_from_file_async (image_file, c);
159
yield cache_image_async (key, image);
164
* Retrieves the image for the given key from the cache.
166
* @return A valid {@link Gdk.Pixbuf}, or null if the image is not found.
168
public Gdk.Pixbuf? get_image (string key) {
169
return image_map.get (key);
174
* Retrieves the image for the given key from the cache. If lookup_file
175
* is true, it attempts to load the image from the disk cache in case
176
* it wasn't found in the table. The lookup_file parameter provides an
177
* efficient way to initialize the cache, since -when set to true-, files
178
* are retrieved from disk as they are needed.
180
* @return null if the key's corresponding image was not found; Otherwise
181
* a valid {@link Gdk.Pixbuf}
183
public async Gdk.Pixbuf? get_image_async (string key, bool lookup_file = true) {
184
if (lookup_file && !has_image (key)) {
185
var image_file = File.new_for_path (get_cached_image_path (key));
186
var image = yield load_image_from_file_async (image_file, null);
188
yield cache_image_internal_async (key, image, false);
191
return image_map.get (key);
195
* Adds an image to the hash map and also writes the image to disk if save_to_disk is true.
197
private async void cache_image_internal_async (string key, Gdk.Pixbuf image, bool save_to_disk) {
198
Gdk.Pixbuf? modified_image = (filter_func != null) ? filter_func (key, image) : image;
200
if (modified_image != null) {
202
image_map.set (key, modified_image);
204
// We store the unmodified image. Otherwise modifications would be applied over and
205
// over again every time the images are retrieved from disk.
207
save_image_to_file (key, image);
213
* Central place for retrieving images from permanent-storage locations. This is not
214
* limited to this cache's local directory.
216
private async Gdk.Pixbuf? load_image_from_file_async (File image_file, Cancellable? cancellable) {
217
Gdk.Pixbuf? image = null;
220
image = yield PixbufUtils.get_pixbuf_from_file_async (image_file, cancellable);
221
} catch (Error err) {
222
warning ("Could not get image from file [%s]: %s", image_file.get_uri (), err.message);
229
* Stores a pixbuf in the cache directory. Not thread-safe
231
private void save_image_to_file (string key, Gdk.Pixbuf to_save) {
232
debug ("Saving cached image for: %s", key);
235
string path = get_cached_image_path (key);
236
if (delete_file (path))
237
to_save.save (path, image_format.get_name ());
238
} catch (Error err) {
239
warning ("Could not save pixbuf: %s", err.message);
244
* Deletes the file pointed by path. It silently fails in case the file
245
* doesn't exist. Not thread-safe.
247
* @return true in case the image was deleted or doesn't exist; false otherwise.
249
private bool delete_file (string path) {
251
File.new_for_path (path).delete ();
252
} catch (Error err) {
253
if (err is IOError.NOT_FOUND) {
256
warning ("Could not delete image: %s", err.message);