/*
* gnome-thumbnail.c: Utilities for handling thumbnails
*
* Copyright (C) 2002 Red Hat, Inc.
* Copyright (C) 2010 Carlos Garcia Campos <carlosgc@gnome.org>
*
* 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 <alexl@redhat.com>
*/
/**
* 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 <firstterm>thumbnailers</firstterm>, each one generating thumbnails
* for a specific set of content-types of files. For example,
* <application>totem-video-thumbnailer</application> generates thumbnails for
* video files using GStreamer; <application>evince-thumbnailer</application>
* 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
* <filename>.thumbnailer</filename> files in
* <filename><replaceable>$PREFIX</replaceable>/share/thumbnailers</filename>.
* Each is in a simple key-file format:
* <informalexample><programlisting>
* [Thumbnailer Entry]
* Exec=evince-thumbnailer -s %s %u %o
* MimeType=application/pdf;application/x-bzpdf;application/x-gzpdf;
* </programlisting></informalexample>
*
* The <filename>.thumbnailer</filename> format supports three keys:
* <variablelist>
* <varlistentry><term><code>Exec</code></term><listitem><para>
* Required. The command to execute the thumbnailer. It supports a few different
* parameters which are replaced before calling the thumbnailer:
* <replaceable>%u</replaceable> is the URI of the file being thumbnailed;
* <replaceable>%i</replaceable> is its path; <replaceable>%o</replaceable>
* is the path of the image file to be written to;
* <replaceable>%s</replaceable> is the maximum desired size of the thumbnail
* image (the maximum width or height, in pixels); and
* <replaceable>%%</replaceable> is a literal percent character.
* </para></listitem></varlistentry>
* <varlistentry><term><code>MimeType</code></term><listitem><para>
* Required. A semicolon-separated list of MIME types which the thumbnailer
* supports generating thumbnails for.
* </para></listitem></varlistentry>
* </variablelist>
*
* So in the example <filename>.thumbnailer</filename> file above, the command
* passes the requested thumbnail size, then the input file’s URI, then the
* path for the output image file to
* <application>evince-thumbnailer</application>.
*
* 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
* <filename><envar>$XDG_CACHE_HOME</envar>/thumbnails/
* <replaceable>$SIZE</replaceable>/</filename>. 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
* <filename><envar>$XDG_CACHE_HOME</envar>/thumbnails/fail/
* gnome-thumbnail-factory/</filename> 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 <config.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#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 (GnomeDesktopThumbnailFactory,
gnome_desktop_thumbnail_factory,
G_TYPE_OBJECT)
#define parent_class gnome_desktop_thumbnail_factory_parent_class
#define GNOME_DESKTOP_THUMBNAIL_FACTORY_GET_PRIVATE(object) \
(G_TYPE_INSTANCE_GET_PRIVATE ((object), GNOME_DESKTOP_TYPE_THUMBNAIL_FACTORY, GnomeDesktopThumbnailFactoryPrivate))
#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);
}
/* 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_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;
g_type_class_add_private (class, sizeof (GnomeDesktopThumbnailFactoryPrivate));
}
/**
* 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",
size == GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE ? "large" : "normal",
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)
{
GdkPixbuf *pixbuf;
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,
NULL, /* GCancellable */
NULL); /* return location for GError */
g_object_unref (file);
if (file_info == NULL)
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)
return NULL;
if (!G_IS_LOADABLE_ICON (object)) {
g_object_unref (object);
return NULL;
}
input_stream = g_loadable_icon_load (G_LOADABLE_ICON (object),
0, /* size */
NULL, /* return location for type */
NULL, /* GCancellable */
NULL); /* 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, NULL, NULL);
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
*
* 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.
*
* Since: 2.2
**/
GdkPixbuf *
gnome_desktop_thumbnail_factory_generate_thumbnail (GnomeDesktopThumbnailFactory *factory,
const char *uri,
const char *mime_type)
{
GdkPixbuf *pixbuf;
char *script;
int size;
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 = 128;
if (factory->priv->size == GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE)
size = 256;
pixbuf = get_preview_thumbnail (uri, size);
if (pixbuf != NULL)
return pixbuf;
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;
GError *error = NULL;
data = gnome_desktop_thumbnail_script_exec (script, size, uri, &error);
if (data)
{
pixbuf = pixbuf_new_from_bytes (data, &error);
if (!pixbuf)
{
g_debug ("Could not load thumbnail pixbuf: %s", error->message);
g_error_free (error);
}
g_bytes_unref (data);
}
else
{
g_debug ("Thumbnail script ('%s') failed for '%s': %s",
script, uri, error ? error->message : "no details");
g_clear_error (&error);
}
}
else
{
g_debug ("Could not find thumbnailer for mime-type '%s'",
mime_type);
}
g_free (script);
return pixbuf;
}
static gboolean
save_thumbnail (GdkPixbuf *pixbuf,
char *path,
const char *uri,
time_t mtime)
{
char *dirname;
char *tmp_path = NULL;
int tmp_fd;
char mtime_str[21];
gboolean ret = FALSE;
GError *error = NULL;
const char *width, *height;
if (pixbuf == NULL)
return FALSE;
dirname = g_path_get_dirname (path);
if (g_mkdir_with_parents (dirname, 0700) != 0)
goto out;
tmp_path = g_strconcat (path, ".XXXXXX", NULL);
tmp_fd = g_mkstemp (tmp_path);
if (tmp_fd == -1)
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");
error = NULL;
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);
out:
if (error != NULL)
{
g_warning ("Failed to create thumbnail %s: %s", tmp_path, error->message);
g_error_free (error);
}
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
*
* 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.
*
* Since: 2.2
**/
void
gnome_desktop_thumbnail_factory_save_thumbnail (GnomeDesktopThumbnailFactory *factory,
GdkPixbuf *thumbnail,
const char *uri,
time_t original_mtime)
{
char *path;
path = thumbnail_path (uri, factory->priv->size);
if (!save_thumbnail (thumbnail, path, uri, original_mtime))
{
thumbnail = make_failed_thumbnail ();
g_free (path);
path = thumbnail_failed_path (uri);
save_thumbnail (thumbnail, path, uri, original_mtime);
g_object_unref (thumbnail);
}
g_free (path);
}
/**
* gnome_desktop_thumbnail_factory_create_failed_thumbnail:
* @factory: a #GnomeDesktopThumbnailFactory
* @uri: the uri of a file
* @mtime: the modification time of the file
*
* 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.
*
* Since: 2.2
**/
void
gnome_desktop_thumbnail_factory_create_failed_thumbnail (GnomeDesktopThumbnailFactory *factory,
const char *uri,
time_t mtime)
{
char *path;
GdkPixbuf *pixbuf;
path = thumbnail_failed_path (uri);
pixbuf = make_failed_thumbnail ();
save_thumbnail (pixbuf, path, uri, mtime);
g_free (path);
g_object_unref (pixbuf);
}
/**
* 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;
}