~victored/+junk/huddle

« back to all changes in this revision

Viewing changes to core/utils/pixbuf-cache.vala

  • Committer: Victor Eduardo
  • Date: 2012-10-09 05:17:40 UTC
  • Revision ID: victor@elementaryos.org-20121009051740-vg6nik7mqjy8mwns
Initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
 
2
/**
 
3
 * Copyright (C) 2012 Victor Eduardo <victoreduardm@gmail.com>
 
4
 *
 
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.
 
9
 *
 
10
 * See the file COPYING for the full license text.
 
11
 */
 
12
 
 
13
/**
 
14
 * A thread-safe place to store cached pixbufs
 
15
 *
 
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.
 
20
 *
 
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.
 
25
 *
 
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.
 
29
 *
 
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.
 
33
 */
 
34
public class Huddle.PixbufCache {
 
35
 
 
36
    /**
 
37
     * Supported image's content types.
 
38
     */
 
39
    public const string[] IMAGE_TYPES = {
 
40
        "image/jpg",
 
41
        "image/jpeg",
 
42
        "image/png",
 
43
        "image/tiff"
 
44
    };
 
45
 
 
46
    public Gee.Map<string, Gdk.Pixbuf> images {
 
47
        owned get { return image_map.read_only_view; }
 
48
    }
 
49
 
 
50
    public Gdk.PixbufFormat image_format { get; private set; }
 
51
 
 
52
    private const string DEFAULT_FORMAT_NAME = "jpeg";
 
53
    private File image_dir;
 
54
    private Gee.HashMap<string, Gdk.Pixbuf> image_map;
 
55
 
 
56
    /**
 
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.
 
59
     *
 
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}.
 
63
     */
 
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;
 
69
                    break;
 
70
                }
 
71
            }
 
72
        } else {
 
73
            this.image_format = image_format;
 
74
        }
 
75
 
 
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 ());
 
78
 
 
79
        this.image_dir = image_dir;
 
80
 
 
81
        try {
 
82
            image_dir.make_directory_with_parents (null);
 
83
        } catch (Error err) {
 
84
            if (!(err is IOError.EXISTS))
 
85
                warning ("Could not create image cache directory: %s", err.message);
 
86
        }
 
87
 
 
88
        image_map = new Gee.HashMap<string, Gdk.Pixbuf> ();
 
89
    }
 
90
 
 
91
    /**
 
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.
 
95
     *
 
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.
 
100
     */
 
101
    public delegate Gdk.Pixbuf? FilterFunction (string key, Gdk.Pixbuf orig_pixbuf);
 
102
    public unowned FilterFunction? filter_func;
 
103
 
 
104
    /**
 
105
     * Verifies whether the key has a corresponding image in the cache.
 
106
     */
 
107
    public bool has_image (string key) {
 
108
        return image_map.has_key (key);
 
109
    }
 
110
 
 
111
    /**
 
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.
 
114
     *
 
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.
 
118
     */
 
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 ();
 
122
    }
 
123
 
 
124
    /**
 
125
     * Removes the image corresponding to the key from the table. It also deletes
 
126
     * the associated file from the cache directory.
 
127
     */
 
128
    public Gdk.Pixbuf? decache_image (string key) {
 
129
        Gdk.Pixbuf? val;
 
130
 
 
131
        lock (image_map) {
 
132
            image_map.unset (key, out val);
 
133
            delete_file (get_cached_image_path (key));
 
134
        }
 
135
 
 
136
        return val;
 
137
    }
 
138
 
 
139
    /**
 
140
     * Associates an image to a key.
 
141
     *
 
142
     * The image is stored on an internal table and on disk, and can be later
 
143
     * retrieved through {@link Huddle.PixbufCache.get_image}.
 
144
     *
 
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.)
 
147
     */
 
148
    public async void cache_image_async (string key, Gdk.Pixbuf image) {
 
149
        yield cache_image_internal_async (key, image, true);
 
150
    }
 
151
 
 
152
    /**
 
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.
 
155
     */
 
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);
 
158
        if (image != null)
 
159
            yield cache_image_async (key, image);
 
160
    }
 
161
 
 
162
 
 
163
    /**
 
164
     * Retrieves the image for the given key from the cache.
 
165
     *
 
166
     * @return A valid {@link Gdk.Pixbuf}, or null if the image is not found.
 
167
     */
 
168
    public Gdk.Pixbuf? get_image (string key) {
 
169
        return image_map.get (key);
 
170
    }
 
171
 
 
172
 
 
173
    /**
 
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.
 
179
     *
 
180
     * @return null if the key's corresponding image was not found; Otherwise
 
181
     *         a valid {@link Gdk.Pixbuf}
 
182
     */
 
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);
 
187
            if (image != null)
 
188
                yield cache_image_internal_async (key, image, false);
 
189
        }
 
190
 
 
191
        return image_map.get (key);
 
192
    }
 
193
 
 
194
    /**
 
195
     * Adds an image to the hash map and also writes the image to disk if save_to_disk is true.
 
196
     */
 
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;
 
199
 
 
200
        if (modified_image != null) {
 
201
            lock (image_map) {
 
202
                image_map.set (key, modified_image);
 
203
 
 
204
                // We store the unmodified image. Otherwise modifications would be applied over and
 
205
                // over again every time the images are retrieved from disk.
 
206
                if (save_to_disk)
 
207
                    save_image_to_file (key, image);
 
208
            }
 
209
        }
 
210
    }
 
211
 
 
212
    /**
 
213
     * Central place for retrieving images from permanent-storage locations. This is not
 
214
     * limited to this cache's local directory.
 
215
     */
 
216
    private async Gdk.Pixbuf? load_image_from_file_async (File image_file, Cancellable? cancellable) {
 
217
        Gdk.Pixbuf? image = null;
 
218
 
 
219
        try {
 
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);
 
223
        }
 
224
 
 
225
        return image;
 
226
    }
 
227
 
 
228
    /**
 
229
     * Stores a pixbuf in the cache directory. Not thread-safe
 
230
     */
 
231
    private void save_image_to_file (string key, Gdk.Pixbuf to_save) {
 
232
        debug ("Saving cached image for: %s", key);
 
233
 
 
234
        try {
 
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);
 
240
        }
 
241
    }
 
242
 
 
243
    /**
 
244
     * Deletes the file pointed by path. It silently fails in case the file
 
245
     * doesn't exist. Not thread-safe.
 
246
     *
 
247
     * @return true in case the image was deleted or doesn't exist; false otherwise.
 
248
     */
 
249
    private bool delete_file (string path) {
 
250
        try {
 
251
            File.new_for_path (path).delete ();
 
252
        } catch (Error err) {
 
253
            if (err is IOError.NOT_FOUND) {
 
254
                debug (err.message);
 
255
            } else {
 
256
                warning ("Could not delete image: %s", err.message);
 
257
                return false;
 
258
            }
 
259
        }
 
260
 
 
261
        return true;
 
262
    }
 
263
}