/* * nautilus-thumbnails.h: Thumbnail code for icon factory. * * Copyright (C) 2000, 2001 Eazel, Inc. * Copyright (C) 2002, 2003 Red Hat, Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program 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 * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, see . * * Author: Andy Hertzfeld */ #define G_LOG_DOMAIN "nautilus-thumbnails" #include #include "nautilus-thumbnails.h" #define GNOME_DESKTOP_USE_UNSTABLE_API #include "nautilus-directory-notify.h" #include "nautilus-global-preferences.h" #include "nautilus-file-utilities.h" #include "nautilus-hash-queue.h" #include #include #include #include #include #include #include #include #include /* Should never be a reasonable actual mtime */ #define INVALID_MTIME 0 /* Cool-off period between last file modification time and thumbnail creation */ #define THUMBNAIL_CREATION_DELAY_SECS 3 /* This specific number of processors seems to work ok even on relatively slow * computers. However, this might not be the effective number of processors * used simultaneously because of main thread load and I/O bounds. */ #define MAX_THUMBNAILING_THREADS ceil (g_get_num_processors () / 2); static gboolean thumbnail_starter_cb (gpointer data); /* structure used for making thumbnails, associating a uri with where the thumbnail is to be stored */ typedef struct { GCancellable *cancellable; GAsyncReadyCallback callback; gpointer user_data; } ThumbnailCreationCallback; typedef struct { char *image_uri; char *mime_type; time_t original_file_mtime; time_t updated_file_mtime; GdkPixbuf *pixbuf; GPtrArray *callbacks; GError *error; } NautilusThumbnailInfo; typedef struct { NautilusThumbnailInfo *info; ThumbnailCreationCallback *callback; } ThumbnailCreationResult; /* * Thumbnail thread state. */ /* The id of the idle handler used to start the thumbnail thread, or 0 if no * idle handler is currently registered. */ static guint thumbnail_thread_starter_id = 0; /* The list of NautilusThumbnailInfo structs containing information about the * thumbnails we are making. */ static NautilusHashQueue *thumbnails_to_make = NULL; /* The icons being currently thumbnailed. */ static GHashTable *currently_thumbnailing_hash = NULL; /* The number of currently running threads. */ static guint running_threads = 0; /* The maximum number of threads allowed. */ static guint max_threads = 0; static void thumbnail_enqueue (NautilusThumbnailInfo *info, ThumbnailCreationCallback *cb_data); static void free_thumbnail_callback (ThumbnailCreationCallback *cb_data) { g_clear_object (&cb_data->cancellable); g_free (cb_data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (ThumbnailCreationCallback, free_thumbnail_callback) static void free_thumbnail_info (NautilusThumbnailInfo *info) { g_free (info->image_uri); g_free (info->mime_type); g_clear_object (&info->pixbuf); g_clear_pointer (&info->callbacks, g_ptr_array_unref); g_clear_error (&info->error); g_free (info); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (NautilusThumbnailInfo, free_thumbnail_info) static GnomeDesktopThumbnailSize get_thumbnail_scale (void) { static gint max_scale = 0; static GnomeDesktopThumbnailSize size; if (G_UNLIKELY (max_scale == 0)) { GdkDisplay *display = gdk_display_get_default (); GListModel *monitors = gdk_display_get_monitors (display); max_scale = 1; for (guint i = 0; i < g_list_model_get_n_items (monitors); i++) { g_autoptr (GdkMonitor) monitor = g_list_model_get_item (monitors, i); max_scale = MAX (max_scale, gdk_monitor_get_scale_factor (monitor)); } if (max_scale <= 1) { size = GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE; } else if (max_scale <= 2) { size = GNOME_DESKTOP_THUMBNAIL_SIZE_XLARGE; } else { size = GNOME_DESKTOP_THUMBNAIL_SIZE_XXLARGE; } } return size; } static GnomeDesktopThumbnailFactory * get_thumbnail_factory (void) { static GnomeDesktopThumbnailFactory *thumbnail_factory = NULL; if (G_UNLIKELY (thumbnail_factory == NULL)) { GnomeDesktopThumbnailSize size = get_thumbnail_scale (); thumbnail_factory = gnome_desktop_thumbnail_factory_new (size); } return thumbnail_factory; } guint nautilus_thumbnail_get_max_size (void) { GnomeDesktopThumbnailSize size = get_thumbnail_scale (); 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 (); } } } char * nautilus_thumbnail_get_path_for_uri (const char *uri) { return gnome_desktop_thumbnail_path_for_uri (uri, get_thumbnail_scale ()); } /*************************************************************************** * Thumbnail Thread Functions. ***************************************************************************/ static GHashTable * get_types_table (void) { static GHashTable *image_mime_types = NULL; GSList *format_list, *l; char **types; int i; if (image_mime_types == NULL) { image_mime_types = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); format_list = gdk_pixbuf_get_formats (); for (l = format_list; l; l = l->next) { types = gdk_pixbuf_format_get_mime_types (l->data); for (i = 0; types[i] != NULL; i++) { g_hash_table_insert (image_mime_types, types [i], GUINT_TO_POINTER (1)); } g_free (types); } g_slist_free (format_list); } return image_mime_types; } static gboolean pixbuf_can_load_type (const char *mime_type) { GHashTable *image_mime_types; image_mime_types = get_types_table (); if (g_hash_table_lookup (image_mime_types, mime_type)) { return TRUE; } return FALSE; } gboolean nautilus_thumbnail_is_mimetype_limited_by_size (const char *mime_type) { return pixbuf_can_load_type (mime_type); } gboolean nautilus_can_thumbnail (const gchar *uri, const gchar *mime_type, time_t modified_time) { GnomeDesktopThumbnailFactory *factory = get_thumbnail_factory (); return gnome_desktop_thumbnail_factory_can_thumbnail (factory, uri, mime_type, modified_time); } static void handle_cancelled_callbacks (NautilusThumbnailInfo *info) { for (guint i = 0; i < info->callbacks->len; i++) { ThumbnailCreationCallback *thumbnail_callback = info->callbacks->pdata[i]; ThumbnailCreationResult res = { .info = info, .callback = thumbnail_callback }; if (thumbnail_callback->cancellable != NULL && g_cancellable_is_cancelled (thumbnail_callback->cancellable)) { g_debug ("Cancelled thumbnail: %s", info->image_uri); if (thumbnail_callback->callback != NULL) { (*thumbnail_callback->callback) (NULL, (GAsyncResult *) &res, thumbnail_callback->user_data); } g_ptr_array_remove_index_fast (info->callbacks, i--); } } } static void handle_callbacks_and_free (NautilusThumbnailInfo *info) { for (uint i = 0; i < info->callbacks->len; i++) { ThumbnailCreationCallback *thumbnail_callback = info->callbacks->pdata[i]; ThumbnailCreationResult res = { .info = info, .callback = thumbnail_callback }; if (thumbnail_callback->callback != NULL) { (*thumbnail_callback->callback) (NULL, (GAsyncResult *) &res, thumbnail_callback->user_data); } } free_thumbnail_info (info); } typedef struct { NautilusThumbnailInfo *info; ThumbnailCreationCallback *cb_data; } ThumbnailMtimeQuery; static void query_file_mtime_callback (GObject *object, GAsyncResult *source, gpointer callback_data) { g_autofree ThumbnailMtimeQuery *mtime_query_data = callback_data; g_autoptr (NautilusThumbnailInfo) info = mtime_query_data->info; g_autoptr (ThumbnailCreationCallback) cb_data = mtime_query_data->cb_data; g_autoptr (GFileInfo) file_info = g_file_query_info_finish (G_FILE (object), source, NULL); if (file_info != NULL && g_file_info_has_attribute (file_info, G_FILE_ATTRIBUTE_TIME_MODIFIED)) { info->original_file_mtime = g_file_info_get_attribute_uint64 (file_info, G_FILE_ATTRIBUTE_TIME_MODIFIED); info->updated_file_mtime = info->original_file_mtime; } else { info->original_file_mtime = INVALID_MTIME; info->updated_file_mtime = INVALID_MTIME; } if (cb_data->cancellable != NULL && g_cancellable_is_cancelled (cb_data->cancellable)) { /* Call the callback immediately */ g_ptr_array_add (info->callbacks, g_steal_pointer (&cb_data)); handle_cancelled_callbacks (info); return; } thumbnail_enqueue (g_steal_pointer (&info), g_steal_pointer (&cb_data)); } void nautilus_create_thumbnail_async (const gchar *uri, const gchar *mime_type, time_t modified_time, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_return_if_fail (uri != NULL && *uri != '\0'); g_autoptr (NautilusThumbnailInfo) info = g_new0 (NautilusThumbnailInfo, 1); g_autoptr (ThumbnailCreationCallback) cb_data = g_new0 (ThumbnailCreationCallback, 1); info->image_uri = g_strdup (uri); info->mime_type = g_strdup (mime_type); info->callbacks = g_ptr_array_new_with_free_func ((GDestroyNotify) free_thumbnail_callback); cb_data->cancellable = cancellable != NULL ? g_object_ref (cancellable) : NULL; cb_data->callback = callback; cb_data->user_data = user_data; if (cancellable != NULL && g_cancellable_is_cancelled (cancellable)) { /* Call the callback immediately */ g_ptr_array_add (info->callbacks, g_steal_pointer (&cb_data)); handle_cancelled_callbacks (info); return; } if (modified_time == INVALID_MTIME) { g_autoptr (GFile) file = g_file_new_for_uri (info->image_uri); g_autofree ThumbnailMtimeQuery *mtime_query_data = g_new0 (ThumbnailMtimeQuery, 1); mtime_query_data->info = g_steal_pointer (&info); mtime_query_data->cb_data = g_steal_pointer (&cb_data); g_file_query_info_async (file, G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, cancellable, query_file_mtime_callback, g_steal_pointer (&mtime_query_data)); return; } info->original_file_mtime = modified_time; info->updated_file_mtime = modified_time; thumbnail_enqueue (g_steal_pointer (&info), g_steal_pointer (&cb_data)); } static void thumbnail_enqueue (NautilusThumbnailInfo *info, ThumbnailCreationCallback *cb_data) { if (G_UNLIKELY (thumbnails_to_make == NULL)) { thumbnails_to_make = nautilus_hash_queue_new (g_str_hash, g_str_equal, NULL, NULL); currently_thumbnailing_hash = g_hash_table_new (g_str_hash, g_str_equal); } /* Check if it is already in the list of thumbnails to make or * currently being made. */ NautilusThumbnailInfo *existing_info = g_hash_table_lookup (currently_thumbnailing_hash, info->image_uri); if (existing_info == NULL) { existing_info = nautilus_hash_queue_find_item (thumbnails_to_make, info->image_uri); } if (existing_info == NULL) { /* Add the thumbnail to the list. */ g_debug ("(Main Thread) Adding thumbnail: %s", info->image_uri); g_ptr_array_add (info->callbacks, cb_data); nautilus_hash_queue_enqueue (thumbnails_to_make, info->image_uri, info); /* If we didn't schedule the thumbnail function to start on idle, do * that now. We don't want to start it until all the other work is * done, so the GUI will be updated as quickly as possible. */ if (thumbnail_thread_starter_id == 0) { thumbnail_thread_starter_id = g_idle_add_full (G_PRIORITY_LOW, thumbnail_starter_cb, NULL, NULL); } } else { g_debug ("(Main Thread) Updating non-current mtime: %s", info->image_uri); /* The file in the queue might need a new original mtime */ existing_info->updated_file_mtime = info->original_file_mtime; g_ptr_array_add (existing_info->callbacks, cb_data); free_thumbnail_info (info); } } GdkPixbuf * nautilus_create_thumbnail_finish (GAsyncResult *res, GError **error) { ThumbnailCreationResult *result = (ThumbnailCreationResult *) res; ThumbnailCreationCallback *callback = result->callback; NautilusThumbnailInfo *info = result->info; if (callback->cancellable != NULL && g_cancellable_is_cancelled (callback->cancellable)) { if (error != NULL) { *error = g_error_new (G_IO_ERROR, G_IO_ERROR_CANCELLED, "Cancelled"); } return NULL; } if (info->error != NULL) { if (error != NULL) { *error = g_error_copy (info->error); } return NULL; } return info->pixbuf != NULL ? g_object_ref (info->pixbuf) : NULL; } static void thumbnail_finalize (NautilusThumbnailInfo *info) { g_hash_table_remove (currently_thumbnailing_hash, info->image_uri); running_threads -= 1; handle_cancelled_callbacks (info); /* If the original file mtime of the request changed, then * we need to redo the thumbnail. */ if (info->original_file_mtime == info->updated_file_mtime || info->callbacks->len == 0) { handle_callbacks_and_free (info); } else { info->original_file_mtime = info->updated_file_mtime; nautilus_hash_queue_enqueue (thumbnails_to_make, info->image_uri, info); } if (nautilus_hash_queue_is_empty (thumbnails_to_make)) { g_debug ("(Thumbnail Async Thread) Exiting"); } else if (thumbnail_thread_starter_id == 0) { thumbnail_thread_starter_id = g_idle_add (thumbnail_starter_cb, NULL); } } static void thumbnail_failed_cb (GObject *source_object, GAsyncResult *result, gpointer data) { GnomeDesktopThumbnailFactory *thumbnail_factory = GNOME_DESKTOP_THUMBNAIL_FACTORY (source_object); NautilusThumbnailInfo *info = data; g_autoptr (GError) error = NULL; gnome_desktop_thumbnail_factory_create_failed_thumbnail_finish (thumbnail_factory, result, &error); if (error != NULL) { g_debug ("(Thumbnail Async Thread) Could not create a failed thumbnail: %s (%s)", info->image_uri, error->message); } thumbnail_finalize (info); } static void thumbnail_saved_cb (GObject *source_object, GAsyncResult *result, gpointer data) { GnomeDesktopThumbnailFactory *thumbnail_factory = GNOME_DESKTOP_THUMBNAIL_FACTORY (source_object); NautilusThumbnailInfo *info = data; g_autoptr (GError) error = NULL; gnome_desktop_thumbnail_factory_save_thumbnail_finish (thumbnail_factory, result, &error); if (error != NULL) { g_debug ("(Thumbnail Async Thread) Saving thumbnail failed: %s (%s)", info->image_uri, error->message); } thumbnail_finalize (info); } static void thumbnail_generated_cb (GObject *source_object, GAsyncResult *result, gpointer data) { GnomeDesktopThumbnailFactory *thumbnail_factory = GNOME_DESKTOP_THUMBNAIL_FACTORY (source_object); NautilusThumbnailInfo *info = data; g_autoptr (GError) error = NULL; GdkPixbuf *pixbuf = NULL; pixbuf = gnome_desktop_thumbnail_factory_generate_thumbnail_finish (thumbnail_factory, result, &error); if (pixbuf != NULL) { g_autofree gchar *mtime = g_strdup_printf ("%" G_GINT64_FORMAT, (gint64) info->updated_file_mtime); g_debug ("(Thumbnail Async Thread) Saving thumbnail: %s", info->image_uri); /* This is needed since the attribute is not set on the pixbuf, * only the written thumbnail file. */ gdk_pixbuf_set_option (pixbuf, "tEXt::Thumb::MTime", mtime); g_clear_object (&info->pixbuf); info->pixbuf = pixbuf; gnome_desktop_thumbnail_factory_save_thumbnail_async (thumbnail_factory, pixbuf, info->image_uri, info->updated_file_mtime, NULL, thumbnail_saved_cb, info); } else { info->error = g_error_copy (error); g_debug ("(Thumbnail Async Thread) Thumbnail failed: %s (%s)", info->image_uri, error->message); gnome_desktop_thumbnail_factory_create_failed_thumbnail_async (thumbnail_factory, info->image_uri, info->updated_file_mtime, NULL, thumbnail_failed_cb, info); } } /* This function is added as a very low priority idle function to start the * async threads to create any needed thumbnails. It is added with a very * low priority so that it doesn't delay showing the directory in the * icon/list views. We want to show the files in the directory as quickly * as possible. */ static gboolean thumbnail_starter_cb (gpointer data) { GnomeDesktopThumbnailFactory *thumbnail_factory; NautilusThumbnailInfo *info = NULL; guint ignored_thumbnails = 0; time_t current_orig_mtime = 0; time_t current_time; guint backoff_time; guint backoff_time_min = THUMBNAIL_CREATION_DELAY_SECS + 1; g_debug ("(Main Thread) Creating thumbnails thread"); thumbnail_factory = get_thumbnail_factory (); thumbnail_thread_starter_id = 0; if (G_UNLIKELY (max_threads == 0)) { max_threads = MAX_THUMBNAILING_THREADS } /* We loop until the queue is empty, or we reach the thread limit. */ while (ignored_thumbnails < nautilus_hash_queue_get_length (thumbnails_to_make) && running_threads <= max_threads) { info = nautilus_hash_queue_peek_head (thumbnails_to_make); nautilus_hash_queue_remove (thumbnails_to_make, info->image_uri); handle_cancelled_callbacks (info); if (info->callbacks->len == 0) { free_thumbnail_info (info); continue; } current_orig_mtime = info->updated_file_mtime; time (¤t_time); /* Don't try to create a thumbnail if the file was modified recently. * This prevents constant re-thumbnailing of changing files. */ if (current_time < current_orig_mtime + THUMBNAIL_CREATION_DELAY_SECS && current_time >= current_orig_mtime) { g_debug ("(Thumbnail Thread) Skipping: %s", info->image_uri); /* Only retain the smallest backoff time */ backoff_time = THUMBNAIL_CREATION_DELAY_SECS - (current_time - current_orig_mtime); backoff_time_min = MIN (backoff_time, backoff_time_min); nautilus_hash_queue_enqueue (thumbnails_to_make, info->image_uri, info); ignored_thumbnails += 1; continue; } /* Create the thumbnail. */ g_debug ("(Thumbnail Thread) Creating thumbnail: %s", info->image_uri); running_threads += 1; g_hash_table_insert (currently_thumbnailing_hash, info->image_uri, info); gnome_desktop_thumbnail_factory_generate_thumbnail_async (thumbnail_factory, info->image_uri, info->mime_type, NULL, thumbnail_generated_cb, info); } /* Reschedule thumbnailing via a change notification */ if (thumbnail_thread_starter_id == 0 && ignored_thumbnails > 0) { thumbnail_thread_starter_id = g_timeout_add_seconds (backoff_time_min, thumbnail_starter_cb, NULL); } return G_SOURCE_REMOVE; }