#include "shell-texture-cache.h" #include "shell-global.h" #include typedef struct { GIcon *icon; guint size; } CacheKey; struct _ShellTextureCachePrivate { GHashTable *gicon_cache; /* CacheKey -> CoglTexture* */ }; static void shell_texture_cache_dispose (GObject *object); static void shell_texture_cache_finalize (GObject *object); G_DEFINE_TYPE(ShellTextureCache, shell_texture_cache, G_TYPE_OBJECT); static guint cache_key_hash (gconstpointer a) { CacheKey *akey = (CacheKey *)a; if (akey->icon) return g_icon_hash (akey->icon) + 31*akey->size; g_assert_not_reached (); } static gboolean cache_key_equal (gconstpointer a, gconstpointer b) { CacheKey *akey = (CacheKey*)a; CacheKey *bkey = (CacheKey*)b; if (akey->size != bkey->size) return FALSE; if (akey->icon && bkey->icon) return g_icon_equal (akey->icon, bkey->icon); g_assert_not_reached (); } static void cache_key_destroy (gpointer a) { CacheKey *akey = (CacheKey*)a; if (akey->icon) g_object_unref (akey->icon); g_free (akey); } static void shell_texture_cache_class_init (ShellTextureCacheClass *klass) { GObjectClass *gobject_class = (GObjectClass *)klass; gobject_class->dispose = shell_texture_cache_dispose; gobject_class->finalize = shell_texture_cache_finalize; } static void shell_texture_cache_init (ShellTextureCache *self) { self->priv = g_new0 (ShellTextureCachePrivate, 1); self->priv->gicon_cache = g_hash_table_new_full (cache_key_hash, cache_key_equal, cache_key_destroy, cogl_handle_unref); } static void shell_texture_cache_dispose (GObject *object) { ShellTextureCache *self = (ShellTextureCache*)object; if (self->priv->gicon_cache) g_hash_table_destroy (self->priv->gicon_cache); self->priv->gicon_cache = NULL; G_OBJECT_CLASS (shell_texture_cache_parent_class)->dispose (object); } static void shell_texture_cache_finalize (GObject *object) { G_OBJECT_CLASS (shell_texture_cache_parent_class)->finalize (object); } ShellTextureCache* shell_texture_cache_new () { return SHELL_TEXTURE_CACHE (g_object_new (SHELL_TYPE_TEXTURE_CACHE, NULL)); } typedef struct { char *uri; GIcon *icon; GtkIconInfo *icon_info; gint width; gint height; gpointer user_data; } AsyncIconLookupData; static gboolean compute_pixbuf_scale (gint width, gint height, gint available_width, gint available_height, gint *new_width, gint *new_height) { int scaled_width, scaled_height; if (width == 0 || height == 0) return FALSE; if (available_width >= 0 && available_height >= 0) { // This should keep the aspect ratio of the image intact, because if // available_width < (available_height * width) / height // than // (available_width * height) / width < available_height // So we are guaranteed to either scale the image to have an available_width // for width and height scaled accordingly OR have the available_height // for height and width scaled accordingly, whichever scaling results // in the image that can fit both available dimensions. scaled_width = MIN (available_width, (available_height * width) / height); scaled_height = MIN (available_height, (available_width * height) / width); } else if (available_width >= 0) { scaled_width = available_width; scaled_height = (available_width * height) / width; } else if (available_height >= 0) { scaled_width = (available_height * width) / height; scaled_height = available_height; } else { scaled_width = scaled_height = 0; } // Scale the image only if that will not increase its original dimensions. if (scaled_width >= 0 && scaled_height >= 0 && scaled_width < width && scaled_height < height) { *new_width = scaled_width; *new_height = scaled_height; return TRUE; } return FALSE; } static GdkPixbuf * impl_load_pixbuf_gicon (GIcon *icon, GtkIconInfo *info, int size, GError **error) { int scaled_width, scaled_height; GdkPixbuf *pixbuf = gtk_icon_info_load_icon (info, error); int width, height; if (!pixbuf) return NULL; width = gdk_pixbuf_get_width (pixbuf); height = gdk_pixbuf_get_height (pixbuf); if (compute_pixbuf_scale (width, height, size, size, &scaled_width, &scaled_height)) { GdkPixbuf *scaled = gdk_pixbuf_scale_simple (pixbuf, width, height, GDK_INTERP_BILINEAR); g_object_unref (pixbuf); pixbuf = scaled; } return pixbuf; } // A private structure for keeping width and height. typedef struct { int width; int height; } Dimensions; static void icon_lookup_data_destroy (gpointer p) { AsyncIconLookupData *data = p; if (data->icon) { g_object_unref (data->icon); gtk_icon_info_free (data->icon_info); } else if (data->uri) g_free (data->uri); g_free (data); } /** * on_image_size_prepared: * * @pixbuf_loader: #GdkPixbufLoader loading the image * @width: the original width of the image * @height: the original height of the image * @data: pointer to the #Dimensions sructure containing available width and height for the image, * available width or height can be -1 if the dimension is not limited * * Private function. * * Sets the size of the image being loaded to fit the available width and height dimensions, * but never scales up the image beyond its actual size. * Intended to be used as a callback for #GdkPixbufLoader "size-prepared" signal. */ static void on_image_size_prepared (GdkPixbufLoader *pixbuf_loader, gint width, gint height, gpointer data) { Dimensions *available_dimensions = data; int available_width = available_dimensions->width; int available_height = available_dimensions->height; int scaled_width; int scaled_height; if (compute_pixbuf_scale (width, height, available_width, available_height, &scaled_width, &scaled_height)) gdk_pixbuf_loader_set_size (pixbuf_loader, scaled_width, scaled_height); } static GdkPixbuf * impl_load_pixbuf_file (const char *uri, int available_width, int available_height, GError **error) { GdkPixbufLoader *pixbuf_loader = NULL; GdkPixbuf *rotated_pixbuf = NULL; GdkPixbuf *pixbuf; GFile *file = NULL; char *contents = NULL; gsize size; gboolean success; Dimensions available_dimensions; int width_before_rotation, width_after_rotation; file = g_file_new_for_uri (uri); success = g_file_load_contents (file, NULL, &contents, &size, NULL, error); if (!success) { goto out; } pixbuf_loader = gdk_pixbuf_loader_new (); available_dimensions.width = available_width; available_dimensions.height = available_height; g_signal_connect (pixbuf_loader, "size-prepared", G_CALLBACK (on_image_size_prepared), &available_dimensions); success = gdk_pixbuf_loader_write (pixbuf_loader, (const guchar *) contents, size, error); if (!success) goto out; success = gdk_pixbuf_loader_close (pixbuf_loader, error); if (!success) goto out; pixbuf = gdk_pixbuf_loader_get_pixbuf (pixbuf_loader); width_before_rotation = gdk_pixbuf_get_width (pixbuf); rotated_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf); width_after_rotation = gdk_pixbuf_get_width (rotated_pixbuf); // There is currently no way to tell if the pixbuf will need to be rotated before it is loaded, // so we only check that once it is loaded, and reload it again if it needs to be rotated in order // to use the available width and height correctly. // http://bugzilla.gnome.org/show_bug.cgi?id=579003 if (width_before_rotation != width_after_rotation) { g_object_unref (pixbuf_loader); g_object_unref (rotated_pixbuf); rotated_pixbuf = NULL; pixbuf_loader = gdk_pixbuf_loader_new (); // We know that the image will later be rotated, so we reverse the available dimensions. available_dimensions.width = available_height; available_dimensions.height = available_width; g_signal_connect (pixbuf_loader, "size-prepared", G_CALLBACK (on_image_size_prepared), &available_dimensions); success = gdk_pixbuf_loader_write (pixbuf_loader, (const guchar *) contents, size, error); if (!success) goto out; success = gdk_pixbuf_loader_close (pixbuf_loader, error); if (!success) goto out; pixbuf = gdk_pixbuf_loader_get_pixbuf (pixbuf_loader); rotated_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf); } out: g_free (contents); if (file) g_object_unref (file); if (pixbuf_loader) g_object_unref (pixbuf_loader); return rotated_pixbuf; } static void load_pixbuf_thread (GSimpleAsyncResult *result, GObject *object, GCancellable *cancellable) { GdkPixbuf *pixbuf; AsyncIconLookupData *data; GError *error = NULL; data = g_object_get_data (result, "load_icon_pixbuf_async"); if (data->uri) pixbuf = impl_load_pixbuf_file (data->uri, data->width, data->height, &error); else if (data->icon) pixbuf = impl_load_pixbuf_gicon (data->icon, data->icon_info, data->width, &error); else g_assert_not_reached (); if (error != NULL) { g_simple_async_result_set_from_error (result, error); return; } g_simple_async_result_set_op_res_gpointer (result, g_object_ref (pixbuf), g_object_unref); } /** * load_icon_pixbuf_async: * * Asynchronously load the #GdkPixbuf associated with a #GIcon. Currently * the #GtkIconInfo must have already been provided. */ static void load_icon_pixbuf_async (ShellTextureCache *cache, GIcon *icon, GtkIconInfo *icon_info, gint size, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *result; AsyncIconLookupData *data; data = g_new0 (AsyncIconLookupData, 1); data->icon = g_object_ref (icon); data->icon_info = gtk_icon_info_copy (icon_info); data->width = data->height = size; data->user_data = user_data; result = g_simple_async_result_new (G_OBJECT (cache), callback, user_data, load_icon_pixbuf_async); g_object_set_data_full (result, "load_icon_pixbuf_async", data, icon_lookup_data_destroy); g_simple_async_result_run_in_thread (result, load_pixbuf_thread, G_PRIORITY_DEFAULT, cancellable); g_object_unref (result); } static void load_uri_pixbuf_async (ShellTextureCache *cache, const char *uri, guint width, guint height, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *result; AsyncIconLookupData *data; data = g_new0 (AsyncIconLookupData, 1); data->uri = g_strdup (uri); data->width = width; data->height = height; data->user_data = user_data; result = g_simple_async_result_new (G_OBJECT (cache), callback, user_data, load_uri_pixbuf_async); g_object_set_data_full (result, "load_uri_pixbuf_async", data, icon_lookup_data_destroy); g_simple_async_result_run_in_thread (result, load_pixbuf_thread, G_PRIORITY_DEFAULT, cancellable); g_object_unref (result); } static GdkPixbuf * load_pixbuf_async_finish (ShellTextureCache *cache, GAsyncResult *result, GError **error) { GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result); if (g_simple_async_result_propagate_error (simple, error)) return NULL; return g_simple_async_result_get_op_res_gpointer (simple); } typedef struct { char *uri; GIcon *icon; GtkIconInfo *icon_info; guint width; guint height; ClutterTexture *texture; } AsyncTextureLoadData; static CoglHandle pixbuf_to_cogl_handle (GdkPixbuf *pixbuf) { return cogl_texture_new_from_data (gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf), COGL_TEXTURE_NONE, gdk_pixbuf_get_has_alpha (pixbuf) ? COGL_PIXEL_FORMAT_RGBA_8888 : COGL_PIXEL_FORMAT_RGB_888, COGL_PIXEL_FORMAT_ANY, gdk_pixbuf_get_rowstride (pixbuf), gdk_pixbuf_get_pixels (pixbuf)); } static void on_pixbuf_loaded (GObject *source, GAsyncResult *result, gpointer user_data) { ShellTextureCache *cache; AsyncTextureLoadData *data; GdkPixbuf *pixbuf; GError *error = NULL; CoglHandle texdata; CacheKey *key; data = user_data; cache = SHELL_TEXTURE_CACHE (source); pixbuf = load_pixbuf_async_finish (cache, result, &error); if (pixbuf == NULL) { /* TODO - we need a "broken image" display of some sort */ goto out; } texdata = pixbuf_to_cogl_handle (pixbuf); g_object_unref (pixbuf); if (data->icon) { gpointer orig_key, value; key = g_new0 (CacheKey, 1); key->icon = g_object_ref (data->icon); key->size = data->width; if (!g_hash_table_lookup_extended (cache->priv->gicon_cache, key, &orig_key, &value)) g_hash_table_insert (cache->priv->gicon_cache, key, texdata); else cache_key_destroy (key); } clutter_texture_set_cogl_texture (data->texture, texdata); out: if (data->icon) { gtk_icon_info_free (data->icon_info); g_object_unref (data->icon); } else if (data->uri) g_free (data->uri); /* Alternatively we could weakref and just do nothing if the texture is destroyed */ g_object_unref (data->texture); g_free (data); } /** * shell_texture_cache_load_gicon: * * This method returns a new #ClutterClone for a given #GIcon. If the * icon isn't loaded already, the texture will be filled asynchronously. * * Return Value: (transfer none): A new #ClutterActor for the icon */ ClutterActor * shell_texture_cache_load_gicon (ShellTextureCache *cache, GIcon *icon, gint size) { ClutterTexture *texture; CoglHandle texdata; CacheKey key; texture = CLUTTER_TEXTURE (clutter_texture_new ()); clutter_actor_set_size (CLUTTER_ACTOR (texture), size, size); key.icon = icon; key.size = size; texdata = g_hash_table_lookup (cache->priv->gicon_cache, &key); if (texdata == NULL) { GtkIconTheme *theme; GtkIconInfo *info; /* Do theme lookups in the main thread to avoid thread-unsafety */ theme = gtk_icon_theme_get_default (); info = gtk_icon_theme_lookup_by_gicon (theme, icon, size, GTK_ICON_LOOKUP_USE_BUILTIN); if (info != NULL) { AsyncTextureLoadData *data; data = g_new0 (AsyncTextureLoadData, 1); data->icon = icon; data->icon_info = info; data->texture = g_object_ref (texture); data->width = data->height = size; load_icon_pixbuf_async (cache, icon, info, size, NULL, on_pixbuf_loaded, data); } } else { clutter_texture_set_cogl_texture (texture, texdata); } return CLUTTER_ACTOR (texture); } /** * shell_texture_cache_load_uri: * * @cache: The texture cache instance * @uri: uri of the image file from which to create a pixbuf * @available_width: available width for the image, can be -1 if not limited * @available_height: available height for the image, can be -1 if not limited * * Asynchronously load an image. Initially, the returned texture will have a natural * size of zero. At some later point, either the image will be loaded successfully * and at that point size will be negotiated, or upon an error, no image will be set. * * Return value: (transfer none): A new #ClutterActor with no image loaded initially. */ ClutterActor * shell_texture_cache_load_uri_async (ShellTextureCache *cache, const gchar *uri, int available_width, int available_height) { ClutterTexture *texture; AsyncTextureLoadData *data; texture = CLUTTER_TEXTURE (clutter_texture_new ()); data = g_new0 (AsyncTextureLoadData, 1); data->uri = g_strdup (uri); data->width = available_width; data->height = available_height; data->texture = g_object_ref (texture); load_uri_pixbuf_async (cache, uri, available_width, available_height, NULL, on_pixbuf_loaded, data); return CLUTTER_ACTOR (texture); } /** * shell_texture_cache_load_uri_sync: * * @cache: The texture cache instance * @uri: uri of the image file from which to create a pixbuf * @available_width: available width for the image, can be -1 if not limited * @available_height: available height for the image, can be -1 if not limited * @error: Return location for error * * Synchronously load an image from a uri. The image is scaled down to fit the * available width and height imensions, but the image is never scaled up beyond * its actual size. The pixbuf is rotated according to the associated orientation * setting. * * Return value: (transfer none): A new #ClutterActor with the image file loaded if it was * generated succesfully, %NULL otherwise */ ClutterActor * shell_texture_cache_load_uri_sync (ShellTextureCache *cache, const gchar *uri, int available_width, int available_height, GError **error) { ClutterTexture *texture; GdkPixbuf *pixbuf; CoglHandle texdata; pixbuf = impl_load_pixbuf_file (uri, available_width, available_height, error); if (!pixbuf) return NULL; texture = CLUTTER_TEXTURE (clutter_texture_new ()); texdata = pixbuf_to_cogl_handle (pixbuf); g_object_unref (pixbuf); clutter_texture_set_cogl_texture (texture, texdata); return CLUTTER_ACTOR (texture); } static ShellTextureCache *instance = NULL; /** * shell_texture_cache_get_default: * * Return value: (transfer none): The global texture cache */ ShellTextureCache* shell_texture_cache_get_default () { if (instance == NULL) instance = g_object_new (SHELL_TYPE_TEXTURE_CACHE, NULL); return instance; }