diff --git a/fiv-browser.c b/fiv-browser.c index 8f176bc..ae8a968 100644 --- a/fiv-browser.c +++ b/fiv-browser.c @@ -504,13 +504,23 @@ reload_thumbnails(FivBrowser *self) // --- Minion management ------------------------------------------------------- -static gboolean thumbnailer_next(Thumbnailer *thumbnailer); +#if !GLIB_CHECK_VERSION(2, 70, 0) +#define g_spawn_check_wait_status g_spawn_check_exit_status +#endif + +static gboolean thumbnailer_next(Thumbnailer *t); static void -thumbnailer_reprocess_entry(FivBrowser *self, Entry *entry) +thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry) { - entry_add_thumbnail(entry, self); - materialize_icon(self, entry); + g_clear_object(&entry->icon); + g_clear_pointer(&entry->thumbnail, cairo_surface_destroy); + if (!output || !(entry->thumbnail = rescale_thumbnail( + fiv_io_deserialize(output), self->item_height))) { + entry_add_thumbnail(entry, self); + materialize_icon(self, entry); + } + gtk_widget_queue_resize(GTK_WIDGET(self)); } @@ -518,64 +528,75 @@ static void on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data) { GSubprocess *subprocess = G_SUBPROCESS(object); - Thumbnailer *thumbnailer = user_data; + Thumbnailer *t = user_data; + // Reading out pixel data directly from a thumbnailer serves two purposes: + // 1. it avoids pointless delays with large thumbnail sizes, + // 2. it enables thumbnailing things that cannot be placed in the cache. GError *error = NULL; - if (!g_subprocess_wait_check_finish(subprocess, res, &error)) { + GBytes *out = NULL; + if (!g_subprocess_communicate_finish(subprocess, res, &out, NULL, &error)) { if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { g_error_free(error); return; } - if (!g_subprocess_get_if_exited(subprocess) || - g_subprocess_get_exit_status(subprocess) != EXIT_FAILURE) - g_warning("%s", error->message); + } else if (!g_subprocess_get_if_exited(subprocess)) { + // If it exited, it probably printed its own message. + g_spawn_check_wait_status(g_subprocess_get_status(subprocess), &error); + } + + if (error) { + g_warning("%s", error->message); g_error_free(error); } - g_return_if_fail(subprocess == thumbnailer->minion); + g_return_if_fail(subprocess == t->minion); gboolean succeeded = g_subprocess_get_if_exited(subprocess) && g_subprocess_get_exit_status(subprocess) == EXIT_SUCCESS; - g_clear_object(&thumbnailer->minion); - if (!thumbnailer->target) { + g_clear_object(&t->minion); + if (!t->target) { g_warning("finished thumbnailing an unknown image"); + g_clear_pointer(&out, g_bytes_unref); return; } if (succeeded) - thumbnailer_reprocess_entry(thumbnailer->self, thumbnailer->target); + thumbnailer_reprocess_entry(t->self, out, t->target); + else + g_clear_pointer(&out, g_bytes_unref); - thumbnailer->target = NULL; - thumbnailer_next(thumbnailer); + t->target = NULL; + thumbnailer_next(t); } static gboolean -thumbnailer_next(Thumbnailer *thumbnailer) +thumbnailer_next(Thumbnailer *t) { - // TODO(p): Ideally, try to keep the minions alive. - FivBrowser *self = thumbnailer->self; + // TODO(p): Try to keep the minions alive (stdout will be a problem). + FivBrowser *self = t->self; GList *link = self->thumbnailers_queue; if (!link) return FALSE; - thumbnailer->target = link->data; + t->target = link->data; self->thumbnailers_queue = g_list_delete_link(self->thumbnailers_queue, self->thumbnailers_queue); GError *error = NULL; - thumbnailer->minion = g_subprocess_new(G_SUBPROCESS_FLAGS_NONE, &error, + t->minion = g_subprocess_new(G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error, PROJECT_NAME, "--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name, "--", - thumbnailer->target->uri, NULL); + t->target->uri, NULL); if (error) { g_warning("%s", error->message); g_error_free(error); return FALSE; } - thumbnailer->cancel = g_cancellable_new(); - g_subprocess_wait_check_async(thumbnailer->minion, thumbnailer->cancel, - on_thumbnailer_ready, thumbnailer); + t->cancel = g_cancellable_new(); + g_subprocess_communicate_async( + t->minion, NULL, t->cancel, on_thumbnailer_ready, t); return TRUE; } @@ -588,15 +609,15 @@ thumbnailers_abort(FivBrowser *self) self->thumbnailers_queue = NULL; for (size_t i = 0; i < self->thumbnailers_len; i++) { - Thumbnailer *thumbnailer = self->thumbnailers + i; - if (thumbnailer->cancel) { - g_cancellable_cancel(thumbnailer->cancel); - g_clear_object(&thumbnailer->cancel); + Thumbnailer *t = self->thumbnailers + i; + if (t->cancel) { + g_cancellable_cancel(t->cancel); + g_clear_object(&t->cancel); } // Just let them exit on their own. - g_clear_object(&thumbnailer->minion); - thumbnailer->target = NULL; + g_clear_object(&t->minion); + t->target = NULL; } } diff --git a/fiv-io.c b/fiv-io.c index 1778120..870624f 100644 --- a/fiv-io.c +++ b/fiv-io.c @@ -2703,13 +2703,77 @@ fiv_io_open_from_data( return surface; } +// --- Thumbnail passing utilities --------------------------------------------- + +typedef struct { + int width, height, stride, format; +} CairoHeader; + +void +fiv_io_serialize_to_stdout(cairo_surface_t *surface) +{ + if (!surface || cairo_surface_get_type(surface) != CAIRO_SURFACE_TYPE_IMAGE) + return; + +#ifdef G_OS_UNIX + // Common courtesy, this is never what the user wants. + if (isatty(fileno(stdout))) + return; +#endif + + CairoHeader h = { + .width = cairo_image_surface_get_width(surface), + .height = cairo_image_surface_get_height(surface), + .stride = cairo_image_surface_get_stride(surface), + .format = cairo_image_surface_get_format(surface), + }; + + // Cairo lets pixman initialize image surfaces. + // pixman allocates stride * height, not omitting those trailing bytes. + const unsigned char *data = cairo_image_surface_get_data(surface); + if (fwrite(&h, sizeof h, 1, stdout) == 1) + fwrite(data, 1, h.stride * h.height, stdout); +} + +cairo_surface_t * +fiv_io_deserialize(GBytes *bytes) +{ + CairoHeader h = {}; + GByteArray *array = g_bytes_unref_to_array(bytes); + if (array->len < sizeof h) { + g_byte_array_unref(array); + return NULL; + } + + h = *(CairoHeader *) array->data; + if (h.width < 1 || h.height < 1 || h.stride < h.width || + G_MAXSIZE / (gsize) h.stride < (gsize) h.height || + array->len - sizeof h < (gsize) h.stride * (gsize) h.height) { + g_byte_array_unref(array); + return NULL; + } + + cairo_surface_t *surface = cairo_image_surface_create_for_data( + array->data + sizeof h, h.format, h.width, h.height, h.stride); + if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { + cairo_surface_destroy(surface); + g_byte_array_unref(array); + return NULL; + } + + static cairo_user_data_key_t key; + cairo_surface_set_user_data( + surface, &key, array, (cairo_destroy_func_t) g_byte_array_unref); + return surface; +} + // --- Filesystem -------------------------------------------------------------- #include "xdg.h" #include -typedef struct _ModelEntry { +typedef struct { gchar *uri; ///< GIO URI gchar *collate_key; ///< Collate key for the filename gint64 mtime_msec; ///< Modification time in milliseconds diff --git a/fiv-io.h b/fiv-io.h index 2423618..8586065 100644 --- a/fiv-io.h +++ b/fiv-io.h @@ -96,6 +96,11 @@ cairo_surface_t *fiv_io_open(const FivIoOpenContext *ctx, GError **error); cairo_surface_t *fiv_io_open_from_data( const char *data, size_t len, const FivIoOpenContext *ctx, GError **error); +// --- Thumbnail passing utilities --------------------------------------------- + +void fiv_io_serialize_to_stdout(cairo_surface_t *surface); +cairo_surface_t *fiv_io_deserialize(GBytes *bytes); + // --- Filesystem -------------------------------------------------------------- typedef enum _FivIoModelSort { diff --git a/fiv-thumbnail.c b/fiv-thumbnail.c index a57d016..328b105 100644 --- a/fiv-thumbnail.c +++ b/fiv-thumbnail.c @@ -230,7 +230,8 @@ save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum) } gboolean -fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error) +fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, + cairo_surface_t **max_size_surface, GError **error) { g_return_val_if_fail(max_size >= FIV_THUMBNAIL_SIZE_MIN && max_size <= FIV_THUMBNAIL_SIZE_MAX, FALSE); @@ -306,8 +307,12 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error) gchar *path = g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_dir, fiv_thumbnail_sizes[use].thumbnail_spec_name, sum); save_thumbnail(scaled, path, thum); - cairo_surface_destroy(scaled); g_free(path); + + if (!*max_size_surface) + *max_size_surface = scaled; + else + cairo_surface_destroy(scaled); } g_string_free(thum, TRUE); diff --git a/fiv-thumbnail.h b/fiv-thumbnail.h index 8d7dfa0..101e8a8 100644 --- a/fiv-thumbnail.h +++ b/fiv-thumbnail.h @@ -56,8 +56,9 @@ extern cairo_user_data_key_t fiv_thumbnail_key_lq; gchar *fiv_thumbnail_get_root(void); /// Generates wide thumbnails of up to the specified size, saves them in cache. -gboolean fiv_thumbnail_produce( - GFile *target, FivThumbnailSize max_size, GError **error); +/// Returns the surface used for the maximum size (if the pointer was NULL). +gboolean fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, + cairo_surface_t **max_size_surface, GError **error); /// Retrieves a thumbnail of the most appropriate quality and resolution /// for the target file. diff --git a/fiv.c b/fiv.c index 6930479..03db9f7 100644 --- a/fiv.c +++ b/fiv.c @@ -1822,9 +1822,14 @@ main(int argc, char *argv[]) exit_fatal("unknown thumbnail size: %s", thumbnail_size); GFile *target = g_file_new_for_commandline_arg(path_arg); - if (!fiv_thumbnail_produce(target, size, &error)) + cairo_surface_t *surface = NULL; + if (!fiv_thumbnail_produce(target, size, &surface, &error)) exit_fatal("%s", error->message); g_object_unref(target); + if (surface) { + fiv_io_serialize_to_stdout(surface); + cairo_surface_destroy(surface); + } return 0; }