[recipes/image-download: 9/12] Rework GrImage with new loading APIs



commit 32a304cfbc0c89c918a150e36a043aeed5d9d52b
Author: Matthias Clasen <mclasen redhat com>
Date:   Sun Mar 5 18:14:08 2017 -0500

    Rework GrImage with new loading APIs
    
    Add an async API to GrImage. The implementation uses libsoup
    to possibly load images from the network (the location where
    to load images from still has to be determined).
    
    The implementation is inspired by gnome-software's code for loading
    screenshots, but is generally simpler. We do support loading a small
    thumbnail first, and displaying it blurred, while the main image is
    downloading.
    
    Downloaded images are stored in XDG_CACHE_DIR/gnome-recipes.

 src/gr-image.c |  411 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 src/gr-image.h |   33 ++++-
 2 files changed, 437 insertions(+), 7 deletions(-)
---
diff --git a/src/gr-image.c b/src/gr-image.c
index dd61b2a..e8e4d24 100644
--- a/src/gr-image.c
+++ b/src/gr-image.c
@@ -20,12 +20,42 @@
 
 #include "config.h"
 
+#include <libsoup/soup.h>
+
 #include "gr-image.h"
+#include "gr-utils.h"
+
+
+typedef struct {
+        GtkImage *image;
+        int width;
+        int height;
+        gboolean fit;
+        GCancellable *cancellable;
+        GrImageCallback callback;
+        gpointer data;
+} TaskData;
+
+static void
+task_data_free (gpointer data)
+{
+        TaskData *td = data;
+
+        g_clear_object (&td->cancellable);
+
+        g_free (td);
+}
 
 struct _GrImage
 {
         GObject parent_instance;
+        char *id;
         char *path;
+
+        SoupSession *session;
+        SoupMessage *thumbnail_message;
+        SoupMessage *image_message;
+        GList *pending;
 };
 
 G_DEFINE_TYPE (GrImage, gr_image, G_TYPE_OBJECT)
@@ -33,9 +63,24 @@ G_DEFINE_TYPE (GrImage, gr_image, G_TYPE_OBJECT)
 static void
 gr_image_finalize (GObject *object)
 {
-        GrImage *image = GR_IMAGE (object);
+        GrImage *ri = GR_IMAGE (object);
 
-        g_free (image->path);
+        g_list_free_full (ri->pending, task_data_free);
+        ri->pending = NULL;
+
+        if (ri->thumbnail_message)
+                soup_session_cancel_message (ri->session,
+                                             ri->thumbnail_message,
+                                             SOUP_STATUS_CANCELLED);
+        g_clear_object (&ri->thumbnail_message);
+        if (ri->image_message)
+                soup_session_cancel_message (ri->session,
+                                             ri->image_message,
+                                             SOUP_STATUS_CANCELLED);
+        g_clear_object (&ri->image_message);
+        g_clear_object (&ri->session);
+        g_free (ri->path);
+        g_free (ri->id);
 
         G_OBJECT_CLASS (gr_image_parent_class)->finalize (object);
 }
@@ -54,17 +99,29 @@ gr_image_init (GrImage *image)
 }
 
 GrImage *
-gr_image_new (const char *path)
+gr_image_new (SoupSession *session,
+              const char  *id,
+              const char  *path)
 {
         GrImage *image;
 
         image = g_object_new (GR_TYPE_IMAGE, NULL);
+        image->session = g_object_ref (session);
+        gr_image_set_id (image, id);
         gr_image_set_path (image, path);
 
         return image;
 }
 
 void
+gr_image_set_id (GrImage    *image,
+                 const char *id)
+{
+        g_free (image->id);
+        image->id = g_strdup (id);
+}
+
+void
 gr_image_set_path (GrImage    *image,
                    const char *path)
 {
@@ -83,3 +140,351 @@ gr_image_array_new (void)
 {
         return g_ptr_array_new_with_free_func (g_object_unref);
 }
+
+static GdkPixbuf *
+load_pixbuf (const char *path,
+             int         width,
+             int         height,
+             gboolean    fit)
+{
+        GdkPixbuf *pixbuf;
+
+        if (fit)
+                pixbuf = load_pixbuf_fit_size (path, width, height, FALSE);
+        else
+                pixbuf = load_pixbuf_fill_size (path, width, height);
+
+        return pixbuf;
+}
+
+#define BASE_URL "https://static.gnome.org/recipes/v1";
+
+static char *
+get_image_url (GrImage *ri)
+{
+        g_autofree char *basename = NULL;
+
+        basename = g_path_get_basename (ri->path);
+        return g_strconcat (BASE_URL, "/images/", ri->id, "/", basename, NULL);
+}
+
+static char *
+get_thumbnail_url (GrImage *ri)
+{
+        g_autofree char *basename = NULL;
+
+        basename = g_path_get_basename (ri->path);
+        return g_strconcat (BASE_URL, "/thumbnails/", ri->id, "/", basename, NULL);
+}
+
+static char *
+get_image_cache_path (GrImage *ri)
+{
+        char *filename;
+        g_autofree char *cache_dir = NULL;
+        g_autofree char *basename = NULL;
+
+        basename = g_path_get_basename (ri->path);
+        filename = g_build_filename (g_get_user_cache_dir (), PACKAGE_NAME, "images", ri->id,  basename, 
NULL);
+        cache_dir = g_path_get_dirname (filename);
+        g_mkdir_with_parents (cache_dir, 0755);
+
+        return filename;
+}
+
+static char *
+get_thumbnail_cache_path (GrImage *ri)
+{
+        char *filename;
+        g_autofree char *cache_dir = NULL;
+        g_autofree char *basename = NULL;
+
+        basename = g_path_get_basename (ri->path);
+        filename = g_build_filename (g_get_user_cache_dir (), PACKAGE_NAME, "thumbnails", ri->id, basename, 
NULL);
+        cache_dir = g_path_get_dirname (filename);
+        g_mkdir_with_parents (cache_dir, 0755);
+
+        return filename;
+}
+
+static gboolean
+check_recent_failure (const char *path)
+{
+        g_autoptr(GFile) file = NULL;
+        g_autoptr(GFileInfo) info = NULL;
+
+        // We create negative cache entries as empty files, and we
+        // limit our requests to once per day
+
+        file = g_file_new_for_path (path);
+        info = g_file_query_info (file,
+                                  G_FILE_ATTRIBUTE_STANDARD_SIZE ","
+                                  G_FILE_ATTRIBUTE_TIME_MODIFIED,
+                                  G_FILE_QUERY_INFO_NONE,
+                                  NULL,
+                                  NULL);
+        if (info) {
+                goffset size;
+                GTimeVal tv;
+
+                size = g_file_info_get_size (info);
+                g_file_info_get_modification_time (info, &tv);
+
+                if (size == 6) {
+                        g_autoptr(GDateTime) now = g_date_time_new_now_utc ();
+                        g_autoptr(GDateTime) mtime = g_date_time_new_from_timeval_utc (&tv);
+
+                        return g_date_time_difference (now, mtime) < G_TIME_SPAN_DAY;
+                }
+        }
+
+        return FALSE;
+}
+
+static void
+write_negative_cache_entry (const char *path)
+{
+        if (!g_file_set_contents (path, "failed", 6, NULL))
+                g_warning ("Failed to write a negative cache entry for %s", path);
+}
+
+static void
+set_image (SoupSession *session,
+           SoupMessage *msg,
+           gpointer     data)
+{
+        GrImage *ri = data;
+        g_autofree char *cache_path = NULL;
+        GList *l;
+
+        if (msg->status_code == SOUP_STATUS_CANCELLED || ri->session == NULL) {
+                g_debug ("Message cancelled");
+                goto error;
+        }
+
+        if (msg == ri->thumbnail_message)
+                cache_path = get_thumbnail_cache_path (ri);
+        else
+                cache_path = get_image_cache_path (ri);
+
+        if (msg->status_code != SOUP_STATUS_OK) {
+                g_autofree char *url = get_image_url (ri);
+                g_debug ("Got status %d, record failure to load %s", msg->status_code, url);
+                write_negative_cache_entry (cache_path);
+                goto out;
+        }
+
+        g_debug ("Saving image to %s", cache_path);
+        if (!g_file_set_contents (cache_path, msg->response_body->data, msg->response_body->length, NULL)) {
+                g_debug ("Saving image to %s failed", cache_path);
+                goto out;
+        }
+
+        g_debug ("Loading image for %s", ri->path);
+
+        l = ri->pending;
+        while (l) {
+                GList *next = l->next;
+                TaskData *td = l->data;
+                g_autoptr(GdkPixbuf) pixbuf = NULL;
+
+                if (g_cancellable_is_cancelled (td->cancellable)) {
+                        ri->pending = g_list_remove (ri->pending, td);
+                        task_data_free (td);
+                }
+                else if (msg == ri->thumbnail_message &&
+                    (td->width > 150 || td->height > 150)) {
+                        g_autoptr(GdkPixbuf) tmp = NULL;
+                        int w, h;
+
+                        if (td->width < td->height) {
+                                h = 150;
+                                w = 150 * td->width / td->height;
+                        }
+                        else {
+                                w = 150;
+                                h = 150 * td->height / td->width;
+                        }
+
+                        tmp = load_pixbuf (cache_path, w, h, td->fit);
+                        pixbuf = gdk_pixbuf_scale_simple (tmp, td->width, td->height, GDK_INTERP_BILINEAR);
+                        pixbuf_blur (pixbuf, 5, 3);
+                        td->callback (ri, pixbuf, td->data);
+                }
+                else {
+                        pixbuf = load_pixbuf (cache_path, td->width, td->height, td->fit);
+                        td->callback (ri, pixbuf, td->data);
+
+                        ri->pending = g_list_remove (ri->pending, td);
+                        task_data_free (td);
+                }
+
+                l = next;
+        }
+
+out:
+        if (msg == ri->thumbnail_message)
+                g_clear_object (&ri->thumbnail_message);
+        else
+                g_clear_object (&ri->image_message);
+
+        if (ri->thumbnail_message || ri->image_message)
+                return;
+
+error:
+        g_list_free_full (ri->pending, task_data_free);
+        ri->pending = NULL;
+}
+
+void
+gr_image_load (GrImage         *ri,
+               int              width,
+               int              height,
+               gboolean         fit,
+               GCancellable    *cancellable,
+               GrImageCallback  callback,
+               gpointer         data)
+{
+        TaskData *td;
+        g_autofree char *image_cache_path = NULL;
+        g_autofree char *thumbnail_cache_path = NULL;
+        g_autoptr(GdkPixbuf) pixbuf = NULL;
+        gboolean need_image = TRUE;
+        gboolean need_thumbnail = TRUE;
+        int w, h;
+
+        if (ri->path[0] == '/') {
+                pixbuf = load_pixbuf (ri->path, width, height, fit);
+                if (pixbuf) {
+                        g_debug ("Use local image for %s", ri->path);
+                        callback (ri, pixbuf, data);
+                        return;
+                }
+        }
+
+        image_cache_path = get_image_cache_path (ri);
+        thumbnail_cache_path = get_thumbnail_cache_path (ri);
+
+        if (width <= 150 && height <= 150) {
+                pixbuf = load_pixbuf (thumbnail_cache_path, width, height, fit);
+                need_image = FALSE;
+        }
+        else {
+                pixbuf = load_pixbuf (image_cache_path, width, height, fit);
+        }
+
+        if (pixbuf) {
+                 g_debug ("Use cached %s for %s",
+                          width <= 150 && height <= 150 ? "thumbnail" : "image",
+                          ri->path);
+                callback (ri, pixbuf, data);
+                return;
+        }
+
+        if (width < height) {
+                h = 150;
+                w = 150 * width / height;
+        }
+        else {
+                w = 150;
+                h = 150 * height / width;
+        }
+
+        pixbuf = load_pixbuf (thumbnail_cache_path, w, h, fit);
+        if (pixbuf) {
+                g_autoptr(GdkPixbuf) blurred = NULL;
+
+                g_debug ("Use cached thumbnail for %s", ri->path);
+                blurred = gdk_pixbuf_scale_simple (pixbuf, width, height, GDK_INTERP_BILINEAR);
+                pixbuf_blur (blurred, 5, 3);
+                callback (ri, blurred, data);
+                need_thumbnail = FALSE;
+        }
+
+        if (need_thumbnail && check_recent_failure (thumbnail_cache_path)) {
+                g_autofree char *url = get_thumbnail_url (ri);
+                g_debug ("Recently failed to load %s, skipping", url);
+                need_thumbnail = FALSE;
+        }
+
+        if (need_image && check_recent_failure (image_cache_path)) {
+                g_autofree char *url = get_image_url (ri);
+                g_debug ("Recently failed to load %s, skipping", url);
+                need_image = FALSE;
+        }
+
+        if (!need_thumbnail && !need_image)
+                return;
+
+        td = g_new0 (TaskData, 1);
+        td->width = width;
+        td->height = height;
+        td->fit = fit;
+        td->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
+        td->callback = callback;
+        td->data = data;
+
+        ri->pending = g_list_prepend (ri->pending, td);
+
+        if (need_thumbnail && ri->thumbnail_message == NULL) {
+                        g_autofree char *url = NULL;
+                        g_autoptr(SoupURI) base_uri = NULL;
+
+                        url = get_thumbnail_url (ri);
+                        base_uri = soup_uri_new (url);
+                        ri->thumbnail_message = soup_message_new_from_uri (SOUP_METHOD_GET, base_uri);
+                        g_debug ("Load thumbnail for %s from %s", ri->path, url);
+                        soup_session_queue_message (ri->session, g_object_ref (ri->thumbnail_message), 
set_image, ri);
+        }
+
+        if (need_image && ri->image_message == NULL) {
+                g_autofree char *url = NULL;
+                g_autoptr(SoupURI) base_uri = NULL;
+
+                url = get_image_url (ri);
+                base_uri = soup_uri_new (url);
+                ri->image_message = soup_message_new_from_uri (SOUP_METHOD_GET, base_uri);
+                g_debug ("Load image for %s from %s", ri->path, url);
+                soup_session_queue_message (ri->session, g_object_ref (ri->image_message), set_image, ri);
+        }
+}
+
+void
+gr_image_set_pixbuf (GrImage   *ri,
+                     GdkPixbuf *pixbuf,
+                     gpointer   data)
+{
+        gtk_image_set_from_pixbuf (GTK_IMAGE (data), pixbuf);
+}
+
+GdkPixbuf  *
+gr_image_load_sync (GrImage   *ri,
+                    int        width,
+                    int        height,
+                    gboolean   fit)
+{
+        GdkPixbuf *pixbuf;
+
+        if (ri->path[0] == '/') {
+                pixbuf = load_pixbuf (ri->path, width, height, fit);
+        }
+        else {
+                g_autofree char *cache_path = NULL;
+
+                cache_path = get_image_cache_path (ri);
+                pixbuf = load_pixbuf (cache_path, width, height, fit);
+        }
+
+        if (!pixbuf) {
+                g_autoptr(GtkIconInfo) info = NULL;
+
+                info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
+                                                   "org.gnome.Recipes",
+                                                    256,
+                                                    GTK_ICON_LOOKUP_FORCE_SIZE);
+                pixbuf = load_pixbuf (gtk_icon_info_get_filename (info), width, height, fit);
+
+        }
+
+        return pixbuf;
+}
diff --git a/src/gr-image.h b/src/gr-image.h
index f438bef..23fdb00 100644
--- a/src/gr-image.h
+++ b/src/gr-image.h
@@ -21,6 +21,7 @@
 #pragma once
 
 #include <gtk/gtk.h>
+#include <libsoup/soup.h>
 
 G_BEGIN_DECLS
 
@@ -28,10 +29,34 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GrImage, gr_image, GR, IMAGE, GObject)
 
-GrImage    *gr_image_new      (const char *path);
-void        gr_image_set_path (GrImage    *image,
-                               const char *path);
-const char *gr_image_get_path (GrImage    *image);
+GrImage    *gr_image_new         (SoupSession       *session,
+                                  const char        *id,
+                                  const char        *path);
+void        gr_image_set_id      (GrImage           *image,
+                                  const char        *id);
+void        gr_image_set_path    (GrImage           *image,
+                                  const char        *path);
+const char *gr_image_get_path    (GrImage           *image);
+GdkPixbuf  *gr_image_load_sync   (GrImage           *image,
+                                  int                 width,
+                                  int                 height,
+                                  gboolean            fit);
+
+typedef void (*GrImageCallback) (GrImage   *ri,
+                                 GdkPixbuf *pixbuf,
+                                 gpointer   data);
+
+void        gr_image_load        (GrImage            *ri,
+                                  int                 width,
+                                  int                 height,
+                                  gboolean            fit,
+                                  GCancellable       *cancellable,
+                                  GrImageCallback     callback,
+                                  gpointer            data);
+
+void        gr_image_set_pixbuf  (GrImage   *ri,
+                                  GdkPixbuf *pixbuf,
+                                  gpointer   data);
 
 GPtrArray *gr_image_array_new (void);
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]