/* * gnome-thumbnail.c: Utilities for handling thumbnails * * Copyright (C) 2002 Red Hat, Inc. * Copyright (C) 2010 Carlos Garcia Campos * * This file is part of the Gnome Library. * * The Gnome Library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * The Gnome Library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with the Gnome Library; see the file COPYING.LIB. If not, * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * Author: Alexander Larsson */ /** * SECTION:gnome-desktop-thumbnail * @short_description: Generates and looks up thumbnails of files and * directories * @stability: Unstable * @include: libgnome-desktop/gnome-desktop-thumbnail.h * * #GnomeDesktopThumbnailFactory allows generation and loading of thumbnails for * local and remote files and directories. It uses a collection of programs * called thumbnailers, each one generating thumbnails * for a specific set of content-types of files. For example, * totem-video-thumbnailer generates thumbnails for * video files using GStreamer; evince-thumbnailer * generates thumbnails for PDFs and other document files. If no specific * thumbnailer exists for a file, or if the thumbnailer fails, gdk-pixbuf is * used as a fallback. * * To generate a thumbnail, an appropriate thumbnailer program is selected then * executed, passing it the URI of the file to thumbnail, plus a path to write * the thumbnail image to. If thumbnailing succeeds, the thumbnailer should have * written the image to disk before terminating; but if thumbnailing fails, no * image should be written, and the thumbnailer should return a non-zero exit * status. #GnomeDesktopThumbnailFactory will then fall back to using gdk-pixbuf * to generate a thumbnail, if possible. * * Thumbnailers are chosen by examining a series of * .thumbnailer files in * $PREFIX/share/thumbnailers. * Each is in a simple key-file format: * * [Thumbnailer Entry] * Exec=evince-thumbnailer -s %s %u %o * MimeType=application/pdf;application/x-bzpdf;application/x-gzpdf; * * * The .thumbnailer format supports three keys: * * Exec * Required. The command to execute the thumbnailer. It supports a few different * parameters which are replaced before calling the thumbnailer: * %u is the URI of the file being thumbnailed; * %i is its path; %o * is the path of the image file to be written to; * %s is the maximum desired size of the thumbnail * image (the maximum width or height, in pixels); and * %% is a literal percent character. * * MimeType * Required. A semicolon-separated list of MIME types which the thumbnailer * supports generating thumbnails for. * * * * So in the example .thumbnailer file above, the command * passes the requested thumbnail size, then the input file’s URI, then the * path for the output image file to * evince-thumbnailer. * * The code to examine and call a thumbnailer is contained in * #GnomeDesktopThumbnailFactory, which handles looking up the right thumbnailer * script, building and executing the command for it, and loading the resulting * thumbnail image into a #GdkPixbuf. * * Thumbnail caching is also supported by #GnomeDesktopThumbnailFactory. When * calling a thumbnailer, the path passed for the output image file is in * $XDG_CACHE_HOME/thumbnails/ * $SIZE/. The cached image file is given * a (probably) unique filename, generated by hashing the original file’s URI, * so the thumbnail can be looked up in future. #GnomeDesktopThumbnailFactory * supports two sizes of thumbnails: %GNOME_DESKTOP_THUMBNAIL_SIZE_NORMAL and * %GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE. Normal thumbnails are up to 128×128 * pixels, whereas large thumbnails are up to 256×256 pixels. Thumbnails which * are larger than this are scaled down before being cached, and non-square * thumbnails are scaled so their largest dimension is at most 128 or 256 * pixels. * * #GnomeDesktopThumbnailFactory also handles failed thumbnails. If a * thumbnailer can’t generate a thumbnail for a file (e.g. because the file is * corrupt or because the right video codecs aren’t available), it returns a * non-zero exit status. The thumbnail factory then writes an entry to * $XDG_CACHE_HOME/thumbnails/fail/ * gnome-thumbnail-factory/ which is named after the hash of the * input file URI (just like a successful cached thumbnail). For future queries * for thumbnails for that file, #GnomeDesktopThumbnailFactory can immediately * return an error after looking up the fail entry. * * If a file changes content, #GnomeDesktopThumbnailFactory will generate a new * thumbnail because each cached image has associated metadata (stored as PNG * tEXt keys) storing the full URI of the thumbnailed file (to check for hash * collisions) and its last modification time at the point of thumbnailing. If * the stored modification time doesn’t match the file’s current one, a new * thumbnail is generated. * * Since: 2.2 */ #include #include #include #include #include #include #include #include #include #define GNOME_DESKTOP_USE_UNSTABLE_API #include "gnome-desktop-thumbnail.h" #include "gnome-desktop-thumbnail-script.h" static void thumbnailers_directory_changed (GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, GnomeDesktopThumbnailFactory *factory); struct _GnomeDesktopThumbnailFactoryPrivate { GnomeDesktopThumbnailSize size; GMutex lock; GList *thumbnailers; GHashTable *mime_types_map; GList *monitors; GSettings *settings; gboolean loaded : 1; gboolean disabled : 1; gchar **disabled_types; }; static const char *appname = "gnome-thumbnail-factory"; G_DEFINE_TYPE_WITH_CODE (GnomeDesktopThumbnailFactory, gnome_desktop_thumbnail_factory, G_TYPE_OBJECT, G_ADD_PRIVATE (GnomeDesktopThumbnailFactory)) #define parent_class gnome_desktop_thumbnail_factory_parent_class #define THUMBNAILER_ENTRY_GROUP "Thumbnailer Entry" #define THUMBNAILER_EXTENSION ".thumbnailer" typedef struct { volatile gint ref_count; gchar *path; gchar *command; gchar **mime_types; } Thumbnailer; static Thumbnailer * thumbnailer_ref (Thumbnailer *thumb) { g_return_val_if_fail (thumb != NULL, NULL); g_return_val_if_fail (thumb->ref_count > 0, NULL); g_atomic_int_inc (&thumb->ref_count); return thumb; } static void thumbnailer_unref (Thumbnailer *thumb) { g_return_if_fail (thumb != NULL); g_return_if_fail (thumb->ref_count > 0); if (g_atomic_int_dec_and_test (&thumb->ref_count)) { g_free (thumb->path); g_free (thumb->command); g_strfreev (thumb->mime_types); g_slice_free (Thumbnailer, thumb); } } static Thumbnailer * thumbnailer_load (Thumbnailer *thumb) { GKeyFile *key_file; GError *error = NULL; key_file = g_key_file_new (); if (!g_key_file_load_from_file (key_file, thumb->path, 0, &error)) { g_warning ("Failed to load thumbnailer from \"%s\": %s\n", thumb->path, error->message); g_error_free (error); thumbnailer_unref (thumb); g_key_file_free (key_file); return NULL; } if (!g_key_file_has_group (key_file, THUMBNAILER_ENTRY_GROUP)) { g_warning ("Invalid thumbnailer: missing group \"%s\"\n", THUMBNAILER_ENTRY_GROUP); thumbnailer_unref (thumb); g_key_file_free (key_file); return NULL; } thumb->command = g_key_file_get_string (key_file, THUMBNAILER_ENTRY_GROUP, "Exec", NULL); if (!thumb->command) { g_warning ("Invalid thumbnailer: missing Exec key\n"); thumbnailer_unref (thumb); g_key_file_free (key_file); return NULL; } thumb->mime_types = g_key_file_get_string_list (key_file, THUMBNAILER_ENTRY_GROUP, "MimeType", NULL, NULL); if (!thumb->mime_types) { g_warning ("Invalid thumbnailer: missing MimeType key\n"); thumbnailer_unref (thumb); g_key_file_free (key_file); return NULL; } g_key_file_free (key_file); return thumb; } static Thumbnailer * thumbnailer_reload (Thumbnailer *thumb) { g_return_val_if_fail (thumb != NULL, NULL); g_free (thumb->command); thumb->command = NULL; g_strfreev (thumb->mime_types); thumb->mime_types = NULL; return thumbnailer_load (thumb); } static Thumbnailer * thumbnailer_new (const gchar *path) { Thumbnailer *thumb; thumb = g_slice_new0 (Thumbnailer); thumb->ref_count = 1; thumb->path = g_strdup (path); return thumbnailer_load (thumb); } static gpointer init_thumbnailers_dirs (gpointer data) { const gchar * const *data_dirs; GPtrArray *thumbs_dirs; guint i; data_dirs = g_get_system_data_dirs (); thumbs_dirs = g_ptr_array_new (); g_ptr_array_add (thumbs_dirs, g_build_filename (g_get_user_data_dir (), "thumbnailers", NULL)); for (i = 0; data_dirs[i] != NULL; i++) g_ptr_array_add (thumbs_dirs, g_build_filename (data_dirs[i], "thumbnailers", NULL)); g_ptr_array_add (thumbs_dirs, NULL); return g_ptr_array_free (thumbs_dirs, FALSE); } static const gchar * const * get_thumbnailers_dirs (void) { static GOnce once_init = G_ONCE_INIT; return g_once (&once_init, init_thumbnailers_dirs, NULL); } static const char * gnome_desktop_thumbnail_size_to_dirname (GnomeDesktopThumbnailSize size) { switch (size) { case GNOME_DESKTOP_THUMBNAIL_SIZE_NORMAL: return "normal"; case GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE: return "large"; case GNOME_DESKTOP_THUMBNAIL_SIZE_XLARGE: return "x-large"; case GNOME_DESKTOP_THUMBNAIL_SIZE_XXLARGE: return "xx-large"; default: g_assert_not_reached (); } } static guint gnome_desktop_thumbnail_size_to_size (GnomeDesktopThumbnailSize size) { switch (size) { case GNOME_DESKTOP_THUMBNAIL_SIZE_NORMAL: return 128; case GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE: return 256; case GNOME_DESKTOP_THUMBNAIL_SIZE_XLARGE: return 512; case GNOME_DESKTOP_THUMBNAIL_SIZE_XXLARGE: return 1024; default: g_assert_not_reached (); } } /* These should be called with the lock held */ static void gnome_desktop_thumbnail_factory_register_mime_types (GnomeDesktopThumbnailFactory *factory, Thumbnailer *thumb) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; gint i; for (i = 0; thumb->mime_types[i]; i++) { if (!g_hash_table_lookup (priv->mime_types_map, thumb->mime_types[i])) g_hash_table_insert (priv->mime_types_map, g_strdup (thumb->mime_types[i]), thumbnailer_ref (thumb)); } } static void gnome_desktop_thumbnail_factory_add_thumbnailer (GnomeDesktopThumbnailFactory *factory, Thumbnailer *thumb) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; gnome_desktop_thumbnail_factory_register_mime_types (factory, thumb); priv->thumbnailers = g_list_prepend (priv->thumbnailers, thumb); } static gboolean gnome_desktop_thumbnail_factory_is_disabled (GnomeDesktopThumbnailFactory *factory, const gchar *mime_type) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; guint i; if (priv->disabled) return TRUE; if (!priv->disabled_types) return FALSE; for (i = 0; priv->disabled_types[i]; i++) { if (g_strcmp0 (priv->disabled_types[i], mime_type) == 0) return TRUE; } return FALSE; } static gboolean remove_thumbnailer_from_mime_type_map (gchar *key, Thumbnailer *value, gchar *path) { return (strcmp (value->path, path) == 0); } static void update_or_create_thumbnailer (GnomeDesktopThumbnailFactory *factory, const gchar *path) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; GList *l; Thumbnailer *thumb; gboolean found = FALSE; g_mutex_lock (&priv->lock); for (l = priv->thumbnailers; l && !found; l = g_list_next (l)) { thumb = (Thumbnailer *)l->data; if (strcmp (thumb->path, path) == 0) { found = TRUE; /* First remove the mime_types associated to this thumbnailer */ g_hash_table_foreach_remove (priv->mime_types_map, (GHRFunc)remove_thumbnailer_from_mime_type_map, (gpointer)path); if (!thumbnailer_reload (thumb)) priv->thumbnailers = g_list_delete_link (priv->thumbnailers, l); else gnome_desktop_thumbnail_factory_register_mime_types (factory, thumb); } } if (!found) { thumb = thumbnailer_new (path); if (thumb) gnome_desktop_thumbnail_factory_add_thumbnailer (factory, thumb); } g_mutex_unlock (&priv->lock); } static void remove_thumbnailer (GnomeDesktopThumbnailFactory *factory, const gchar *path) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; GList *l; Thumbnailer *thumb; g_mutex_lock (&priv->lock); for (l = priv->thumbnailers; l; l = g_list_next (l)) { thumb = (Thumbnailer *)l->data; if (strcmp (thumb->path, path) == 0) { priv->thumbnailers = g_list_delete_link (priv->thumbnailers, l); g_hash_table_foreach_remove (priv->mime_types_map, (GHRFunc)remove_thumbnailer_from_mime_type_map, (gpointer)path); thumbnailer_unref (thumb); break; } } g_mutex_unlock (&priv->lock); } static void remove_thumbnailers_for_dir (GnomeDesktopThumbnailFactory *factory, const gchar *thumbnailer_dir, GFileMonitor *monitor) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; GList *l; Thumbnailer *thumb; g_mutex_lock (&priv->lock); /* Remove all the thumbnailers inside this @thumbnailer_dir. */ for (l = priv->thumbnailers; l; l = g_list_next (l)) { thumb = (Thumbnailer *)l->data; if (g_str_has_prefix (thumb->path, thumbnailer_dir) == TRUE) { priv->thumbnailers = g_list_delete_link (priv->thumbnailers, l); g_hash_table_foreach_remove (priv->mime_types_map, (GHRFunc)remove_thumbnailer_from_mime_type_map, (gpointer)thumb->path); thumbnailer_unref (thumb); break; } } /* Remove the monitor for @thumbnailer_dir. */ priv->monitors = g_list_remove (priv->monitors, monitor); g_signal_handlers_disconnect_by_func (monitor, thumbnailers_directory_changed, factory); g_mutex_unlock (&priv->lock); } static void gnome_desktop_thumbnail_factory_load_thumbnailers_for_dir (GnomeDesktopThumbnailFactory *factory, const gchar *path) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; GDir *dir; GFile *dir_file; GFileMonitor *monitor; const gchar *dirent; dir = g_dir_open (path, 0, NULL); if (!dir) return; /* Monitor dir */ dir_file = g_file_new_for_path (path); monitor = g_file_monitor_directory (dir_file, G_FILE_MONITOR_NONE, NULL, NULL); if (monitor) { g_signal_connect (monitor, "changed", G_CALLBACK (thumbnailers_directory_changed), factory); priv->monitors = g_list_prepend (priv->monitors, monitor); } g_object_unref (dir_file); while ((dirent = g_dir_read_name (dir))) { Thumbnailer *thumb; gchar *filename; if (!g_str_has_suffix (dirent, THUMBNAILER_EXTENSION)) continue; filename = g_build_filename (path, dirent, NULL); thumb = thumbnailer_new (filename); g_free (filename); if (thumb) gnome_desktop_thumbnail_factory_add_thumbnailer (factory, thumb); } g_dir_close (dir); } static void thumbnailers_directory_changed (GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, GnomeDesktopThumbnailFactory *factory) { gchar *path; switch (event_type) { case G_FILE_MONITOR_EVENT_CREATED: case G_FILE_MONITOR_EVENT_CHANGED: case G_FILE_MONITOR_EVENT_DELETED: path = g_file_get_path (file); if (!g_str_has_suffix (path, THUMBNAILER_EXTENSION)) { g_free (path); return; } if (event_type == G_FILE_MONITOR_EVENT_DELETED) remove_thumbnailer (factory, path); else update_or_create_thumbnailer (factory, path); g_free (path); break; case G_FILE_MONITOR_EVENT_UNMOUNTED: case G_FILE_MONITOR_EVENT_MOVED: path = g_file_get_path (file); remove_thumbnailers_for_dir (factory, path, monitor); if (event_type == G_FILE_MONITOR_EVENT_MOVED) gnome_desktop_thumbnail_factory_load_thumbnailers_for_dir (factory, path); g_free (path); break; case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: case G_FILE_MONITOR_EVENT_PRE_UNMOUNT: case G_FILE_MONITOR_EVENT_RENAMED: case G_FILE_MONITOR_EVENT_MOVED_IN: case G_FILE_MONITOR_EVENT_MOVED_OUT: default: break; } } static void gnome_desktop_thumbnail_factory_load_thumbnailers (GnomeDesktopThumbnailFactory *factory) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; const gchar * const *dirs; guint i; if (priv->loaded) return; dirs = get_thumbnailers_dirs (); for (i = 0; dirs[i]; i++) { gnome_desktop_thumbnail_factory_load_thumbnailers_for_dir (factory, dirs[i]); } priv->loaded = TRUE; } static void external_thumbnailers_disabled_all_changed_cb (GSettings *settings, const gchar *key, GnomeDesktopThumbnailFactory *factory) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; g_mutex_lock (&priv->lock); priv->disabled = g_settings_get_boolean (priv->settings, "disable-all"); if (priv->disabled) { g_strfreev (priv->disabled_types); priv->disabled_types = NULL; } else { priv->disabled_types = g_settings_get_strv (priv->settings, "disable"); gnome_desktop_thumbnail_factory_load_thumbnailers (factory); } g_mutex_unlock (&priv->lock); } static void external_thumbnailers_disabled_changed_cb (GSettings *settings, const gchar *key, GnomeDesktopThumbnailFactory *factory) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; g_mutex_lock (&priv->lock); if (!priv->disabled) { g_strfreev (priv->disabled_types); priv->disabled_types = g_settings_get_strv (priv->settings, "disable"); } g_mutex_unlock (&priv->lock); } static void gnome_desktop_thumbnail_factory_init (GnomeDesktopThumbnailFactory *factory) { GnomeDesktopThumbnailFactoryPrivate *priv; factory->priv = gnome_desktop_thumbnail_factory_get_instance_private (factory); priv = factory->priv; priv->size = GNOME_DESKTOP_THUMBNAIL_SIZE_NORMAL; priv->mime_types_map = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)thumbnailer_unref); g_mutex_init (&priv->lock); priv->settings = g_settings_new ("org.gnome.desktop.thumbnailers"); priv->disabled = g_settings_get_boolean (priv->settings, "disable-all"); if (!priv->disabled) priv->disabled_types = g_settings_get_strv (priv->settings, "disable"); g_signal_connect (priv->settings, "changed::disable-all", G_CALLBACK (external_thumbnailers_disabled_all_changed_cb), factory); g_signal_connect (priv->settings, "changed::disable", G_CALLBACK (external_thumbnailers_disabled_changed_cb), factory); if (!priv->disabled) gnome_desktop_thumbnail_factory_load_thumbnailers (factory); } static void gnome_desktop_thumbnail_factory_finalize (GObject *object) { GnomeDesktopThumbnailFactory *factory; GnomeDesktopThumbnailFactoryPrivate *priv; factory = GNOME_DESKTOP_THUMBNAIL_FACTORY (object); priv = factory->priv; if (priv->thumbnailers) { g_list_free_full (priv->thumbnailers, (GDestroyNotify)thumbnailer_unref); priv->thumbnailers = NULL; } g_clear_pointer (&priv->mime_types_map, g_hash_table_destroy); if (priv->monitors) { g_list_free_full (priv->monitors, (GDestroyNotify)g_object_unref); priv->monitors = NULL; } g_mutex_clear (&priv->lock); g_clear_pointer (&priv->disabled_types, g_strfreev); if (priv->settings) { g_signal_handlers_disconnect_by_func (priv->settings, external_thumbnailers_disabled_all_changed_cb, factory); g_signal_handlers_disconnect_by_func (priv->settings, external_thumbnailers_disabled_changed_cb, factory); g_clear_object (&priv->settings); } if (G_OBJECT_CLASS (parent_class)->finalize) (* G_OBJECT_CLASS (parent_class)->finalize) (object); } static void gnome_desktop_thumbnail_factory_class_init (GnomeDesktopThumbnailFactoryClass *class) { GObjectClass *gobject_class; gobject_class = G_OBJECT_CLASS (class); gobject_class->finalize = gnome_desktop_thumbnail_factory_finalize; } /** * gnome_desktop_thumbnail_factory_new: * @size: The thumbnail size to use * * Creates a new #GnomeDesktopThumbnailFactory. * * This function must be called on the main thread and is non-blocking. * * Return value: a new #GnomeDesktopThumbnailFactory * * Since: 2.2 **/ GnomeDesktopThumbnailFactory * gnome_desktop_thumbnail_factory_new (GnomeDesktopThumbnailSize size) { GnomeDesktopThumbnailFactory *factory; factory = g_object_new (GNOME_DESKTOP_TYPE_THUMBNAIL_FACTORY, NULL); factory->priv->size = size; return factory; } static char * thumbnail_filename (const char *uri) { GChecksum *checksum; guint8 digest[16]; gsize digest_len = sizeof (digest); char *file; checksum = g_checksum_new (G_CHECKSUM_MD5); g_checksum_update (checksum, (const guchar *) uri, strlen (uri)); g_checksum_get_digest (checksum, digest, &digest_len); g_assert (digest_len == 16); file = g_strconcat (g_checksum_get_string (checksum), ".png", NULL); g_checksum_free (checksum); return file; } static char * thumbnail_path (const char *uri, GnomeDesktopThumbnailSize size) { char *path, *file; file = thumbnail_filename (uri); path = g_build_filename (g_get_user_cache_dir (), "thumbnails", gnome_desktop_thumbnail_size_to_dirname (size), file, NULL); g_free (file); return path; } static char * thumbnail_failed_path (const char *uri) { char *path, *file; file = thumbnail_filename (uri); /* XXX: appname is only used for failed thumbnails. Is this a mistake? */ path = g_build_filename (g_get_user_cache_dir (), "thumbnails", "fail", appname, file, NULL); g_free (file); return path; } static char * validate_thumbnail_path (char *path, const char *uri, time_t mtime, GnomeDesktopThumbnailSize size) { GdkPixbuf *pixbuf; pixbuf = gdk_pixbuf_new_from_file (path, NULL); if (pixbuf == NULL || !gnome_desktop_thumbnail_is_valid (pixbuf, uri, mtime)) { g_free (path); return NULL; } g_clear_object (&pixbuf); return path; } static char * lookup_thumbnail_path (const char *uri, time_t mtime, GnomeDesktopThumbnailSize size) { char *path = thumbnail_path (uri, size); return validate_thumbnail_path (path, uri, mtime, size); } static char * lookup_failed_thumbnail_path (const char *uri, time_t mtime, GnomeDesktopThumbnailSize size) { char *path = thumbnail_failed_path (uri); return validate_thumbnail_path (path, uri, mtime, size); } /** * gnome_desktop_thumbnail_factory_lookup: * @factory: a #GnomeDesktopThumbnailFactory * @uri: the uri of a file * @mtime: the mtime of the file * * Tries to locate an existing thumbnail for the file specified. * * Usage of this function is threadsafe and does blocking I/O. * * Return value: The absolute path of the thumbnail, or %NULL if none exist. * * Since: 2.2 **/ char * gnome_desktop_thumbnail_factory_lookup (GnomeDesktopThumbnailFactory *factory, const char *uri, time_t mtime) { GnomeDesktopThumbnailFactoryPrivate *priv = factory->priv; g_return_val_if_fail (uri != NULL, NULL); return lookup_thumbnail_path (uri, mtime, priv->size); } /** * gnome_desktop_thumbnail_factory_has_valid_failed_thumbnail: * @factory: a #GnomeDesktopThumbnailFactory * @uri: the uri of a file * @mtime: the mtime of the file * * Tries to locate an failed thumbnail for the file specified. Writing * and looking for failed thumbnails is important to avoid to try to * thumbnail e.g. broken images several times. * * Usage of this function is threadsafe and does blocking I/O. * * Return value: TRUE if there is a failed thumbnail for the file. * * Since: 2.2 **/ gboolean gnome_desktop_thumbnail_factory_has_valid_failed_thumbnail (GnomeDesktopThumbnailFactory *factory, const char *uri, time_t mtime) { char *path; g_return_val_if_fail (uri != NULL, FALSE); path = lookup_failed_thumbnail_path (uri, mtime, factory->priv->size); if (path == NULL) return FALSE; g_free (path); return TRUE; } /** * gnome_desktop_thumbnail_factory_can_thumbnail: * @factory: a #GnomeDesktopThumbnailFactory * @uri: the uri of a file * @mime_type: the mime type of the file * @mtime: the mtime of the file * * Returns TRUE if this GnomeDesktopThumbnailFactory can (at least try) to thumbnail * this file. Thumbnails or files with failed thumbnails won't be thumbnailed. * * Usage of this function is threadsafe and does blocking I/O. * * Return value: TRUE if the file can be thumbnailed. * * Since: 2.2 **/ gboolean gnome_desktop_thumbnail_factory_can_thumbnail (GnomeDesktopThumbnailFactory *factory, const char *uri, const char *mime_type, time_t mtime) { gboolean have_script = FALSE; /* Don't thumbnail thumbnails */ if (uri && strncmp (uri, "file:/", 6) == 0 && strstr (uri, "/thumbnails/") != NULL) return FALSE; if (!mime_type) return FALSE; g_mutex_lock (&factory->priv->lock); if (!gnome_desktop_thumbnail_factory_is_disabled (factory, mime_type)) { Thumbnailer *thumb; thumb = g_hash_table_lookup (factory->priv->mime_types_map, mime_type); have_script = (thumb != NULL); } g_mutex_unlock (&factory->priv->lock); if (have_script) { return !gnome_desktop_thumbnail_factory_has_valid_failed_thumbnail (factory, uri, mtime); } return FALSE; } static GdkPixbuf * get_preview_thumbnail (const char *uri, int size, GCancellable *cancellable, GError **error) { GdkPixbuf *pixbuf = NULL; GFile *file; GFileInfo *file_info; GInputStream *input_stream; GObject *object; g_return_val_if_fail (uri != NULL, NULL); input_stream = NULL; file = g_file_new_for_uri (uri); /* First see if we can get an input stream via preview::icon */ file_info = g_file_query_info (file, G_FILE_ATTRIBUTE_PREVIEW_ICON, G_FILE_QUERY_INFO_NONE, cancellable, error); g_object_unref (file); if (!file_info) return NULL; object = g_file_info_get_attribute_object (file_info, G_FILE_ATTRIBUTE_PREVIEW_ICON); if (object) g_object_ref (object); g_object_unref (file_info); if (!object) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("File %s does not have a preview icon attribute"), uri); return NULL; } if (!G_IS_LOADABLE_ICON (object)) { g_object_unref (object); g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("No loadable icon for %s"), uri); return NULL; } input_stream = g_loadable_icon_load (G_LOADABLE_ICON (object), 0, /* size */ NULL, /* return location for type */ cancellable, /* GCancellable */ error); /* return location for GError */ g_object_unref (object); if (!input_stream) return NULL; pixbuf = gdk_pixbuf_new_from_stream_at_scale (input_stream, size, size, TRUE, cancellable, error); g_object_unref (input_stream); return pixbuf; } static GdkPixbuf * pixbuf_new_from_bytes (GBytes *bytes, GError **error) { g_autoptr(GdkPixbufLoader) loader = NULL; loader = gdk_pixbuf_loader_new_with_mime_type ("image/png", error); if (!loader) return NULL; if (!gdk_pixbuf_loader_write (loader, g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), error)) { return NULL; } if (!gdk_pixbuf_loader_close (loader, error)) return NULL; return g_object_ref (gdk_pixbuf_loader_get_pixbuf (loader)); } /** * gnome_desktop_thumbnail_factory_generate_thumbnail: * @factory: a #GnomeDesktopThumbnailFactory * @uri: the uri of a file * @mime_type: the mime type of the file * @cancellable: a #GCancellable object or NULL * @error: a pointer to a GError object or NULL * * Tries to generate a thumbnail for the specified file. If it succeeds * it returns a pixbuf that can be used as a thumbnail. * * Usage of this function is threadsafe and does blocking I/O. * * Return value: (transfer full): thumbnail pixbuf if thumbnailing succeeded, %NULL otherwise and error will be set * * Since: 42.0 **/ GdkPixbuf * gnome_desktop_thumbnail_factory_generate_thumbnail (GnomeDesktopThumbnailFactory *factory, const char *uri, const char *mime_type, GCancellable *cancellable, GError **error) { GdkPixbuf *pixbuf; char *script; int size; GError *inner_error = NULL; g_return_val_if_fail (error == NULL || *error == NULL, NULL); g_return_val_if_fail (uri != NULL, NULL); g_return_val_if_fail (mime_type != NULL, NULL); /* Doesn't access any volatile fields in factory, so it's threadsafe */ size = gnome_desktop_thumbnail_size_to_size (factory->priv->size); pixbuf = get_preview_thumbnail (uri, size, cancellable, &inner_error); if (pixbuf != NULL) return pixbuf; if (g_error_matches (inner_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { g_propagate_error (error, inner_error); return NULL; } g_error_free (inner_error); script = NULL; g_mutex_lock (&factory->priv->lock); if (!gnome_desktop_thumbnail_factory_is_disabled (factory, mime_type)) { Thumbnailer *thumb; thumb = g_hash_table_lookup (factory->priv->mime_types_map, mime_type); if (thumb) script = g_strdup (thumb->command); } g_mutex_unlock (&factory->priv->lock); if (script) { GBytes *data; data = gnome_desktop_thumbnail_script_exec (script, size, uri, error); if (data) { pixbuf = pixbuf_new_from_bytes (data, error); g_bytes_unref (data); } g_free (script); return pixbuf; } else { g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("Could not find thumbnailer for mime-type '%s'"), mime_type); return NULL; } } typedef struct { char *uri; char *mime_type; GdkPixbuf *thumbnail; time_t time; } ThumbnailFactoryAsyncData; static void thumbnail_factory_async_data_free (ThumbnailFactoryAsyncData *thumbnail_factory_data) { g_free (thumbnail_factory_data->uri); g_free (thumbnail_factory_data->mime_type); if (thumbnail_factory_data->thumbnail) g_object_unref (thumbnail_factory_data->thumbnail); g_slice_free (ThumbnailFactoryAsyncData, thumbnail_factory_data); } static void thumbnail_factory_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { GnomeDesktopThumbnailFactory *self = source_object; ThumbnailFactoryAsyncData *thumbnail_factory_data = task_data; GdkPixbuf *thumbnail; GError *error = NULL; thumbnail = gnome_desktop_thumbnail_factory_generate_thumbnail (self, thumbnail_factory_data->uri, thumbnail_factory_data->mime_type, cancellable, &error); if (thumbnail) g_task_return_pointer (task, thumbnail, g_object_unref); else g_task_return_error (task, error); } /** * gnome_desktop_thumbnail_factory_generate_thumbnail_async: * @factory: a #GnomeDesktopThumbnailFactory * @uri: the URI of a file * @mime_type: the MIME type of the file * @cancellable: a Cancellable object * @callback: a function that will be called when the task has ended * @user_data: user data * * Asynchronous version of gnome_desktop_thumbnail_factory_generate_thumbnail() * * Since 43.0 * **/ void gnome_desktop_thumbnail_factory_generate_thumbnail_async (GnomeDesktopThumbnailFactory *factory, const char *uri, const char *mime_type, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { ThumbnailFactoryAsyncData *thumbnail_factory_data; GTask *task; thumbnail_factory_data = g_slice_new (ThumbnailFactoryAsyncData); thumbnail_factory_data->uri = g_strdup (uri); thumbnail_factory_data->mime_type = g_strdup (mime_type); thumbnail_factory_data->thumbnail = NULL; task = g_task_new (factory, cancellable, callback, user_data); g_task_set_task_data (task, thumbnail_factory_data, (GDestroyNotify) thumbnail_factory_async_data_free); g_task_run_in_thread (task, thumbnail_factory_thread); g_object_unref (task); } /** * gnome_desktop_thumbnail_factory_generate_thumbnail_finish: * @factory: a #GnomeDesktopThumbnailFactory * @result: the result of the operation * @error: a pointer where a GError object is returned if the task failed * * Return value: (transfer full): thumbnail pixbuf if thumbnailing succeeded, %NULL otherwise. * * Since 43.0 * **/ GdkPixbuf * gnome_desktop_thumbnail_factory_generate_thumbnail_finish (GnomeDesktopThumbnailFactory *factory, GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (result, factory), NULL); return g_task_propagate_pointer (G_TASK (result), error); } static gboolean save_thumbnail (GdkPixbuf *pixbuf, char *path, const char *uri, time_t mtime, GCancellable *cancellable, GError **error) { char *dirname; char *tmp_path = NULL; int tmp_fd; char mtime_str[21]; gboolean ret = FALSE; const char *width, *height; g_return_val_if_fail (error == NULL || *error == NULL, FALSE); if (pixbuf == NULL) return FALSE; dirname = g_path_get_dirname (path); if (g_mkdir_with_parents (dirname, 0700) != 0) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Failed to create folder '%s'"), dirname); goto out; } tmp_path = g_strconcat (path, ".XXXXXX", NULL); tmp_fd = g_mkstemp (tmp_path); if (tmp_fd == -1) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("The output folder '%s' is not writable"), path); goto out; } close (tmp_fd); g_snprintf (mtime_str, 21, "%" G_GINT64_FORMAT, (gint64) mtime); width = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::Image::Width"); height = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::Image::Height"); if (width != NULL && height != NULL) ret = gdk_pixbuf_save (pixbuf, tmp_path, "png", error, "tEXt::Thumb::Image::Width", width, "tEXt::Thumb::Image::Height", height, "tEXt::Thumb::URI", uri, "tEXt::Thumb::MTime", mtime_str, "tEXt::Software", "GNOME::ThumbnailFactory", NULL); else ret = gdk_pixbuf_save (pixbuf, tmp_path, "png", error, "tEXt::Thumb::URI", uri, "tEXt::Thumb::MTime", mtime_str, "tEXt::Software", "GNOME::ThumbnailFactory", NULL); if (!ret) goto out; g_chmod (tmp_path, 0600); g_rename (tmp_path, path); if (g_cancellable_is_cancelled (cancellable)) { g_cancellable_set_error_if_cancelled (cancellable, error); ret = FALSE; } out: g_unlink (tmp_path); g_free (tmp_path); g_free (dirname); return ret; } static GdkPixbuf * make_failed_thumbnail (void) { GdkPixbuf *pixbuf; pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, 1, 1); gdk_pixbuf_fill (pixbuf, 0x00000000); return pixbuf; } /** * gnome_desktop_thumbnail_factory_save_thumbnail: * @factory: a #GnomeDesktopThumbnailFactory * @thumbnail: the thumbnail as a pixbuf * @uri: the uri of a file * @original_mtime: the modification time of the original file * @cancellable: a GCancellable object, or NULL * @error: where to store the exit error, or NULL * * Saves @thumbnail at the right place. If the save fails a * failed thumbnail is written. * * Usage of this function is threadsafe and does blocking I/O. * * Return value: TRUE if everything went fine; FALSE if there was an error. * * Since: 2.2 **/ gboolean gnome_desktop_thumbnail_factory_save_thumbnail (GnomeDesktopThumbnailFactory *factory, GdkPixbuf *thumbnail, const char *uri, time_t original_mtime, GCancellable *cancellable, GError **error) { g_autofree char *path = NULL; g_autofree char *failed_path = NULL; gboolean ret; GError *inner_error = NULL; g_return_val_if_fail (error == NULL || *error == NULL, FALSE); path = thumbnail_path (uri, factory->priv->size); failed_path = thumbnail_failed_path (uri); ret = save_thumbnail (thumbnail, path, uri, original_mtime, cancellable, &inner_error); if (!ret && !g_error_matches (inner_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { g_autoptr (GdkPixbuf) failed_thumbnail = make_failed_thumbnail (); save_thumbnail (failed_thumbnail, failed_path, uri, original_mtime, cancellable, NULL); } else if (ret) { g_autoptr (GFile) failed_file = g_file_new_for_path (failed_path); g_file_delete (failed_file, cancellable, NULL); } if (!ret) g_propagate_error (error, inner_error); return ret; } static void thumbnail_factory_save_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { GnomeDesktopThumbnailFactory *self = source_object; ThumbnailFactoryAsyncData *thumbnail_factory_data = task_data; GError *error = NULL; gboolean ret; ret = gnome_desktop_thumbnail_factory_save_thumbnail (self, thumbnail_factory_data->thumbnail, thumbnail_factory_data->uri, thumbnail_factory_data->time, cancellable, &error); if (ret) g_task_return_boolean (task, ret); else g_task_return_error (task, error); } /** * gnome_desktop_thumbnail_factory_save_thumbnail_async: * @factory: a #GnomeDesktopThumbnailFactory * @thumbnail: the thumbnail as a pixbuf * @uri: the uri of a file * @original_mtime: the modification time of the original file * @cancellable: a Cancellable object * @callback: a function that will be called when the task has ended * @user_data: user data * * Asynchronous version of gnome_desktop_thumbnail_factory_save_thumbnail() * * Since 43.0 * **/ void gnome_desktop_thumbnail_factory_save_thumbnail_async (GnomeDesktopThumbnailFactory *factory, GdkPixbuf *thumbnail, const char *uri, time_t original_mtime, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { ThumbnailFactoryAsyncData *thumbnail_factory_data; GTask *task; thumbnail_factory_data = g_slice_new (ThumbnailFactoryAsyncData); thumbnail_factory_data->uri = g_strdup (uri); thumbnail_factory_data->mime_type = NULL; thumbnail_factory_data->thumbnail = g_object_ref (thumbnail); thumbnail_factory_data->time = original_mtime; task = g_task_new (factory, cancellable, callback, user_data); g_task_set_task_data (task, thumbnail_factory_data, (GDestroyNotify) thumbnail_factory_async_data_free); g_task_run_in_thread (task, thumbnail_factory_save_thread); g_object_unref (task); } /** * gnome_desktop_thumbnail_factory_save_thumbnail_finish: * @factory: a #GnomeDesktopThumbnailFactory * @result: the result of the operation * @error: a pointer where a GError object is returned if the task failed * * Return value: TRUE if the operation was correct; FALSE if there was an error * * Since 43.0 * **/ gboolean gnome_desktop_thumbnail_factory_save_thumbnail_finish (GnomeDesktopThumbnailFactory *factory, GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (result, factory), FALSE); return g_task_propagate_boolean (G_TASK (result), error); } /** * gnome_desktop_thumbnail_factory_create_failed_thumbnail: * @factory: a #GnomeDesktopThumbnailFactory * @uri: the uri of a file * @mtime: the modification time of the file * @cancellable: a GCancellable object, or NULL * @error: where to store the exit error, or NULL * * Creates a failed thumbnail for the file so that we don't try * to re-thumbnail the file later. * * Usage of this function is threadsafe and does blocking I/O. * * Return value: TRUE if everything went fine; FALSE if there was an error. * * Since: 2.2 **/ gboolean gnome_desktop_thumbnail_factory_create_failed_thumbnail (GnomeDesktopThumbnailFactory *factory, const char *uri, time_t mtime, GCancellable *cancellable, GError **error) { char *path; GdkPixbuf *pixbuf; gboolean ret; g_return_val_if_fail (error == NULL || *error == NULL, FALSE); path = thumbnail_failed_path (uri); pixbuf = make_failed_thumbnail (); ret = save_thumbnail (pixbuf, path, uri, mtime, cancellable, error); g_free (path); g_object_unref (pixbuf); return ret; } static void thumbnail_factory_create_failed_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { GnomeDesktopThumbnailFactory *self = source_object; ThumbnailFactoryAsyncData *thumbnail_factory_data = task_data; GError *error = NULL; gboolean ret; ret = gnome_desktop_thumbnail_factory_create_failed_thumbnail (self, thumbnail_factory_data->uri, thumbnail_factory_data->time, cancellable, &error); if (ret) g_task_return_boolean (task, ret); else g_task_return_error (task, error); } /** * gnome_desktop_thumbnail_factory_create_failed_thumbnail_async: * @factory: a #GnomeDesktopThumbnailFactory * @uri: the uri of a file * @original_mtime: the modification time of the original file * @cancellable: a Cancellable object * @callback: a function that will be called when the task has ended * @user_data: user data * * Asynchronous version of gnome_desktop_thumbnail_factory_create_failed_thumbnail() * * Since 43.0 * **/ void gnome_desktop_thumbnail_factory_create_failed_thumbnail_async (GnomeDesktopThumbnailFactory *factory, const char *uri, time_t original_mtime, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { ThumbnailFactoryAsyncData *thumbnail_factory_data; GTask *task; thumbnail_factory_data = g_slice_new (ThumbnailFactoryAsyncData); thumbnail_factory_data->uri = g_strdup (uri); thumbnail_factory_data->mime_type = NULL; thumbnail_factory_data->thumbnail = NULL; thumbnail_factory_data->time = original_mtime; task = g_task_new (factory, cancellable, callback, user_data); g_task_set_task_data (task, thumbnail_factory_data, (GDestroyNotify) thumbnail_factory_async_data_free); g_task_run_in_thread (task, thumbnail_factory_create_failed_thread); g_object_unref (task); } /** * gnome_desktop_thumbnail_factory_create_failed_thumbnail_finish: * @factory: a #GnomeDesktopThumbnailFactory * @result: the result of the operation * @error: a pointer where a GError object is returned if the task failed * * Return value: TRUE if the operation was correct; FALSE if there was an error * * Since 43.0 * **/ gboolean gnome_desktop_thumbnail_factory_create_failed_thumbnail_finish (GnomeDesktopThumbnailFactory *factory, GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (result, factory), FALSE); return g_task_propagate_boolean (G_TASK (result), error); } /** * gnome_desktop_thumbnail_path_for_uri: * @uri: an uri * @size: a thumbnail size * * Returns the filename that a thumbnail of size @size for @uri would have. * This function is threadsafe and does no blocking I/O. * * Return value: an absolute filename * * Since: 2.2 **/ char * gnome_desktop_thumbnail_path_for_uri (const char *uri, GnomeDesktopThumbnailSize size) { return thumbnail_path (uri, size); } /** * gnome_desktop_thumbnail_is_valid: * @pixbuf: an loaded thumbnail #GdkPixbuf * @uri: a uri * @mtime: the mtime * * Returns whether the thumbnail has the correct uri and mtime embedded in the * png options. This function is threadsafe and does no blocking I/O. * * Return value: TRUE if the thumbnail has the right @uri and @mtime * * Since: 2.2 **/ gboolean gnome_desktop_thumbnail_is_valid (GdkPixbuf *pixbuf, const char *uri, time_t mtime) { const char *thumb_uri, *thumb_mtime_str; time_t thumb_mtime; thumb_uri = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::URI"); if (g_strcmp0 (uri, thumb_uri) != 0) return FALSE; thumb_mtime_str = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::MTime"); if (!thumb_mtime_str) return FALSE; thumb_mtime = atol (thumb_mtime_str); if (mtime != thumb_mtime) return FALSE; return TRUE; }