From b78010ccb1e6663ced40d212c3239b07b0065365 Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Sat, 18 Dec 2021 06:38:30 +0100 Subject: Adopt shorter identifiers Also, slightly reformat the source code according to clang-format. --- README.adoc | 2 +- fastiv-browser.c | 1069 --------------------- fastiv-browser.h | 28 - fastiv-io-benchmark.c | 66 -- fastiv-io.c | 2499 ------------------------------------------------- fastiv-io.h | 122 --- fastiv-sidebar.c | 433 --------- fastiv-sidebar.h | 28 - fastiv-view.c | 923 ------------------ fastiv-view.h | 52 - fastiv.c | 108 ++- fiv-browser.c | 1064 +++++++++++++++++++++ fiv-browser.h | 28 + fiv-io-benchmark.c | 66 ++ fiv-io.c | 2491 ++++++++++++++++++++++++++++++++++++++++++++++++ fiv-io.h | 122 +++ fiv-sidebar.c | 433 +++++++++ fiv-sidebar.h | 27 + fiv-view.c | 918 ++++++++++++++++++ fiv-view.h | 52 + meson.build | 7 +- 21 files changed, 5259 insertions(+), 5279 deletions(-) delete mode 100644 fastiv-browser.c delete mode 100644 fastiv-browser.h delete mode 100644 fastiv-io-benchmark.c delete mode 100644 fastiv-io.c delete mode 100644 fastiv-io.h delete mode 100644 fastiv-sidebar.c delete mode 100644 fastiv-sidebar.h delete mode 100644 fastiv-view.c delete mode 100644 fastiv-view.h create mode 100644 fiv-browser.c create mode 100644 fiv-browser.h create mode 100644 fiv-io-benchmark.c create mode 100644 fiv-io.c create mode 100644 fiv-io.h create mode 100644 fiv-sidebar.c create mode 100644 fiv-sidebar.h create mode 100644 fiv-view.c create mode 100644 fiv-view.h diff --git a/README.adoc b/README.adoc index ceab31c..2f4f353 100644 --- a/README.adoc +++ b/README.adoc @@ -44,7 +44,7 @@ The standard means to adjust the looks of the program is through GTK+ 3 CSS. As an example, to tightly pack browser items, put the following in your _~/.config/gtk-3.0/gtk.css_: - fastiv-browser { -FastivBrowser-spacing: 0; padding: 0; border: 0; margin: 0; } + fiv-browser { -FivBrowser-spacing: 0; padding: 0; border: 0; margin: 0; } The GTK+ inspector will be very helpful, should you want to experiment. diff --git a/fastiv-browser.c b/fastiv-browser.c deleted file mode 100644 index 78607ed..0000000 --- a/fastiv-browser.c +++ /dev/null @@ -1,1069 +0,0 @@ -// -// fastiv-browser.c: fast image viewer - filesystem browser widget -// -// Copyright (c) 2021, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -// - -#include -#include - -#include "fastiv-browser.h" -#include "fastiv-io.h" -#include "fastiv-view.h" - -// --- Widget ------------------------------------------------------------------ -// _________________________________ -// │ p a d d i n g -// │ p ╭───────────────────╮ s ╭┄┄┄┄┄ -// │ a │ glow border ┊ │ p ┊ -// │ d │ ┄ ╔═══════════╗ ┄ │ a ┊ -// │ d │ ║ thumbnail ║ │ c ┊ ... -// │ i │ ┄ ╚═══════════╝ ┄ │ i ┊ -// │ n │ ┊ glow border │ n ┊ -// │ g ╰───────────────────╯ g ╰┄┄┄┄┄ -// │ s p a c i n g -// │ ╭┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄ -// -// The glow is actually a glowing margin, the border is rendered in two parts. -// - -struct _FastivBrowser { - GtkWidget parent_instance; - - FastivIoThumbnailSize item_size; ///< Thumbnail size - int item_height; ///< Thumbnail height in pixels - int item_spacing; ///< Space between items in pixels - - GArray *entries; ///< [Entry] - GArray *layouted_rows; ///< [Row] - int selected; - - GdkCursor *pointer; ///< Cached pointer cursor - cairo_surface_t *glow; ///< CAIRO_FORMAT_A8 mask - int item_border_x; ///< L/R .item margin + border - int item_border_y; ///< T/B .item margin + border -}; - -typedef struct entry Entry; -typedef struct item Item; -typedef struct row Row; - -static const double g_permitted_width_multiplier = 2; - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -struct entry { - char *uri; ///< GIO URI - cairo_surface_t *thumbnail; ///< Prescaled thumbnail - GIcon *icon; ///< If no thumbnail, use this icon -}; - -static void -entry_free(Entry *self) -{ - g_free(self->uri); - g_clear_pointer(&self->thumbnail, cairo_surface_destroy); - g_clear_object(&self->icon); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -struct item { - const Entry *entry; - int x_offset; ///< Offset within the row -}; - -struct row { - Item *items; ///< Ends with a NULL entry - int x_offset; ///< Start position outside borders - int y_offset; ///< Start position inside borders -}; - -static void -row_free(Row *self) -{ - g_free(self->items); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -append_row(FastivBrowser *self, int *y, int x, GArray *items_array) -{ - if (self->layouted_rows->len) - *y += self->item_spacing; - - *y += self->item_border_y; - g_array_append_val(self->layouted_rows, ((Row) { - .items = g_array_steal(items_array, NULL), - .x_offset = x, - .y_offset = *y, - })); - - // Not trying to pack them vertically, but this would be the place to do it. - *y += self->item_height; - *y += self->item_border_y; -} - -static int -relayout(FastivBrowser *self, int width) -{ - GtkWidget *widget = GTK_WIDGET(self); - GtkStyleContext *style = gtk_widget_get_style_context(widget); - - GtkBorder padding = {}; - gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding); - int available_width = width - padding.left - padding.right; - - g_array_set_size(self->layouted_rows, 0); - - GArray *items = g_array_new(TRUE, TRUE, sizeof(Item)); - int x = 0, y = padding.top; - for (guint i = 0; i < self->entries->len; i++) { - const Entry *entry = &g_array_index(self->entries, Entry, i); - if (!entry->thumbnail) - continue; - - int width = cairo_image_surface_get_width(entry->thumbnail) + - 2 * self->item_border_x; - if (!items->len) { - // Just insert it, whether or not there's any space. - } else if (x + self->item_spacing + width <= available_width) { - x += self->item_spacing; - } else { - append_row(self, &y, - padding.left + MAX(0, available_width - x) / 2, items); - x = 0; - } - - g_array_append_val(items, - ((Item) {.entry = entry, .x_offset = x + self->item_border_x})); - x += width; - } - if (items->len) { - append_row(self, &y, - padding.left + MAX(0, available_width - x) / 2, items); - } - - g_array_free(items, TRUE); - return y + padding.bottom; -} - -static void -draw_outer_border(FastivBrowser *self, cairo_t *cr, int width, int height) -{ - int offset_x = cairo_image_surface_get_width(self->glow); - int offset_y = cairo_image_surface_get_height(self->glow); - cairo_pattern_t *mask = cairo_pattern_create_for_surface(self->glow); - cairo_matrix_t matrix; - - cairo_pattern_set_extend(mask, CAIRO_EXTEND_PAD); - cairo_save(cr); - cairo_translate(cr, -offset_x, -offset_y); - cairo_rectangle(cr, 0, 0, offset_x + width, offset_y + height); - cairo_clip(cr); - cairo_mask(cr, mask); - cairo_restore(cr); - cairo_save(cr); - cairo_translate(cr, width + offset_x, height + offset_y); - cairo_rectangle(cr, 0, 0, -offset_x - width, -offset_y - height); - cairo_clip(cr); - cairo_scale(cr, -1, -1); - cairo_mask(cr, mask); - cairo_restore(cr); - - cairo_pattern_set_extend(mask, CAIRO_EXTEND_NONE); - cairo_matrix_init_scale(&matrix, -1, 1); - cairo_matrix_translate(&matrix, -width - offset_x, offset_y); - cairo_pattern_set_matrix(mask, &matrix); - cairo_mask(cr, mask); - cairo_matrix_init_scale(&matrix, 1, -1); - cairo_matrix_translate(&matrix, offset_x, -height - offset_y); - cairo_pattern_set_matrix(mask, &matrix); - cairo_mask(cr, mask); - - cairo_pattern_destroy(mask); -} - -static GdkRectangle -item_extents(FastivBrowser *self, const Item *item, const Row *row) -{ - int width = cairo_image_surface_get_width(item->entry->thumbnail); - int height = cairo_image_surface_get_height(item->entry->thumbnail); - return (GdkRectangle) { - .x = row->x_offset + item->x_offset, - .y = row->y_offset + self->item_height - height, - .width = width, - .height = height, - }; -} - -static const Entry * -entry_at(FastivBrowser *self, int x, int y) -{ - for (guint i = 0; i < self->layouted_rows->len; i++) { - const Row *row = &g_array_index(self->layouted_rows, Row, i); - for (Item *item = row->items; item->entry; item++) { - GdkRectangle extents = item_extents(self, item, row); - if (x >= extents.x && - y >= extents.y && - x <= extents.x + extents.width && - y <= extents.y + extents.height) - return item->entry; - } - } - return NULL; -} - -static void -draw_row(FastivBrowser *self, cairo_t *cr, const Row *row) -{ - GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(self)); - gtk_style_context_save(style); - gtk_style_context_add_class(style, "item"); - - GdkRGBA glow_color = {}; - GtkStateFlags state = gtk_style_context_get_state (style); - gtk_style_context_get_color(style, state, &glow_color); - - GtkBorder border; - gtk_style_context_get_border(style, state, &border); - for (Item *item = row->items; item->entry; item++) { - cairo_save(cr); - GdkRectangle extents = item_extents(self, item, row); - cairo_translate(cr, extents.x - border.left, extents.y - border.top); - - gtk_style_context_save(style); - if (item->entry->icon) { - gtk_style_context_add_class(style, "symbolic"); - } else { - gdk_cairo_set_source_rgba(cr, &glow_color); - draw_outer_border(self, cr, - border.left + extents.width + border.right, - border.top + extents.height + border.bottom); - } - - gtk_render_background( - style, cr, border.left, border.top, extents.width, extents.height); - - gtk_render_frame(style, cr, 0, 0, - border.left + extents.width + border.right, - border.top + extents.height + border.bottom); - - if (item->entry->icon) { - GdkRGBA color = {}; - gtk_style_context_get_color(style, state, &color); - gdk_cairo_set_source_rgba(cr, &color); - cairo_mask_surface( - cr, item->entry->thumbnail, border.left, border.top); - } else { - cairo_set_source_surface( - cr, item->entry->thumbnail, border.left, border.top); - cairo_paint(cr); - } - - cairo_restore(cr); - gtk_style_context_restore(style); - } - gtk_style_context_restore(style); -} - -// --- Thumbnails -------------------------------------------------------------- - -// NOTE: "It is important to note that when an image with an alpha channel is -// scaled, linear encoded, pre-multiplied component values must be used!" -static cairo_surface_t * -rescale_thumbnail(cairo_surface_t *thumbnail, double row_height) -{ - if (!thumbnail) - return thumbnail; - - int width = cairo_image_surface_get_width(thumbnail); - int height = cairo_image_surface_get_height(thumbnail); - - double scale_x = 1; - double scale_y = 1; - if (width > g_permitted_width_multiplier * height) { - scale_x = g_permitted_width_multiplier * row_height / width; - scale_y = round(scale_x * height) / height; - } else { - scale_y = row_height / height; - scale_x = round(scale_y * width) / width; - } - if (scale_x == 1 && scale_y == 1) - return thumbnail; - - int projected_width = round(scale_x * width); - int projected_height = round(scale_y * height); - cairo_surface_t *scaled = cairo_image_surface_create( - CAIRO_FORMAT_ARGB32, projected_width, projected_height); - - // pixman can take gamma into account when scaling, unlike Cairo. - struct pixman_f_transform xform_floating; - struct pixman_transform xform; - - // PIXMAN_a8r8g8b8_sRGB can be used for gamma-correct results, - // but it's an incredibly slow transformation - pixman_format_code_t format = PIXMAN_a8r8g8b8; - - pixman_image_t *src = pixman_image_create_bits(format, width, height, - (uint32_t *) cairo_image_surface_get_data(thumbnail), - cairo_image_surface_get_stride(thumbnail)); - pixman_image_t *dest = pixman_image_create_bits(format, - cairo_image_surface_get_width(scaled), - cairo_image_surface_get_height(scaled), - (uint32_t *) cairo_image_surface_get_data(scaled), - cairo_image_surface_get_stride(scaled)); - - pixman_f_transform_init_scale(&xform_floating, scale_x, scale_y); - pixman_f_transform_invert(&xform_floating, &xform_floating); - pixman_transform_from_pixman_f_transform(&xform, &xform_floating); - pixman_image_set_transform(src, &xform); - pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); - pixman_image_set_repeat(src, PIXMAN_REPEAT_PAD); - - pixman_image_composite(PIXMAN_OP_SRC, src, NULL, dest, 0, 0, 0, 0, 0, 0, - projected_width, projected_height); - pixman_image_unref(src); - pixman_image_unref(dest); - - cairo_surface_destroy(thumbnail); - cairo_surface_mark_dirty(scaled); - return scaled; -} - -static void -entry_add_thumbnail(gpointer data, gpointer user_data) -{ - Entry *self = data; - g_clear_object(&self->icon); - g_clear_pointer(&self->thumbnail, cairo_surface_destroy); - - FastivBrowser *browser = FASTIV_BROWSER(user_data); - GFile *file = g_file_new_for_uri(self->uri); - self->thumbnail = rescale_thumbnail( - fastiv_io_lookup_thumbnail(file, browser->item_size), - browser->item_height); - if (self->thumbnail) - goto out; - - // Fall back to symbolic icons, though there's only so much we can do - // in parallel--GTK+ isn't thread-safe. - GFileInfo *info = g_file_query_info(file, - G_FILE_ATTRIBUTE_STANDARD_NAME - "," G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - if (info) { - GIcon *icon = g_file_info_get_symbolic_icon(info); - if (icon) - self->icon = g_object_ref(icon); - g_object_unref(info); - } -out: - g_object_unref(file); -} - -static void -materialize_icon(FastivBrowser *self, Entry *entry) -{ - if (!entry->icon) - return; - - // Fucker will still give us non-symbolic icons, no more playing nice. - // TODO(p): Investigate a bit closer. We may want to abandon the idea - // of using GLib to look up icons for us, derive a list from a guessed - // MIME type, with "-symbolic" prefixes and fallbacks, - // and use gtk_icon_theme_choose_icon() instead. - // TODO(p): Make sure we have /some/ icon for every entry. - // TODO(p): We might want to populate these on an as-needed basis. - GtkIconInfo *icon_info = gtk_icon_theme_lookup_by_gicon( - gtk_icon_theme_get_default(), entry->icon, self->item_height / 2, - GTK_ICON_LOOKUP_FORCE_SYMBOLIC); - if (!icon_info) - return; - - // Bílá, bílá, bílá, bílá... komu by se nelíbí-lá... - // We do not want any highlights, nor do we want to remember the style. - const GdkRGBA white = {1, 1, 1, 1}; - GdkPixbuf *pixbuf = gtk_icon_info_load_symbolic( - icon_info, &white, &white, &white, &white, NULL, NULL); - if (pixbuf) { - int outer_size = self->item_height; - entry->thumbnail = - cairo_image_surface_create(CAIRO_FORMAT_A8, outer_size, outer_size); - - // "Note that the resulting pixbuf may not be exactly this size;" - // though GTK_ICON_LOOKUP_FORCE_SIZE is also an option. - int x = (outer_size - gdk_pixbuf_get_width(pixbuf)) / 2; - int y = (outer_size - gdk_pixbuf_get_height(pixbuf)) / 2; - - cairo_t *cr = cairo_create(entry->thumbnail); - gdk_cairo_set_source_pixbuf(cr, pixbuf, x, y); - cairo_paint(cr); - cairo_destroy(cr); - - g_object_unref(pixbuf); - } - g_object_unref(icon_info); -} - -static void -reload_thumbnails(FastivBrowser *self) -{ - GThreadPool *pool = g_thread_pool_new( - entry_add_thumbnail, self, g_get_num_processors(), FALSE, NULL); - for (guint i = 0; i < self->entries->len; i++) - g_thread_pool_push(pool, &g_array_index(self->entries, Entry, i), NULL); - g_thread_pool_free(pool, FALSE, TRUE); - - for (guint i = 0; i < self->entries->len; i++) - materialize_icon(self, &g_array_index(self->entries, Entry, i)); - - gtk_widget_queue_resize(GTK_WIDGET(self)); -} - -// --- Context menu------------------------------------------------------------- - -typedef struct _OpenContext { - GWeakRef widget; - GFile *file; - char *content_type; - GAppInfo *app_info; -} OpenContext; - -static void -open_context_notify(gpointer data, G_GNUC_UNUSED GClosure *closure) -{ - OpenContext *self = data; - g_weak_ref_clear(&self->widget); - g_clear_object(&self->app_info); - g_clear_object(&self->file); - g_free(self->content_type); - g_free(self); -} - -static void -open_context_launch(GtkWidget *widget, OpenContext *self) -{ - GdkAppLaunchContext *context = - gdk_display_get_app_launch_context(gtk_widget_get_display(widget)); - gdk_app_launch_context_set_screen(context, gtk_widget_get_screen(widget)); - gdk_app_launch_context_set_timestamp(context, gtk_get_current_event_time()); - - // TODO(p): Display errors. - GList *files = g_list_append(NULL, self->file); - if (g_app_info_launch( - self->app_info, files, G_APP_LAUNCH_CONTEXT(context), NULL)) { - g_app_info_set_as_last_used_for_type( - self->app_info, self->content_type, NULL); - } - g_list_free(files); - g_object_unref(context); -} - -static void -append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template) -{ - OpenContext *ctx = g_malloc0(sizeof *ctx); - g_weak_ref_init(&ctx->widget, NULL); - ctx->file = g_object_ref(template->file); - ctx->content_type = g_strdup(template->content_type); - ctx->app_info = opener; - - // It's documented that we can touch the child, if we want formatting: - // https://docs.gtk.org/gtk3/class.MenuItem.html - // XXX: Would g_app_info_get_display_name() be any better? - gchar *name = g_strdup_printf("Open With %s", g_app_info_get_name(opener)); - GtkWidget *item = gtk_menu_item_new_with_label(name); - g_free(name); - g_signal_connect_data(item, "activate", G_CALLBACK(open_context_launch), - ctx, open_context_notify, 0); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); -} - -static void -on_chooser_activate(GtkMenuItem *item, gpointer user_data) -{ - OpenContext *ctx = user_data; - GtkWindow *window = NULL; - GtkWidget *widget = g_weak_ref_get(&ctx->widget); - if (widget) { - if (GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget)))) - window = GTK_WINDOW(widget); - } - - GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window, - GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type); - if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) { - ctx->app_info = gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(dialog)); - open_context_launch(GTK_WIDGET(item), ctx); - } - gtk_widget_destroy(dialog); -} - -static gboolean -destroy_widget_idle_source_func(GtkWidget *widget) -{ - // The whole menu is deactivated /before/ any item is activated, - // and a destroyed child item will not activate. - gtk_widget_destroy(widget); - return FALSE; -} - -static void -show_context_menu(GtkWidget *widget, const char *uri) -{ - GFile *file = g_file_new_for_uri(uri); - GFileInfo *info = g_file_query_info(file, - G_FILE_ATTRIBUTE_STANDARD_NAME - "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - if (!info) { - g_object_unref(file); - return; - } - - // This will have no application pre-assigned, for use with GTK+'s dialog. - OpenContext *ctx = g_malloc0(sizeof *ctx); - g_weak_ref_init(&ctx->widget, widget); - ctx->file = file; - ctx->content_type = g_strdup(g_file_info_get_content_type(info)); - g_object_unref(info); - - GAppInfo *default_ = - g_app_info_get_default_for_type(ctx->content_type, FALSE); - GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type); - GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type); - - GtkWidget *menu = gtk_menu_new(); - if (default_) { - append_opener(menu, default_, ctx); - gtk_menu_shell_append( - GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - } - - for (GList *iter = recommended; iter; iter = iter->next) { - if (g_app_info_should_show(iter->data) && - (!default_ || !g_app_info_equal(iter->data, default_))) - append_opener(menu, iter->data, ctx); - else - g_object_unref(iter->data); - } - if (recommended) { - g_list_free(recommended); - gtk_menu_shell_append( - GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - } - - for (GList *iter = fallback; iter; iter = iter->next) { - if (g_app_info_should_show(iter->data) && - (!default_ || !g_app_info_equal(iter->data, default_))) - append_opener(menu, iter->data, ctx); - else - g_object_unref(iter->data); - } - if (fallback) { - g_list_free(fallback); - gtk_menu_shell_append( - GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - } - - GtkWidget *item = gtk_menu_item_new_with_label("Open With..."); - g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate), - ctx, open_context_notify, 0); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); - - // As per GTK+ 3 Common Questions, 1.5. - g_object_ref_sink(menu); - g_signal_connect_swapped(menu, "deactivate", - G_CALLBACK(g_idle_add), destroy_widget_idle_source_func); - g_signal_connect(menu, "destroy", G_CALLBACK(g_object_unref), NULL); - - gtk_widget_show_all(menu); - gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL); -} - -// --- Boilerplate ------------------------------------------------------------- - -// TODO(p): For proper navigation, we need to implement GtkScrollable. -G_DEFINE_TYPE_EXTENDED(FastivBrowser, fastiv_browser, GTK_TYPE_WIDGET, 0, - /* G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE, - fastiv_browser_scrollable_init) */) - -enum { - PROP_THUMBNAIL_SIZE = 1, - N_PROPERTIES -}; - -static GParamSpec *browser_properties[N_PROPERTIES]; - -enum { - ITEM_ACTIVATED, - LAST_SIGNAL, -}; - -// Globals are, sadly, the canonical way of storing signal numbers. -static guint browser_signals[LAST_SIGNAL]; - -static void -fastiv_browser_finalize(GObject *gobject) -{ - FastivBrowser *self = FASTIV_BROWSER(gobject); - g_array_free(self->entries, TRUE); - g_array_free(self->layouted_rows, TRUE); - cairo_surface_destroy(self->glow); - g_clear_object(&self->pointer); - - G_OBJECT_CLASS(fastiv_browser_parent_class)->finalize(gobject); -} - -static void -fastiv_browser_get_property( - GObject *object, guint property_id, GValue *value, GParamSpec *pspec) -{ - FastivBrowser *self = FASTIV_BROWSER(object); - switch (property_id) { - case PROP_THUMBNAIL_SIZE: - g_value_set_enum(value, self->item_size); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); - } -} - -static void -set_item_size(FastivBrowser *self, FastivIoThumbnailSize size) -{ - if (size < FASTIV_IO_THUMBNAIL_SIZE_MIN || - size > FASTIV_IO_THUMBNAIL_SIZE_MAX) - return; - - if (size != self->item_size) { - self->item_size = size; - self->item_height = fastiv_io_thumbnail_sizes[self->item_size].size; - reload_thumbnails(self); - - g_object_notify_by_pspec( - G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]); - } -} - -static void -fastiv_browser_set_property( - GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) -{ - FastivBrowser *self = FASTIV_BROWSER(object); - switch (property_id) { - case PROP_THUMBNAIL_SIZE: - set_item_size(self, g_value_get_enum(value)); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); - } -} - -static GtkSizeRequestMode -fastiv_browser_get_request_mode(G_GNUC_UNUSED GtkWidget *widget) -{ - return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; -} - -static void -fastiv_browser_get_preferred_width( - GtkWidget *widget, gint *minimum, gint *natural) -{ - FastivBrowser *self = FASTIV_BROWSER(widget); - GtkStyleContext *style = gtk_widget_get_style_context(widget); - - GtkBorder padding = {}; - gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding); - *minimum = *natural = - g_permitted_width_multiplier * self->item_height + - padding.left + 2 * self->item_border_x + padding.right; -} - -static void -fastiv_browser_get_preferred_height_for_width( - GtkWidget *widget, gint width, gint *minimum, gint *natural) -{ - // XXX: This is rather ugly, the caller is only asking. - *minimum = *natural = relayout(FASTIV_BROWSER(widget), width); -} - -static void -fastiv_browser_realize(GtkWidget *widget) -{ - GtkAllocation allocation; - gtk_widget_get_allocation(widget, &allocation); - - GdkWindowAttr attributes = { - .window_type = GDK_WINDOW_CHILD, - .x = allocation.x, - .y = allocation.y, - .width = allocation.width, - .height = allocation.height, - - // Input-only would presumably also work (as in GtkPathBar, e.g.), - // but it merely seems to involve more work. - .wclass = GDK_INPUT_OUTPUT, - - .visual = gtk_widget_get_visual(widget), - .event_mask = gtk_widget_get_events(widget) | GDK_KEY_PRESS_MASK | - GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | - GDK_SCROLL_MASK, - }; - - // We need this window to receive input events at all. - // TODO(p): See if input events bubble up to parents. - GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget), - &attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); - gtk_widget_register_window(widget, window); - gtk_widget_set_window(widget, window); - gtk_widget_set_realized(widget, TRUE); - - FastivBrowser *self = FASTIV_BROWSER(widget); - g_clear_object(&self->pointer); - self->pointer = - gdk_cursor_new_from_name(gdk_window_get_display(window), "pointer"); -} - -static void -fastiv_browser_size_allocate(GtkWidget *widget, GtkAllocation *allocation) -{ - GTK_WIDGET_CLASS(fastiv_browser_parent_class) - ->size_allocate(widget, allocation); - - relayout(FASTIV_BROWSER(widget), allocation->width); -} - -static gboolean -fastiv_browser_draw(GtkWidget *widget, cairo_t *cr) -{ - FastivBrowser *self = FASTIV_BROWSER(widget); - if (!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget))) - return TRUE; - - GtkAllocation allocation; - gtk_widget_get_allocation(widget, &allocation); - gtk_render_background(gtk_widget_get_style_context(widget), cr, 0, 0, - allocation.width, allocation.height); - - GdkRectangle clip = {}; - gboolean have_clip = gdk_cairo_get_clip_rectangle(cr, &clip); - - for (guint i = 0; i < self->layouted_rows->len; i++) { - const Row *row = &g_array_index(self->layouted_rows, Row, i); - GdkRectangle extents = { - .x = 0, - .y = row->y_offset - self->item_border_y, - .width = allocation.width, - .height = self->item_height + 2 * self->item_border_y, - }; - if (!have_clip || gdk_rectangle_intersect(&clip, &extents, NULL)) - draw_row(self, cr, row); - } - return TRUE; -} - -static gboolean -open_entry(GtkWidget *self, const Entry *entry, gboolean new_window) -{ - GFile *location = g_file_new_for_uri(entry->uri); - g_signal_emit(self, browser_signals[ITEM_ACTIVATED], 0, location, - new_window ? GTK_PLACES_OPEN_NEW_WINDOW : GTK_PLACES_OPEN_NORMAL); - g_object_unref(location); - return TRUE; -} - -static gboolean -fastiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event) -{ - GTK_WIDGET_CLASS(fastiv_browser_parent_class) - ->button_press_event(widget, event); - - FastivBrowser *self = FASTIV_BROWSER(widget); - if (event->type != GDK_BUTTON_PRESS) - return FALSE; - - guint state = event->state & gtk_accelerator_get_default_mod_mask(); - if (event->button == GDK_BUTTON_PRIMARY && state == 0 && - gtk_widget_get_focus_on_click(widget)) - gtk_widget_grab_focus(widget); - - const Entry *entry = entry_at(self, event->x, event->y); - if (!entry) - return FALSE; - - switch (event->button) { - case GDK_BUTTON_PRIMARY: - if (state == 0) - return open_entry(widget, entry, FALSE); - if (state == GDK_CONTROL_MASK) - return open_entry(widget, entry, TRUE); - return FALSE; - case GDK_BUTTON_MIDDLE: - if (state == 0) - return open_entry(widget, entry, TRUE); - return FALSE; - case GDK_BUTTON_SECONDARY: - // On X11, after closing the menu, the pointer otherwise remains, - // no matter what its new location is. - gdk_window_set_cursor(gtk_widget_get_window(widget), NULL); - show_context_menu(widget, entry->uri); - return TRUE; - default: - return FALSE; - } -} - -gboolean -fastiv_browser_motion_notify_event(GtkWidget *widget, GdkEventMotion *event) -{ - GTK_WIDGET_CLASS(fastiv_browser_parent_class) - ->motion_notify_event(widget, event); - - FastivBrowser *self = FASTIV_BROWSER(widget); - if (event->state != 0) - return FALSE; - - const Entry *entry = entry_at(self, event->x, event->y); - GdkWindow *window = gtk_widget_get_window(widget); - gdk_window_set_cursor(window, entry ? self->pointer : NULL); - return TRUE; -} - -static gboolean -fastiv_browser_scroll_event(GtkWidget *widget, GdkEventScroll *event) -{ - FastivBrowser *self = FASTIV_BROWSER(widget); - if ((event->state & gtk_accelerator_get_default_mod_mask()) != - GDK_CONTROL_MASK) - return FALSE; - - switch (event->direction) { - case GDK_SCROLL_UP: - set_item_size(self, self->item_size + 1); - return TRUE; - case GDK_SCROLL_DOWN: - set_item_size(self, self->item_size - 1); - return TRUE; - default: - // For some reason, we can also get GDK_SCROLL_SMOOTH. - // Left/right are good to steal from GtkScrolledWindow for consistency. - return TRUE; - } -} - -static gboolean -fastiv_browser_query_tooltip(GtkWidget *widget, gint x, gint y, - G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip) -{ - FastivBrowser *self = FASTIV_BROWSER(widget); - const Entry *entry = entry_at(self, x, y); - if (!entry) - return FALSE; - - GFile *file = g_file_new_for_uri(entry->uri); - GFileInfo *info = g_file_query_info(file, - G_FILE_ATTRIBUTE_STANDARD_NAME - "," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - g_object_unref(file); - if (!info) - return FALSE; - - gtk_tooltip_set_text(tooltip, g_file_info_get_display_name(info)); - g_object_unref(info); - return TRUE; -} - -static void -fastiv_browser_style_updated(GtkWidget *widget) -{ - GTK_WIDGET_CLASS(fastiv_browser_parent_class)->style_updated(widget); - - FastivBrowser *self = FASTIV_BROWSER(widget); - GtkStyleContext *style = gtk_widget_get_style_context(widget); - GtkBorder border = {}, margin = {}; - - int item_spacing = self->item_spacing; - gtk_widget_style_get(widget, "spacing", &self->item_spacing, NULL); - if (item_spacing != self->item_spacing) - gtk_widget_queue_resize(widget); - - // Using a pseudo-class, because GTK+ regions are deprecated. - gtk_style_context_save(style); - gtk_style_context_add_class(style, "item"); - gtk_style_context_get_margin(style, GTK_STATE_FLAG_NORMAL, &margin); - gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL, &border); - gtk_style_context_restore(style); - - const int glow_w = (margin.left + margin.right) / 2; - const int glow_h = (margin.top + margin.bottom) / 2; - - // Don't set different opposing sides, it will misrender, your problem. - // When the style of the class changes, this virtual method isn't invoked, - // so the update check is mildly pointless. - int item_border_x = glow_w + (border.left + border.right) / 2; - int item_border_y = glow_h + (border.top + border.bottom) / 2; - if (item_border_x != self->item_border_x || - item_border_y != self->item_border_y) { - self->item_border_x = item_border_x; - self->item_border_y = item_border_y; - gtk_widget_queue_resize(widget); - } - - if (self->glow) - cairo_surface_destroy(self->glow); - if (glow_w <= 0 || glow_h <= 0) { - self->glow = cairo_image_surface_create(CAIRO_FORMAT_A1, 0, 0); - return; - } - - self->glow = - cairo_image_surface_create(CAIRO_FORMAT_A8, glow_w, glow_h); - unsigned char *data = cairo_image_surface_get_data(self->glow); - int stride = cairo_image_surface_get_stride(self->glow); - - // Smooth out the curve, so that the edge of the glow isn't too jarring. - const double fade_factor = 1.5; - - const int x_max = glow_w - 1; - const int y_max = glow_h - 1; - const double x_scale = 1. / MAX(1, x_max); - const double y_scale = 1. / MAX(1, y_max); - for (int y = 0; y <= y_max; y++) - for (int x = 0; x <= x_max; x++) { - const double xn = x_scale * (x_max - x); - const double yn = y_scale * (y_max - y); - double v = MIN(sqrt(xn * xn + yn * yn), 1); - data[y * stride + x] = round(pow(1 - v, fade_factor) * 255); - } - cairo_surface_mark_dirty(self->glow); -} - -static void -fastiv_browser_class_init(FastivBrowserClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS(klass); - object_class->finalize = fastiv_browser_finalize; - object_class->get_property = fastiv_browser_get_property; - object_class->set_property = fastiv_browser_set_property; - - browser_properties[PROP_THUMBNAIL_SIZE] = g_param_spec_enum( - "thumbnail-size", "Thumbnail size", "The thumbnail height to use", - FASTIV_TYPE_IO_THUMBNAIL_SIZE, FASTIV_IO_THUMBNAIL_SIZE_NORMAL, - G_PARAM_READWRITE); - g_object_class_install_properties( - object_class, N_PROPERTIES, browser_properties); - - browser_signals[ITEM_ACTIVATED] = g_signal_new("item-activated", - G_TYPE_FROM_CLASS(klass), 0, 0, NULL, NULL, NULL, - G_TYPE_NONE, 2, G_TYPE_FILE, GTK_TYPE_PLACES_OPEN_FLAGS); - - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); - widget_class->get_request_mode = fastiv_browser_get_request_mode; - widget_class->get_preferred_width = fastiv_browser_get_preferred_width; - widget_class->get_preferred_height_for_width = - fastiv_browser_get_preferred_height_for_width; - widget_class->realize = fastiv_browser_realize; - widget_class->draw = fastiv_browser_draw; - widget_class->size_allocate = fastiv_browser_size_allocate; - widget_class->button_press_event = fastiv_browser_button_press_event; - widget_class->motion_notify_event = fastiv_browser_motion_notify_event; - widget_class->scroll_event = fastiv_browser_scroll_event; - widget_class->query_tooltip = fastiv_browser_query_tooltip; - widget_class->style_updated = fastiv_browser_style_updated; - - // Could be split to also-idiomatic row-spacing/column-spacing properties. - // The GParamSpec is sinked by this call. - gtk_widget_class_install_style_property(widget_class, - g_param_spec_int("spacing", "Spacing", "Space between items", - 0, G_MAXINT, 1, G_PARAM_READWRITE)); - - // TODO(p): Later override "screen_changed", recreate Pango layouts there, - // if we get to have any, or otherwise reflect DPI changes. - gtk_widget_class_set_css_name(widget_class, "fastiv-browser"); -} - -static void -fastiv_browser_init(FastivBrowser *self) -{ - gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); - gtk_widget_set_has_tooltip(GTK_WIDGET(self), TRUE); - - self->entries = g_array_new(FALSE, TRUE, sizeof(Entry)); - g_array_set_clear_func(self->entries, (GDestroyNotify) entry_free); - self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row)); - g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free); - - set_item_size(self, FASTIV_IO_THUMBNAIL_SIZE_NORMAL); - self->selected = -1; - self->glow = cairo_image_surface_create(CAIRO_FORMAT_A1, 0, 0); - - g_signal_connect_swapped(gtk_settings_get_default(), - "notify::gtk-icon-theme-name", G_CALLBACK(reload_thumbnails), self); -} - -// --- Public interface -------------------------------------------------------- - -static gint -entry_compare(gconstpointer a, gconstpointer b) -{ - const Entry *entry1 = a; - const Entry *entry2 = b; - GFile *location1 = g_file_new_for_uri(entry1->uri); - GFile *location2 = g_file_new_for_uri(entry2->uri); - gint result = fastiv_io_filecmp(location1, location2); - g_object_unref(location1); - g_object_unref(location2); - return result; -} - -void -fastiv_browser_load( - FastivBrowser *self, FastivBrowserFilterCallback cb, const char *path) -{ - g_array_set_size(self->entries, 0); - g_array_set_size(self->layouted_rows, 0); - - GFile *file = g_file_new_for_path(path); - GFileEnumerator *enumerator = g_file_enumerate_children(file, - G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - g_object_unref(file); - if (!enumerator) - return; - - while (TRUE) { - GFileInfo *info = NULL; - GFile *child = NULL; - if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) || - !info) - break; - if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) - continue; - if (cb && !cb(g_file_info_get_name(info))) - continue; - - g_array_append_val(self->entries, - ((Entry) {.thumbnail = NULL, .uri = g_file_get_uri(child)})); - } - g_object_unref(enumerator); - - // TODO(p): Support being passed a sort function. - g_array_sort(self->entries, entry_compare); - - reload_thumbnails(self); -} diff --git a/fastiv-browser.h b/fastiv-browser.h deleted file mode 100644 index 66d75ee..0000000 --- a/fastiv-browser.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// fastiv-browser.h: fast image viewer - filesystem browser widget -// -// Copyright (c) 2021, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -// - -#pragma once - -#include - -#define FASTIV_TYPE_BROWSER (fastiv_browser_get_type()) -G_DECLARE_FINAL_TYPE(FastivBrowser, fastiv_browser, FASTIV, BROWSER, GtkWidget) - -typedef gboolean (*FastivBrowserFilterCallback) (const char *); - -void fastiv_browser_load( - FastivBrowser *self, FastivBrowserFilterCallback cb, const char *path); diff --git a/fastiv-io-benchmark.c b/fastiv-io-benchmark.c deleted file mode 100644 index 250b5c2..0000000 --- a/fastiv-io-benchmark.c +++ /dev/null @@ -1,66 +0,0 @@ -// -// fastiv-io-benchmark.c: see if we're worth the name -// -// Copyright (c) 2021, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -// - -#include -#include -#include - -#include "fastiv-io.h" - -static double -timestamp(void) -{ - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return ts.tv_sec + ts.tv_nsec / 1.e9; -} - -static void -one_file(const char *filename) -{ - double since_us = timestamp(); - cairo_surface_t *loaded_by_us = fastiv_io_open(filename, NULL); - if (!loaded_by_us) - return; - - cairo_surface_destroy(loaded_by_us); - double us = timestamp() - since_us; - - double since_pixbuf = timestamp(); - GdkPixbuf *gdk_pixbuf = gdk_pixbuf_new_from_file(filename, NULL); - if (!gdk_pixbuf) - return; - - cairo_surface_t *loaded_by_pixbuf = - gdk_cairo_surface_create_from_pixbuf(gdk_pixbuf, 1, NULL); - g_object_unref(gdk_pixbuf); - cairo_surface_destroy(loaded_by_pixbuf); - double pixbuf = timestamp() - since_pixbuf; - - printf("%f\t%f\t%.0f%%\t%s\n", us, pixbuf, us / pixbuf * 100, filename); -} - -int -main(int argc, char *argv[]) -{ - // Needed for gdk_cairo_surface_create_from_pixbuf(). - gdk_init(&argc, &argv); - - for (int i = 1; i < argc; i++) - one_file(argv[i]); - return 0; -} diff --git a/fastiv-io.c b/fastiv-io.c deleted file mode 100644 index e8d2588..0000000 --- a/fastiv-io.c +++ /dev/null @@ -1,2499 +0,0 @@ -// -// fastiv-io.c: image operations -// -// Copyright (c) 2021, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -// - -#include "config.h" - -#include -#include -#include -#include - -#include -#include -#ifdef HAVE_LIBRAW -#include -#endif // HAVE_LIBRAW -#ifdef HAVE_LIBRSVG -#include -#endif // HAVE_LIBRSVG -#ifdef HAVE_XCURSOR -#include -#endif // HAVE_XCURSOR -#ifdef HAVE_LIBWEBP -#include -#include -#include -#include -#endif // HAVE_LIBWEBP -#ifdef HAVE_LIBHEIF -#include -#endif // HAVE_LIBHEIF -#ifdef HAVE_LIBTIFF -#include -#include -#endif // HAVE_LIBTIFF -#ifdef HAVE_GDKPIXBUF -#include -#include -#endif // HAVE_GDKPIXBUF - -#define WUFFS_IMPLEMENTATION -#define WUFFS_CONFIG__MODULES -#define WUFFS_CONFIG__MODULE__ADLER32 -#define WUFFS_CONFIG__MODULE__BASE -#define WUFFS_CONFIG__MODULE__BMP -#define WUFFS_CONFIG__MODULE__CRC32 -#define WUFFS_CONFIG__MODULE__DEFLATE -#define WUFFS_CONFIG__MODULE__GIF -#define WUFFS_CONFIG__MODULE__LZW -#define WUFFS_CONFIG__MODULE__PNG -#define WUFFS_CONFIG__MODULE__ZLIB -#include "wuffs-mirror-release-c/release/c/wuffs-v0.3.c" - -#include "xdg.h" -#include "fastiv-io.h" - -#if CAIRO_VERSION >= 11702 && X11_ACTUALLY_SUPPORTS_RGBA128F_OR_WE_USE_OPENGL -#define FASTIV_CAIRO_RGBA128F -#endif - -// A subset of shared-mime-info that produces an appropriate list of -// file extensions. Chiefly motivated by the suckiness of raw photo formats: -// someone else will maintain the list of file extensions for us. -const char *fastiv_io_supported_media_types[] = { - "image/bmp", - "image/gif", - "image/png", - "image/jpeg", -#ifdef HAVE_LIBRAW - "image/x-dcraw", -#endif // HAVE_LIBRAW -#ifdef HAVE_LIBRSVG - "image/svg+xml", -#endif // HAVE_LIBRSVG -#ifdef HAVE_XCURSOR - "image/x-xcursor", -#endif // HAVE_XCURSOR -#ifdef HAVE_LIBWEBP - "image/webp", -#endif // HAVE_LIBWEBP -#ifdef HAVE_LIBHEIF - "image/heic", - "image/heif", - "image/avif", -#endif // HAVE_LIBHEIF -#ifdef HAVE_LIBTIFF - "image/tiff", -#endif // HAVE_LIBTIFF - NULL -}; - -char ** -fastiv_io_all_supported_media_types(void) -{ - GPtrArray *types = g_ptr_array_new(); - for (const char **p = fastiv_io_supported_media_types; *p; p++) - g_ptr_array_add(types, g_strdup(*p)); - -#ifdef HAVE_GDKPIXBUF - GSList *formats = gdk_pixbuf_get_formats(); - for (GSList *iter = formats; iter; iter = iter->next) { - gchar **subtypes = gdk_pixbuf_format_get_mime_types(iter->data); - for (gchar **p = subtypes; *p; p++) - g_ptr_array_add(types, *p); - g_free(subtypes); - } - g_slist_free(formats); -#endif // HAVE_GDKPIXBUF - - g_ptr_array_add(types, NULL); - return (char **) g_ptr_array_free(types, FALSE); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#define FASTIV_IO_ERROR fastiv_io_error_quark() - -G_DEFINE_QUARK(fastiv-io-error-quark, fastiv_io_error) - -enum FastivIoError { - FASTIV_IO_ERROR_OPEN -}; - -static void -set_error(GError **error, const char *message) -{ - g_set_error_literal(error, FASTIV_IO_ERROR, FASTIV_IO_ERROR_OPEN, message); -} - -static bool -try_append_page(cairo_surface_t *surface, cairo_surface_t **result, - cairo_surface_t **result_tail) -{ - if (!surface) - return false; - - if (*result) { - cairo_surface_set_user_data(*result_tail, - &fastiv_io_key_page_next, surface, - (cairo_destroy_func_t) cairo_surface_destroy); - cairo_surface_set_user_data(surface, - &fastiv_io_key_page_previous, *result_tail, NULL); - *result_tail = surface; - } else { - *result = *result_tail = surface; - } - return true; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// From libwebp, verified to exactly match [x * a / 255]. -#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23) - -static bool -pull_passthrough(const wuffs_base__more_information *minfo, - wuffs_base__io_buffer *src, wuffs_base__io_buffer *dst, GError **error) -{ - wuffs_base__range_ie_u64 r = - wuffs_base__more_information__metadata_raw_passthrough__range(minfo); - if (wuffs_base__range_ie_u64__is_empty(&r)) - return true; - - // This should currently be zero, because we read files all at once. - uint64_t pos = src->meta.pos; - if (pos > r.min_incl || - wuffs_base__u64__sat_sub(r.max_excl, pos) > src->meta.wi) { - set_error(error, "metadata is outside the read buffer"); - return false; - } - - // Mimic WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_RAW_TRANSFORM. - *dst = wuffs_base__make_io_buffer(src->data, - wuffs_base__make_io_buffer_meta( - wuffs_base__u64__sat_sub(r.max_excl, pos), - wuffs_base__u64__sat_sub(r.min_incl, pos), pos, TRUE)); - - // Seeking to the end of it seems to be a requirement in decode_gif.wuffs. - // Just not in case the block was empty. :^) - src->meta.ri = dst->meta.wi; - return true; -} - -static GBytes * -pull_metadata(wuffs_base__image_decoder *dec, wuffs_base__io_buffer *src, - wuffs_base__more_information *minfo, GError **error) -{ - uint8_t buf[8192] = {}; - GByteArray *array = g_byte_array_new(); - while (true) { - *minfo = wuffs_base__empty_more_information(); - wuffs_base__io_buffer dst = wuffs_base__ptr_u8__writer(buf, sizeof buf); - wuffs_base__status status = - wuffs_base__image_decoder__tell_me_more(dec, &dst, minfo, src); - switch (minfo->flavor) { - case 0: - // Most likely as a result of an error, we'll handle that below. - case WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_RAW_TRANSFORM: - // Wuffs is reading it into the buffer. - case WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_PARSED: - // Use Wuffs accessor functions in the caller. - break; - default: - set_error(error, "Wuffs metadata API incompatibility"); - goto fail; - - case WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_RAW_PASSTHROUGH: - // The insane case: error checking really should come first, - // and it can say "even more information". See decode_gif.wuffs. - if (!pull_passthrough(minfo, src, &dst, error)) - goto fail; - } - - g_byte_array_append(array, - wuffs_base__io_buffer__reader_pointer(&dst), - wuffs_base__io_buffer__reader_length(&dst)); - if (wuffs_base__status__is_ok(&status)) - return g_byte_array_free_to_bytes(array); - - if (status.repr != wuffs_base__suspension__even_more_information && - status.repr != wuffs_base__suspension__short_write) { - set_error(error, wuffs_base__status__message(&status)); - goto fail; - } - } - -fail: - g_byte_array_unref(array); - return NULL; -} - -struct load_wuffs_frame_context { - wuffs_base__image_decoder *dec; ///< Wuffs decoder abstraction - wuffs_base__io_buffer *src; ///< Wuffs source buffer - wuffs_base__image_config cfg; ///< Wuffs image configuration - wuffs_base__slice_u8 workbuf; ///< Work buffer for Wuffs - wuffs_base__frame_config last_fc; ///< Previous frame configuration - uint32_t width; ///< Copied from cfg.pixcfg - uint32_t height; ///< Copied from cfg.pixcfg - cairo_format_t cairo_format; ///< Target format for surfaces - bool pack_16_10; ///< Custom copying swizzle for RGB30 - bool expand_16_float; ///< Custom copying swizzle for RGBA128F - GBytes *meta_exif; ///< Reference-counted Exif - GBytes *meta_iccp; ///< Reference-counted ICC profile - GBytes *meta_xmp; ///< Reference-counted XMP - - cairo_surface_t *result; ///< The resulting surface (referenced) - cairo_surface_t *result_tail; ///< The final animation frame -}; - -static bool -load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error) -{ - wuffs_base__frame_config fc = {}; - wuffs_base__status status = - wuffs_base__image_decoder__decode_frame_config(ctx->dec, &fc, ctx->src); - if (status.repr == wuffs_base__note__end_of_data && ctx->result) - return false; - if (!wuffs_base__status__is_ok(&status)) { - set_error(error, wuffs_base__status__message(&status)); - return false; - } - - bool success = false; - unsigned char *targetbuf = NULL; - cairo_surface_t *surface = - cairo_image_surface_create(ctx->cairo_format, ctx->width, ctx->height); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - goto fail; - } - - // CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with - // ARGB/BGR/XRGB/BGRX. This function does not support a stride different - // from the width, maybe Wuffs internals do not either. - unsigned char *surface_data = cairo_image_surface_get_data(surface); - int surface_stride = cairo_image_surface_get_stride(surface); - wuffs_base__pixel_buffer pb = {0}; - if (ctx->expand_16_float) { - uint32_t targetbuf_size = ctx->height * ctx->width * 64; - targetbuf = g_malloc(targetbuf_size); - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg, - wuffs_base__make_slice_u8(targetbuf, targetbuf_size)); - } else if (ctx->pack_16_10) { - uint32_t targetbuf_size = ctx->height * ctx->width * 16; - targetbuf = g_malloc(targetbuf_size); - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg, - wuffs_base__make_slice_u8(targetbuf, targetbuf_size)); - } else { - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg, - wuffs_base__make_slice_u8(surface_data, - surface_stride * cairo_image_surface_get_height(surface))); - } - if (!wuffs_base__status__is_ok(&status)) { - set_error(error, wuffs_base__status__message(&status)); - goto fail; - } - - // Starting to modify pixel data directly. Probably an unnecessary call. - cairo_surface_flush(surface); - - status = wuffs_base__image_decoder__decode_frame(ctx->dec, &pb, ctx->src, - WUFFS_BASE__PIXEL_BLEND__SRC, ctx->workbuf, NULL); - if (!wuffs_base__status__is_ok(&status)) { - set_error(error, wuffs_base__status__message(&status)); - goto fail; - } - - if (ctx->expand_16_float) { - g_debug("Wuffs to Cairo RGBA128F"); - uint16_t *in = (uint16_t *) targetbuf; - float *out = (float *) surface_data; - for (uint32_t y = 0; y < ctx->height; y++) { - for (uint32_t x = 0; x < ctx->width; x++) { - float b = *in++ / 65535., g = *in++ / 65535., - r = *in++ / 65535., a = *in++ / 65535.; - *out++ = r * a; - *out++ = g * a; - *out++ = b * a; - *out++ = a; - } - } - } else if (ctx->pack_16_10) { - g_debug("Wuffs to Cairo RGB30"); - uint16_t *in = (uint16_t *) targetbuf; - uint32_t *out = (uint32_t *) surface_data; - for (uint32_t y = 0; y < ctx->height; y++) { - for (uint32_t x = 0; x < ctx->width; x++) { - uint32_t b = *in++, g = *in++, r = *in++, X = *in++; - *out++ = (X >> 14) << 30 | - (r >> 6) << 20 | (g >> 6) << 10 | (b >> 6); - } - } - } - - // Pixel data has been written, need to let Cairo know. - cairo_surface_mark_dirty(surface); - - // Single-frame images get a fast path, animations are are handled slowly: - if (wuffs_base__frame_config__index(&fc) > 0) { - // Copy the previous frame to a new surface. - cairo_surface_t *canvas = cairo_image_surface_create( - ctx->cairo_format, ctx->width, ctx->height); - int stride = cairo_image_surface_get_stride(canvas); - int height = cairo_image_surface_get_height(canvas); - memcpy(cairo_image_surface_get_data(canvas), - cairo_image_surface_get_data(ctx->result_tail), stride * height); - cairo_surface_mark_dirty(canvas); - - // Apply that frame's disposal method. - wuffs_base__rect_ie_u32 bounds = - wuffs_base__frame_config__bounds(&ctx->last_fc); - wuffs_base__color_u32_argb_premul bg = - wuffs_base__frame_config__background_color(&ctx->last_fc); - - double a = (bg >> 24) / 255., r = 0, g = 0, b = 0; - if (a) { - r = (uint8_t) (bg >> 16) / 255. / a; - g = (uint8_t) (bg >> 8) / 255. / a; - b = (uint8_t) (bg) / 255. / a; - } - - cairo_t *cr = cairo_create(canvas); - switch (wuffs_base__frame_config__disposal(&ctx->last_fc)) { - case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_BACKGROUND: - cairo_rectangle(cr, bounds.min_incl_x, bounds.min_incl_y, - bounds.max_excl_x - bounds.min_incl_x, - bounds.max_excl_y - bounds.min_incl_y); - cairo_set_source_rgba(cr, r, g, b, a); - cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); - cairo_fill(cr); - break; - case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_PREVIOUS: - // TODO(p): Implement, it seems tricky. - // Might need another surface to keep track of the state. - break; - } - - // Paint the current frame over that, within its bounds. - bounds = wuffs_base__frame_config__bounds(&fc); - cairo_rectangle(cr, bounds.min_incl_x, bounds.min_incl_y, - bounds.max_excl_x - bounds.min_incl_x, - bounds.max_excl_y - bounds.min_incl_y); - cairo_clip(cr); - - cairo_set_operator(cr, - wuffs_base__frame_config__overwrite_instead_of_blend(&fc) - ? CAIRO_OPERATOR_SOURCE - : CAIRO_OPERATOR_OVER); - - cairo_set_source_surface(cr, surface, 0, 0); - cairo_paint(cr); - cairo_destroy(cr); - cairo_surface_destroy(surface); - surface = canvas; - } - - if (ctx->meta_exif) - cairo_surface_set_user_data(surface, &fastiv_io_key_exif, - g_bytes_ref(ctx->meta_exif), (cairo_destroy_func_t) g_bytes_unref); - if (ctx->meta_iccp) - cairo_surface_set_user_data(surface, &fastiv_io_key_icc, - g_bytes_ref(ctx->meta_iccp), (cairo_destroy_func_t) g_bytes_unref); - if (ctx->meta_xmp) - cairo_surface_set_user_data(surface, &fastiv_io_key_xmp, - g_bytes_ref(ctx->meta_xmp), (cairo_destroy_func_t) g_bytes_unref); - - cairo_surface_set_user_data(surface, &fastiv_io_key_loops, - (void *) (uintptr_t) wuffs_base__image_decoder__num_animation_loops( - ctx->dec), NULL); - cairo_surface_set_user_data(surface, &fastiv_io_key_frame_duration, - (void *) (intptr_t) (wuffs_base__frame_config__duration(&fc) / - WUFFS_BASE__FLICKS_PER_MILLISECOND), NULL); - - cairo_surface_set_user_data(surface, &fastiv_io_key_frame_previous, - ctx->result_tail, NULL); - if (ctx->result_tail) - cairo_surface_set_user_data(ctx->result_tail, &fastiv_io_key_frame_next, - surface, (cairo_destroy_func_t) cairo_surface_destroy); - else - ctx->result = surface; - - success = true; - ctx->result_tail = surface; - ctx->last_fc = fc; - -fail: - if (!success) { - cairo_surface_destroy(surface); - g_clear_pointer(&ctx->result, cairo_surface_destroy); - ctx->result_tail = NULL; - } - - g_free(targetbuf); - return success; -} - -// https://github.com/google/wuffs/blob/main/example/gifplayer/gifplayer.c -// is pure C, and a good reference. I can't use the auxiliary libraries, -// since they depend on C++, which is undesirable. -static cairo_surface_t * -open_wuffs( - wuffs_base__image_decoder *dec, wuffs_base__io_buffer src, GError **error) -{ - struct load_wuffs_frame_context ctx = {.dec = dec, .src = &src}; - - // TODO(p): PNG also has sRGB and gAMA, as well as text chunks (Wuffs #58). - // The former two use WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_PARSED. - wuffs_base__image_decoder__set_report_metadata( - ctx.dec, WUFFS_BASE__FOURCC__EXIF, true); - wuffs_base__image_decoder__set_report_metadata( - ctx.dec, WUFFS_BASE__FOURCC__ICCP, true); - - while (true) { - wuffs_base__status status = - wuffs_base__image_decoder__decode_image_config( - ctx.dec, &ctx.cfg, ctx.src); - if (wuffs_base__status__is_ok(&status)) - break; - - if (status.repr != wuffs_base__note__metadata_reported) { - set_error(error, wuffs_base__status__message(&status)); - goto fail; - } - - wuffs_base__more_information minfo = {}; - GBytes *bytes = NULL; - if (!(bytes = pull_metadata(ctx.dec, ctx.src, &minfo, error))) - goto fail; - - switch (wuffs_base__more_information__metadata__fourcc(&minfo)) { - case WUFFS_BASE__FOURCC__EXIF: - if (ctx.meta_exif) { - g_warning("ignoring repeated Exif"); - break; - } - ctx.meta_exif = bytes; - continue; - case WUFFS_BASE__FOURCC__ICCP: - if (ctx.meta_iccp) { - g_warning("ignoring repeated ICC profile"); - break; - } - ctx.meta_iccp = bytes; - continue; - case WUFFS_BASE__FOURCC__XMP: - if (ctx.meta_xmp) { - g_warning("ignoring repeated XMP"); - break; - } - ctx.meta_xmp = bytes; - continue; - } - - g_bytes_unref(bytes); - } - - // This, at least currently, seems excessive. - if (!wuffs_base__image_config__is_valid(&ctx.cfg)) { - set_error(error, "invalid Wuffs image configuration"); - goto fail; - } - - // We need to check because of the Cairo API. - ctx.width = wuffs_base__pixel_config__width(&ctx.cfg.pixcfg); - ctx.height = wuffs_base__pixel_config__height(&ctx.cfg.pixcfg); - if (ctx.width > INT_MAX || ctx.height > INT_MAX) { - set_error(error, "image dimensions overflow"); - goto fail; - } - - // Wuffs maps tRNS to BGRA in `decoder.decode_trns?`, we should be fine. - // wuffs_base__pixel_format__transparency() doesn't reflect the image file. - // TODO(p): See if wuffs_base__image_config__first_frame_is_opaque() causes - // issues with animations, and eventually ensure an alpha-capable format. - bool opaque = wuffs_base__image_config__first_frame_is_opaque(&ctx.cfg); - - // Wuffs' API is kind of awful--we want to catch deep RGB and deep grey. - wuffs_base__pixel_format srcfmt = - wuffs_base__pixel_config__pixel_format(&ctx.cfg.pixcfg); - uint32_t bpp = wuffs_base__pixel_format__bits_per_pixel(&srcfmt); - - // Cairo doesn't support transparency with RGB30, so no premultiplication. - ctx.pack_16_10 = opaque && (bpp > 24 || (bpp < 24 && bpp > 8)); -#ifdef FASTIV_CAIRO_RGBA128F - ctx.expand_16_float = !opaque && (bpp > 24 || (bpp < 24 && bpp > 8)); -#endif // FASTIV_CAIRO_RGBA128F - - // In Wuffs, /doc/note/pixel-formats.md declares "memory order", which, - // for our purposes, means big endian, and BGRA results in 32-bit ARGB - // on most machines. - // - // XXX: WUFFS_BASE__PIXEL_FORMAT__ARGB_PREMUL is not expressible, only RGBA. - // Wuffs doesn't support big-endian architectures at all, we might want to - // fall back to spng in such cases, or do a second conversion. - uint32_t wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL; - - // CAIRO_FORMAT_ARGB32: "The 32-bit quantities are stored native-endian. - // Pre-multiplied alpha is used." CAIRO_FORMAT_RGB{24,30} are analogous. - ctx.cairo_format = CAIRO_FORMAT_ARGB32; - -#ifdef FASTIV_CAIRO_RGBA128F - if (ctx.expand_16_float) { - wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL_4X16LE; - ctx.cairo_format = CAIRO_FORMAT_RGBA128F; - } else -#endif // FASTIV_CAIRO_RGBA128F - if (ctx.pack_16_10) { - // TODO(p): Make Wuffs support A2RGB30 as a destination format; - // in general, 16-bit depth swizzlers are stubbed. - // See also wuffs_base__pixel_swizzler__prepare__*(). - wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL_4X16LE; - ctx.cairo_format = CAIRO_FORMAT_RGB30; - } else if (opaque) { - // BGRX doesn't have as wide swizzler support, namely in GIF. - wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL; - ctx.cairo_format = CAIRO_FORMAT_RGB24; - } - - wuffs_base__pixel_config__set(&ctx.cfg.pixcfg, wuffs_format, - WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, ctx.width, ctx.height); - - uint64_t workbuf_len_max_incl = - wuffs_base__image_decoder__workbuf_len(ctx.dec).max_incl; - if (workbuf_len_max_incl) { - ctx.workbuf = wuffs_base__malloc_slice_u8(malloc, workbuf_len_max_incl); - if (!ctx.workbuf.ptr) { - set_error(error, "failed to allocate a work buffer"); - goto fail; - } - } - - while (load_wuffs_frame(&ctx, error)) - ; - - // Wrap the chain around, since our caller receives only one pointer. - if (ctx.result) - cairo_surface_set_user_data(ctx.result, &fastiv_io_key_frame_previous, - ctx.result_tail, NULL); - -fail: - free(ctx.workbuf.ptr); - g_clear_pointer(&ctx.meta_exif, g_bytes_unref); - g_clear_pointer(&ctx.meta_iccp, g_bytes_unref); - g_clear_pointer(&ctx.meta_xmp, g_bytes_unref); - return ctx.result; -} - -static cairo_surface_t * -open_wuffs_using(wuffs_base__image_decoder *(*allocate)(), - const gchar *data, gsize len, GError **error) -{ - wuffs_base__image_decoder *dec = allocate(); - if (!dec) { - set_error(error, "memory allocation failed or internal error"); - return NULL; - } - - cairo_surface_t *surface = open_wuffs( - dec, wuffs_base__ptr_u8__reader((uint8_t *) data, len, TRUE), error); - free(dec); - return surface; -} - -static void -trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len) -{ - // Inspired by gdk-pixbuf's io-jpeg.c: - // - // Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop - // does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096 - while (len--) { - int c = p[0], m = p[1], y = p[2], k = p[3]; -#if G_BYTE_ORDER == G_LITTLE_ENDIAN - p[0] = k * y / 255; - p[1] = k * m / 255; - p[2] = k * c / 255; - p[3] = 255; -#else - p[3] = k * y / 255; - p[2] = k * m / 255; - p[1] = k * c / 255; - p[0] = 255; -#endif - p += 4; - } -} - -static void -parse_jpeg_metadata(cairo_surface_t *surface, const gchar *data, gsize len) -{ - // Because the JPEG file format is simple, just do it manually. - // See: https://www.w3.org/Graphics/JPEG/itu-t81.pdf - enum { - APP0 = 0xE0, - APP1, - APP2, - RST0 = 0xD0, - RST1, - RST2, - RST3, - RST4, - RST5, - RST6, - RST7, - SOI = 0xD8, - EOI = 0xD9, - SOS = 0xDA, - TEM = 0x01, - }; - - GByteArray *exif = g_byte_array_new(), *icc = g_byte_array_new(); - int icc_sequence = 0, icc_done = FALSE; - - const guint8 *p = (const guint8 *) data, *end = p + len; - while (p + 3 < end && *p++ == 0xFF && *p != SOS && *p != EOI) { - // The previous byte is a fill byte, restart. - if (*p == 0xFF) - continue; - - // These markers stand alone, not starting a marker segment. - guint8 marker = *p++; - switch (marker) { - case RST0: - case RST1: - case RST2: - case RST3: - case RST4: - case RST5: - case RST6: - case RST7: - case SOI: - case TEM: - continue; - } - - // Do not bother validating the structure. - guint16 length = p[0] << 8 | p[1]; - const guint8 *payload = p + 2; - if ((p += length) > end) - break; - - // https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf 4.7.2 - // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 - // Not checking the padding byte is intentional. - if (marker == APP1 && p - payload >= 6 && - !memcmp(payload, "Exif\0", 5) && !exif->len) { - payload += 6; - g_byte_array_append(exif, payload, p - payload); - } - - // https://www.color.org/specification/ICC1v43_2010-12.pdf B.4 - if (marker == APP2 && p - payload >= 14 && - !memcmp(payload, "ICC_PROFILE\0", 12) && !icc_done && - payload[12] == ++icc_sequence && payload[13] >= payload[12]) { - payload += 14; - g_byte_array_append(icc, payload, p - payload); - icc_done = payload[-1] == icc_sequence; - } - - // TODO(p): Extract the main XMP segment. - } - - if (exif->len) - cairo_surface_set_user_data(surface, &fastiv_io_key_exif, - g_byte_array_free_to_bytes(exif), - (cairo_destroy_func_t) g_bytes_unref); - else - g_byte_array_free(exif, TRUE); - - if (icc_done) - cairo_surface_set_user_data(surface, &fastiv_io_key_icc, - g_byte_array_free_to_bytes(icc), - (cairo_destroy_func_t) g_bytes_unref); - else - g_byte_array_free(icc, TRUE); -} - -static cairo_surface_t * -open_libjpeg_turbo(const gchar *data, gsize len, GError **error) -{ - tjhandle dec = tjInitDecompress(); - if (!dec) { - set_error(error, tjGetErrorStr2(dec)); - return NULL; - } - - int width = 0, height = 0, subsampling = TJSAMP_444, colorspace = TJCS_RGB; - if (tjDecompressHeader3(dec, (const unsigned char *) data, len, - &width, &height, &subsampling, &colorspace)) { - set_error(error, tjGetErrorStr2(dec)); - tjDestroy(dec); - return NULL; - } - - int pixel_format = (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK) - ? TJPF_CMYK - : (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRA : TJPF_ARGB); - - cairo_surface_t *surface = - cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); - tjDestroy(dec); - return NULL; - } - - // Starting to modify pixel data directly. Probably an unnecessary call. - cairo_surface_flush(surface); - - int stride = cairo_image_surface_get_stride(surface); - if (tjDecompress2(dec, (const unsigned char *) data, len, - cairo_image_surface_get_data(surface), width, stride, height, - pixel_format, TJFLAG_ACCURATEDCT)) { - if (tjGetErrorCode(dec) == TJERR_WARNING) { - g_warning("%s", tjGetErrorStr2(dec)); - } else { - set_error(error, tjGetErrorStr2(dec)); - cairo_surface_destroy(surface); - tjDestroy(dec); - return NULL; - } - } - - if (pixel_format == TJPF_CMYK) { - // CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with - // ARGB/BGR/XRGB/BGRX. - trivial_cmyk_to_host_byte_order_argb( - cairo_image_surface_get_data(surface), width * height); - } - - // Pixel data has been written, need to let Cairo know. - cairo_surface_mark_dirty(surface); - - tjDestroy(dec); - parse_jpeg_metadata(surface, data, len); - return surface; -} - -#ifdef HAVE_LIBRAW // --------------------------------------------------------- - -static cairo_surface_t * -open_libraw(const gchar *data, gsize len, GError **error) -{ - // https://github.com/LibRaw/LibRaw/issues/418 - libraw_data_t *iprc = libraw_init( - LIBRAW_OPIONS_NO_MEMERR_CALLBACK | LIBRAW_OPIONS_NO_DATAERR_CALLBACK); - if (!iprc) { - set_error(error, "failed to obtain a LibRaw handle"); - return NULL; - } - -#if 0 - // TODO(p): Consider setting this--the image is still likely to be - // rendered suboptimally, so why not make it faster. - iprc->params.half_size = 1; -#endif - - // TODO(p): Check if we need to set anything for autorotation (sizes.flip). - iprc->params.use_camera_wb = 1; - iprc->params.output_color = 1; // sRGB, TODO(p): Is this used? - iprc->params.output_bps = 8; // This should be the default value. - - int err = 0; - if ((err = libraw_open_buffer(iprc, (void *) data, len))) { - set_error(error, libraw_strerror(err)); - libraw_close(iprc); - return NULL; - } - - // TODO(p): Do we need to check iprc->idata.raw_count? Maybe for TIFFs? - if ((err = libraw_unpack(iprc))) { - set_error(error, libraw_strerror(err)); - libraw_close(iprc); - return NULL; - } - -#if 0 - // TODO(p): I'm not sure when this is necessary or useful yet. - if ((err = libraw_adjust_sizes_info_only(iprc))) { - set_error(error, libraw_strerror(err)); - libraw_close(iprc); - return NULL; - } -#endif - - // TODO(p): Documentation says I should look at the code and do it myself. - if ((err = libraw_dcraw_process(iprc))) { - set_error(error, libraw_strerror(err)); - libraw_close(iprc); - return NULL; - } - - // FIXME: This is shittily written to iterate over the range of - // idata.colors, and will be naturally slow. - libraw_processed_image_t *image = libraw_dcraw_make_mem_image(iprc, &err); - if (!image) { - set_error(error, libraw_strerror(err)); - libraw_close(iprc); - return NULL; - } - - // This should have been transformed, and kept, respectively. - if (image->colors != 3 || image->bits != 8) { - set_error(error, "unexpected number of colours, or bit depth"); - libraw_dcraw_clear_mem(image); - libraw_close(iprc); - return NULL; - } - - int width = image->width, height = image->height; - cairo_surface_t *surface = - cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); - libraw_dcraw_clear_mem(image); - libraw_close(iprc); - return NULL; - } - - // Starting to modify pixel data directly. Probably an unnecessary call. - cairo_surface_flush(surface); - - uint32_t *pixels = (uint32_t *) cairo_image_surface_get_data(surface); - unsigned char *p = image->data; - for (ushort y = 0; y < image->height; y++) { - for (ushort x = 0; x < image->width; x++) { - *pixels++ = 0xff000000 | (uint32_t) p[0] << 16 | - (uint32_t) p[1] << 8 | (uint32_t) p[2]; - p += 3; - } - } - - // Pixel data has been written, need to let Cairo know. - cairo_surface_mark_dirty(surface); - - libraw_dcraw_clear_mem(image); - libraw_close(iprc); - return surface; -} - -#endif // HAVE_LIBRAW --------------------------------------------------------- -#ifdef HAVE_LIBRSVG // -------------------------------------------------------- - -#ifdef FASTIV_RSVG_DEBUG -#include -#include -#endif - -// FIXME: librsvg rasterizes filters, so this method isn't fully appropriate. -static cairo_surface_t * -open_librsvg(const gchar *data, gsize len, const gchar *path, GError **error) -{ - GFile *base_file = g_file_new_for_path(path); - GInputStream *is = g_memory_input_stream_new_from_data(data, len, NULL); - RsvgHandle *handle = rsvg_handle_new_from_stream_sync(is, base_file, - RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA, NULL, error); - g_object_unref(base_file); - g_object_unref(is); - if (!handle) - return NULL; - - // TODO(p): Acquire this from somewhere else. - rsvg_handle_set_dpi(handle, 96); - - double w = 0, h = 0; -#if LIBRSVG_CHECK_VERSION(2, 51, 0) - if (!rsvg_handle_get_intrinsic_size_in_pixels(handle, &w, &h)) { -#else - RsvgDimensionData dd = {}; - rsvg_handle_get_dimensions(handle, &dd); - if ((w = dd.width) <= 0 || (h = dd.height) <= 0) { -#endif - RsvgRectangle viewbox = {}; - gboolean has_viewport = FALSE; - rsvg_handle_get_intrinsic_dimensions(handle, NULL, NULL, NULL, NULL, - &has_viewport, &viewbox); - if (!has_viewport) { - set_error(error, "cannot compute pixel dimensions"); - g_object_unref(handle); - return NULL; - } - - w = viewbox.width; - h = viewbox.height; - } - - cairo_rectangle_t extents = { - .x = 0, .y = 0, .width = ceil(w), .height = ceil(h)}; - cairo_surface_t *surface = - cairo_recording_surface_create(CAIRO_CONTENT_COLOR_ALPHA, &extents); - -#ifdef FASTIV_RSVG_DEBUG - cairo_device_t *script = cairo_script_create("cairo.script"); - cairo_surface_t *tee = - cairo_script_surface_create_for_target(script, surface); - cairo_t *cr = cairo_create(tee); - cairo_device_destroy(script); - cairo_surface_destroy(tee); -#else - cairo_t *cr = cairo_create(surface); -#endif - - RsvgRectangle viewport = {.x = 0, .y = 0, .width = w, .height = h}; - if (!rsvg_handle_render_document(handle, cr, &viewport, error)) { - cairo_surface_destroy(surface); - cairo_destroy(cr); - g_object_unref(handle); - return NULL; - } - - cairo_destroy(cr); - g_object_unref(handle); - -#ifdef FASTIV_RSVG_DEBUG - cairo_surface_t *svg = cairo_svg_surface_create("cairo.svg", w, h); - cr = cairo_create(svg); - cairo_set_source_surface(cr, surface, 0, 0); - cairo_paint(cr); - cairo_destroy(cr); - cairo_surface_destroy(svg); - - cairo_surface_t *png = - cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w * 10, h * 10); - cr = cairo_create(png); - cairo_scale(cr, 10, 10); - cairo_set_source_surface(cr, surface, 0, 0); - cairo_paint(cr); - cairo_destroy(cr); - cairo_surface_write_to_png(png, "cairo.png"); - cairo_surface_destroy(png); -#endif - return surface; -} - -#endif // HAVE_LIBRSVG -------------------------------------------------------- -#ifdef HAVE_XCURSOR //--------------------------------------------------------- - -// fmemopen is part of POSIX-1.2008, this exercise is technically unnecessary. -// libXcursor checks for EOF rather than -1, it may eat your hamster. -struct fastiv_io_xcursor { - XcursorFile parent; - unsigned char *data; - long position, len; -}; - -static int -fastiv_io_xcursor_read(XcursorFile *file, unsigned char *buf, int len) -{ - struct fastiv_io_xcursor *fix = (struct fastiv_io_xcursor *) file; - if (fix->position < 0 || fix->position > fix->len) { - errno = EOVERFLOW; - return -1; - } - long n = MIN(fix->len - fix->position, len); - if (n > G_MAXINT) { - errno = EIO; - return -1; - } - memcpy(buf, fix->data + fix->position, n); - fix->position += n; - return n; -} - -static int -fastiv_io_xcursor_write(G_GNUC_UNUSED XcursorFile *file, - G_GNUC_UNUSED unsigned char *buf, G_GNUC_UNUSED int len) -{ - errno = EBADF; - return -1; -} - -static int -fastiv_io_xcursor_seek(XcursorFile *file, long offset, int whence) -{ - struct fastiv_io_xcursor *fix = (struct fastiv_io_xcursor *) file; - switch (whence) { - case SEEK_SET: - fix->position = offset; - break; - case SEEK_CUR: - fix->position += offset; - break; - case SEEK_END: - fix->position = fix->len + offset; - break; - default: - errno = EINVAL; - return -1; - } - // This is technically too late for fseek(), but libXcursor doesn't care. - if (fix->position < 0) { - errno = EINVAL; - return -1; - } - return fix->position; -} - -static const XcursorFile fastiv_io_xcursor_adaptor = { - .closure = NULL, - .read = fastiv_io_xcursor_read, - .write = fastiv_io_xcursor_write, - .seek = fastiv_io_xcursor_seek, -}; - -static cairo_surface_t * -open_xcursor(const gchar *data, gsize len, GError **error) -{ - if (len > G_MAXLONG) { - set_error(error, "size overflow"); - return NULL; - } - - struct fastiv_io_xcursor file = { - .parent = fastiv_io_xcursor_adaptor, - .data = (unsigned char *) data, - .position = 0, - .len = len, - }; - - XcursorImages *images = XcursorXcFileLoadAllImages(&file.parent); - if (!images) { - set_error(error, "general failure"); - return NULL; - } - - // Interpret cursors as animated pages. - cairo_surface_t *pages = NULL, *frames_head = NULL, *frames_tail = NULL; - - // XXX: Assuming that all "nominal sizes" have the same dimensions. - XcursorDim last_nominal = -1; - for (int i = 0; i < images->nimage; i++) { - XcursorImage *image = images->images[i]; - - // The library automatically byte swaps in _XcursorReadImage(). - cairo_surface_t *surface = cairo_image_surface_create_for_data( - (unsigned char *) image->pixels, CAIRO_FORMAT_ARGB32, - image->width, image->height, image->width * sizeof *image->pixels); - cairo_surface_set_user_data(surface, &fastiv_io_key_frame_duration, - (void *) (intptr_t) image->delay, NULL); - - if (pages && image->size == last_nominal) { - cairo_surface_set_user_data(surface, - &fastiv_io_key_frame_previous, frames_tail, NULL); - cairo_surface_set_user_data(frames_tail, - &fastiv_io_key_frame_next, surface, - (cairo_destroy_func_t) cairo_surface_destroy); - } else if (frames_head) { - cairo_surface_set_user_data(frames_head, - &fastiv_io_key_frame_previous, frames_tail, NULL); - - cairo_surface_set_user_data(frames_head, - &fastiv_io_key_page_next, surface, - (cairo_destroy_func_t) cairo_surface_destroy); - cairo_surface_set_user_data(surface, - &fastiv_io_key_page_previous, frames_head, NULL); - frames_head = surface; - } else { - pages = frames_head = surface; - } - - frames_tail = surface; - last_nominal = image->size; - } - if (!pages) { - XcursorImagesDestroy(images); - return NULL; - } - - // Wrap around animations in the last page. - cairo_surface_set_user_data( - frames_head, &fastiv_io_key_frame_previous, frames_tail, NULL); - - // There is no need to copy data, assign it to the surface. - static cairo_user_data_key_t key = {}; - cairo_surface_set_user_data( - pages, &key, images, (cairo_destroy_func_t) XcursorImagesDestroy); - return pages; -} - -#endif // HAVE_XCURSOR -------------------------------------------------------- -#ifdef HAVE_LIBWEBP //--------------------------------------------------------- - -static cairo_surface_t * -load_libwebp_nonanimated( - WebPDecoderConfig *config, const WebPData *wd, GError **error) -{ - cairo_surface_t *surface = cairo_image_surface_create( - config->input.has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, - config->input.width, config->input.height); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); - return NULL; - } - - config->options.use_threads = true; - - config->output.width = config->input.width; - config->output.height = config->input.height; - config->output.is_external_memory = true; - config->output.u.RGBA.rgba = cairo_image_surface_get_data(surface); - config->output.u.RGBA.stride = cairo_image_surface_get_stride(surface); - config->output.u.RGBA.size = - config->output.u.RGBA.stride * cairo_image_surface_get_height(surface); - if (G_BYTE_ORDER == G_LITTLE_ENDIAN) - config->output.colorspace = MODE_bgrA; - else - config->output.colorspace = MODE_Argb; - - VP8StatusCode err = 0; - if ((err = WebPDecode(wd->bytes, wd->size, config))) { - set_error(error, "WebP decoding error"); - cairo_surface_destroy(surface); - return NULL; - } - - cairo_surface_mark_dirty(surface); - return surface; -} - -static cairo_surface_t * -load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info, - int *last_timestamp, GError **error) -{ - uint8_t *buf = NULL; - int timestamp = 0; - if (!WebPAnimDecoderGetNext(dec, &buf, ×tamp)) { - set_error(error, "WebP decoding error"); - return NULL; - } - - bool is_opaque = (info->bgcolor & 0xFF) == 0xFF; - uint64_t area = info->canvas_width * info->canvas_height; - cairo_surface_t *surface = cairo_image_surface_create( - is_opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, - info->canvas_width, info->canvas_height); - - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); - return NULL; - } - - uint32_t *dst = (uint32_t *) cairo_image_surface_get_data(surface); - if (G_BYTE_ORDER == G_LITTLE_ENDIAN) { - memcpy(dst, buf, area * sizeof *dst); - } else { - uint32_t *src = (uint32_t *) buf; - for (uint64_t i = 0; i < area; i++) - *dst++ = GUINT32_FROM_LE(*src++); - } - - cairo_surface_mark_dirty(surface); - - // This API is confusing and awkward. - cairo_surface_set_user_data(surface, &fastiv_io_key_frame_duration, - (void *) (intptr_t) (timestamp - *last_timestamp), NULL); - *last_timestamp = timestamp; - return surface; -} - -static cairo_surface_t * -load_libwebp_animated(const WebPData *wd, GError **error) -{ - WebPAnimDecoderOptions options = {}; - WebPAnimDecoderOptionsInit(&options); - options.use_threads = true; - options.color_mode = MODE_bgrA; - - WebPAnimInfo info = {}; - WebPAnimDecoder *dec = WebPAnimDecoderNew(wd, &options); - WebPAnimDecoderGetInfo(dec, &info); - - cairo_surface_t *frames = NULL, *frames_tail = NULL; - if (info.canvas_width > INT_MAX || info.canvas_height > INT_MAX) { - set_error(error, "image dimensions overflow"); - goto fail; - } - - int last_timestamp = 0; - while (WebPAnimDecoderHasMoreFrames(dec)) { - cairo_surface_t *surface = - load_libwebp_frame(dec, &info, &last_timestamp, error); - if (!surface) { - g_clear_pointer(&frames, cairo_surface_destroy); - goto fail; - } - - if (frames_tail) - cairo_surface_set_user_data(frames_tail, - &fastiv_io_key_frame_next, surface, - (cairo_destroy_func_t) cairo_surface_destroy); - else - frames = surface; - - cairo_surface_set_user_data(surface, - &fastiv_io_key_frame_previous, frames_tail, NULL); - frames_tail = surface; - } - - if (frames) { - cairo_surface_set_user_data(frames, &fastiv_io_key_frame_previous, - frames_tail, NULL); - } else { - set_error(error, "the animation has no frames"); - g_clear_pointer(&frames, cairo_surface_destroy); - } - -fail: - WebPAnimDecoderDelete(dec); - return frames; -} - -static cairo_surface_t * -open_libwebp(const gchar *data, gsize len, const gchar *path, GError **error) -{ - // It is wholly zero-initialized by libwebp. - WebPDecoderConfig config = {}; - if (!WebPInitDecoderConfig(&config)) { - set_error(error, "libwebp version mismatch"); - return NULL; - } - - // TODO(p): Differentiate between a bad WebP, and not a WebP. - VP8StatusCode err = 0; - WebPData wd = {.bytes = (const uint8_t *) data, .size = len}; - if ((err = WebPGetFeatures(wd.bytes, wd.size, &config.input))) { - set_error(error, "WebP decoding error"); - return NULL; - } - - cairo_surface_t *result = config.input.has_animation - ? load_libwebp_animated(&wd, error) - : load_libwebp_nonanimated(&config, &wd, error); - if (!result) - goto fail; - - // Of course everything has to use a different abstraction. - WebPDemuxer *demux = WebPDemux(&wd); - if (!demux) { - g_warning("%s: %s", path, "demux failure"); - goto fail; - } - - // Releasing the demux chunk iterator is actually a no-op. - WebPChunkIterator chunk_iter = {}; - uint32_t flags = WebPDemuxGetI(demux, WEBP_FF_FORMAT_FLAGS); - if ((flags & EXIF_FLAG) && - WebPDemuxGetChunk(demux, "EXIF", 1, &chunk_iter)) { - cairo_surface_set_user_data(result, &fastiv_io_key_exif, - g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), - (cairo_destroy_func_t) g_bytes_unref); - WebPDemuxReleaseChunkIterator(&chunk_iter); - } - if ((flags & ICCP_FLAG) && - WebPDemuxGetChunk(demux, "ICCP", 1, &chunk_iter)) { - cairo_surface_set_user_data(result, &fastiv_io_key_icc, - g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), - (cairo_destroy_func_t) g_bytes_unref); - WebPDemuxReleaseChunkIterator(&chunk_iter); - } - if ((flags & XMP_FLAG) && - WebPDemuxGetChunk(demux, "XMP ", 1, &chunk_iter)) { - cairo_surface_set_user_data(result, &fastiv_io_key_xmp, - g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), - (cairo_destroy_func_t) g_bytes_unref); - WebPDemuxReleaseChunkIterator(&chunk_iter); - } - if (flags & ANIMATION_FLAG) { - cairo_surface_set_user_data(result, &fastiv_io_key_loops, - (void *) (uintptr_t) WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT), - NULL); - } - - WebPDemuxDelete(demux); - -fail: - WebPFreeDecBuffer(&config.output); - return result; -} - -#endif // HAVE_LIBWEBP -------------------------------------------------------- -#ifdef HAVE_LIBHEIF //--------------------------------------------------------- - -static cairo_surface_t * -load_libheif_image(struct heif_image_handle *handle, GError **error) -{ - cairo_surface_t *surface = NULL; - int has_alpha = heif_image_handle_has_alpha_channel(handle); - int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle); - if (bit_depth < 0) { - set_error(error, "undefined bit depth"); - goto fail; - } - - // Setting `convert_hdr_to_8bit` seems to be a no-op for RGBA32/64. - struct heif_decoding_options *opts = heif_decoding_options_alloc(); - - // TODO(p): We can get 16-bit depth, in reality most likely 10-bit. - struct heif_image *image = NULL; - struct heif_error err = heif_decode_image(handle, &image, - heif_colorspace_RGB, heif_chroma_interleaved_RGBA, opts); - if (err.code != heif_error_Ok) { - set_error(error, err.message); - goto fail_decode; - } - - int w = heif_image_get_width(image, heif_channel_interleaved); - int h = heif_image_get_height(image, heif_channel_interleaved); - - surface = cairo_image_surface_create( - has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, w, h); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); - surface = NULL; - goto fail_process; - } - - // As of writing, the library is using 16-byte alignment, unlike Cairo. - int src_stride = 0; - const uint8_t *src = heif_image_get_plane_readonly( - image, heif_channel_interleaved, &src_stride); - int dst_stride = cairo_image_surface_get_stride(surface); - const uint8_t *dst = cairo_image_surface_get_data(surface); - - for (int y = 0; y < h; y++) { - uint32_t *dstp = (uint32_t *) (dst + dst_stride * y); - const uint32_t *srcp = (const uint32_t *) (src + src_stride * y); - for (int x = 0; x < w; x++) { - uint32_t rgba = g_ntohl(srcp[x]); - *dstp++ = rgba << 24 | rgba >> 8; - } - } - - // TODO(p): Test real behaviour on real transparent images. - if (has_alpha && !heif_image_handle_is_premultiplied_alpha(handle)) { - for (int y = 0; y < h; y++) { - uint32_t *dstp = (uint32_t *) (dst + dst_stride * y); - for (int x = 0; x < w; x++) { - uint32_t argb = dstp[x], a = argb >> 24; - dstp[x] = a << 24 | - PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 | - PREMULTIPLY8(a, 0xFF & (argb >> 8)) << 8 | - PREMULTIPLY8(a, 0xFF & argb); - } - } - } - - heif_item_id exif_id = 0; - if (heif_image_handle_get_list_of_metadata_block_IDs( - handle, "Exif", &exif_id, 1)) { - size_t exif_len = heif_image_handle_get_metadata_size(handle, exif_id); - void *exif = g_malloc0(exif_len); - err = heif_image_handle_get_metadata(handle, exif_id, exif); - if (err.code) { - g_warning("%s", err.message); - g_free(exif); - } else { - cairo_surface_set_user_data(surface, &fastiv_io_key_exif, - g_bytes_new_take(exif, exif_len), - (cairo_destroy_func_t) g_bytes_unref); - } - } - - // https://loc.gov/preservation/digital/formats/fdd/fdd000526.shtml#factors - if (heif_image_handle_get_color_profile_type(handle) == - heif_color_profile_type_prof) { - size_t icc_len = heif_image_handle_get_raw_color_profile_size(handle); - void *icc = g_malloc0(icc_len); - err = heif_image_handle_get_raw_color_profile(handle, icc); - if (err.code) { - g_warning("%s", err.message); - g_free(icc); - } else { - cairo_surface_set_user_data(surface, &fastiv_io_key_icc, - g_bytes_new_take(icc, icc_len), - (cairo_destroy_func_t) g_bytes_unref); - } - } - - cairo_surface_mark_dirty(surface); - -fail_process: - heif_image_release(image); -fail_decode: - heif_decoding_options_free(opts); -fail: - return surface; -} - -static void -load_libheif_aux_images(const gchar *path, struct heif_image_handle *top, - cairo_surface_t **result, cairo_surface_t **result_tail) -{ - // Include the depth image, we have no special processing for it now. - int filter = LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA; - - int n = heif_image_handle_get_number_of_auxiliary_images(top, filter); - heif_item_id *ids = g_malloc0_n(n, sizeof *ids); - n = heif_image_handle_get_list_of_auxiliary_image_IDs(top, filter, ids, n); - for (int i = 0; i < n; i++) { - struct heif_image_handle *handle = NULL; - struct heif_error err = heif_image_handle_get_auxiliary_image_handle( - top, ids[i], &handle); - if (err.code != heif_error_Ok) { - g_warning("%s: %s", path, err.message); - continue; - } - - GError *e = NULL; - if (!try_append_page( - load_libheif_image(handle, &e), result, result_tail)) { - g_warning("%s: %s", path, e->message); - g_error_free(e); - } - - heif_image_handle_release(handle); - } - - g_free(ids); -} - -static cairo_surface_t * -open_libheif(const gchar *data, gsize len, const gchar *path, GError **error) -{ - // libheif will throw C++ exceptions on allocation failures. - // The library is generally awful through and through. - struct heif_context *ctx = heif_context_alloc(); - cairo_surface_t *result = NULL, *result_tail = NULL; - - struct heif_error err; - err = heif_context_read_from_memory_without_copy(ctx, data, len, NULL); - if (err.code != heif_error_Ok) { - set_error(error, err.message); - goto fail_read; - } - - int n = heif_context_get_number_of_top_level_images(ctx); - heif_item_id *ids = g_malloc0_n(n, sizeof *ids); - n = heif_context_get_list_of_top_level_image_IDs(ctx, ids, n); - for (int i = 0; i < n; i++) { - struct heif_image_handle *handle = NULL; - err = heif_context_get_image_handle(ctx, ids[i], &handle); - if (err.code != heif_error_Ok) { - g_warning("%s: %s", path, err.message); - continue; - } - - GError *e = NULL; - if (!try_append_page( - load_libheif_image(handle, &e), &result, &result_tail)) { - g_warning("%s: %s", path, e->message); - g_error_free(e); - } - - // TODO(p): Possibly add thumbnail images as well. - load_libheif_aux_images(path, handle, &result, &result_tail); - heif_image_handle_release(handle); - } - if (!result) { - g_clear_pointer(&result, cairo_surface_destroy); - set_error(error, "empty or unsupported image"); - } - - g_free(ids); -fail_read: - heif_context_free(ctx); - return result; -} - -#endif // HAVE_LIBHEIF -------------------------------------------------------- -#ifdef HAVE_LIBTIFF //--------------------------------------------------------- - -struct fastiv_io_tiff { - unsigned char *data; - gchar *error; - - // No, libtiff, the offset is not supposed to be unsigned (also see: - // man 0p sys_types.h), but at least it's fewer cases for us to care about. - toff_t position, len; -}; - -static tsize_t -fastiv_io_tiff_read(thandle_t h, tdata_t buf, tsize_t len) -{ - struct fastiv_io_tiff *io = h; - if (len < 0) { - // What the FUCK! This argument is not supposed to be signed! - // How many mistakes can you make in such a basic API? - errno = EOWNERDEAD; - return -1; - } - if (io->position > io->len) { - errno = EOVERFLOW; - return -1; - } - toff_t n = MIN(io->len - io->position, (toff_t) len); - if (n > TIFF_TMSIZE_T_MAX) { - errno = EIO; - return -1; - } - memcpy(buf, io->data + io->position, n); - io->position += n; - return n; -} - -static tsize_t -fastiv_io_tiff_write(G_GNUC_UNUSED thandle_t h, - G_GNUC_UNUSED tdata_t buf, G_GNUC_UNUSED tsize_t len) -{ - errno = EBADF; - return -1; -} - -static toff_t -fastiv_io_tiff_seek(thandle_t h, toff_t offset, int whence) -{ - struct fastiv_io_tiff *io = h; - switch (whence) { - case SEEK_SET: - io->position = offset; - break; - case SEEK_CUR: - io->position += offset; - break; - case SEEK_END: - io->position = io->len + offset; - break; - default: - errno = EINVAL; - return -1; - } - return io->position; -} - -static int -fastiv_io_tiff_close(G_GNUC_UNUSED thandle_t h) -{ - return 0; -} - -static toff_t -fastiv_io_tiff_size(thandle_t h) -{ - return ((struct fastiv_io_tiff *) h)->len; -} - -static void -fastiv_io_tiff_error(thandle_t h, - const char *module, const char *format, va_list ap) -{ - struct fastiv_io_tiff *io = h; - gchar *message = g_strdup_vprintf(format, ap); - if (io->error) - // I'm not sure if two errors can ever come in a succession, - // but make sure to log them in any case. - g_warning("tiff: %s: %s", module, message); - else - io->error = g_strconcat(module, ": ", message, NULL); - g_free(message); -} - -static void -fastiv_io_tiff_warning(G_GNUC_UNUSED thandle_t h, - const char *module, const char *format, va_list ap) -{ - gchar *message = g_strdup_vprintf(format, ap); - g_debug("tiff: %s: %s", module, message); - g_free(message); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static cairo_surface_t * -load_libtiff_directory(TIFF *tiff, GError **error) -{ - char emsg[1024] = ""; - if (!TIFFRGBAImageOK(tiff, emsg)) { - set_error(error, emsg); - return NULL; - } - - // TODO(p): Are there cases where we might not want to "stop on error"? - TIFFRGBAImage image; - if (!TIFFRGBAImageBegin(&image, tiff, 1 /* stop on error */, emsg)) { - set_error(error, emsg); - return NULL; - } - - cairo_surface_t *surface = NULL; - if (image.width > G_MAXINT || image.height >= G_MAXINT || - G_MAXUINT32 / image.width < image.height) { - set_error(error, "image dimensions too large"); - goto fail; - } - - surface = cairo_image_surface_create( - CAIRO_FORMAT_ARGB32, image.width, image.height); - - image.req_orientation = ORIENTATION_LEFTTOP; - uint32_t *raster = (uint32_t *) cairo_image_surface_get_data(surface); - if (!TIFFRGBAImageGet(&image, raster, image.width, image.height)) { - g_clear_pointer(&surface, cairo_surface_destroy); - goto fail; - } - - // Needs to be converted from ABGR to alpha-premultiplied ARGB for Cairo. - for (uint32_t i = image.width * image.height; i--;) { - uint32_t pixel = raster[i], - a = TIFFGetA(pixel), - b = TIFFGetB(pixel) * a / 255, - g = TIFFGetG(pixel) * a / 255, - r = TIFFGetR(pixel) * a / 255; - raster[i] = a << 24 | r << 16 | g << 8 | b; - } - - cairo_surface_mark_dirty(surface); - // XXX: The whole file is essentially an Exif, any ideas? - - const uint32_t meta_len = 0; - const void *meta = NULL; - if (TIFFGetField(tiff, TIFFTAG_ICCPROFILE, &meta_len, &meta)) { - cairo_surface_set_user_data(surface, &fastiv_io_key_icc, - g_bytes_new(meta, meta_len), - (cairo_destroy_func_t) g_bytes_unref); - } - if (TIFFGetField(tiff, TIFFTAG_XMLPACKET, &meta_len, &meta)) { - cairo_surface_set_user_data(surface, &fastiv_io_key_xmp, - g_bytes_new(meta, meta_len), - (cairo_destroy_func_t) g_bytes_unref); - } - - // Don't ask. The API is high, alright, I'm just not sure about the level. - uint16_t orientation = 0; - if (TIFFGetField(tiff, TIFFTAG_ORIENTATION, &orientation)) { - if (orientation == 5 || orientation == 7) - cairo_surface_set_user_data(surface, &fastiv_io_key_orientation, - (void *) (uintptr_t) 5, NULL); - if (orientation == 6 || orientation == 8) - cairo_surface_set_user_data(surface, &fastiv_io_key_orientation, - (void *) (uintptr_t) 7, NULL); - } - -fail: - TIFFRGBAImageEnd(&image); - // TODO(p): It's possible to implement ClipPath easily with Cairo. - return surface; -} - -static cairo_surface_t * -open_libtiff(const gchar *data, gsize len, const gchar *path, GError **error) -{ - // Both kinds of handlers are called, redirect everything. - TIFFErrorHandler eh = TIFFSetErrorHandler(NULL); - TIFFErrorHandler wh = TIFFSetWarningHandler(NULL); - TIFFErrorHandlerExt ehe = TIFFSetErrorHandlerExt(fastiv_io_tiff_error); - TIFFErrorHandlerExt whe = TIFFSetWarningHandlerExt(fastiv_io_tiff_warning); - struct fastiv_io_tiff h = { - .data = (unsigned char *) data, - .position = 0, - .len = len, - }; - - cairo_surface_t *result = NULL, *result_tail = NULL; - TIFF *tiff = TIFFClientOpen(path, "rm" /* Avoid mmap. */, &h, - fastiv_io_tiff_read, fastiv_io_tiff_write, fastiv_io_tiff_seek, - fastiv_io_tiff_close, fastiv_io_tiff_size, NULL, NULL); - if (!tiff) - goto fail; - - // In Nikon NEF files, IFD0 is a tiny uncompressed thumbnail with SubIFDs-- - // two of them JPEGs, the remaining one is raw. libtiff cannot read either - // of those better versions. - // - // TODO(p): If NewSubfileType is ReducedImage, and it has SubIFDs compressed - // as old JPEG (6), decode JPEGInterchangeFormat/JPEGInterchangeFormatLength - // with libjpeg-turbo and insert them as the starting pages. - // - // This is not possible with libtiff directly, because TIFFSetSubDirectory() - // requires an ImageLength tag that's missing, and TIFFReadCustomDirectory() - // takes a privately defined struct that cannot be omitted. - // - // TODO(p): Samsung Android DNGs also claim to be TIFF/EP, but use a smaller - // uncompressed YCbCr image. Apple ProRAW uses the new JPEG Compression (7), - // with a weird Orientation. It also uses that value for its raw data. - uint32_t subtype = 0; - uint16_t subifd_count = 0; - const uint64_t *subifd_offsets = NULL; - if (TIFFGetField(tiff, TIFFTAG_SUBFILETYPE, &subtype) && - (subtype & FILETYPE_REDUCEDIMAGE) && - TIFFGetField(tiff, TIFFTAG_SUBIFD, &subifd_count, &subifd_offsets) && - subifd_count > 0 && subifd_offsets) { - } - - do { - // We inform about unsupported directories, but do not fail on them. - GError *err = NULL; - if (!try_append_page( - load_libtiff_directory(tiff, &err), &result, &result_tail)) { - g_warning("%s: %s", path, err->message); - g_error_free(err); - } - } while (TIFFReadDirectory(tiff)); - TIFFClose(tiff); - -fail: - if (h.error) { - g_clear_pointer(&result, cairo_surface_destroy); - set_error(error, h.error); - g_free(h.error); - } else if (!result) { - set_error(error, "empty or unsupported image"); - } - - TIFFSetErrorHandlerExt(ehe); - TIFFSetWarningHandlerExt(whe); - TIFFSetErrorHandler(eh); - TIFFSetWarningHandler(wh); - return result; -} - -#endif // HAVE_LIBTIFF -------------------------------------------------------- -#ifdef HAVE_GDKPIXBUF // ------------------------------------------------------ - -static cairo_surface_t * -open_gdkpixbuf(const gchar *data, gsize len, GError **error) -{ - // gdk-pixbuf controls the playback itself, there is no reliable method of - // extracting individual frames (due to loops). - GInputStream *is = g_memory_input_stream_new_from_data(data, len, NULL); - GdkPixbuf *pixbuf = gdk_pixbuf_new_from_stream(is, NULL, error); - g_object_unref(is); - if (!pixbuf) - return NULL; - - cairo_surface_t *surface = - gdk_cairo_surface_create_from_pixbuf(pixbuf, 1, NULL); - - const char *orientation = gdk_pixbuf_get_option(pixbuf, "orientation"); - if (orientation && strlen(orientation) == 1) { - int n = *orientation - '0'; - if (n >= 1 && n <= 8) - cairo_surface_set_user_data(surface, &fastiv_io_key_orientation, - (void *) (uintptr_t) n, NULL); - } - - const char *icc_profile = gdk_pixbuf_get_option(pixbuf, "icc-profile"); - if (icc_profile) { - gsize out_len = 0; - guchar *raw = g_base64_decode(icc_profile, &out_len); - if (raw) { - cairo_surface_set_user_data(surface, &fastiv_io_key_icc, - g_bytes_new_take(raw, out_len), - (cairo_destroy_func_t) g_bytes_unref); - } - } - - g_object_unref(pixbuf); - return surface; -} - -#endif // HAVE_GDKPIXBUF ------------------------------------------------------ - -cairo_user_data_key_t fastiv_io_key_exif; -cairo_user_data_key_t fastiv_io_key_orientation; -cairo_user_data_key_t fastiv_io_key_icc; -cairo_user_data_key_t fastiv_io_key_xmp; - -cairo_user_data_key_t fastiv_io_key_frame_next; -cairo_user_data_key_t fastiv_io_key_frame_previous; -cairo_user_data_key_t fastiv_io_key_frame_duration; -cairo_user_data_key_t fastiv_io_key_loops; - -cairo_user_data_key_t fastiv_io_key_page_next; -cairo_user_data_key_t fastiv_io_key_page_previous; - -cairo_surface_t * -fastiv_io_open(const gchar *path, GError **error) -{ - // TODO(p): Don't always load everything into memory, test type first, - // so that we can reject non-pictures early. Wuffs only needs the first - // 16 bytes to make a guess right now. - // - // LibRaw poses an issue--there is no good registry for identification - // of supported files. Many of them are compliant TIFF files. - // The only good filtering method for RAWs are currently file extensions - // extracted from shared-mime-info. - // - // SVG is also problematic, an unbounded search for its root element. - // But problematic files can be assumed to be crafted. - // - // gdk-pixbuf exposes its detection data through gdk_pixbuf_get_formats(). - // This may also be unbounded, as per format_check(). - gchar *data = NULL; - gsize len = 0; - if (!g_file_get_contents(path, &data, &len, error)) - return NULL; - - cairo_surface_t *surface = fastiv_io_open_from_data(data, len, path, error); - free(data); - return surface; -} - -cairo_surface_t * -fastiv_io_open_from_data(const char *data, size_t len, const gchar *path, - GError **error) -{ - wuffs_base__slice_u8 prefix = - wuffs_base__make_slice_u8((uint8_t *) data, len); - - cairo_surface_t *surface = NULL; - switch (wuffs_base__magic_number_guess_fourcc(prefix)) { - case WUFFS_BASE__FOURCC__BMP: - // Note that BMP can redirect into another format, - // which is so far unsupported here. - surface = open_wuffs_using( - wuffs_bmp__decoder__alloc_as__wuffs_base__image_decoder, data, len, - error); - break; - case WUFFS_BASE__FOURCC__GIF: - surface = open_wuffs_using( - wuffs_gif__decoder__alloc_as__wuffs_base__image_decoder, data, len, - error); - break; - case WUFFS_BASE__FOURCC__PNG: - surface = open_wuffs_using( - wuffs_png__decoder__alloc_as__wuffs_base__image_decoder, data, len, - error); - break; - case WUFFS_BASE__FOURCC__JPEG: - surface = open_libjpeg_turbo(data, len, error); - break; - default: -#ifdef HAVE_LIBRAW // --------------------------------------------------------- - if ((surface = open_libraw(data, len, error))) - break; - - // TODO(p): We should try to pass actual processing errors through, - // notably only continue with LIBRAW_FILE_UNSUPPORTED. - if (error) { - g_debug("%s", (*error)->message); - g_clear_error(error); - } -#endif // HAVE_LIBRAW --------------------------------------------------------- -#ifdef HAVE_LIBRSVG // -------------------------------------------------------- - if ((surface = open_librsvg(data, len, path, error))) - break; - - // XXX: It doesn't look like librsvg can return sensible errors. - if (error) { - g_debug("%s", (*error)->message); - g_clear_error(error); - } -#endif // HAVE_LIBRSVG -------------------------------------------------------- -#ifdef HAVE_XCURSOR //--------------------------------------------------------- - if ((surface = open_xcursor(data, len, error))) - break; - if (error) { - g_debug("%s", (*error)->message); - g_clear_error(error); - } -#endif // HAVE_XCURSOR -------------------------------------------------------- -#ifdef HAVE_LIBWEBP //--------------------------------------------------------- - // TODO(p): https://github.com/google/wuffs/commit/4c04ac1 - if ((surface = open_libwebp(data, len, path, error))) - break; - if (error) { - g_debug("%s", (*error)->message); - g_clear_error(error); - } -#endif // HAVE_LIBWEBP -------------------------------------------------------- -#ifdef HAVE_LIBHEIF //--------------------------------------------------------- - if ((surface = open_libheif(data, len, path, error))) - break; - if (error) { - g_debug("%s", (*error)->message); - g_clear_error(error); - } -#endif // HAVE_LIBHEIF -------------------------------------------------------- -#ifdef HAVE_LIBTIFF //--------------------------------------------------------- - // This needs to be positioned after LibRaw. - if ((surface = open_libtiff(data, len, path, error))) - break; - if (error) { - g_debug("%s", (*error)->message); - g_clear_error(error); - } -#endif // HAVE_LIBTIFF -------------------------------------------------------- -#ifdef HAVE_GDKPIXBUF // ------------------------------------------------------ - // This is only used as a last resort, the rest above is special-cased. - if ((surface = open_gdkpixbuf(data, len, error))) - break; - if (error && (*error)->code != GDK_PIXBUF_ERROR_UNKNOWN_TYPE) - break; - - if (error) { - g_debug("%s", (*error)->message); - g_clear_error(error); - } -#endif // HAVE_GDKPIXBUF ------------------------------------------------------ - - set_error(error, "unsupported file type"); - } - - // gdk-pixbuf only gives out this single field--cater to its limitations, - // since we'd really like to have it. - // TODO(p): The Exif orientation should be ignored in JPEG-XL at minimum. - GBytes *exif = NULL; - gsize exif_len = 0; - gconstpointer exif_data = NULL; - if (surface && - (exif = cairo_surface_get_user_data(surface, &fastiv_io_key_exif)) && - (exif_data = g_bytes_get_data(exif, &exif_len))) { - cairo_surface_set_user_data(surface, &fastiv_io_key_orientation, - (void *) (uintptr_t) fastiv_io_exif_orientation( - exif_data, exif_len), NULL); - } - return surface; -} - -// --- Export ------------------------------------------------------------------ -#ifdef HAVE_LIBWEBP - -static WebPData -encode_lossless_webp(cairo_surface_t *surface) -{ - cairo_format_t format = cairo_image_surface_get_format(surface); - int w = cairo_image_surface_get_width(surface); - int h = cairo_image_surface_get_height(surface); - if (format != CAIRO_FORMAT_ARGB32 && - format != CAIRO_FORMAT_RGB24) { - cairo_surface_t *converted = - cairo_image_surface_create((format = CAIRO_FORMAT_ARGB32), w, h); - cairo_t *cr = cairo_create(converted); - cairo_set_source_surface(cr, surface, 0, 0); - cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); - cairo_paint(cr); - cairo_destroy(cr); - surface = converted; - } else { - surface = cairo_surface_reference(surface); - } - - WebPConfig config = {}; - WebPPicture picture = {}; - if (!WebPConfigInit(&config) || - !WebPConfigLosslessPreset(&config, 6) || - !WebPPictureInit(&picture)) - goto fail; - - config.thread_level = true; - if (!WebPValidateConfig(&config)) - goto fail; - - picture.use_argb = true; - picture.width = w; - picture.height = h; - if (!WebPPictureAlloc(&picture)) - goto fail; - - // Cairo uses a similar internal format, so we should be able to - // copy it over and fix up the minor differences. - // This is written to be easy to follow rather than fast. - int stride = cairo_image_surface_get_stride(surface); - if (picture.argb_stride != w || - picture.argb_stride * (int) sizeof *picture.argb != stride || - INT_MAX / picture.argb_stride < h) - goto fail_compatibility; - - uint32_t *argb = - memcpy(picture.argb, cairo_image_surface_get_data(surface), stride * h); - if (format == CAIRO_FORMAT_ARGB32) - for (int i = h * picture.argb_stride; i-- > 0; argb++) - *argb = wuffs_base__color_u32_argb_premul__as__color_u32_argb_nonpremul(*argb); - else - for (int i = h * picture.argb_stride; i-- > 0; argb++) - *argb |= 0xFF000000; - - WebPMemoryWriter writer = {}; - WebPMemoryWriterInit(&writer); - picture.writer = WebPMemoryWrite; - picture.custom_ptr = &writer; - if (!WebPEncode(&config, &picture)) - g_debug("WebPEncode: %d\n", picture.error_code); - -fail_compatibility: - WebPPictureFree(&picture); -fail: - cairo_surface_destroy(surface); - return (WebPData) {.bytes = writer.mem, .size = writer.size}; -} - -static gboolean -encode_webp_image(WebPMux *mux, cairo_surface_t *frame) -{ - WebPData bitstream = encode_lossless_webp(frame); - gboolean ok = WebPMuxSetImage(mux, &bitstream, true) == WEBP_MUX_OK; - WebPDataClear(&bitstream); - return ok; -} - -static gboolean -encode_webp_animation(WebPMux *mux, cairo_surface_t *page) -{ - gboolean ok = TRUE; - for (cairo_surface_t *frame = page; ok && frame; frame = - cairo_surface_get_user_data(frame, &fastiv_io_key_frame_next)) { - WebPMuxFrameInfo info = { - .bitstream = encode_lossless_webp(frame), - .duration = (intptr_t) cairo_surface_get_user_data( - frame, &fastiv_io_key_frame_duration), - .id = WEBP_CHUNK_ANMF, - .dispose_method = WEBP_MUX_DISPOSE_NONE, - .blend_method = WEBP_MUX_NO_BLEND, - }; - ok = WebPMuxPushFrame(mux, &info, true) == WEBP_MUX_OK; - WebPDataClear(&info.bitstream); - } - WebPMuxAnimParams params = { - .bgcolor = 0x00000000, // BGRA, curiously. - .loop_count = (uintptr_t) - cairo_surface_get_user_data(page, &fastiv_io_key_loops), - }; - return ok && WebPMuxSetAnimationParams(mux, ¶ms) == WEBP_MUX_OK; -} - -static gboolean -transfer_metadata(WebPMux *mux, const char *fourcc, cairo_surface_t *page, - const cairo_user_data_key_t *kind) -{ - GBytes *data = cairo_surface_get_user_data(page, kind); - if (!data) - return TRUE; - - gsize len = 0; - gconstpointer p = g_bytes_get_data(data, &len); - return WebPMuxSetChunk(mux, fourcc, &(WebPData) {.bytes = p, .size = len}, - false) == WEBP_MUX_OK; -} - -gboolean -fastiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, const gchar *path, - GError **error) -{ - g_return_val_if_fail(page != NULL, FALSE); - g_return_val_if_fail(path != NULL, FALSE); - - gboolean ok = TRUE; - WebPMux *mux = WebPMuxNew(); - if (frame) - ok = encode_webp_image(mux, frame); - else if (!cairo_surface_get_user_data(page, &fastiv_io_key_frame_next)) - ok = encode_webp_image(mux, page); - else - ok = encode_webp_animation(mux, page); - - ok = ok && transfer_metadata(mux, "EXIF", page, &fastiv_io_key_exif); - ok = ok && transfer_metadata(mux, "ICCP", page, &fastiv_io_key_icc); - ok = ok && transfer_metadata(mux, "XMP ", page, &fastiv_io_key_xmp); - - WebPData assembled = {}; - WebPDataInit(&assembled); - if (!(ok = ok && WebPMuxAssemble(mux, &assembled) == WEBP_MUX_OK)) - set_error(error, "encoding failed"); - else - ok = g_file_set_contents( - path, (const gchar *) assembled.bytes, assembled.size, error); - - WebPMuxDelete(mux); - WebPDataClear(&assembled); - return ok; -} - -#endif // HAVE_LIBWEBP -// --- Metadata ---------------------------------------------------------------- - -FastivIoOrientation -fastiv_io_exif_orientation(const guint8 *tiff, gsize len) -{ - // libtiff also knows how to do this, but it's not a lot of code. - // The "Orientation" tag/field is part of Baseline TIFF 6.0 (1992), - // it just so happens that Exif is derived from this format. - // There is no other meaningful placement for this than right in IFD0, - // describing the main image. - const uint8_t *end = tiff + len, - le[4] = {'I', 'I', 42, 0}, - be[4] = {'M', 'M', 0, 42}; - - uint16_t (*u16)(const uint8_t *) = NULL; - uint32_t (*u32)(const uint8_t *) = NULL; - if (tiff + 8 > end) { - return FastivIoOrientationUnknown; - } else if (!memcmp(tiff, le, sizeof le)) { - u16 = wuffs_base__peek_u16le__no_bounds_check; - u32 = wuffs_base__peek_u32le__no_bounds_check; - } else if (!memcmp(tiff, be, sizeof be)) { - u16 = wuffs_base__peek_u16be__no_bounds_check; - u32 = wuffs_base__peek_u32be__no_bounds_check; - } else { - return FastivIoOrientationUnknown; - } - - const uint8_t *ifd0 = tiff + u32(tiff + 4); - if (ifd0 + 2 > end) - return FastivIoOrientationUnknown; - - uint16_t fields = u16(ifd0); - enum { BYTE = 1, ASCII, SHORT, LONG, RATIONAL, - SBYTE, UNDEFINED, SSHORT, SLONG, SRATIONAL, FLOAT, DOUBLE }; - enum { Orientation = 274 }; - for (const guint8 *p = ifd0 + 2; fields-- && p + 12 <= end; p += 12) { - uint16_t tag = u16(p), type = u16(p + 2), value16 = u16(p + 8); - uint32_t count = u32(p + 4); - if (tag == Orientation && type == SHORT && count == 1 && - value16 >= 1 && value16 <= 8) - return value16; - } - return FastivIoOrientationUnknown; -} - -gboolean -fastiv_io_save_metadata( - cairo_surface_t *page, const gchar *path, GError **error) -{ - g_return_val_if_fail(page != NULL, FALSE); - - FILE *fp = fopen(path, "wb"); - if (!fp) { - g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), - "%s: %s", path, g_strerror(errno)); - return FALSE; - } - - // This does not constitute a valid JPEG codestream--it's a TEM marker - // (standalone) with trailing nonsense. - fprintf(fp, "\xFF\001Exiv2"); - - GBytes *data = NULL; - gsize len = 0; - gconstpointer p = NULL; - - // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 - // I don't care if Exiv2 supports it this way. - if ((data = cairo_surface_get_user_data(page, &fastiv_io_key_exif)) && - (p = g_bytes_get_data(data, &len))) { - while (len) { - gsize chunk = MIN(len, 0xFFFF - 2 - 6); - uint8_t header[10] = "\xFF\xE1\000\000Exif\000\000"; - header[2] = (chunk + 2 + 6) >> 8; - header[3] = (chunk + 2 + 6); - - fwrite(header, 1, sizeof header, fp); - fwrite(p, 1, chunk, fp); - - len -= chunk; - p += chunk; - } - } - - // https://www.color.org/specification/ICC1v43_2010-12.pdf B.4 - if ((data = cairo_surface_get_user_data(page, &fastiv_io_key_icc)) && - (p = g_bytes_get_data(data, &len))) { - gsize limit = 0xFFFF - 2 - 12; - uint8_t current = 0, total = (len + limit - 1) / limit; - while (len) { - gsize chunk = MIN(len, limit); - uint8_t header[18] = "\xFF\xE2\000\000ICC_PROFILE\000\000\000"; - header[2] = (chunk + 2 + 12 + 2) >> 8; - header[3] = (chunk + 2 + 12 + 2); - header[16] = ++current; - header[17] = total; - - fwrite(header, 1, sizeof header, fp); - fwrite(p, 1, chunk, fp); - - len -= chunk; - p += chunk; - } - } - - // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 - // If the main segment overflows, then it's a sign of bad luck, - // because 1.1.3.1 is way too complex. - if ((data = cairo_surface_get_user_data(page, &fastiv_io_key_xmp)) && - (p = g_bytes_get_data(data, &len))) { - while (len) { - gsize chunk = MIN(len, 0xFFFF - 2 - 29); - uint8_t header[33] = - "\xFF\xE1\000\000http://ns.adobe.com/xap/1.0/\000"; - header[2] = (chunk + 2 + 29) >> 8; - header[3] = (chunk + 2 + 29); - - fwrite(header, 1, sizeof header, fp); - fwrite(p, 1, chunk, fp); - break; - } - } - - fprintf(fp, "\xFF\xD9"); - if (ferror(fp)) { - g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), - "%s: %s", path, g_strerror(errno)); - fclose(fp); - return FALSE; - } - if (fclose(fp)) { - g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), - "%s: %s", path, g_strerror(errno)); - return FALSE; - } - return TRUE; -} - -// --- Thumbnails -------------------------------------------------------------- - -GType -fastiv_io_thumbnail_size_get_type(void) -{ - static gsize guard; - if (g_once_init_enter(&guard)) { -#define XX(name, value, dir) {FASTIV_IO_THUMBNAIL_SIZE_ ## name, \ - "FASTIV_IO_THUMBNAIL_SIZE_" #name, #name}, - static const GEnumValue values[] = {FASTIV_IO_THUMBNAIL_SIZES(XX) {}}; -#undef XX - GType type = g_enum_register_static( - g_intern_static_string("FastivIoThumbnailSize"), values); - g_once_init_leave(&guard, type); - } - return guard; -} - -#define XX(name, value, dir) {value, dir}, -FastivIoThumbnailSizeInfo - fastiv_io_thumbnail_sizes[FASTIV_IO_THUMBNAIL_SIZE_COUNT] = { - FASTIV_IO_THUMBNAIL_SIZES(XX)}; -#undef XX - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#ifndef __linux__ -#define st_mtim st_mtimespec -#endif // ! __linux__ - -static int // tri-state -check_spng_thumbnail_texts(struct spng_text *texts, uint32_t texts_len, - const gchar *target, time_t mtime) -{ - // May contain Thumb::Image::Width Thumb::Image::Height, - // but those aren't interesting currently (would be for fast previews). - bool need_uri = true, need_mtime = true; - for (uint32_t i = 0; i < texts_len; i++) { - struct spng_text *text = texts + i; - if (!strcmp(text->keyword, "Thumb::URI")) { - need_uri = false; - if (strcmp(target, text->text)) - return false; - } - if (!strcmp(text->keyword, "Thumb::MTime")) { - need_mtime = false; - if (atol(text->text) != mtime) - return false; - } - } - return need_uri || need_mtime ? -1 : true; -} - -static int // tri-state -check_spng_thumbnail(spng_ctx *ctx, const gchar *target, time_t mtime, int *err) -{ - uint32_t texts_len = 0; - if ((*err = spng_get_text(ctx, NULL, &texts_len))) - return false; - - int result = false; - struct spng_text *texts = g_malloc0_n(texts_len, sizeof *texts); - if (!(*err = spng_get_text(ctx, texts, &texts_len))) - result = check_spng_thumbnail_texts(texts, texts_len, target, mtime); - g_free(texts); - return result; -} - -static cairo_surface_t * -read_spng_thumbnail( - const gchar *path, const gchar *uri, time_t mtime, GError **error) -{ - FILE *fp; - cairo_surface_t *result = NULL; - if (!(fp = fopen(path, "rb"))) { - set_error(error, g_strerror(errno)); - return NULL; - } - - errno = 0; - spng_ctx *ctx = spng_ctx_new(0); - if (!ctx) { - set_error(error, g_strerror(errno)); - goto fail_init; - } - - int err; - size_t size = 0; - if ((err = spng_set_png_file(ctx, fp)) || - (err = spng_set_image_limits(ctx, INT16_MAX, INT16_MAX)) || - (err = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &size))) { - set_error(error, spng_strerror(err)); - goto fail; - } - if (check_spng_thumbnail(ctx, uri, mtime, &err) == false) { - set_error(error, err ? spng_strerror(err) : "mismatch"); - goto fail; - } - - struct spng_ihdr ihdr = {}; - spng_get_ihdr(ctx, &ihdr); - cairo_surface_t *surface = cairo_image_surface_create( - CAIRO_FORMAT_ARGB32, ihdr.width, ihdr.height); - - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - goto fail_data; - } - - uint32_t *data = (uint32_t *) cairo_image_surface_get_data(surface); - g_assert((size_t) cairo_image_surface_get_stride(surface) * - cairo_image_surface_get_height(surface) == size); - - cairo_surface_flush(surface); - if ((err = spng_decode_image(ctx, data, size, SPNG_FMT_RGBA8, - SPNG_DECODE_TRNS | SPNG_DECODE_GAMMA))) { - set_error(error, spng_strerror(err)); - goto fail_data; - } - - // The specification does not say where the required metadata should be, - // it could very well be broken up into two parts. - if (check_spng_thumbnail(ctx, uri, mtime, &err) != true) { - set_error( - error, err ? spng_strerror(err) : "mismatch or not a thumbnail"); - goto fail_data; - } - - // pixman can be mildly abused to do this operation, but it won't be faster. - struct spng_trns trns = {}; - if (ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA || - ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA || - !spng_get_trns(ctx, &trns)) { - for (size_t i = size / sizeof *data; i--; ) { - const uint8_t *unit = (const uint8_t *) &data[i]; - uint32_t a = unit[3], - b = PREMULTIPLY8(a, unit[2]), - g = PREMULTIPLY8(a, unit[1]), - r = PREMULTIPLY8(a, unit[0]); - data[i] = a << 24 | r << 16 | g << 8 | b; - } - } else { - for (size_t i = size / sizeof *data; i--; ) { - uint32_t rgba = g_ntohl(data[i]); - data[i] = rgba << 24 | rgba >> 8; - } - } - - cairo_surface_mark_dirty((result = surface)); - -fail_data: - if (!result) - cairo_surface_destroy(surface); -fail: - spng_ctx_free(ctx); -fail_init: - fclose(fp); - return result; -} - -cairo_surface_t * -fastiv_io_lookup_thumbnail(GFile *target, FastivIoThumbnailSize size) -{ - g_return_val_if_fail(size >= FASTIV_IO_THUMBNAIL_SIZE_MIN && - size <= FASTIV_IO_THUMBNAIL_SIZE_MAX, NULL); - - // Local files only, at least for now. - gchar *path = g_file_get_path(target); - if (!path) - return NULL; - - GStatBuf st = {}; - int err = g_stat(path, &st); - g_free(path); - if (err) - return NULL; - - gchar *uri = g_file_get_uri(target); - gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1); - gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache"); - - // The lookup sequence is: nominal..max, then mirroring back to ..min. - cairo_surface_t *result = NULL; - GError *error = NULL; - for (int i = 0; i < FASTIV_IO_THUMBNAIL_SIZE_COUNT; i++) { - int use = size + i; - if (use > FASTIV_IO_THUMBNAIL_SIZE_MAX) - use = FASTIV_IO_THUMBNAIL_SIZE_MAX - i; - - gchar *path = g_strdup_printf("%s/thumbnails/%s/%s.png", cache_dir, - fastiv_io_thumbnail_sizes[use].thumbnail_spec_name, sum); - result = read_spng_thumbnail(path, uri, st.st_mtim.tv_sec, &error); - if (error) { - g_debug("%s: %s", path, error->message); - g_clear_error(&error); - } - g_free(path); - if (result) - break; - } - - g_free(cache_dir); - g_free(sum); - g_free(uri); - return result; -} - -int -fastiv_io_filecmp(GFile *location1, GFile *location2) -{ - if (g_file_has_prefix(location1, location2)) - return +1; - if (g_file_has_prefix(location2, location1)) - return -1; - - gchar *name1 = g_file_get_parse_name(location1); - gchar *name2 = g_file_get_parse_name(location2); - int result = g_utf8_collate(name1, name2); - g_free(name1); - g_free(name2); - return result; -} diff --git a/fastiv-io.h b/fastiv-io.h deleted file mode 100644 index 01150a6..0000000 --- a/fastiv-io.h +++ /dev/null @@ -1,122 +0,0 @@ -// -// fastiv-io.h: image operations -// -// Copyright (c) 2021, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -// - -#pragma once - -#include -#include -#include - -extern const char *fastiv_io_supported_media_types[]; - -char **fastiv_io_all_supported_media_types(void); - -// Userdata are typically attached to all Cairo surfaces in an animation. - -/// GBytes with plain Exif/TIFF data. -extern cairo_user_data_key_t fastiv_io_key_exif; -/// FastivIoOrientation, as a uintptr_t. -extern cairo_user_data_key_t fastiv_io_key_orientation; -/// GBytes with plain ICC profile data. -extern cairo_user_data_key_t fastiv_io_key_icc; -/// GBytes with plain XMP data. -extern cairo_user_data_key_t fastiv_io_key_xmp; - -/// The next frame in a sequence, as a surface, in a chain, pre-composited. -/// There is no wrap-around. -extern cairo_user_data_key_t fastiv_io_key_frame_next; -/// The previous frame in a sequence, as a surface, in a chain, pre-composited. -/// This is a weak pointer that wraps around, and needn't be present -/// for static images. -extern cairo_user_data_key_t fastiv_io_key_frame_previous; -/// Frame duration in milliseconds as an intptr_t. -extern cairo_user_data_key_t fastiv_io_key_frame_duration; -/// How many times to repeat the animation, or zero for +inf, as a uintptr_t. -extern cairo_user_data_key_t fastiv_io_key_loops; - -/// The first frame of the next page, as a surface, in a chain. -/// There is no wrap-around. -extern cairo_user_data_key_t fastiv_io_key_page_next; -/// The first frame of the previous page, as a surface, in a chain. -/// There is no wrap-around. This is a weak pointer. -extern cairo_user_data_key_t fastiv_io_key_page_previous; - -cairo_surface_t *fastiv_io_open(const gchar *path, GError **error); -cairo_surface_t *fastiv_io_open_from_data( - const char *data, size_t len, const gchar *path, GError **error); - -int fastiv_io_filecmp(GFile *f1, GFile *f2); - -// --- Export ------------------------------------------------------------------ - -/// Requires libwebp. -/// If no exact frame is specified, this potentially creates an animation. -gboolean fastiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, - const gchar *path, GError **error); - -// --- Metadata ---------------------------------------------------------------- - -// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6 -typedef enum _FastivIoOrientation { - FastivIoOrientationUnknown = 0, - FastivIoOrientation0 = 1, - FastivIoOrientationMirror0 = 2, - FastivIoOrientation180 = 3, - FastivIoOrientationMirror180 = 4, - FastivIoOrientationMirror270 = 5, - FastivIoOrientation90 = 6, - FastivIoOrientationMirror90 = 7, - FastivIoOrientation270 = 8 -} FastivIoOrientation; - -FastivIoOrientation fastiv_io_exif_orientation(const guint8 *exif, gsize len); - -/// Save metadata attached by this module in Exiv2 format. -gboolean fastiv_io_save_metadata( - cairo_surface_t *page, const gchar *path, GError **error); - -// --- Thumbnails -------------------------------------------------------------- - -// And this is how you avoid glib-mkenums. -typedef enum _FastivIoThumbnailSize { -#define FASTIV_IO_THUMBNAIL_SIZES(XX) \ - XX(SMALL, 128, "normal") \ - XX(NORMAL, 256, "large") \ - XX(LARGE, 512, "x-large") \ - XX(HUGE, 1024, "xx-large") -#define XX(name, value, dir) FASTIV_IO_THUMBNAIL_SIZE_ ## name, - FASTIV_IO_THUMBNAIL_SIZES(XX) -#undef XX - FASTIV_IO_THUMBNAIL_SIZE_COUNT, - - FASTIV_IO_THUMBNAIL_SIZE_MIN = 0, - FASTIV_IO_THUMBNAIL_SIZE_MAX = FASTIV_IO_THUMBNAIL_SIZE_COUNT - 1 -} FastivIoThumbnailSize; - -GType fastiv_io_thumbnail_size_get_type(void) G_GNUC_CONST; -#define FASTIV_TYPE_IO_THUMBNAIL_SIZE (fastiv_io_thumbnail_size_get_type()) - -typedef struct _FastivIoThumbnailSizeInfo { - int size; ///< Nominal size in pixels - const char *thumbnail_spec_name; ///< thumbnail-spec directory name -} FastivIoThumbnailSizeInfo; - -extern FastivIoThumbnailSizeInfo - fastiv_io_thumbnail_sizes[FASTIV_IO_THUMBNAIL_SIZE_COUNT]; - -cairo_surface_t *fastiv_io_lookup_thumbnail( - GFile *target, FastivIoThumbnailSize size); diff --git a/fastiv-sidebar.c b/fastiv-sidebar.c deleted file mode 100644 index ecfec3e..0000000 --- a/fastiv-sidebar.c +++ /dev/null @@ -1,433 +0,0 @@ -// -// fastiv-sidebar.c: molesting GtkPlacesSidebar -// -// Copyright (c) 2021, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -// - -#include - -#include "fastiv-io.h" // fastiv_io_filecmp -#include "fastiv-sidebar.h" - -struct _FastivSidebar { - GtkScrolledWindow parent_instance; - GtkPlacesSidebar *places; - GtkWidget *toolbar; - GtkWidget *listbox; - GFile *location; -}; - -G_DEFINE_TYPE(FastivSidebar, fastiv_sidebar, GTK_TYPE_SCROLLED_WINDOW) - -G_DEFINE_QUARK(fastiv-sidebar-location-quark, fastiv_sidebar_location) - -enum { - OPEN_LOCATION, - LAST_SIGNAL, -}; - -// Globals are, sadly, the canonical way of storing signal numbers. -static guint sidebar_signals[LAST_SIGNAL]; - -static void -fastiv_sidebar_dispose(GObject *gobject) -{ - FastivSidebar *self = FASTIV_SIDEBAR(gobject); - g_clear_object(&self->location); - - G_OBJECT_CLASS(fastiv_sidebar_parent_class)->dispose(gobject); -} - -static void -fastiv_sidebar_class_init(FastivSidebarClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS(klass); - object_class->dispose = fastiv_sidebar_dispose; - - // You're giving me no choice, Adwaita. - // Your style is hardcoded to match against the class' CSS name. - // And I need replicate the internal widget structure. - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); - gtk_widget_class_set_css_name(widget_class, "placessidebar"); - - // TODO(p): Consider a return value, and using it. - sidebar_signals[OPEN_LOCATION] = - g_signal_new("open_location", G_TYPE_FROM_CLASS(klass), 0, 0, - NULL, NULL, NULL, G_TYPE_NONE, - 2, G_TYPE_FILE, GTK_TYPE_PLACES_OPEN_FLAGS); -} - -static gboolean -on_rowlabel_query_tooltip(GtkWidget *widget, - G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y, - G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip) -{ - GtkLabel *label = GTK_LABEL(widget); - if (!pango_layout_is_ellipsized(gtk_label_get_layout(label))) - return FALSE; - - gtk_tooltip_set_text(tooltip, gtk_label_get_text(label)); - return TRUE; -} - -static GtkWidget * -create_row(GFile *file, const char *icon_name) -{ - // TODO(p): Handle errors better. - GFileInfo *info = - g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, - G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); - if (!info) - return NULL; - - const char *name = g_file_info_get_display_name(info); - GtkWidget *rowbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - - GtkWidget *rowimage = - gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU); - gtk_style_context_add_class( - gtk_widget_get_style_context(rowimage), "sidebar-icon"); - gtk_container_add(GTK_CONTAINER(rowbox), rowimage); - - GtkWidget *rowlabel = gtk_label_new(name); - gtk_label_set_ellipsize(GTK_LABEL(rowlabel), PANGO_ELLIPSIZE_END); - gtk_widget_set_has_tooltip(rowlabel, TRUE); - g_signal_connect(rowlabel, "query-tooltip", - G_CALLBACK(on_rowlabel_query_tooltip), NULL); - gtk_style_context_add_class( - gtk_widget_get_style_context(rowlabel), "sidebar-label"); - gtk_container_add(GTK_CONTAINER(rowbox), rowlabel); - - GtkWidget *revealer = gtk_revealer_new(); - gtk_revealer_set_reveal_child( - GTK_REVEALER(revealer), TRUE); - gtk_revealer_set_transition_type( - GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_NONE); - gtk_container_add(GTK_CONTAINER(revealer), rowbox); - - GtkWidget *row = gtk_list_box_row_new(); - g_object_set_qdata_full(G_OBJECT(row), fastiv_sidebar_location_quark(), - g_object_ref(file), (GDestroyNotify) g_object_unref); - gtk_container_add(GTK_CONTAINER(row), revealer); - gtk_widget_show_all(row); - return row; -} - -static gint -listbox_compare( - GtkListBoxRow *row1, GtkListBoxRow *row2, G_GNUC_UNUSED gpointer user_data) -{ - return fastiv_io_filecmp( - g_object_get_qdata(G_OBJECT(row1), fastiv_sidebar_location_quark()), - g_object_get_qdata(G_OBJECT(row2), fastiv_sidebar_location_quark())); -} - -static void -update_location(FastivSidebar *self, GFile *location) -{ - if (location) { - g_clear_object(&self->location); - self->location = g_object_ref(location); - } - - gtk_places_sidebar_set_location(self->places, self->location); - gtk_container_foreach(GTK_CONTAINER(self->listbox), - (GtkCallback) gtk_widget_destroy, NULL); - g_return_if_fail(self->location != NULL); - - GFile *iter = g_object_ref(self->location); - while (TRUE) { - GFile *parent = g_file_get_parent(iter); - g_object_unref(iter); - if (!(iter = parent)) - break; - - gtk_list_box_prepend(GTK_LIST_BOX(self->listbox), - create_row(parent, "go-up-symbolic")); - } - - // Other options are "folder-{visiting,open}-symbolic", though the former - // is mildly inappropriate (means: open in another window). - gtk_container_add(GTK_CONTAINER(self->listbox), - create_row(self->location, "circle-filled-symbolic")); - - GFileEnumerator *enumerator = g_file_enumerate_children(self->location, - G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME - "," G_FILE_ATTRIBUTE_STANDARD_NAME - "," G_FILE_ATTRIBUTE_STANDARD_TYPE - "," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - if (!enumerator) - return; - - // TODO(p): gtk_list_box_set_filter_func(), or even use a model, - // which could be shared with FastivBrowser. - while (TRUE) { - GFileInfo *info = NULL; - GFile *child = NULL; - if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) || - !info) - break; - - if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY && - !g_file_info_get_is_hidden(info)) - gtk_container_add(GTK_CONTAINER(self->listbox), - create_row(child, "go-down-symbolic")); - } - g_object_unref(enumerator); -} - -static void -on_open_breadcrumb( - G_GNUC_UNUSED GtkListBox *listbox, GtkListBoxRow *row, gpointer user_data) -{ - FastivSidebar *self = FASTIV_SIDEBAR(user_data); - GFile *location = - g_object_get_qdata(G_OBJECT(row), fastiv_sidebar_location_quark()); - g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, - location, GTK_PLACES_OPEN_NORMAL); -} - -static void -on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location, - GtkPlacesOpenFlags flags, gpointer user_data) -{ - FastivSidebar *self = FASTIV_SIDEBAR(user_data); - g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, location, flags); - - // Deselect the item in GtkPlacesSidebar, if unsuccessful. - update_location(self, NULL); -} - -static void -complete_path(GFile *location, GtkListStore *model) -{ - // TODO(p): Do not enter directories unless followed by '/'. - // This information has already been stripped from `location`. - GFile *parent = G_FILE_TYPE_DIRECTORY == - g_file_query_file_type(location, G_FILE_QUERY_INFO_NONE, NULL) - ? g_object_ref(location) - : g_file_get_parent(location); - if (!parent) - return; - - GFileEnumerator *enumerator = g_file_enumerate_children(parent, - G_FILE_ATTRIBUTE_STANDARD_NAME - "," G_FILE_ATTRIBUTE_STANDARD_TYPE - "," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - if (!enumerator) - goto fail_enumerator; - - while (TRUE) { - GFileInfo *info = NULL; - GFile *child = NULL; - if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) || - !info) - break; - - if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY || - g_file_info_get_is_hidden(info)) - continue; - - char *parse_name = g_file_get_parse_name(child); - if (!g_str_has_suffix(parse_name, G_DIR_SEPARATOR_S)) { - char *save = parse_name; - parse_name = g_strdup_printf("%s%c", parse_name, G_DIR_SEPARATOR); - g_free(save); - } - gtk_list_store_insert_with_values(model, NULL, -1, 0, parse_name, -1); - g_free(parse_name); - } - - g_object_unref(enumerator); -fail_enumerator: - g_object_unref(parent); -} - -static GFile * -resolve_location(FastivSidebar *self, const char *text) -{ - // Relative paths produce invalid GFile objects with this function. - // And even if they didn't, we have our own root for them. - GFile *file = g_file_parse_name(text); - if (g_uri_is_valid(text, G_URI_FLAGS_PARSE_RELAXED, NULL) || - g_file_peek_path(file)) - return file; - - GFile *absolute = - g_file_get_child_for_display_name(self->location, text, NULL); - if (!absolute) - return file; - - g_object_unref(file); - return absolute; -} - -static void -on_enter_location_changed(GtkEntry *entry, gpointer user_data) -{ - FastivSidebar *self = FASTIV_SIDEBAR(user_data); - const char *text = gtk_entry_get_text(entry); - GFile *location = resolve_location(self, text); - - // Don't touch the network anywhere around here, URIs are a no-no. - GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(entry)); - if (!g_file_peek_path(location) || g_file_query_exists(location, NULL)) - gtk_style_context_remove_class(style, GTK_STYLE_CLASS_WARNING); - else - gtk_style_context_add_class(style, GTK_STYLE_CLASS_WARNING); - - // XXX: For some reason, this jumps around with longer lists. - GtkEntryCompletion *completion = gtk_entry_get_completion(entry); - GtkTreeModel *model = gtk_entry_completion_get_model(completion); - gtk_list_store_clear(GTK_LIST_STORE(model)); - if (g_file_peek_path(location)) - complete_path(location, GTK_LIST_STORE(model)); - g_object_unref(location); -} - -static void -on_show_enter_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, - G_GNUC_UNUSED gpointer user_data) -{ - FastivSidebar *self = FASTIV_SIDEBAR(user_data); - GtkWidget *dialog = gtk_dialog_new_with_buttons("Enter location", - GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(self))), - GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL | - GTK_DIALOG_USE_HEADER_BAR, - "_Open", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_CANCEL, NULL); - - GtkListStore *model = gtk_list_store_new(1, G_TYPE_STRING); - gtk_tree_sortable_set_sort_column_id( - GTK_TREE_SORTABLE(model), 0, GTK_SORT_ASCENDING); - - GtkEntryCompletion *completion = gtk_entry_completion_new(); - gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model)); - gtk_entry_completion_set_text_column(completion, 0); - // TODO(p): Complete ~ paths so that they start with ~, then we can filter. - gtk_entry_completion_set_match_func( - completion, (GtkEntryCompletionMatchFunc) gtk_true, NULL, NULL); - g_object_unref(model); - - GtkWidget *entry = gtk_entry_new(); - gtk_entry_set_completion(GTK_ENTRY(entry), completion); - gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); - g_signal_connect(entry, "changed", - G_CALLBACK(on_enter_location_changed), self); - - GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); - gtk_container_add(GTK_CONTAINER(content), entry); - gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); - gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE); - gtk_window_set_default_size(GTK_WINDOW(dialog), 800, -1); - - GdkGeometry geometry = {.max_width = G_MAXSHORT, .max_height = -1}; - gtk_window_set_geometry_hints( - GTK_WINDOW(dialog), NULL, &geometry, GDK_HINT_MAX_SIZE); - gtk_widget_show_all(dialog); - - if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { - const char *text = gtk_entry_get_text(GTK_ENTRY(entry)); - GFile *location = resolve_location(self, text); - g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, - location, GTK_PLACES_OPEN_NORMAL); - g_object_unref(location); - } - gtk_widget_destroy(dialog); - g_object_unref(completion); - - // Deselect the item in GtkPlacesSidebar, if unsuccessful. - update_location(self, NULL); -} - -static void -fastiv_sidebar_init(FastivSidebar *self) -{ - // TODO(p): Transplant functionality from the shitty GtkPlacesSidebar. - // We cannot reasonably place any new items within its own GtkListBox, - // so we need to replicate the style hierarchy to some extent. - self->places = GTK_PLACES_SIDEBAR(gtk_places_sidebar_new()); - gtk_places_sidebar_set_show_recent(self->places, FALSE); - gtk_places_sidebar_set_show_trash(self->places, FALSE); - gtk_places_sidebar_set_open_flags(self->places, - GTK_PLACES_OPEN_NORMAL | GTK_PLACES_OPEN_NEW_WINDOW); - g_signal_connect(self->places, "open-location", - G_CALLBACK(on_open_location), self); - - gtk_places_sidebar_set_show_enter_location(self->places, TRUE); - g_signal_connect(self->places, "show-enter-location", - G_CALLBACK(on_show_enter_location), self); - gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->places), - GTK_POLICY_NEVER, GTK_POLICY_NEVER); - - // None of GtkActionBar, GtkToolbar, .inline-toolbar is appropriate. - // It is either side-favouring borders or excess button padding. - self->toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); - gtk_style_context_add_class( - gtk_widget_get_style_context(self->toolbar), GTK_STYLE_CLASS_TOOLBAR); - - self->listbox = gtk_list_box_new(); - gtk_list_box_set_selection_mode( - GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE); - g_signal_connect(self->listbox, "row-activated", - G_CALLBACK(on_open_breadcrumb), self); - gtk_list_box_set_sort_func( - GTK_LIST_BOX(self->listbox), listbox_compare, self, NULL); - - // Fill up what would otherwise be wasted space, - // as it is in the examples of Nautilus and Thunar. - GtkWidget *superbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - gtk_container_add( - GTK_CONTAINER(superbox), GTK_WIDGET(self->places)); - gtk_container_add( - GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL)); - gtk_container_add( - GTK_CONTAINER(superbox), self->toolbar); - gtk_container_add( - GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL)); - gtk_container_add( - GTK_CONTAINER(superbox), self->listbox); - gtk_container_add(GTK_CONTAINER(self), superbox); - - gtk_scrolled_window_set_policy( - GTK_SCROLLED_WINDOW(self), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); - gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)), - GTK_STYLE_CLASS_SIDEBAR); - gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)), - "fastiv"); -} - -// --- Public interface -------------------------------------------------------- - -void -fastiv_sidebar_set_location(FastivSidebar *self, GFile *location) -{ - g_return_if_fail(FASTIV_IS_SIDEBAR(self)); - update_location(self, location); -} - -void -fastiv_sidebar_show_enter_location(FastivSidebar *self) -{ - g_return_if_fail(FASTIV_IS_SIDEBAR(self)); - g_signal_emit_by_name(self->places, "show-enter-location"); -} - -GtkBox * -fastiv_sidebar_get_toolbar(FastivSidebar *self) -{ - g_return_val_if_fail(FASTIV_IS_SIDEBAR(self), NULL); - return GTK_BOX(self->toolbar); -} diff --git a/fastiv-sidebar.h b/fastiv-sidebar.h deleted file mode 100644 index 447dce2..0000000 --- a/fastiv-sidebar.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// fastiv-sidebar.h: molesting GtkPlacesSidebar -// -// Copyright (c) 2021, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -// - -#pragma once - -#include - -#define FASTIV_TYPE_SIDEBAR (fastiv_sidebar_get_type()) -G_DECLARE_FINAL_TYPE( - FastivSidebar, fastiv_sidebar, FASTIV, SIDEBAR, GtkScrolledWindow) - -void fastiv_sidebar_set_location(FastivSidebar *self, GFile *location); -void fastiv_sidebar_show_enter_location(FastivSidebar *self); -GtkBox *fastiv_sidebar_get_toolbar(FastivSidebar *self); diff --git a/fastiv-view.c b/fastiv-view.c deleted file mode 100644 index 5039aec..0000000 --- a/fastiv-view.c +++ /dev/null @@ -1,923 +0,0 @@ -// -// fastiv-view.c: fast image viewer - view widget -// -// Copyright (c) 2021, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -// - -#include "config.h" - -#include -#include - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif // GDK_WINDOWING_X11 -#ifdef GDK_WINDOWING_QUARTZ -#include -#endif // GDK_WINDOWING_QUARTZ - -#include "fastiv-io.h" -#include "fastiv-view.h" - -struct _FastivView { - GtkWidget parent_instance; - cairo_surface_t *image; ///< The loaded image (sequence) - cairo_surface_t *page; ///< Current page within image, weak - cairo_surface_t *frame; ///< Current frame within page, weak - FastivIoOrientation orientation; ///< Current page orientation - bool filter; ///< Smooth scaling toggle - bool scale_to_fit; ///< Image no larger than the allocation - double scale; ///< Scaling factor - - int remaining_loops; ///< Greater than zero if limited - gint64 frame_time; ///< Current frame's start, µs precision - gulong frame_update_connection; ///< GdkFrameClock::update -}; - -G_DEFINE_TYPE(FastivView, fastiv_view, GTK_TYPE_WIDGET) - -static FastivIoOrientation view_left[9] = { - [FastivIoOrientationUnknown] = FastivIoOrientationUnknown, - [FastivIoOrientation0] = FastivIoOrientation270, - [FastivIoOrientationMirror0] = FastivIoOrientationMirror270, - [FastivIoOrientation180] = FastivIoOrientation90, - [FastivIoOrientationMirror180] = FastivIoOrientationMirror90, - [FastivIoOrientationMirror270] = FastivIoOrientationMirror180, - [FastivIoOrientation90] = FastivIoOrientation0, - [FastivIoOrientationMirror90] = FastivIoOrientationMirror0, - [FastivIoOrientation270] = FastivIoOrientation180 -}; - -static FastivIoOrientation view_mirror[9] = { - [FastivIoOrientationUnknown] = FastivIoOrientationUnknown, - [FastivIoOrientation0] = FastivIoOrientationMirror0, - [FastivIoOrientationMirror0] = FastivIoOrientation0, - [FastivIoOrientation180] = FastivIoOrientationMirror180, - [FastivIoOrientationMirror180] = FastivIoOrientation180, - [FastivIoOrientationMirror270] = FastivIoOrientation270, - [FastivIoOrientation90] = FastivIoOrientationMirror270, - [FastivIoOrientationMirror90] = FastivIoOrientation90, - [FastivIoOrientation270] = FastivIoOrientationMirror270 -}; - -static FastivIoOrientation view_right[9] = { - [FastivIoOrientationUnknown] = FastivIoOrientationUnknown, - [FastivIoOrientation0] = FastivIoOrientation90, - [FastivIoOrientationMirror0] = FastivIoOrientationMirror90, - [FastivIoOrientation180] = FastivIoOrientation270, - [FastivIoOrientationMirror180] = FastivIoOrientationMirror270, - [FastivIoOrientationMirror270] = FastivIoOrientationMirror0, - [FastivIoOrientation90] = FastivIoOrientation180, - [FastivIoOrientationMirror90] = FastivIoOrientationMirror180, - [FastivIoOrientation270] = FastivIoOrientation0 -}; - -enum { - PROP_SCALE = 1, - PROP_SCALE_TO_FIT, - PROP_FILTER, - N_PROPERTIES -}; - -static GParamSpec *view_properties[N_PROPERTIES]; - -static void -fastiv_view_finalize(GObject *gobject) -{ - FastivView *self = FASTIV_VIEW(gobject); - cairo_surface_destroy(self->image); - - G_OBJECT_CLASS(fastiv_view_parent_class)->finalize(gobject); -} - -static void -fastiv_view_get_property( - GObject *object, guint property_id, GValue *value, GParamSpec *pspec) -{ - FastivView *self = FASTIV_VIEW(object); - switch (property_id) { - case PROP_SCALE: - g_value_set_double(value, self->scale); - break; - case PROP_SCALE_TO_FIT: - g_value_set_boolean(value, self->scale_to_fit); - break; - case PROP_FILTER: - g_value_set_boolean(value, self->filter); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); - } -} - -static void -get_surface_dimensions(FastivView *self, double *width, double *height) -{ - *width = *height = 0; - if (!self->image) - return; - - cairo_rectangle_t extents = {}; - switch (cairo_surface_get_type(self->page)) { - case CAIRO_SURFACE_TYPE_IMAGE: - switch (self->orientation) { - case FastivIoOrientation90: - case FastivIoOrientationMirror90: - case FastivIoOrientation270: - case FastivIoOrientationMirror270: - *width = cairo_image_surface_get_height(self->page); - *height = cairo_image_surface_get_width(self->page); - break; - default: - *width = cairo_image_surface_get_width(self->page); - *height = cairo_image_surface_get_height(self->page); - } - return; - case CAIRO_SURFACE_TYPE_RECORDING: - if (!cairo_recording_surface_get_extents(self->page, &extents)) { - cairo_recording_surface_ink_extents(self->page, - &extents.x, &extents.y, &extents.width, &extents.height); - } - - *width = extents.width; - *height = extents.height; - return; - default: - g_assert_not_reached(); - } -} - -static void -get_display_dimensions(FastivView *self, int *width, int *height) -{ - double w, h; - get_surface_dimensions(self, &w, &h); - - *width = ceil(w * self->scale); - *height = ceil(h * self->scale); -} - -static cairo_matrix_t -get_orientation_matrix(FastivIoOrientation o, double width, double height) -{ - cairo_matrix_t matrix = {}; - cairo_matrix_init_identity(&matrix); - switch (o) { - case FastivIoOrientation90: - cairo_matrix_rotate(&matrix, -M_PI_2); - cairo_matrix_translate(&matrix, -width, 0); - break; - case FastivIoOrientation180: - cairo_matrix_scale(&matrix, -1, -1); - cairo_matrix_translate(&matrix, -width, -height); - break; - case FastivIoOrientation270: - cairo_matrix_rotate(&matrix, +M_PI_2); - cairo_matrix_translate(&matrix, 0, -height); - break; - case FastivIoOrientationMirror0: - cairo_matrix_scale(&matrix, -1, +1); - cairo_matrix_translate(&matrix, -width, 0); - break; - case FastivIoOrientationMirror90: - cairo_matrix_rotate(&matrix, +M_PI_2); - cairo_matrix_scale(&matrix, -1, +1); - cairo_matrix_translate(&matrix, -width, -height); - break; - case FastivIoOrientationMirror180: - cairo_matrix_scale(&matrix, +1, -1); - cairo_matrix_translate(&matrix, 0, -height); - break; - case FastivIoOrientationMirror270: - cairo_matrix_rotate(&matrix, -M_PI_2); - cairo_matrix_scale(&matrix, -1, +1); - default: - break; - } - return matrix; -} - -static void -fastiv_view_get_preferred_height( - GtkWidget *widget, gint *minimum, gint *natural) -{ - FastivView *self = FASTIV_VIEW(widget); - if (self->scale_to_fit) { - double sw, sh; - get_surface_dimensions(self, &sw, &sh); - *natural = ceil(sh); - *minimum = 1; - } else { - int dw, dh; - get_display_dimensions(self, &dw, &dh); - *minimum = *natural = dh; - } -} - -static void -fastiv_view_get_preferred_width(GtkWidget *widget, gint *minimum, gint *natural) -{ - FastivView *self = FASTIV_VIEW(widget); - if (self->scale_to_fit) { - double sw, sh; - get_surface_dimensions(self, &sw, &sh); - *natural = ceil(sw); - *minimum = 1; - } else { - int dw, dh; - get_display_dimensions(self, &dw, &dh); - *minimum = *natural = dw; - } -} - -static void -fastiv_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation) -{ - GTK_WIDGET_CLASS(fastiv_view_parent_class) - ->size_allocate(widget, allocation); - - FastivView *self = FASTIV_VIEW(widget); - if (!self->image || !self->scale_to_fit) - return; - - double w, h; - get_surface_dimensions(self, &w, &h); - - self->scale = 1; - if (ceil(w * self->scale) > allocation->width) - self->scale = allocation->width / w; - if (ceil(h * self->scale) > allocation->height) - self->scale = allocation->height / h; - g_object_notify_by_pspec(G_OBJECT(widget), view_properties[PROP_SCALE]); -} - -static void -fastiv_view_realize(GtkWidget *widget) -{ - GtkAllocation allocation; - gtk_widget_get_allocation(widget, &allocation); - - GdkWindowAttr attributes = { - .window_type = GDK_WINDOW_CHILD, - .x = allocation.x, - .y = allocation.y, - .width = allocation.width, - .height = allocation.height, - - // Input-only would presumably also work (as in GtkPathBar, e.g.), - // but it merely seems to involve more work. - .wclass = GDK_INPUT_OUTPUT, - - // Assuming here that we can't ask for a higher-precision Visual - // than what we get automatically. - .visual = gtk_widget_get_visual(widget), - .event_mask = gtk_widget_get_events(widget) | GDK_SCROLL_MASK | - GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK, - }; - - // We need this window to receive input events at all. - GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget), - &attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); - - // Without the following call, or the rendering mode set to "recording", - // RGB30 degrades to RGB24, because gdk_window_begin_paint_internal() - // creates backing stores using cairo_content_t constants. - // - // It completely breaks the Quartz backend, so limit it to X11. -#ifdef GDK_WINDOWING_X11 - // FIXME: This causes some flicker while scrolling, because it disables - // double buffering, see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560 - // - // If GTK+'s OpenGL integration fails to deliver, we need to use the window - // directly, sidestepping the toolkit entirely. - if (GDK_IS_X11_WINDOW(window)) - gdk_window_ensure_native(window); -#endif // GDK_WINDOWING_X11 - - gtk_widget_register_window(widget, window); - gtk_widget_set_window(widget, window); - gtk_widget_set_realized(widget, TRUE); -} - -static gboolean -fastiv_view_draw(GtkWidget *widget, cairo_t *cr) -{ - // Placed here due to our using a native GdkWindow on X11, - // which makes the widget have no double buffering or default background. - GtkAllocation allocation; - gtk_widget_get_allocation(widget, &allocation); - gtk_render_background(gtk_widget_get_style_context(widget), cr, 0, 0, - allocation.width, allocation.height); - - FastivView *self = FASTIV_VIEW(widget); - if (!self->image || - !gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget))) - return TRUE; - - int w, h; - double sw, sh; - get_display_dimensions(self, &w, &h); - get_surface_dimensions(self, &sw, &sh); - - double x = 0; - double y = 0; - if (w < allocation.width) - x = round((allocation.width - w) / 2.); - if (h < allocation.height) - y = round((allocation.height - h) / 2.); - - // FIXME: Recording surfaces do not work well with CAIRO_SURFACE_TYPE_XLIB, - // we always get a shitty pixmap, where transparency contains junk. - if (cairo_surface_get_type(self->frame) == CAIRO_SURFACE_TYPE_RECORDING) { - cairo_surface_t *image = - cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); - cairo_t *tcr = cairo_create(image); - cairo_scale(tcr, self->scale, self->scale); - cairo_set_source_surface(tcr, self->frame, 0, 0); - cairo_paint(tcr); - cairo_destroy(tcr); - - cairo_set_source_surface(cr, image, x, y); - cairo_paint(cr); - cairo_surface_destroy(image); - return TRUE; - } - - // XXX: The rounding together with padding may result in up to - // a pixel's worth of made-up picture data. - cairo_rectangle(cr, x, y, w, h); - cairo_clip(cr); - - cairo_translate(cr, x, y); - cairo_scale(cr, self->scale, self->scale); - cairo_set_source_surface(cr, self->frame, 0, 0); - - cairo_matrix_t matrix = get_orientation_matrix(self->orientation, sw, sh); - cairo_pattern_t *pattern = cairo_get_source(cr); - cairo_pattern_set_matrix(pattern, &matrix); - cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD); - - // TODO(p): Prescale it ourselves to an off-screen bitmap, gamma-correctly. - if (self->filter) - cairo_pattern_set_filter(pattern, CAIRO_FILTER_GOOD); - else - cairo_pattern_set_filter(pattern, CAIRO_FILTER_NEAREST); - -#ifdef GDK_WINDOWING_QUARTZ - // Not supported there. Acts a bit like repeating, but weirdly offset. - if (GDK_IS_QUARTZ_WINDOW(gtk_widget_get_window(widget))) - cairo_pattern_set_extend(pattern, CAIRO_EXTEND_NONE); -#endif // GDK_WINDOWING_QUARTZ - - cairo_paint(cr); - return TRUE; -} - -static gboolean -fastiv_view_button_press_event(GtkWidget *widget, GdkEventButton *event) -{ - GTK_WIDGET_CLASS(fastiv_view_parent_class) - ->button_press_event(widget, event); - - if (event->button == GDK_BUTTON_PRIMARY && - gtk_widget_get_focus_on_click(widget)) - gtk_widget_grab_focus(widget); - - // TODO(p): Use for left button scroll drag, which may rather be a gesture. - return FALSE; -} - -#define SCALE_STEP 1.4 - -static gboolean -set_scale_to_fit(FastivView *self, bool scale_to_fit) -{ - self->scale_to_fit = scale_to_fit; - - gtk_widget_queue_resize(GTK_WIDGET(self)); - g_object_notify_by_pspec( - G_OBJECT(self), view_properties[PROP_SCALE_TO_FIT]); - return TRUE; -} - -static gboolean -set_scale(FastivView *self, double scale) -{ - self->scale = scale; - g_object_notify_by_pspec( - G_OBJECT(self), view_properties[PROP_SCALE]); - return set_scale_to_fit(self, false); -} - -static gboolean -fastiv_view_scroll_event(GtkWidget *widget, GdkEventScroll *event) -{ - FastivView *self = FASTIV_VIEW(widget); - if (!self->image) - return FALSE; - if (event->state & gtk_accelerator_get_default_mod_mask()) - return FALSE; - - switch (event->direction) { - case GDK_SCROLL_UP: - return set_scale(self, self->scale * SCALE_STEP); - case GDK_SCROLL_DOWN: - return set_scale(self, self->scale / SCALE_STEP); - default: - // For some reason, we can also get GDK_SCROLL_SMOOTH. - // Left/right are good to steal from GtkScrolledWindow for consistency. - return TRUE; - } -} - -static void -stop_animating(FastivView *self) -{ - GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self)); - if (!clock || !self->frame_update_connection) - return; - - g_signal_handler_disconnect(clock, self->frame_update_connection); - gdk_frame_clock_end_updating(clock); - - self->frame_time = 0; - self->frame_update_connection = 0; - self->remaining_loops = 0; -} - -static gboolean -advance_frame(FastivView *self) -{ - cairo_surface_t *next = - cairo_surface_get_user_data(self->frame, &fastiv_io_key_frame_next); - if (next) { - self->frame = next; - } else { - if (self->remaining_loops && !--self->remaining_loops) - return FALSE; - - self->frame = self->page; - } - return TRUE; -} - -static gboolean -advance_animation(FastivView *self, GdkFrameClock *clock) -{ - gint64 now = gdk_frame_clock_get_frame_time(clock); - while (true) { - // TODO(p): See if infinite frames can actually happen, and how. - intptr_t duration = (intptr_t) cairo_surface_get_user_data( - self->frame, &fastiv_io_key_frame_duration); - if (duration < 0) - return FALSE; - - // Do not busy loop. GIF timings are given in hundredths of a second. - // Note that browsers seem to do [< 10] => 100: - // https://bugs.webkit.org/show_bug.cgi?id=36082 - if (duration == 0) - duration = gdk_frame_timings_get_refresh_interval( - gdk_frame_clock_get_current_timings(clock)) / 1000; - if (duration == 0) - duration = 1; - - gint64 then = self->frame_time + duration * 1000; - if (then > now) - return TRUE; - if (!advance_frame(self)) - return FALSE; - - self->frame_time = then; - gtk_widget_queue_draw(GTK_WIDGET(self)); - } -} - -static void -on_frame_clock_update(GdkFrameClock *clock, gpointer user_data) -{ - FastivView *self = FASTIV_VIEW(user_data); - if (!advance_animation(self, clock)) - stop_animating(self); -} - -static void -start_animating(FastivView *self) -{ - stop_animating(self); - - GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self)); - if (!clock || !self->image || - !cairo_surface_get_user_data(self->page, &fastiv_io_key_frame_next)) - return; - - self->frame_time = gdk_frame_clock_get_frame_time(clock); - self->frame_update_connection = g_signal_connect( - clock, "update", G_CALLBACK(on_frame_clock_update), self); - self->remaining_loops = (uintptr_t) cairo_surface_get_user_data( - self->page, &fastiv_io_key_loops); - - gdk_frame_clock_begin_updating(clock); -} - -static void -switch_page(FastivView *self, cairo_surface_t *page) -{ - self->frame = self->page = page; - if ((self->orientation = (uintptr_t) cairo_surface_get_user_data( - self->page, &fastiv_io_key_orientation)) == - FastivIoOrientationUnknown) - self->orientation = FastivIoOrientation0; - - start_animating(self); - gtk_widget_queue_resize(GTK_WIDGET(self)); -} - -static void -fastiv_view_map(GtkWidget *widget) -{ - GTK_WIDGET_CLASS(fastiv_view_parent_class)->map(widget); - - // Loading before mapping will fail to obtain a GdkFrameClock. - start_animating(FASTIV_VIEW(widget)); -} - -void -fastiv_view_unmap(GtkWidget *widget) -{ - stop_animating(FASTIV_VIEW(widget)); - GTK_WIDGET_CLASS(fastiv_view_parent_class)->unmap(widget); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -show_error_dialog(GtkWindow *parent, GError *error) -{ - GtkWidget *dialog = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL, - GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - g_error_free(error); -} - -static GtkWindow * -get_toplevel(GtkWidget *widget) -{ - if (GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget)))) - return GTK_WINDOW(widget); - return NULL; -} - -static void -on_draw_page(G_GNUC_UNUSED GtkPrintOperation *operation, - GtkPrintContext *context, G_GNUC_UNUSED int page_nr, FastivView *self) -{ - double surface_width_px = 0, surface_height_px = 0; - get_surface_dimensions(self, &surface_width_px, &surface_height_px); - - // Any DPI will be wrong, unless we import that information from the image. - double scale = 1 / 96.; - double w = surface_width_px * scale, h = surface_height_px * scale; - - // Scale down to fit the print area, taking care to not divide by zero. - double areaw = gtk_print_context_get_width(context); - double areah = gtk_print_context_get_height(context); - scale *= fmin((areaw < w) ? areaw / w : 1, (areah < h) ? areah / h : 1); - - cairo_t *cr = gtk_print_context_get_cairo_context(context); - cairo_scale(cr, scale, scale); - cairo_set_source_surface(cr, self->frame, 0, 0); - cairo_matrix_t matrix = get_orientation_matrix( - self->orientation, surface_width_px, surface_height_px); - cairo_pattern_set_matrix(cairo_get_source(cr), &matrix); - cairo_paint(cr); -} - -static gboolean -print(FastivView *self) -{ - GtkPrintOperation *print = gtk_print_operation_new(); - gtk_print_operation_set_n_pages(print, 1); - gtk_print_operation_set_embed_page_setup(print, TRUE); - gtk_print_operation_set_unit(print, GTK_UNIT_INCH); - gtk_print_operation_set_job_name(print, "Image"); - g_signal_connect(print, "draw-page", G_CALLBACK(on_draw_page), self); - - static GtkPrintSettings *settings = NULL; - if (settings != NULL) - gtk_print_operation_set_print_settings(print, settings); - - GError *error = NULL; - GtkWindow *window = get_toplevel(GTK_WIDGET(self)); - GtkPrintOperationResult res = gtk_print_operation_run( - print, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, window, &error); - if (res == GTK_PRINT_OPERATION_RESULT_APPLY) { - if (settings != NULL) - g_object_unref(settings); - settings = g_object_ref(gtk_print_operation_get_print_settings(print)); - } - if (error) - show_error_dialog(window, error); - - g_object_unref(print); - return TRUE; -} - -static gboolean -save_as(FastivView *self, gboolean frame) -{ - GtkWindow *window = get_toplevel(GTK_WIDGET(self)); - GtkWidget *dialog = gtk_file_chooser_dialog_new( - frame ? "Save frame as" : "Save page as", - window, GTK_FILE_CHOOSER_ACTION_SAVE, - "_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL); - - GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog); - - // TODO(p): Consider a hard dependency on libwebp, or clean this up. -#ifdef HAVE_LIBWEBP - // This is the best general format: supports lossless encoding, animations, - // alpha channel, and Exif and ICC profile metadata. - // PNG is another viable option, but sPNG can't do APNG, Wuffs can't save, - // and libpng is a pain in the arse. - GtkFileFilter *webp_filter = gtk_file_filter_new(); - gtk_file_filter_add_mime_type(webp_filter, "image/webp"); - gtk_file_filter_add_pattern(webp_filter, "*.webp"); - gtk_file_filter_set_name(webp_filter, "Lossless WebP"); - gtk_file_chooser_add_filter(chooser, webp_filter); - - // TODO(p): Derive it from the currently displayed filename, - // and set the directory to the same place. - gtk_file_chooser_set_current_name( - chooser, frame ? "frame.webp" : "page.webp"); -#endif // HAVE_LIBWEBP - - // The format is supported by Exiv2 and ExifTool. - // This is mostly a developer tool. - GtkFileFilter *exv_filter = gtk_file_filter_new(); - gtk_file_filter_add_mime_type(exv_filter, "image/x-exv"); - gtk_file_filter_add_pattern(exv_filter, "*.exv"); - gtk_file_filter_set_name(exv_filter, "Exiv2 metadata"); - gtk_file_chooser_add_filter(chooser, exv_filter); - - switch (gtk_dialog_run(GTK_DIALOG(dialog))) { - gchar *path; - case GTK_RESPONSE_ACCEPT: - path = gtk_file_chooser_get_filename(chooser); - - GError *error = NULL; -#ifdef HAVE_LIBWEBP - if (gtk_file_chooser_get_filter(chooser) == webp_filter) - fastiv_io_save(self->page, - frame ? self->frame : NULL, path, &error); - else -#endif // HAVE_LIBWEBP - fastiv_io_save_metadata(self->page, path, &error); - if (error) - show_error_dialog(window, error); - g_free(path); - - // Fall-through. - default: - gtk_widget_destroy(dialog); - // Fall-through. - case GTK_RESPONSE_NONE: - return TRUE; - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static inline gboolean -command(FastivView *self, FastivViewCommand command) -{ - fastiv_view_command(self, command); - return TRUE; -} - -static gboolean -fastiv_view_key_press_event(GtkWidget *widget, GdkEventKey *event) -{ - FastivView *self = FASTIV_VIEW(widget); - if (!self->image) - return FALSE; - - // It should not matter that GDK_KEY_plus involves holding Shift. - guint state = event->state & gtk_accelerator_get_default_mod_mask() & - ~GDK_SHIFT_MASK; - - // The standard, intuitive bindings. - if (state == GDK_CONTROL_MASK) { - switch (event->keyval) { - case GDK_KEY_0: - return command(self, FASTIV_VIEW_COMMAND_ZOOM_1); - case GDK_KEY_plus: - return command(self, FASTIV_VIEW_COMMAND_ZOOM_IN); - case GDK_KEY_minus: - return command(self, FASTIV_VIEW_COMMAND_ZOOM_OUT); - case GDK_KEY_p: - return command(self, FASTIV_VIEW_COMMAND_PRINT); - case GDK_KEY_s: - return command(self, FASTIV_VIEW_COMMAND_SAVE_PAGE); - case GDK_KEY_S: - return save_as(self, TRUE); - } - } - if (state != 0) - return FALSE; - - switch (event->keyval) { - case GDK_KEY_1: - case GDK_KEY_2: - case GDK_KEY_3: - case GDK_KEY_4: - case GDK_KEY_5: - case GDK_KEY_6: - case GDK_KEY_7: - case GDK_KEY_8: - case GDK_KEY_9: - return set_scale(self, event->keyval - GDK_KEY_0); - case GDK_KEY_plus: - return command(self, FASTIV_VIEW_COMMAND_ZOOM_IN); - case GDK_KEY_minus: - return command(self, FASTIV_VIEW_COMMAND_ZOOM_OUT); - - case GDK_KEY_x: // Inspired by gThumb. - return set_scale_to_fit(self, !self->scale_to_fit); - - case GDK_KEY_i: - self->filter = !self->filter; - g_object_notify_by_pspec( - G_OBJECT(self), view_properties[PROP_FILTER]); - gtk_widget_queue_draw(widget); - return TRUE; - - case GDK_KEY_less: - return command(self, FASTIV_VIEW_COMMAND_ROTATE_LEFT); - case GDK_KEY_equal: - return command(self, FASTIV_VIEW_COMMAND_MIRROR); - case GDK_KEY_greater: - return command(self, FASTIV_VIEW_COMMAND_ROTATE_RIGHT); - - case GDK_KEY_bracketleft: - return command(self, FASTIV_VIEW_COMMAND_PAGE_PREVIOUS); - case GDK_KEY_bracketright: - return command(self, FASTIV_VIEW_COMMAND_PAGE_NEXT); - - case GDK_KEY_braceleft: - return command(self, FASTIV_VIEW_COMMAND_FRAME_PREVIOUS); - case GDK_KEY_braceright: - return command(self, FASTIV_VIEW_COMMAND_FRAME_NEXT); - } - return FALSE; -} - -static void -fastiv_view_class_init(FastivViewClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS(klass); - object_class->finalize = fastiv_view_finalize; - object_class->get_property = fastiv_view_get_property; - - view_properties[PROP_SCALE] = g_param_spec_double( - "scale", "Scale", "Zoom level", - 0, G_MAXDOUBLE, 1.0, G_PARAM_READABLE); - view_properties[PROP_SCALE_TO_FIT] = g_param_spec_boolean( - "scale-to-fit", "Scale to fit", "Scale images down to fit the window", - TRUE, G_PARAM_READABLE); - view_properties[PROP_FILTER] = g_param_spec_boolean( - "filter", "Use filtering", "Scale images smoothly", - TRUE, G_PARAM_READABLE); - g_object_class_install_properties( - object_class, N_PROPERTIES, view_properties); - - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); - widget_class->get_preferred_height = fastiv_view_get_preferred_height; - widget_class->get_preferred_width = fastiv_view_get_preferred_width; - widget_class->size_allocate = fastiv_view_size_allocate; - widget_class->map = fastiv_view_map; - widget_class->unmap = fastiv_view_unmap; - widget_class->realize = fastiv_view_realize; - widget_class->draw = fastiv_view_draw; - widget_class->button_press_event = fastiv_view_button_press_event; - widget_class->scroll_event = fastiv_view_scroll_event; - widget_class->key_press_event = fastiv_view_key_press_event; - - // TODO(p): Later override "screen_changed", recreate Pango layouts there, - // if we get to have any, or otherwise reflect DPI changes. - gtk_widget_class_set_css_name(widget_class, "fastiv-view"); -} - -static void -fastiv_view_init(FastivView *self) -{ - gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); - - self->filter = true; - self->scale = 1.0; -} - -// --- Picture loading --------------------------------------------------------- - -// TODO(p): Progressive picture loading, or at least async/cancellable. -gboolean -fastiv_view_open(FastivView *self, const gchar *path, GError **error) -{ - cairo_surface_t *surface = fastiv_io_open(path, error); - if (!surface) - return FALSE; - if (self->image) - cairo_surface_destroy(self->image); - - self->frame = self->page = NULL; - self->image = surface; - switch_page(self, self->image); - set_scale_to_fit(self, true); - return TRUE; -} - -static void -page_step(FastivView *self, int step) -{ - cairo_user_data_key_t *key = - step < 0 ? &fastiv_io_key_page_previous : &fastiv_io_key_page_next; - cairo_surface_t *page = cairo_surface_get_user_data(self->page, key); - if (page) - switch_page(self, page); -} - -static void -frame_step(FastivView *self, int step) -{ - stop_animating(self); - cairo_user_data_key_t *key = - step < 0 ? &fastiv_io_key_frame_previous : &fastiv_io_key_frame_next; - if (!step || !(self->frame = cairo_surface_get_user_data(self->frame, key))) - self->frame = self->page; - gtk_widget_queue_draw(GTK_WIDGET(self)); -} - -void -fastiv_view_command(FastivView *self, FastivViewCommand command) -{ - g_return_if_fail(FASTIV_IS_VIEW(self)); - - GtkWidget *widget = GTK_WIDGET(self); - if (!self->image) - return; - - switch (command) { - break; case FASTIV_VIEW_COMMAND_ROTATE_LEFT: - self->orientation = view_left[self->orientation]; - gtk_widget_queue_resize(widget); - break; case FASTIV_VIEW_COMMAND_MIRROR: - self->orientation = view_mirror[self->orientation]; - gtk_widget_queue_resize(widget); - break; case FASTIV_VIEW_COMMAND_ROTATE_RIGHT: - self->orientation = view_right[self->orientation]; - gtk_widget_queue_resize(widget); - - break; case FASTIV_VIEW_COMMAND_PAGE_FIRST: - switch_page(self, self->image); - break; case FASTIV_VIEW_COMMAND_PAGE_PREVIOUS: - page_step(self, -1); - break; case FASTIV_VIEW_COMMAND_PAGE_NEXT: - page_step(self, +1); - break; case FASTIV_VIEW_COMMAND_PAGE_LAST: - for (cairo_surface_t *s = self->page; - (s = cairo_surface_get_user_data(s, &fastiv_io_key_page_next)); ) - self->page = s; - switch_page(self, self->page); - - break; case FASTIV_VIEW_COMMAND_FRAME_FIRST: - frame_step(self, 0); - break; case FASTIV_VIEW_COMMAND_FRAME_PREVIOUS: - frame_step(self, -1); - break; case FASTIV_VIEW_COMMAND_FRAME_NEXT: - frame_step(self, +1); - - break; case FASTIV_VIEW_COMMAND_PRINT: - print(self); - break; case FASTIV_VIEW_COMMAND_SAVE_PAGE: - save_as(self, FALSE); - - break; case FASTIV_VIEW_COMMAND_ZOOM_IN: - set_scale(self, self->scale * SCALE_STEP); - break; case FASTIV_VIEW_COMMAND_ZOOM_OUT: - set_scale(self, self->scale / SCALE_STEP); - break; case FASTIV_VIEW_COMMAND_ZOOM_1: - set_scale(self, 1.0); - } -} diff --git a/fastiv-view.h b/fastiv-view.h deleted file mode 100644 index 20ff445..0000000 --- a/fastiv-view.h +++ /dev/null @@ -1,52 +0,0 @@ -// -// fastiv-view.h: fast image viewer - view widget -// -// Copyright (c) 2021, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION -// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -// - -#pragma once - -#include - -#define FASTIV_TYPE_VIEW (fastiv_view_get_type()) -G_DECLARE_FINAL_TYPE(FastivView, fastiv_view, FASTIV, VIEW, GtkWidget) - -/// Try to open the given file, synchronously, to be displayed by the widget. -gboolean fastiv_view_open(FastivView *self, const gchar *path, GError **error); - -typedef enum _FastivViewCommand { - FASTIV_VIEW_COMMAND_ROTATE_LEFT = 1, - FASTIV_VIEW_COMMAND_MIRROR, - FASTIV_VIEW_COMMAND_ROTATE_RIGHT, - - FASTIV_VIEW_COMMAND_PAGE_FIRST, - FASTIV_VIEW_COMMAND_PAGE_PREVIOUS, - FASTIV_VIEW_COMMAND_PAGE_NEXT, - FASTIV_VIEW_COMMAND_PAGE_LAST, - - FASTIV_VIEW_COMMAND_FRAME_FIRST, - FASTIV_VIEW_COMMAND_FRAME_PREVIOUS, - FASTIV_VIEW_COMMAND_FRAME_NEXT, - // Going to the end frame makes no sense, wrap around if needed. - - FASTIV_VIEW_COMMAND_PRINT, - FASTIV_VIEW_COMMAND_SAVE_PAGE, - - FASTIV_VIEW_COMMAND_ZOOM_IN, - FASTIV_VIEW_COMMAND_ZOOM_OUT, - FASTIV_VIEW_COMMAND_ZOOM_1 -} FastivViewCommand; - -/// Execute a user action. -void fastiv_view_command(FastivView *self, FastivViewCommand command); diff --git a/fastiv.c b/fastiv.c index e51f7f3..25b8109 100644 --- a/fastiv.c +++ b/fastiv.c @@ -28,10 +28,10 @@ #include #include "config.h" -#include "fastiv-browser.h" -#include "fastiv-io.h" -#include "fastiv-sidebar.h" -#include "fastiv-view.h" +#include "fiv-browser.h" +#include "fiv-io.h" +#include "fiv-sidebar.h" +#include "fiv-view.h" #include "xdg.h" // --- Utilities --------------------------------------------------------------- @@ -191,10 +191,10 @@ load_directory(const gchar *dirname) g.files_index = -1; GFile *file = g_file_new_for_path(g.directory); - fastiv_sidebar_set_location(FASTIV_SIDEBAR(g.browser_sidebar), file); + fiv_sidebar_set_location(FIV_SIDEBAR(g.browser_sidebar), file); g_object_unref(file); - fastiv_browser_load(FASTIV_BROWSER(g.browser), - g.filtering ? is_supported : NULL, g.directory); + fiv_browser_load( + FIV_BROWSER(g.browser), g.filtering ? is_supported : NULL, g.directory); GError *error = NULL; GDir *dir = g_dir_open(g.directory, 0, &error); @@ -240,7 +240,7 @@ open(const gchar *path) g_return_if_fail(g_path_is_absolute(path)); GError *error = NULL; - if (!fastiv_view_open(FASTIV_VIEW(g.view), path, &error)) { + if (!fiv_view_open(FIV_VIEW(g.view), path, &error)) { char *base = g_filename_display_basename(path); g_prefix_error(&error, "%s: ", base); show_error_dialog(error); @@ -284,7 +284,7 @@ create_open_dialog(void) "_Open", GTK_RESPONSE_ACCEPT, NULL); GtkFileFilter *filter = gtk_file_filter_new(); - for (const char **p = fastiv_io_supported_media_types; *p; p++) + for (const char **p = fiv_io_supported_media_types; *p; p++) gtk_file_filter_add_mime_type(filter, *p); #ifdef HAVE_GDKPIXBUF gtk_file_filter_add_pixbuf_formats(filter); @@ -356,13 +356,13 @@ spawn_path(const char *path) { char *argv[] = {PROJECT_NAME, (char *) path, NULL}; GError *error = NULL; - g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, - NULL, &error); + g_spawn_async( + NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error); g_clear_error(&error); } static void -on_item_activated(G_GNUC_UNUSED FastivBrowser *browser, GFile *location, +on_item_activated(G_GNUC_UNUSED FivBrowser *browser, GFile *location, GtkPlacesOpenFlags flags, G_GNUC_UNUSED gpointer data) { gchar *path = g_file_get_path(location); @@ -416,12 +416,12 @@ on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location, static void on_toolbar_zoom(G_GNUC_UNUSED GtkButton *button, gpointer user_data) { - FastivIoThumbnailSize size = FASTIV_IO_THUMBNAIL_SIZE_COUNT; + FivIoThumbnailSize size = FIV_IO_THUMBNAIL_SIZE_COUNT; g_object_get(g.browser, "thumbnail-size", &size, NULL); size += (gintptr) user_data; - g_return_if_fail(size >= FASTIV_IO_THUMBNAIL_SIZE_MIN && - size <= FASTIV_IO_THUMBNAIL_SIZE_MAX); + g_return_if_fail(size >= FIV_IO_THUMBNAIL_SIZE_MIN && + size <= FIV_IO_THUMBNAIL_SIZE_MAX); g_object_set(g.browser, "thumbnail-size", size, NULL); } @@ -430,10 +430,10 @@ static void on_notify_thumbnail_size( GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data) { - FastivIoThumbnailSize size = 0; + FivIoThumbnailSize size = 0; g_object_get(object, g_param_spec_get_name(param_spec), &size, NULL); - gtk_widget_set_sensitive(g.plus, size < FASTIV_IO_THUMBNAIL_SIZE_MAX); - gtk_widget_set_sensitive(g.minus, size > FASTIV_IO_THUMBNAIL_SIZE_MIN); + gtk_widget_set_sensitive(g.plus, size < FIV_IO_THUMBNAIL_SIZE_MAX); + gtk_widget_set_sensitive(g.minus, size > FIV_IO_THUMBNAIL_SIZE_MIN); } static void @@ -482,8 +482,7 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event, on_open(); return TRUE; case GDK_KEY_l: - fastiv_sidebar_show_enter_location( - FASTIV_SIDEBAR(g.browser_sidebar)); + fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar)); return TRUE; case GDK_KEY_n: spawn_path(g.directory); @@ -641,11 +640,11 @@ toolbar_connect(int index, GCallback callback) static void on_command(intptr_t command) { - fastiv_view_command(FASTIV_VIEW(g.view), command); + fiv_view_command(FIV_VIEW(g.view), command); } static void -toolbar_command(int index, FastivViewCommand command) +toolbar_command(int index, FivViewCommand command) { g_signal_connect_swapped(g.toolbar[index], "clicked", G_CALLBACK(on_command), (void *) (intptr_t) command); @@ -677,21 +676,21 @@ make_view_toolbar(void) toolbar_connect(TOOLBAR_BROWSE, G_CALLBACK(switch_to_browser)); toolbar_connect(TOOLBAR_FILE_PREVIOUS, G_CALLBACK(on_previous)); toolbar_connect(TOOLBAR_FILE_NEXT, G_CALLBACK(on_next)); - toolbar_command(TOOLBAR_PAGE_FIRST, FASTIV_VIEW_COMMAND_PAGE_FIRST); - toolbar_command(TOOLBAR_PAGE_PREVIOUS, FASTIV_VIEW_COMMAND_PAGE_PREVIOUS); - toolbar_command(TOOLBAR_PAGE_NEXT, FASTIV_VIEW_COMMAND_PAGE_NEXT); - toolbar_command(TOOLBAR_PAGE_LAST, FASTIV_VIEW_COMMAND_PAGE_LAST); - toolbar_command(TOOLBAR_SKIP_BACK, FASTIV_VIEW_COMMAND_FRAME_FIRST); - toolbar_command(TOOLBAR_SEEK_BACK, FASTIV_VIEW_COMMAND_FRAME_PREVIOUS); - toolbar_command(TOOLBAR_SEEK_FORWARD, FASTIV_VIEW_COMMAND_FRAME_NEXT); - toolbar_command(TOOLBAR_PLUS, FASTIV_VIEW_COMMAND_ZOOM_IN); - toolbar_command(TOOLBAR_MINUS, FASTIV_VIEW_COMMAND_ZOOM_OUT); - toolbar_command(TOOLBAR_ONE, FASTIV_VIEW_COMMAND_ZOOM_1); - toolbar_command(TOOLBAR_PRINT, FASTIV_VIEW_COMMAND_PRINT); - toolbar_command(TOOLBAR_SAVE, FASTIV_VIEW_COMMAND_SAVE_PAGE); - toolbar_command(TOOLBAR_LEFT, FASTIV_VIEW_COMMAND_ROTATE_LEFT); - toolbar_command(TOOLBAR_MIRROR, FASTIV_VIEW_COMMAND_MIRROR); - toolbar_command(TOOLBAR_RIGHT, FASTIV_VIEW_COMMAND_ROTATE_RIGHT); + toolbar_command(TOOLBAR_PAGE_FIRST, FIV_VIEW_COMMAND_PAGE_FIRST); + toolbar_command(TOOLBAR_PAGE_PREVIOUS, FIV_VIEW_COMMAND_PAGE_PREVIOUS); + toolbar_command(TOOLBAR_PAGE_NEXT, FIV_VIEW_COMMAND_PAGE_NEXT); + toolbar_command(TOOLBAR_PAGE_LAST, FIV_VIEW_COMMAND_PAGE_LAST); + toolbar_command(TOOLBAR_SKIP_BACK, FIV_VIEW_COMMAND_FRAME_FIRST); + toolbar_command(TOOLBAR_SEEK_BACK, FIV_VIEW_COMMAND_FRAME_PREVIOUS); + toolbar_command(TOOLBAR_SEEK_FORWARD, FIV_VIEW_COMMAND_FRAME_NEXT); + toolbar_command(TOOLBAR_PLUS, FIV_VIEW_COMMAND_ZOOM_IN); + toolbar_command(TOOLBAR_MINUS, FIV_VIEW_COMMAND_ZOOM_OUT); + toolbar_command(TOOLBAR_ONE, FIV_VIEW_COMMAND_ZOOM_1); + toolbar_command(TOOLBAR_PRINT, FIV_VIEW_COMMAND_PRINT); + toolbar_command(TOOLBAR_SAVE, FIV_VIEW_COMMAND_SAVE_PAGE); + toolbar_command(TOOLBAR_LEFT, FIV_VIEW_COMMAND_ROTATE_LEFT); + toolbar_command(TOOLBAR_MIRROR, FIV_VIEW_COMMAND_MIRROR); + toolbar_command(TOOLBAR_RIGHT, FIV_VIEW_COMMAND_ROTATE_RIGHT); toolbar_connect(TOOLBAR_FULLSCREEN, G_CALLBACK(toggle_fullscreen)); return view_toolbar; } @@ -724,7 +723,7 @@ main(int argc, char *argv[]) return 0; } if (show_supported_media_types) { - for (char **types = fastiv_io_all_supported_media_types(); *types; ) + for (char **types = fiv_io_all_supported_media_types(); *types; ) g_print("%s\n", *types++); return 0; } @@ -747,29 +746,29 @@ main(int argc, char *argv[]) // XXX: button.flat is too generic, it's only for the view toolbar. // XXX: Similarly, box > separator.horizontal is a temporary hack. // Consider using a #name or a .class here, possibly for a parent widget. - const char *style = "@define-color fastiv-tile #3c3c3c; \ - fastiv-view, fastiv-browser { background: @content_view_bg; } \ - placessidebar.fastiv .toolbar { padding: 2px 6px; } \ - placessidebar.fastiv box > separator { margin: 4px 0; } \ + const char *style = "@define-color fiv-tile #3c3c3c; \ + fiv-view, fiv-browser { background: @content_view_bg; } \ + placessidebar.fiv .toolbar { padding: 2px 6px; } \ + placessidebar.fiv box > separator { margin: 4px 0; } \ button.flat { padding-left: 0; padding-right: 0 } \ box > separator.horizontal { \ background: mix(@insensitive_fg_color, \ @insensitive_bg_color, 0.4); margin: 6px 0; \ } \ - fastiv-browser { padding: 5px; } \ - fastiv-browser.item { \ + fiv-browser { padding: 5px; } \ + fiv-browser.item { \ border: 1px solid rgba(255, 255, 255, 0.375); \ margin: 10px; color: #000; \ background: #333; \ background-image: \ - linear-gradient(45deg, @fastiv-tile 26%, transparent 26%), \ - linear-gradient(-45deg, @fastiv-tile 26%, transparent 26%), \ - linear-gradient(45deg, transparent 74%, @fastiv-tile 74%), \ - linear-gradient(-45deg, transparent 74%, @fastiv-tile 74%); \ + linear-gradient(45deg, @fiv-tile 26%, transparent 26%), \ + linear-gradient(-45deg, @fiv-tile 26%, transparent 26%), \ + linear-gradient(45deg, transparent 74%, @fiv-tile 74%), \ + linear-gradient(-45deg, transparent 74%, @fiv-tile 74%); \ background-size: 40px 40px; \ background-position: 0 0, 0 20px, 20px -20px, -20px 0px; \ } \ - fastiv-browser.item.symbolic { \ + fiv-browser.item.symbolic { \ border-color: transparent; color: @content_view_bg; \ background: @theme_bg_color; background-image: none; \ }"; @@ -781,7 +780,7 @@ main(int argc, char *argv[]) g_object_unref(provider); GtkWidget *view_scroller = gtk_scrolled_window_new(NULL, NULL); - g.view = g_object_new(FASTIV_TYPE_VIEW, NULL); + g.view = g_object_new(FIV_TYPE_VIEW, NULL); g_signal_connect(g.view, "key-press-event", G_CALLBACK(on_key_press_view), NULL); g_signal_connect(g.view, "button-press-event", @@ -806,7 +805,7 @@ main(int argc, char *argv[]) gtk_widget_show_all(g.view_box); g.browser_scroller = gtk_scrolled_window_new(NULL, NULL); - g.browser = g_object_new(FASTIV_TYPE_BROWSER, NULL); + g.browser = g_object_new(FIV_TYPE_BROWSER, NULL); gtk_widget_set_vexpand(g.browser, TRUE); gtk_widget_set_hexpand(g.browser, TRUE); g_signal_connect(g.browser, "item-activated", @@ -824,7 +823,7 @@ main(int argc, char *argv[]) // - C-h to filtering, // - M-Up to going a level above, // - mayhaps forward the rest to the sidebar, somehow. - g.browser_sidebar = g_object_new(FASTIV_TYPE_SIDEBAR, NULL); + g.browser_sidebar = g_object_new(FIV_TYPE_SIDEBAR, NULL); g_signal_connect(g.browser_sidebar, "open-location", G_CALLBACK(on_open_location), NULL); @@ -862,8 +861,7 @@ main(int argc, char *argv[]) g_signal_connect(funnel, "toggled", G_CALLBACK(on_filtering_toggled), NULL); - GtkBox *toolbar = - fastiv_sidebar_get_toolbar(FASTIV_SIDEBAR(g.browser_sidebar)); + GtkBox *toolbar = fiv_sidebar_get_toolbar(FIV_SIDEBAR(g.browser_sidebar)); gtk_box_pack_start(toolbar, zoom_group, FALSE, FALSE, 0); gtk_box_pack_start(toolbar, funnel, FALSE, FALSE, 0); gtk_widget_set_halign(GTK_WIDGET(toolbar), GTK_ALIGN_CENTER); @@ -890,7 +888,7 @@ main(int argc, char *argv[]) G_CALLBACK(on_window_state_event), NULL); gtk_container_add(GTK_CONTAINER(g.window), g.stack); - char **types = fastiv_io_all_supported_media_types(); + char **types = fiv_io_all_supported_media_types(); g.supported_globs = extract_mime_globs((const char **) types); g_strfreev(types); diff --git a/fiv-browser.c b/fiv-browser.c new file mode 100644 index 0000000..7759c3a --- /dev/null +++ b/fiv-browser.c @@ -0,0 +1,1064 @@ +// +// fiv-browser.c: fast image viewer - filesystem browser widget +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#include +#include + +#include "fiv-browser.h" +#include "fiv-io.h" +#include "fiv-view.h" + +// --- Widget ------------------------------------------------------------------ +// _________________________________ +// │ p a d d i n g +// │ p ╭───────────────────╮ s ╭┄┄┄┄┄ +// │ a │ glow border ┊ │ p ┊ +// │ d │ ┄ ╔═══════════╗ ┄ │ a ┊ +// │ d │ ║ thumbnail ║ │ c ┊ ... +// │ i │ ┄ ╚═══════════╝ ┄ │ i ┊ +// │ n │ ┊ glow border │ n ┊ +// │ g ╰───────────────────╯ g ╰┄┄┄┄┄ +// │ s p a c i n g +// │ ╭┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄ +// +// The glow is actually a glowing margin, the border is rendered in two parts. +// + +struct _FivBrowser { + GtkWidget parent_instance; + + FivIoThumbnailSize item_size; ///< Thumbnail size + int item_height; ///< Thumbnail height in pixels + int item_spacing; ///< Space between items in pixels + + GArray *entries; ///< [Entry] + GArray *layouted_rows; ///< [Row] + int selected; + + GdkCursor *pointer; ///< Cached pointer cursor + cairo_surface_t *glow; ///< CAIRO_FORMAT_A8 mask + int item_border_x; ///< L/R .item margin + border + int item_border_y; ///< T/B .item margin + border +}; + +typedef struct entry Entry; +typedef struct item Item; +typedef struct row Row; + +static const double g_permitted_width_multiplier = 2; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct entry { + char *uri; ///< GIO URI + cairo_surface_t *thumbnail; ///< Prescaled thumbnail + GIcon *icon; ///< If no thumbnail, use this icon +}; + +static void +entry_free(Entry *self) +{ + g_free(self->uri); + g_clear_pointer(&self->thumbnail, cairo_surface_destroy); + g_clear_object(&self->icon); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct item { + const Entry *entry; + int x_offset; ///< Offset within the row +}; + +struct row { + Item *items; ///< Ends with a NULL entry + int x_offset; ///< Start position outside borders + int y_offset; ///< Start position inside borders +}; + +static void +row_free(Row *self) +{ + g_free(self->items); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +append_row(FivBrowser *self, int *y, int x, GArray *items_array) +{ + if (self->layouted_rows->len) + *y += self->item_spacing; + + *y += self->item_border_y; + g_array_append_val(self->layouted_rows, ((Row) { + .items = g_array_steal(items_array, NULL), + .x_offset = x, + .y_offset = *y, + })); + + // Not trying to pack them vertically, but this would be the place to do it. + *y += self->item_height; + *y += self->item_border_y; +} + +static int +relayout(FivBrowser *self, int width) +{ + GtkWidget *widget = GTK_WIDGET(self); + GtkStyleContext *style = gtk_widget_get_style_context(widget); + + GtkBorder padding = {}; + gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding); + int available_width = width - padding.left - padding.right; + + g_array_set_size(self->layouted_rows, 0); + + GArray *items = g_array_new(TRUE, TRUE, sizeof(Item)); + int x = 0, y = padding.top; + for (guint i = 0; i < self->entries->len; i++) { + const Entry *entry = &g_array_index(self->entries, Entry, i); + if (!entry->thumbnail) + continue; + + int width = cairo_image_surface_get_width(entry->thumbnail) + + 2 * self->item_border_x; + if (!items->len) { + // Just insert it, whether or not there's any space. + } else if (x + self->item_spacing + width <= available_width) { + x += self->item_spacing; + } else { + append_row(self, &y, + padding.left + MAX(0, available_width - x) / 2, items); + x = 0; + } + + g_array_append_val(items, + ((Item) {.entry = entry, .x_offset = x + self->item_border_x})); + x += width; + } + if (items->len) { + append_row(self, &y, + padding.left + MAX(0, available_width - x) / 2, items); + } + + g_array_free(items, TRUE); + return y + padding.bottom; +} + +static void +draw_outer_border(FivBrowser *self, cairo_t *cr, int width, int height) +{ + int offset_x = cairo_image_surface_get_width(self->glow); + int offset_y = cairo_image_surface_get_height(self->glow); + cairo_pattern_t *mask = cairo_pattern_create_for_surface(self->glow); + cairo_matrix_t matrix; + + cairo_pattern_set_extend(mask, CAIRO_EXTEND_PAD); + cairo_save(cr); + cairo_translate(cr, -offset_x, -offset_y); + cairo_rectangle(cr, 0, 0, offset_x + width, offset_y + height); + cairo_clip(cr); + cairo_mask(cr, mask); + cairo_restore(cr); + cairo_save(cr); + cairo_translate(cr, width + offset_x, height + offset_y); + cairo_rectangle(cr, 0, 0, -offset_x - width, -offset_y - height); + cairo_clip(cr); + cairo_scale(cr, -1, -1); + cairo_mask(cr, mask); + cairo_restore(cr); + + cairo_pattern_set_extend(mask, CAIRO_EXTEND_NONE); + cairo_matrix_init_scale(&matrix, -1, 1); + cairo_matrix_translate(&matrix, -width - offset_x, offset_y); + cairo_pattern_set_matrix(mask, &matrix); + cairo_mask(cr, mask); + cairo_matrix_init_scale(&matrix, 1, -1); + cairo_matrix_translate(&matrix, offset_x, -height - offset_y); + cairo_pattern_set_matrix(mask, &matrix); + cairo_mask(cr, mask); + + cairo_pattern_destroy(mask); +} + +static GdkRectangle +item_extents(FivBrowser *self, const Item *item, const Row *row) +{ + int width = cairo_image_surface_get_width(item->entry->thumbnail); + int height = cairo_image_surface_get_height(item->entry->thumbnail); + return (GdkRectangle) { + .x = row->x_offset + item->x_offset, + .y = row->y_offset + self->item_height - height, + .width = width, + .height = height, + }; +} + +static const Entry * +entry_at(FivBrowser *self, int x, int y) +{ + for (guint i = 0; i < self->layouted_rows->len; i++) { + const Row *row = &g_array_index(self->layouted_rows, Row, i); + for (Item *item = row->items; item->entry; item++) { + GdkRectangle extents = item_extents(self, item, row); + if (x >= extents.x && + y >= extents.y && + x <= extents.x + extents.width && + y <= extents.y + extents.height) + return item->entry; + } + } + return NULL; +} + +static void +draw_row(FivBrowser *self, cairo_t *cr, const Row *row) +{ + GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(self)); + gtk_style_context_save(style); + gtk_style_context_add_class(style, "item"); + + GdkRGBA glow_color = {}; + GtkStateFlags state = gtk_style_context_get_state (style); + gtk_style_context_get_color(style, state, &glow_color); + + GtkBorder border; + gtk_style_context_get_border(style, state, &border); + for (Item *item = row->items; item->entry; item++) { + cairo_save(cr); + GdkRectangle extents = item_extents(self, item, row); + cairo_translate(cr, extents.x - border.left, extents.y - border.top); + + gtk_style_context_save(style); + if (item->entry->icon) { + gtk_style_context_add_class(style, "symbolic"); + } else { + gdk_cairo_set_source_rgba(cr, &glow_color); + draw_outer_border(self, cr, + border.left + extents.width + border.right, + border.top + extents.height + border.bottom); + } + + gtk_render_background( + style, cr, border.left, border.top, extents.width, extents.height); + + gtk_render_frame(style, cr, 0, 0, + border.left + extents.width + border.right, + border.top + extents.height + border.bottom); + + if (item->entry->icon) { + GdkRGBA color = {}; + gtk_style_context_get_color(style, state, &color); + gdk_cairo_set_source_rgba(cr, &color); + cairo_mask_surface( + cr, item->entry->thumbnail, border.left, border.top); + } else { + cairo_set_source_surface( + cr, item->entry->thumbnail, border.left, border.top); + cairo_paint(cr); + } + + cairo_restore(cr); + gtk_style_context_restore(style); + } + gtk_style_context_restore(style); +} + +// --- Thumbnails -------------------------------------------------------------- + +// NOTE: "It is important to note that when an image with an alpha channel is +// scaled, linear encoded, pre-multiplied component values must be used!" +static cairo_surface_t * +rescale_thumbnail(cairo_surface_t *thumbnail, double row_height) +{ + if (!thumbnail) + return thumbnail; + + int width = cairo_image_surface_get_width(thumbnail); + int height = cairo_image_surface_get_height(thumbnail); + + double scale_x = 1; + double scale_y = 1; + if (width > g_permitted_width_multiplier * height) { + scale_x = g_permitted_width_multiplier * row_height / width; + scale_y = round(scale_x * height) / height; + } else { + scale_y = row_height / height; + scale_x = round(scale_y * width) / width; + } + if (scale_x == 1 && scale_y == 1) + return thumbnail; + + int projected_width = round(scale_x * width); + int projected_height = round(scale_y * height); + cairo_surface_t *scaled = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, projected_width, projected_height); + + // pixman can take gamma into account when scaling, unlike Cairo. + struct pixman_f_transform xform_floating; + struct pixman_transform xform; + + // PIXMAN_a8r8g8b8_sRGB can be used for gamma-correct results, + // but it's an incredibly slow transformation + pixman_format_code_t format = PIXMAN_a8r8g8b8; + + pixman_image_t *src = pixman_image_create_bits(format, width, height, + (uint32_t *) cairo_image_surface_get_data(thumbnail), + cairo_image_surface_get_stride(thumbnail)); + pixman_image_t *dest = pixman_image_create_bits(format, + cairo_image_surface_get_width(scaled), + cairo_image_surface_get_height(scaled), + (uint32_t *) cairo_image_surface_get_data(scaled), + cairo_image_surface_get_stride(scaled)); + + pixman_f_transform_init_scale(&xform_floating, scale_x, scale_y); + pixman_f_transform_invert(&xform_floating, &xform_floating); + pixman_transform_from_pixman_f_transform(&xform, &xform_floating); + pixman_image_set_transform(src, &xform); + pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); + pixman_image_set_repeat(src, PIXMAN_REPEAT_PAD); + + pixman_image_composite(PIXMAN_OP_SRC, src, NULL, dest, 0, 0, 0, 0, 0, 0, + projected_width, projected_height); + pixman_image_unref(src); + pixman_image_unref(dest); + + cairo_surface_destroy(thumbnail); + cairo_surface_mark_dirty(scaled); + return scaled; +} + +static void +entry_add_thumbnail(gpointer data, gpointer user_data) +{ + Entry *self = data; + g_clear_object(&self->icon); + g_clear_pointer(&self->thumbnail, cairo_surface_destroy); + + FivBrowser *browser = FIV_BROWSER(user_data); + GFile *file = g_file_new_for_uri(self->uri); + self->thumbnail = rescale_thumbnail( + fiv_io_lookup_thumbnail(file, browser->item_size), + browser->item_height); + if (self->thumbnail) + goto out; + + // Fall back to symbolic icons, though there's only so much we can do + // in parallel--GTK+ isn't thread-safe. + GFileInfo *info = g_file_query_info(file, + G_FILE_ATTRIBUTE_STANDARD_NAME + "," G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (info) { + GIcon *icon = g_file_info_get_symbolic_icon(info); + if (icon) + self->icon = g_object_ref(icon); + g_object_unref(info); + } +out: + g_object_unref(file); +} + +static void +materialize_icon(FivBrowser *self, Entry *entry) +{ + if (!entry->icon) + return; + + // Fucker will still give us non-symbolic icons, no more playing nice. + // TODO(p): Investigate a bit closer. We may want to abandon the idea + // of using GLib to look up icons for us, derive a list from a guessed + // MIME type, with "-symbolic" prefixes and fallbacks, + // and use gtk_icon_theme_choose_icon() instead. + // TODO(p): Make sure we have /some/ icon for every entry. + // TODO(p): We might want to populate these on an as-needed basis. + GtkIconInfo *icon_info = gtk_icon_theme_lookup_by_gicon( + gtk_icon_theme_get_default(), entry->icon, self->item_height / 2, + GTK_ICON_LOOKUP_FORCE_SYMBOLIC); + if (!icon_info) + return; + + // Bílá, bílá, bílá, bílá... komu by se nelíbí-lá... + // We do not want any highlights, nor do we want to remember the style. + const GdkRGBA white = {1, 1, 1, 1}; + GdkPixbuf *pixbuf = gtk_icon_info_load_symbolic( + icon_info, &white, &white, &white, &white, NULL, NULL); + if (pixbuf) { + int outer_size = self->item_height; + entry->thumbnail = + cairo_image_surface_create(CAIRO_FORMAT_A8, outer_size, outer_size); + + // "Note that the resulting pixbuf may not be exactly this size;" + // though GTK_ICON_LOOKUP_FORCE_SIZE is also an option. + int x = (outer_size - gdk_pixbuf_get_width(pixbuf)) / 2; + int y = (outer_size - gdk_pixbuf_get_height(pixbuf)) / 2; + + cairo_t *cr = cairo_create(entry->thumbnail); + gdk_cairo_set_source_pixbuf(cr, pixbuf, x, y); + cairo_paint(cr); + cairo_destroy(cr); + + g_object_unref(pixbuf); + } + g_object_unref(icon_info); +} + +static void +reload_thumbnails(FivBrowser *self) +{ + GThreadPool *pool = g_thread_pool_new( + entry_add_thumbnail, self, g_get_num_processors(), FALSE, NULL); + for (guint i = 0; i < self->entries->len; i++) + g_thread_pool_push(pool, &g_array_index(self->entries, Entry, i), NULL); + g_thread_pool_free(pool, FALSE, TRUE); + + for (guint i = 0; i < self->entries->len; i++) + materialize_icon(self, &g_array_index(self->entries, Entry, i)); + + gtk_widget_queue_resize(GTK_WIDGET(self)); +} + +// --- Context menu------------------------------------------------------------- + +typedef struct _OpenContext { + GWeakRef widget; + GFile *file; + char *content_type; + GAppInfo *app_info; +} OpenContext; + +static void +open_context_notify(gpointer data, G_GNUC_UNUSED GClosure *closure) +{ + OpenContext *self = data; + g_weak_ref_clear(&self->widget); + g_clear_object(&self->app_info); + g_clear_object(&self->file); + g_free(self->content_type); + g_free(self); +} + +static void +open_context_launch(GtkWidget *widget, OpenContext *self) +{ + GdkAppLaunchContext *context = + gdk_display_get_app_launch_context(gtk_widget_get_display(widget)); + gdk_app_launch_context_set_screen(context, gtk_widget_get_screen(widget)); + gdk_app_launch_context_set_timestamp(context, gtk_get_current_event_time()); + + // TODO(p): Display errors. + GList *files = g_list_append(NULL, self->file); + if (g_app_info_launch( + self->app_info, files, G_APP_LAUNCH_CONTEXT(context), NULL)) { + g_app_info_set_as_last_used_for_type( + self->app_info, self->content_type, NULL); + } + g_list_free(files); + g_object_unref(context); +} + +static void +append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template) +{ + OpenContext *ctx = g_malloc0(sizeof *ctx); + g_weak_ref_init(&ctx->widget, NULL); + ctx->file = g_object_ref(template->file); + ctx->content_type = g_strdup(template->content_type); + ctx->app_info = opener; + + // It's documented that we can touch the child, if we want formatting: + // https://docs.gtk.org/gtk3/class.MenuItem.html + // XXX: Would g_app_info_get_display_name() be any better? + gchar *name = g_strdup_printf("Open With %s", g_app_info_get_name(opener)); + GtkWidget *item = gtk_menu_item_new_with_label(name); + g_free(name); + g_signal_connect_data(item, "activate", G_CALLBACK(open_context_launch), + ctx, open_context_notify, 0); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); +} + +static void +on_chooser_activate(GtkMenuItem *item, gpointer user_data) +{ + OpenContext *ctx = user_data; + GtkWindow *window = NULL; + GtkWidget *widget = g_weak_ref_get(&ctx->widget); + if (widget) { + if (GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget)))) + window = GTK_WINDOW(widget); + } + + GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window, + GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type); + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) { + ctx->app_info = gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(dialog)); + open_context_launch(GTK_WIDGET(item), ctx); + } + gtk_widget_destroy(dialog); +} + +static gboolean +destroy_widget_idle_source_func(GtkWidget *widget) +{ + // The whole menu is deactivated /before/ any item is activated, + // and a destroyed child item will not activate. + gtk_widget_destroy(widget); + return FALSE; +} + +static void +show_context_menu(GtkWidget *widget, const char *uri) +{ + GFile *file = g_file_new_for_uri(uri); + GFileInfo *info = g_file_query_info(file, + G_FILE_ATTRIBUTE_STANDARD_NAME + "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (!info) { + g_object_unref(file); + return; + } + + // This will have no application pre-assigned, for use with GTK+'s dialog. + OpenContext *ctx = g_malloc0(sizeof *ctx); + g_weak_ref_init(&ctx->widget, widget); + ctx->file = file; + ctx->content_type = g_strdup(g_file_info_get_content_type(info)); + g_object_unref(info); + + GAppInfo *default_ = + g_app_info_get_default_for_type(ctx->content_type, FALSE); + GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type); + GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type); + + GtkWidget *menu = gtk_menu_new(); + if (default_) { + append_opener(menu, default_, ctx); + gtk_menu_shell_append( + GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + } + + for (GList *iter = recommended; iter; iter = iter->next) { + if (g_app_info_should_show(iter->data) && + (!default_ || !g_app_info_equal(iter->data, default_))) + append_opener(menu, iter->data, ctx); + else + g_object_unref(iter->data); + } + if (recommended) { + g_list_free(recommended); + gtk_menu_shell_append( + GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + } + + for (GList *iter = fallback; iter; iter = iter->next) { + if (g_app_info_should_show(iter->data) && + (!default_ || !g_app_info_equal(iter->data, default_))) + append_opener(menu, iter->data, ctx); + else + g_object_unref(iter->data); + } + if (fallback) { + g_list_free(fallback); + gtk_menu_shell_append( + GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + } + + GtkWidget *item = gtk_menu_item_new_with_label("Open With..."); + g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate), + ctx, open_context_notify, 0); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); + + // As per GTK+ 3 Common Questions, 1.5. + g_object_ref_sink(menu); + g_signal_connect_swapped(menu, "deactivate", + G_CALLBACK(g_idle_add), destroy_widget_idle_source_func); + g_signal_connect(menu, "destroy", G_CALLBACK(g_object_unref), NULL); + + gtk_widget_show_all(menu); + gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL); +} + +// --- Boilerplate ------------------------------------------------------------- + +// TODO(p): For proper navigation, we need to implement GtkScrollable. +G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0, + /* G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE, + fiv_browser_scrollable_init) */) + +enum { + PROP_THUMBNAIL_SIZE = 1, + N_PROPERTIES +}; + +static GParamSpec *browser_properties[N_PROPERTIES]; + +enum { + ITEM_ACTIVATED, + LAST_SIGNAL, +}; + +// Globals are, sadly, the canonical way of storing signal numbers. +static guint browser_signals[LAST_SIGNAL]; + +static void +fiv_browser_finalize(GObject *gobject) +{ + FivBrowser *self = FIV_BROWSER(gobject); + g_array_free(self->entries, TRUE); + g_array_free(self->layouted_rows, TRUE); + cairo_surface_destroy(self->glow); + g_clear_object(&self->pointer); + + G_OBJECT_CLASS(fiv_browser_parent_class)->finalize(gobject); +} + +static void +fiv_browser_get_property( + GObject *object, guint property_id, GValue *value, GParamSpec *pspec) +{ + FivBrowser *self = FIV_BROWSER(object); + switch (property_id) { + case PROP_THUMBNAIL_SIZE: + g_value_set_enum(value, self->item_size); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + +static void +set_item_size(FivBrowser *self, FivIoThumbnailSize size) +{ + if (size < FIV_IO_THUMBNAIL_SIZE_MIN || size > FIV_IO_THUMBNAIL_SIZE_MAX) + return; + + if (size != self->item_size) { + self->item_size = size; + self->item_height = fiv_io_thumbnail_sizes[self->item_size].size; + reload_thumbnails(self); + + g_object_notify_by_pspec( + G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]); + } +} + +static void +fiv_browser_set_property( + GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) +{ + FivBrowser *self = FIV_BROWSER(object); + switch (property_id) { + case PROP_THUMBNAIL_SIZE: + set_item_size(self, g_value_get_enum(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + +static GtkSizeRequestMode +fiv_browser_get_request_mode(G_GNUC_UNUSED GtkWidget *widget) +{ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +fiv_browser_get_preferred_width(GtkWidget *widget, gint *minimum, gint *natural) +{ + FivBrowser *self = FIV_BROWSER(widget); + GtkStyleContext *style = gtk_widget_get_style_context(widget); + + GtkBorder padding = {}; + gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding); + *minimum = *natural = g_permitted_width_multiplier * self->item_height + + padding.left + 2 * self->item_border_x + padding.right; +} + +static void +fiv_browser_get_preferred_height_for_width( + GtkWidget *widget, gint width, gint *minimum, gint *natural) +{ + // XXX: This is rather ugly, the caller is only asking. + *minimum = *natural = relayout(FIV_BROWSER(widget), width); +} + +static void +fiv_browser_realize(GtkWidget *widget) +{ + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + + GdkWindowAttr attributes = { + .window_type = GDK_WINDOW_CHILD, + .x = allocation.x, + .y = allocation.y, + .width = allocation.width, + .height = allocation.height, + + // Input-only would presumably also work (as in GtkPathBar, e.g.), + // but it merely seems to involve more work. + .wclass = GDK_INPUT_OUTPUT, + + .visual = gtk_widget_get_visual(widget), + .event_mask = gtk_widget_get_events(widget) | GDK_KEY_PRESS_MASK | + GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_SCROLL_MASK, + }; + + // We need this window to receive input events at all. + // TODO(p): See if input events bubble up to parents. + GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget), + &attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); + gtk_widget_register_window(widget, window); + gtk_widget_set_window(widget, window); + gtk_widget_set_realized(widget, TRUE); + + FivBrowser *self = FIV_BROWSER(widget); + g_clear_object(&self->pointer); + self->pointer = + gdk_cursor_new_from_name(gdk_window_get_display(window), "pointer"); +} + +static void +fiv_browser_size_allocate(GtkWidget *widget, GtkAllocation *allocation) +{ + GTK_WIDGET_CLASS(fiv_browser_parent_class) + ->size_allocate(widget, allocation); + + relayout(FIV_BROWSER(widget), allocation->width); +} + +static gboolean +fiv_browser_draw(GtkWidget *widget, cairo_t *cr) +{ + FivBrowser *self = FIV_BROWSER(widget); + if (!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget))) + return TRUE; + + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + gtk_render_background(gtk_widget_get_style_context(widget), cr, 0, 0, + allocation.width, allocation.height); + + GdkRectangle clip = {}; + gboolean have_clip = gdk_cairo_get_clip_rectangle(cr, &clip); + + for (guint i = 0; i < self->layouted_rows->len; i++) { + const Row *row = &g_array_index(self->layouted_rows, Row, i); + GdkRectangle extents = { + .x = 0, + .y = row->y_offset - self->item_border_y, + .width = allocation.width, + .height = self->item_height + 2 * self->item_border_y, + }; + if (!have_clip || gdk_rectangle_intersect(&clip, &extents, NULL)) + draw_row(self, cr, row); + } + return TRUE; +} + +static gboolean +open_entry(GtkWidget *self, const Entry *entry, gboolean new_window) +{ + GFile *location = g_file_new_for_uri(entry->uri); + g_signal_emit(self, browser_signals[ITEM_ACTIVATED], 0, location, + new_window ? GTK_PLACES_OPEN_NEW_WINDOW : GTK_PLACES_OPEN_NORMAL); + g_object_unref(location); + return TRUE; +} + +static gboolean +fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event) +{ + GTK_WIDGET_CLASS(fiv_browser_parent_class) + ->button_press_event(widget, event); + + FivBrowser *self = FIV_BROWSER(widget); + if (event->type != GDK_BUTTON_PRESS) + return FALSE; + + guint state = event->state & gtk_accelerator_get_default_mod_mask(); + if (event->button == GDK_BUTTON_PRIMARY && state == 0 && + gtk_widget_get_focus_on_click(widget)) + gtk_widget_grab_focus(widget); + + const Entry *entry = entry_at(self, event->x, event->y); + if (!entry) + return FALSE; + + switch (event->button) { + case GDK_BUTTON_PRIMARY: + if (state == 0) + return open_entry(widget, entry, FALSE); + if (state == GDK_CONTROL_MASK) + return open_entry(widget, entry, TRUE); + return FALSE; + case GDK_BUTTON_MIDDLE: + if (state == 0) + return open_entry(widget, entry, TRUE); + return FALSE; + case GDK_BUTTON_SECONDARY: + // On X11, after closing the menu, the pointer otherwise remains, + // no matter what its new location is. + gdk_window_set_cursor(gtk_widget_get_window(widget), NULL); + show_context_menu(widget, entry->uri); + return TRUE; + default: + return FALSE; + } +} + +gboolean +fiv_browser_motion_notify_event(GtkWidget *widget, GdkEventMotion *event) +{ + GTK_WIDGET_CLASS(fiv_browser_parent_class) + ->motion_notify_event(widget, event); + + FivBrowser *self = FIV_BROWSER(widget); + if (event->state != 0) + return FALSE; + + const Entry *entry = entry_at(self, event->x, event->y); + GdkWindow *window = gtk_widget_get_window(widget); + gdk_window_set_cursor(window, entry ? self->pointer : NULL); + return TRUE; +} + +static gboolean +fiv_browser_scroll_event(GtkWidget *widget, GdkEventScroll *event) +{ + FivBrowser *self = FIV_BROWSER(widget); + if ((event->state & gtk_accelerator_get_default_mod_mask()) != + GDK_CONTROL_MASK) + return FALSE; + + switch (event->direction) { + case GDK_SCROLL_UP: + set_item_size(self, self->item_size + 1); + return TRUE; + case GDK_SCROLL_DOWN: + set_item_size(self, self->item_size - 1); + return TRUE; + default: + // For some reason, we can also get GDK_SCROLL_SMOOTH. + // Left/right are good to steal from GtkScrolledWindow for consistency. + return TRUE; + } +} + +static gboolean +fiv_browser_query_tooltip(GtkWidget *widget, gint x, gint y, + G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip) +{ + FivBrowser *self = FIV_BROWSER(widget); + const Entry *entry = entry_at(self, x, y); + if (!entry) + return FALSE; + + GFile *file = g_file_new_for_uri(entry->uri); + GFileInfo *info = g_file_query_info(file, + G_FILE_ATTRIBUTE_STANDARD_NAME + "," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + g_object_unref(file); + if (!info) + return FALSE; + + gtk_tooltip_set_text(tooltip, g_file_info_get_display_name(info)); + g_object_unref(info); + return TRUE; +} + +static void +fiv_browser_style_updated(GtkWidget *widget) +{ + GTK_WIDGET_CLASS(fiv_browser_parent_class)->style_updated(widget); + + FivBrowser *self = FIV_BROWSER(widget); + GtkStyleContext *style = gtk_widget_get_style_context(widget); + GtkBorder border = {}, margin = {}; + + int item_spacing = self->item_spacing; + gtk_widget_style_get(widget, "spacing", &self->item_spacing, NULL); + if (item_spacing != self->item_spacing) + gtk_widget_queue_resize(widget); + + // Using a pseudo-class, because GTK+ regions are deprecated. + gtk_style_context_save(style); + gtk_style_context_add_class(style, "item"); + gtk_style_context_get_margin(style, GTK_STATE_FLAG_NORMAL, &margin); + gtk_style_context_get_border(style, GTK_STATE_FLAG_NORMAL, &border); + gtk_style_context_restore(style); + + const int glow_w = (margin.left + margin.right) / 2; + const int glow_h = (margin.top + margin.bottom) / 2; + + // Don't set different opposing sides, it will misrender, your problem. + // When the style of the class changes, this virtual method isn't invoked, + // so the update check is mildly pointless. + int item_border_x = glow_w + (border.left + border.right) / 2; + int item_border_y = glow_h + (border.top + border.bottom) / 2; + if (item_border_x != self->item_border_x || + item_border_y != self->item_border_y) { + self->item_border_x = item_border_x; + self->item_border_y = item_border_y; + gtk_widget_queue_resize(widget); + } + + if (self->glow) + cairo_surface_destroy(self->glow); + if (glow_w <= 0 || glow_h <= 0) { + self->glow = cairo_image_surface_create(CAIRO_FORMAT_A1, 0, 0); + return; + } + + self->glow = cairo_image_surface_create(CAIRO_FORMAT_A8, glow_w, glow_h); + unsigned char *data = cairo_image_surface_get_data(self->glow); + int stride = cairo_image_surface_get_stride(self->glow); + + // Smooth out the curve, so that the edge of the glow isn't too jarring. + const double fade_factor = 1.5; + + const int x_max = glow_w - 1; + const int y_max = glow_h - 1; + const double x_scale = 1. / MAX(1, x_max); + const double y_scale = 1. / MAX(1, y_max); + for (int y = 0; y <= y_max; y++) + for (int x = 0; x <= x_max; x++) { + const double xn = x_scale * (x_max - x); + const double yn = y_scale * (y_max - y); + double v = MIN(sqrt(xn * xn + yn * yn), 1); + data[y * stride + x] = round(pow(1 - v, fade_factor) * 255); + } + cairo_surface_mark_dirty(self->glow); +} + +static void +fiv_browser_class_init(FivBrowserClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->finalize = fiv_browser_finalize; + object_class->get_property = fiv_browser_get_property; + object_class->set_property = fiv_browser_set_property; + + browser_properties[PROP_THUMBNAIL_SIZE] = g_param_spec_enum( + "thumbnail-size", "Thumbnail size", "The thumbnail height to use", + FIV_TYPE_IO_THUMBNAIL_SIZE, FIV_IO_THUMBNAIL_SIZE_NORMAL, + G_PARAM_READWRITE); + g_object_class_install_properties( + object_class, N_PROPERTIES, browser_properties); + + browser_signals[ITEM_ACTIVATED] = g_signal_new("item-activated", + G_TYPE_FROM_CLASS(klass), 0, 0, NULL, NULL, NULL, + G_TYPE_NONE, 2, G_TYPE_FILE, GTK_TYPE_PLACES_OPEN_FLAGS); + + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + widget_class->get_request_mode = fiv_browser_get_request_mode; + widget_class->get_preferred_width = fiv_browser_get_preferred_width; + widget_class->get_preferred_height_for_width = + fiv_browser_get_preferred_height_for_width; + widget_class->realize = fiv_browser_realize; + widget_class->draw = fiv_browser_draw; + widget_class->size_allocate = fiv_browser_size_allocate; + widget_class->button_press_event = fiv_browser_button_press_event; + widget_class->motion_notify_event = fiv_browser_motion_notify_event; + widget_class->scroll_event = fiv_browser_scroll_event; + widget_class->query_tooltip = fiv_browser_query_tooltip; + widget_class->style_updated = fiv_browser_style_updated; + + // Could be split to also-idiomatic row-spacing/column-spacing properties. + // The GParamSpec is sinked by this call. + gtk_widget_class_install_style_property(widget_class, + g_param_spec_int("spacing", "Spacing", "Space between items", + 0, G_MAXINT, 1, G_PARAM_READWRITE)); + + // TODO(p): Later override "screen_changed", recreate Pango layouts there, + // if we get to have any, or otherwise reflect DPI changes. + gtk_widget_class_set_css_name(widget_class, "fiv-browser"); +} + +static void +fiv_browser_init(FivBrowser *self) +{ + gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); + gtk_widget_set_has_tooltip(GTK_WIDGET(self), TRUE); + + self->entries = g_array_new(FALSE, TRUE, sizeof(Entry)); + g_array_set_clear_func(self->entries, (GDestroyNotify) entry_free); + self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row)); + g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free); + + set_item_size(self, FIV_IO_THUMBNAIL_SIZE_NORMAL); + self->selected = -1; + self->glow = cairo_image_surface_create(CAIRO_FORMAT_A1, 0, 0); + + g_signal_connect_swapped(gtk_settings_get_default(), + "notify::gtk-icon-theme-name", G_CALLBACK(reload_thumbnails), self); +} + +// --- Public interface -------------------------------------------------------- + +static gint +entry_compare(gconstpointer a, gconstpointer b) +{ + const Entry *entry1 = a; + const Entry *entry2 = b; + GFile *location1 = g_file_new_for_uri(entry1->uri); + GFile *location2 = g_file_new_for_uri(entry2->uri); + gint result = fiv_io_filecmp(location1, location2); + g_object_unref(location1); + g_object_unref(location2); + return result; +} + +void +fiv_browser_load( + FivBrowser *self, FivBrowserFilterCallback cb, const char *path) +{ + g_array_set_size(self->entries, 0); + g_array_set_size(self->layouted_rows, 0); + + GFile *file = g_file_new_for_path(path); + GFileEnumerator *enumerator = g_file_enumerate_children(file, + G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + g_object_unref(file); + if (!enumerator) + return; + + while (TRUE) { + GFileInfo *info = NULL; + GFile *child = NULL; + if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) || + !info) + break; + if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) + continue; + if (cb && !cb(g_file_info_get_name(info))) + continue; + + g_array_append_val(self->entries, + ((Entry) {.thumbnail = NULL, .uri = g_file_get_uri(child)})); + } + g_object_unref(enumerator); + + // TODO(p): Support being passed a sort function. + g_array_sort(self->entries, entry_compare); + + reload_thumbnails(self); +} diff --git a/fiv-browser.h b/fiv-browser.h new file mode 100644 index 0000000..613728c --- /dev/null +++ b/fiv-browser.h @@ -0,0 +1,28 @@ +// +// fiv-browser.h: fast image viewer - filesystem browser widget +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#pragma once + +#include + +#define FIV_TYPE_BROWSER (fiv_browser_get_type()) +G_DECLARE_FINAL_TYPE(FivBrowser, fiv_browser, FIV, BROWSER, GtkWidget) + +typedef gboolean (*FivBrowserFilterCallback) (const char *); + +void fiv_browser_load( + FivBrowser *self, FivBrowserFilterCallback cb, const char *path); diff --git a/fiv-io-benchmark.c b/fiv-io-benchmark.c new file mode 100644 index 0000000..d70d1c9 --- /dev/null +++ b/fiv-io-benchmark.c @@ -0,0 +1,66 @@ +// +// fiv-io-benchmark.c: see if we're worth the name +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#include +#include +#include + +#include "fiv-io.h" + +static double +timestamp(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec / 1.e9; +} + +static void +one_file(const char *filename) +{ + double since_us = timestamp(); + cairo_surface_t *loaded_by_us = fiv_io_open(filename, NULL); + if (!loaded_by_us) + return; + + cairo_surface_destroy(loaded_by_us); + double us = timestamp() - since_us; + + double since_pixbuf = timestamp(); + GdkPixbuf *gdk_pixbuf = gdk_pixbuf_new_from_file(filename, NULL); + if (!gdk_pixbuf) + return; + + cairo_surface_t *loaded_by_pixbuf = + gdk_cairo_surface_create_from_pixbuf(gdk_pixbuf, 1, NULL); + g_object_unref(gdk_pixbuf); + cairo_surface_destroy(loaded_by_pixbuf); + double pixbuf = timestamp() - since_pixbuf; + + printf("%f\t%f\t%.0f%%\t%s\n", us, pixbuf, us / pixbuf * 100, filename); +} + +int +main(int argc, char *argv[]) +{ + // Needed for gdk_cairo_surface_create_from_pixbuf(). + gdk_init(&argc, &argv); + + for (int i = 1; i < argc; i++) + one_file(argv[i]); + return 0; +} diff --git a/fiv-io.c b/fiv-io.c new file mode 100644 index 0000000..568c7c1 --- /dev/null +++ b/fiv-io.c @@ -0,0 +1,2491 @@ +// +// fiv-io.c: image operations +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#ifdef HAVE_LIBRAW +#include +#endif // HAVE_LIBRAW +#ifdef HAVE_LIBRSVG +#include +#endif // HAVE_LIBRSVG +#ifdef HAVE_XCURSOR +#include +#endif // HAVE_XCURSOR +#ifdef HAVE_LIBWEBP +#include +#include +#include +#include +#endif // HAVE_LIBWEBP +#ifdef HAVE_LIBHEIF +#include +#endif // HAVE_LIBHEIF +#ifdef HAVE_LIBTIFF +#include +#include +#endif // HAVE_LIBTIFF +#ifdef HAVE_GDKPIXBUF +#include +#include +#endif // HAVE_GDKPIXBUF + +#define WUFFS_IMPLEMENTATION +#define WUFFS_CONFIG__MODULES +#define WUFFS_CONFIG__MODULE__ADLER32 +#define WUFFS_CONFIG__MODULE__BASE +#define WUFFS_CONFIG__MODULE__BMP +#define WUFFS_CONFIG__MODULE__CRC32 +#define WUFFS_CONFIG__MODULE__DEFLATE +#define WUFFS_CONFIG__MODULE__GIF +#define WUFFS_CONFIG__MODULE__LZW +#define WUFFS_CONFIG__MODULE__PNG +#define WUFFS_CONFIG__MODULE__ZLIB +#include "wuffs-mirror-release-c/release/c/wuffs-v0.3.c" + +#include "fiv-io.h" +#include "xdg.h" + +#if CAIRO_VERSION >= 11702 && X11_ACTUALLY_SUPPORTS_RGBA128F_OR_WE_USE_OPENGL +#define FIV_CAIRO_RGBA128F +#endif + +// A subset of shared-mime-info that produces an appropriate list of +// file extensions. Chiefly motivated by the suckiness of raw photo formats: +// someone else will maintain the list of file extensions for us. +const char *fiv_io_supported_media_types[] = { + "image/bmp", + "image/gif", + "image/png", + "image/jpeg", +#ifdef HAVE_LIBRAW + "image/x-dcraw", +#endif // HAVE_LIBRAW +#ifdef HAVE_LIBRSVG + "image/svg+xml", +#endif // HAVE_LIBRSVG +#ifdef HAVE_XCURSOR + "image/x-xcursor", +#endif // HAVE_XCURSOR +#ifdef HAVE_LIBWEBP + "image/webp", +#endif // HAVE_LIBWEBP +#ifdef HAVE_LIBHEIF + "image/heic", + "image/heif", + "image/avif", +#endif // HAVE_LIBHEIF +#ifdef HAVE_LIBTIFF + "image/tiff", +#endif // HAVE_LIBTIFF + NULL +}; + +char ** +fiv_io_all_supported_media_types(void) +{ + GPtrArray *types = g_ptr_array_new(); + for (const char **p = fiv_io_supported_media_types; *p; p++) + g_ptr_array_add(types, g_strdup(*p)); + +#ifdef HAVE_GDKPIXBUF + GSList *formats = gdk_pixbuf_get_formats(); + for (GSList *iter = formats; iter; iter = iter->next) { + gchar **subtypes = gdk_pixbuf_format_get_mime_types(iter->data); + for (gchar **p = subtypes; *p; p++) + g_ptr_array_add(types, *p); + g_free(subtypes); + } + g_slist_free(formats); +#endif // HAVE_GDKPIXBUF + + g_ptr_array_add(types, NULL); + return (char **) g_ptr_array_free(types, FALSE); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#define FIV_IO_ERROR fiv_io_error_quark() + +G_DEFINE_QUARK(fiv-io-error-quark, fiv_io_error) + +enum FivIoError { + FIV_IO_ERROR_OPEN +}; + +static void +set_error(GError **error, const char *message) +{ + g_set_error_literal(error, FIV_IO_ERROR, FIV_IO_ERROR_OPEN, message); +} + +static bool +try_append_page(cairo_surface_t *surface, cairo_surface_t **result, + cairo_surface_t **result_tail) +{ + if (!surface) + return false; + + if (*result) { + cairo_surface_set_user_data(*result_tail, &fiv_io_key_page_next, + surface, (cairo_destroy_func_t) cairo_surface_destroy); + cairo_surface_set_user_data( + surface, &fiv_io_key_page_previous, *result_tail, NULL); + *result_tail = surface; + } else { + *result = *result_tail = surface; + } + return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// From libwebp, verified to exactly match [x * a / 255]. +#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23) + +static bool +pull_passthrough(const wuffs_base__more_information *minfo, + wuffs_base__io_buffer *src, wuffs_base__io_buffer *dst, GError **error) +{ + wuffs_base__range_ie_u64 r = + wuffs_base__more_information__metadata_raw_passthrough__range(minfo); + if (wuffs_base__range_ie_u64__is_empty(&r)) + return true; + + // This should currently be zero, because we read files all at once. + uint64_t pos = src->meta.pos; + if (pos > r.min_incl || + wuffs_base__u64__sat_sub(r.max_excl, pos) > src->meta.wi) { + set_error(error, "metadata is outside the read buffer"); + return false; + } + + // Mimic WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_RAW_TRANSFORM. + *dst = wuffs_base__make_io_buffer(src->data, + wuffs_base__make_io_buffer_meta( + wuffs_base__u64__sat_sub(r.max_excl, pos), + wuffs_base__u64__sat_sub(r.min_incl, pos), pos, TRUE)); + + // Seeking to the end of it seems to be a requirement in decode_gif.wuffs. + // Just not in case the block was empty. :^) + src->meta.ri = dst->meta.wi; + return true; +} + +static GBytes * +pull_metadata(wuffs_base__image_decoder *dec, wuffs_base__io_buffer *src, + wuffs_base__more_information *minfo, GError **error) +{ + uint8_t buf[8192] = {}; + GByteArray *array = g_byte_array_new(); + while (true) { + *minfo = wuffs_base__empty_more_information(); + wuffs_base__io_buffer dst = wuffs_base__ptr_u8__writer(buf, sizeof buf); + wuffs_base__status status = + wuffs_base__image_decoder__tell_me_more(dec, &dst, minfo, src); + switch (minfo->flavor) { + case 0: + // Most likely as a result of an error, we'll handle that below. + case WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_RAW_TRANSFORM: + // Wuffs is reading it into the buffer. + case WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_PARSED: + // Use Wuffs accessor functions in the caller. + break; + default: + set_error(error, "Wuffs metadata API incompatibility"); + goto fail; + + case WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_RAW_PASSTHROUGH: + // The insane case: error checking really should come first, + // and it can say "even more information". See decode_gif.wuffs. + if (!pull_passthrough(minfo, src, &dst, error)) + goto fail; + } + + g_byte_array_append(array, wuffs_base__io_buffer__reader_pointer(&dst), + wuffs_base__io_buffer__reader_length(&dst)); + if (wuffs_base__status__is_ok(&status)) + return g_byte_array_free_to_bytes(array); + + if (status.repr != wuffs_base__suspension__even_more_information && + status.repr != wuffs_base__suspension__short_write) { + set_error(error, wuffs_base__status__message(&status)); + goto fail; + } + } + +fail: + g_byte_array_unref(array); + return NULL; +} + +struct load_wuffs_frame_context { + wuffs_base__image_decoder *dec; ///< Wuffs decoder abstraction + wuffs_base__io_buffer *src; ///< Wuffs source buffer + wuffs_base__image_config cfg; ///< Wuffs image configuration + wuffs_base__slice_u8 workbuf; ///< Work buffer for Wuffs + wuffs_base__frame_config last_fc; ///< Previous frame configuration + uint32_t width; ///< Copied from cfg.pixcfg + uint32_t height; ///< Copied from cfg.pixcfg + cairo_format_t cairo_format; ///< Target format for surfaces + bool pack_16_10; ///< Custom copying swizzle for RGB30 + bool expand_16_float; ///< Custom copying swizzle for RGBA128F + GBytes *meta_exif; ///< Reference-counted Exif + GBytes *meta_iccp; ///< Reference-counted ICC profile + GBytes *meta_xmp; ///< Reference-counted XMP + + cairo_surface_t *result; ///< The resulting surface (referenced) + cairo_surface_t *result_tail; ///< The final animation frame +}; + +static bool +load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error) +{ + wuffs_base__frame_config fc = {}; + wuffs_base__status status = + wuffs_base__image_decoder__decode_frame_config(ctx->dec, &fc, ctx->src); + if (status.repr == wuffs_base__note__end_of_data && ctx->result) + return false; + if (!wuffs_base__status__is_ok(&status)) { + set_error(error, wuffs_base__status__message(&status)); + return false; + } + + bool success = false; + unsigned char *targetbuf = NULL; + cairo_surface_t *surface = + cairo_image_surface_create(ctx->cairo_format, ctx->width, ctx->height); + cairo_status_t surface_status = cairo_surface_status(surface); + if (surface_status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(surface_status)); + goto fail; + } + + // CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with + // ARGB/BGR/XRGB/BGRX. This function does not support a stride different + // from the width, maybe Wuffs internals do not either. + unsigned char *surface_data = cairo_image_surface_get_data(surface); + int surface_stride = cairo_image_surface_get_stride(surface); + wuffs_base__pixel_buffer pb = {0}; + if (ctx->expand_16_float) { + uint32_t targetbuf_size = ctx->height * ctx->width * 64; + targetbuf = g_malloc(targetbuf_size); + status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg, + wuffs_base__make_slice_u8(targetbuf, targetbuf_size)); + } else if (ctx->pack_16_10) { + uint32_t targetbuf_size = ctx->height * ctx->width * 16; + targetbuf = g_malloc(targetbuf_size); + status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg, + wuffs_base__make_slice_u8(targetbuf, targetbuf_size)); + } else { + status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg, + wuffs_base__make_slice_u8(surface_data, + surface_stride * cairo_image_surface_get_height(surface))); + } + if (!wuffs_base__status__is_ok(&status)) { + set_error(error, wuffs_base__status__message(&status)); + goto fail; + } + + // Starting to modify pixel data directly. Probably an unnecessary call. + cairo_surface_flush(surface); + + status = wuffs_base__image_decoder__decode_frame(ctx->dec, &pb, ctx->src, + WUFFS_BASE__PIXEL_BLEND__SRC, ctx->workbuf, NULL); + if (!wuffs_base__status__is_ok(&status)) { + set_error(error, wuffs_base__status__message(&status)); + goto fail; + } + + if (ctx->expand_16_float) { + g_debug("Wuffs to Cairo RGBA128F"); + uint16_t *in = (uint16_t *) targetbuf; + float *out = (float *) surface_data; + for (uint32_t y = 0; y < ctx->height; y++) { + for (uint32_t x = 0; x < ctx->width; x++) { + float b = *in++ / 65535., g = *in++ / 65535., + r = *in++ / 65535., a = *in++ / 65535.; + *out++ = r * a; + *out++ = g * a; + *out++ = b * a; + *out++ = a; + } + } + } else if (ctx->pack_16_10) { + g_debug("Wuffs to Cairo RGB30"); + uint16_t *in = (uint16_t *) targetbuf; + uint32_t *out = (uint32_t *) surface_data; + for (uint32_t y = 0; y < ctx->height; y++) { + for (uint32_t x = 0; x < ctx->width; x++) { + uint32_t b = *in++, g = *in++, r = *in++, X = *in++; + *out++ = (X >> 14) << 30 | + (r >> 6) << 20 | (g >> 6) << 10 | (b >> 6); + } + } + } + + // Pixel data has been written, need to let Cairo know. + cairo_surface_mark_dirty(surface); + + // Single-frame images get a fast path, animations are are handled slowly: + if (wuffs_base__frame_config__index(&fc) > 0) { + // Copy the previous frame to a new surface. + cairo_surface_t *canvas = cairo_image_surface_create( + ctx->cairo_format, ctx->width, ctx->height); + int stride = cairo_image_surface_get_stride(canvas); + int height = cairo_image_surface_get_height(canvas); + memcpy(cairo_image_surface_get_data(canvas), + cairo_image_surface_get_data(ctx->result_tail), stride * height); + cairo_surface_mark_dirty(canvas); + + // Apply that frame's disposal method. + wuffs_base__rect_ie_u32 bounds = + wuffs_base__frame_config__bounds(&ctx->last_fc); + wuffs_base__color_u32_argb_premul bg = + wuffs_base__frame_config__background_color(&ctx->last_fc); + + double a = (bg >> 24) / 255., r = 0, g = 0, b = 0; + if (a) { + r = (uint8_t) (bg >> 16) / 255. / a; + g = (uint8_t) (bg >> 8) / 255. / a; + b = (uint8_t) (bg) / 255. / a; + } + + cairo_t *cr = cairo_create(canvas); + switch (wuffs_base__frame_config__disposal(&ctx->last_fc)) { + case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_BACKGROUND: + cairo_rectangle(cr, bounds.min_incl_x, bounds.min_incl_y, + bounds.max_excl_x - bounds.min_incl_x, + bounds.max_excl_y - bounds.min_incl_y); + cairo_set_source_rgba(cr, r, g, b, a); + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + cairo_fill(cr); + break; + case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_PREVIOUS: + // TODO(p): Implement, it seems tricky. + // Might need another surface to keep track of the state. + break; + } + + // Paint the current frame over that, within its bounds. + bounds = wuffs_base__frame_config__bounds(&fc); + cairo_rectangle(cr, bounds.min_incl_x, bounds.min_incl_y, + bounds.max_excl_x - bounds.min_incl_x, + bounds.max_excl_y - bounds.min_incl_y); + cairo_clip(cr); + + cairo_set_operator(cr, + wuffs_base__frame_config__overwrite_instead_of_blend(&fc) + ? CAIRO_OPERATOR_SOURCE + : CAIRO_OPERATOR_OVER); + + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + cairo_surface_destroy(surface); + surface = canvas; + } + + if (ctx->meta_exif) + cairo_surface_set_user_data(surface, &fiv_io_key_exif, + g_bytes_ref(ctx->meta_exif), (cairo_destroy_func_t) g_bytes_unref); + if (ctx->meta_iccp) + cairo_surface_set_user_data(surface, &fiv_io_key_icc, + g_bytes_ref(ctx->meta_iccp), (cairo_destroy_func_t) g_bytes_unref); + if (ctx->meta_xmp) + cairo_surface_set_user_data(surface, &fiv_io_key_xmp, + g_bytes_ref(ctx->meta_xmp), (cairo_destroy_func_t) g_bytes_unref); + + cairo_surface_set_user_data(surface, &fiv_io_key_loops, + (void *) (uintptr_t) wuffs_base__image_decoder__num_animation_loops( + ctx->dec), NULL); + cairo_surface_set_user_data(surface, &fiv_io_key_frame_duration, + (void *) (intptr_t) (wuffs_base__frame_config__duration(&fc) / + WUFFS_BASE__FLICKS_PER_MILLISECOND), NULL); + + cairo_surface_set_user_data( + surface, &fiv_io_key_frame_previous, ctx->result_tail, NULL); + if (ctx->result_tail) + cairo_surface_set_user_data(ctx->result_tail, &fiv_io_key_frame_next, + surface, (cairo_destroy_func_t) cairo_surface_destroy); + else + ctx->result = surface; + + success = true; + ctx->result_tail = surface; + ctx->last_fc = fc; + +fail: + if (!success) { + cairo_surface_destroy(surface); + g_clear_pointer(&ctx->result, cairo_surface_destroy); + ctx->result_tail = NULL; + } + + g_free(targetbuf); + return success; +} + +// https://github.com/google/wuffs/blob/main/example/gifplayer/gifplayer.c +// is pure C, and a good reference. I can't use the auxiliary libraries, +// since they depend on C++, which is undesirable. +static cairo_surface_t * +open_wuffs( + wuffs_base__image_decoder *dec, wuffs_base__io_buffer src, GError **error) +{ + struct load_wuffs_frame_context ctx = {.dec = dec, .src = &src}; + + // TODO(p): PNG also has sRGB and gAMA, as well as text chunks (Wuffs #58). + // The former two use WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_PARSED. + wuffs_base__image_decoder__set_report_metadata( + ctx.dec, WUFFS_BASE__FOURCC__EXIF, true); + wuffs_base__image_decoder__set_report_metadata( + ctx.dec, WUFFS_BASE__FOURCC__ICCP, true); + + while (true) { + wuffs_base__status status = + wuffs_base__image_decoder__decode_image_config( + ctx.dec, &ctx.cfg, ctx.src); + if (wuffs_base__status__is_ok(&status)) + break; + + if (status.repr != wuffs_base__note__metadata_reported) { + set_error(error, wuffs_base__status__message(&status)); + goto fail; + } + + wuffs_base__more_information minfo = {}; + GBytes *bytes = NULL; + if (!(bytes = pull_metadata(ctx.dec, ctx.src, &minfo, error))) + goto fail; + + switch (wuffs_base__more_information__metadata__fourcc(&minfo)) { + case WUFFS_BASE__FOURCC__EXIF: + if (ctx.meta_exif) { + g_warning("ignoring repeated Exif"); + break; + } + ctx.meta_exif = bytes; + continue; + case WUFFS_BASE__FOURCC__ICCP: + if (ctx.meta_iccp) { + g_warning("ignoring repeated ICC profile"); + break; + } + ctx.meta_iccp = bytes; + continue; + case WUFFS_BASE__FOURCC__XMP: + if (ctx.meta_xmp) { + g_warning("ignoring repeated XMP"); + break; + } + ctx.meta_xmp = bytes; + continue; + } + + g_bytes_unref(bytes); + } + + // This, at least currently, seems excessive. + if (!wuffs_base__image_config__is_valid(&ctx.cfg)) { + set_error(error, "invalid Wuffs image configuration"); + goto fail; + } + + // We need to check because of the Cairo API. + ctx.width = wuffs_base__pixel_config__width(&ctx.cfg.pixcfg); + ctx.height = wuffs_base__pixel_config__height(&ctx.cfg.pixcfg); + if (ctx.width > INT_MAX || ctx.height > INT_MAX) { + set_error(error, "image dimensions overflow"); + goto fail; + } + + // Wuffs maps tRNS to BGRA in `decoder.decode_trns?`, we should be fine. + // wuffs_base__pixel_format__transparency() doesn't reflect the image file. + // TODO(p): See if wuffs_base__image_config__first_frame_is_opaque() causes + // issues with animations, and eventually ensure an alpha-capable format. + bool opaque = wuffs_base__image_config__first_frame_is_opaque(&ctx.cfg); + + // Wuffs' API is kind of awful--we want to catch deep RGB and deep grey. + wuffs_base__pixel_format srcfmt = + wuffs_base__pixel_config__pixel_format(&ctx.cfg.pixcfg); + uint32_t bpp = wuffs_base__pixel_format__bits_per_pixel(&srcfmt); + + // Cairo doesn't support transparency with RGB30, so no premultiplication. + ctx.pack_16_10 = opaque && (bpp > 24 || (bpp < 24 && bpp > 8)); +#ifdef FIV_CAIRO_RGBA128F + ctx.expand_16_float = !opaque && (bpp > 24 || (bpp < 24 && bpp > 8)); +#endif // FIV_CAIRO_RGBA128F + + // In Wuffs, /doc/note/pixel-formats.md declares "memory order", which, + // for our purposes, means big endian, and BGRA results in 32-bit ARGB + // on most machines. + // + // XXX: WUFFS_BASE__PIXEL_FORMAT__ARGB_PREMUL is not expressible, only RGBA. + // Wuffs doesn't support big-endian architectures at all, we might want to + // fall back to spng in such cases, or do a second conversion. + uint32_t wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL; + + // CAIRO_FORMAT_ARGB32: "The 32-bit quantities are stored native-endian. + // Pre-multiplied alpha is used." CAIRO_FORMAT_RGB{24,30} are analogous. + ctx.cairo_format = CAIRO_FORMAT_ARGB32; + +#ifdef FIV_CAIRO_RGBA128F + if (ctx.expand_16_float) { + wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL_4X16LE; + ctx.cairo_format = CAIRO_FORMAT_RGBA128F; + } else +#endif // FIV_CAIRO_RGBA128F + if (ctx.pack_16_10) { + // TODO(p): Make Wuffs support A2RGB30 as a destination format; + // in general, 16-bit depth swizzlers are stubbed. + // See also wuffs_base__pixel_swizzler__prepare__*(). + wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL_4X16LE; + ctx.cairo_format = CAIRO_FORMAT_RGB30; + } else if (opaque) { + // BGRX doesn't have as wide swizzler support, namely in GIF. + wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL; + ctx.cairo_format = CAIRO_FORMAT_RGB24; + } + + wuffs_base__pixel_config__set(&ctx.cfg.pixcfg, wuffs_format, + WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, ctx.width, ctx.height); + + uint64_t workbuf_len_max_incl = + wuffs_base__image_decoder__workbuf_len(ctx.dec).max_incl; + if (workbuf_len_max_incl) { + ctx.workbuf = wuffs_base__malloc_slice_u8(malloc, workbuf_len_max_incl); + if (!ctx.workbuf.ptr) { + set_error(error, "failed to allocate a work buffer"); + goto fail; + } + } + + while (load_wuffs_frame(&ctx, error)) + ; + + // Wrap the chain around, since our caller receives only one pointer. + if (ctx.result) + cairo_surface_set_user_data( + ctx.result, &fiv_io_key_frame_previous, ctx.result_tail, NULL); + +fail: + free(ctx.workbuf.ptr); + g_clear_pointer(&ctx.meta_exif, g_bytes_unref); + g_clear_pointer(&ctx.meta_iccp, g_bytes_unref); + g_clear_pointer(&ctx.meta_xmp, g_bytes_unref); + return ctx.result; +} + +static cairo_surface_t * +open_wuffs_using(wuffs_base__image_decoder *(*allocate)(), + const gchar *data, gsize len, GError **error) +{ + wuffs_base__image_decoder *dec = allocate(); + if (!dec) { + set_error(error, "memory allocation failed or internal error"); + return NULL; + } + + cairo_surface_t *surface = open_wuffs( + dec, wuffs_base__ptr_u8__reader((uint8_t *) data, len, TRUE), error); + free(dec); + return surface; +} + +static void +trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len) +{ + // Inspired by gdk-pixbuf's io-jpeg.c: + // + // Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop + // does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096 + while (len--) { + int c = p[0], m = p[1], y = p[2], k = p[3]; +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + p[0] = k * y / 255; + p[1] = k * m / 255; + p[2] = k * c / 255; + p[3] = 255; +#else + p[3] = k * y / 255; + p[2] = k * m / 255; + p[1] = k * c / 255; + p[0] = 255; +#endif + p += 4; + } +} + +static void +parse_jpeg_metadata(cairo_surface_t *surface, const gchar *data, gsize len) +{ + // Because the JPEG file format is simple, just do it manually. + // See: https://www.w3.org/Graphics/JPEG/itu-t81.pdf + enum { + APP0 = 0xE0, + APP1, + APP2, + RST0 = 0xD0, + RST1, + RST2, + RST3, + RST4, + RST5, + RST6, + RST7, + SOI = 0xD8, + EOI = 0xD9, + SOS = 0xDA, + TEM = 0x01, + }; + + GByteArray *exif = g_byte_array_new(), *icc = g_byte_array_new(); + int icc_sequence = 0, icc_done = FALSE; + + const guint8 *p = (const guint8 *) data, *end = p + len; + while (p + 3 < end && *p++ == 0xFF && *p != SOS && *p != EOI) { + // The previous byte is a fill byte, restart. + if (*p == 0xFF) + continue; + + // These markers stand alone, not starting a marker segment. + guint8 marker = *p++; + switch (marker) { + case RST0: + case RST1: + case RST2: + case RST3: + case RST4: + case RST5: + case RST6: + case RST7: + case SOI: + case TEM: + continue; + } + + // Do not bother validating the structure. + guint16 length = p[0] << 8 | p[1]; + const guint8 *payload = p + 2; + if ((p += length) > end) + break; + + // https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf 4.7.2 + // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 + // Not checking the padding byte is intentional. + if (marker == APP1 && p - payload >= 6 && + !memcmp(payload, "Exif\0", 5) && !exif->len) { + payload += 6; + g_byte_array_append(exif, payload, p - payload); + } + + // https://www.color.org/specification/ICC1v43_2010-12.pdf B.4 + if (marker == APP2 && p - payload >= 14 && + !memcmp(payload, "ICC_PROFILE\0", 12) && !icc_done && + payload[12] == ++icc_sequence && payload[13] >= payload[12]) { + payload += 14; + g_byte_array_append(icc, payload, p - payload); + icc_done = payload[-1] == icc_sequence; + } + + // TODO(p): Extract the main XMP segment. + } + + if (exif->len) + cairo_surface_set_user_data(surface, &fiv_io_key_exif, + g_byte_array_free_to_bytes(exif), + (cairo_destroy_func_t) g_bytes_unref); + else + g_byte_array_free(exif, TRUE); + + if (icc_done) + cairo_surface_set_user_data(surface, &fiv_io_key_icc, + g_byte_array_free_to_bytes(icc), + (cairo_destroy_func_t) g_bytes_unref); + else + g_byte_array_free(icc, TRUE); +} + +static cairo_surface_t * +open_libjpeg_turbo(const gchar *data, gsize len, GError **error) +{ + tjhandle dec = tjInitDecompress(); + if (!dec) { + set_error(error, tjGetErrorStr2(dec)); + return NULL; + } + + int width = 0, height = 0, subsampling = TJSAMP_444, colorspace = TJCS_RGB; + if (tjDecompressHeader3(dec, (const unsigned char *) data, len, + &width, &height, &subsampling, &colorspace)) { + set_error(error, tjGetErrorStr2(dec)); + tjDestroy(dec); + return NULL; + } + + int pixel_format = (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK) + ? TJPF_CMYK + : (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRA : TJPF_ARGB); + + cairo_surface_t *surface = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_status_t surface_status = cairo_surface_status(surface); + if (surface_status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(surface_status)); + cairo_surface_destroy(surface); + tjDestroy(dec); + return NULL; + } + + // Starting to modify pixel data directly. Probably an unnecessary call. + cairo_surface_flush(surface); + + int stride = cairo_image_surface_get_stride(surface); + if (tjDecompress2(dec, (const unsigned char *) data, len, + cairo_image_surface_get_data(surface), width, stride, height, + pixel_format, TJFLAG_ACCURATEDCT)) { + if (tjGetErrorCode(dec) == TJERR_WARNING) { + g_warning("%s", tjGetErrorStr2(dec)); + } else { + set_error(error, tjGetErrorStr2(dec)); + cairo_surface_destroy(surface); + tjDestroy(dec); + return NULL; + } + } + + if (pixel_format == TJPF_CMYK) { + // CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with + // ARGB/BGR/XRGB/BGRX. + trivial_cmyk_to_host_byte_order_argb( + cairo_image_surface_get_data(surface), width * height); + } + + // Pixel data has been written, need to let Cairo know. + cairo_surface_mark_dirty(surface); + + tjDestroy(dec); + parse_jpeg_metadata(surface, data, len); + return surface; +} + +#ifdef HAVE_LIBRAW // --------------------------------------------------------- + +static cairo_surface_t * +open_libraw(const gchar *data, gsize len, GError **error) +{ + // https://github.com/LibRaw/LibRaw/issues/418 + libraw_data_t *iprc = libraw_init( + LIBRAW_OPIONS_NO_MEMERR_CALLBACK | LIBRAW_OPIONS_NO_DATAERR_CALLBACK); + if (!iprc) { + set_error(error, "failed to obtain a LibRaw handle"); + return NULL; + } + +#if 0 + // TODO(p): Consider setting this--the image is still likely to be + // rendered suboptimally, so why not make it faster. + iprc->params.half_size = 1; +#endif + + // TODO(p): Check if we need to set anything for autorotation (sizes.flip). + iprc->params.use_camera_wb = 1; + iprc->params.output_color = 1; // sRGB, TODO(p): Is this used? + iprc->params.output_bps = 8; // This should be the default value. + + int err = 0; + if ((err = libraw_open_buffer(iprc, (void *) data, len))) { + set_error(error, libraw_strerror(err)); + libraw_close(iprc); + return NULL; + } + + // TODO(p): Do we need to check iprc->idata.raw_count? Maybe for TIFFs? + if ((err = libraw_unpack(iprc))) { + set_error(error, libraw_strerror(err)); + libraw_close(iprc); + return NULL; + } + +#if 0 + // TODO(p): I'm not sure when this is necessary or useful yet. + if ((err = libraw_adjust_sizes_info_only(iprc))) { + set_error(error, libraw_strerror(err)); + libraw_close(iprc); + return NULL; + } +#endif + + // TODO(p): Documentation says I should look at the code and do it myself. + if ((err = libraw_dcraw_process(iprc))) { + set_error(error, libraw_strerror(err)); + libraw_close(iprc); + return NULL; + } + + // FIXME: This is shittily written to iterate over the range of + // idata.colors, and will be naturally slow. + libraw_processed_image_t *image = libraw_dcraw_make_mem_image(iprc, &err); + if (!image) { + set_error(error, libraw_strerror(err)); + libraw_close(iprc); + return NULL; + } + + // This should have been transformed, and kept, respectively. + if (image->colors != 3 || image->bits != 8) { + set_error(error, "unexpected number of colours, or bit depth"); + libraw_dcraw_clear_mem(image); + libraw_close(iprc); + return NULL; + } + + int width = image->width, height = image->height; + cairo_surface_t *surface = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_status_t surface_status = cairo_surface_status(surface); + if (surface_status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(surface_status)); + cairo_surface_destroy(surface); + libraw_dcraw_clear_mem(image); + libraw_close(iprc); + return NULL; + } + + // Starting to modify pixel data directly. Probably an unnecessary call. + cairo_surface_flush(surface); + + uint32_t *pixels = (uint32_t *) cairo_image_surface_get_data(surface); + unsigned char *p = image->data; + for (ushort y = 0; y < image->height; y++) { + for (ushort x = 0; x < image->width; x++) { + *pixels++ = 0xff000000 | (uint32_t) p[0] << 16 | + (uint32_t) p[1] << 8 | (uint32_t) p[2]; + p += 3; + } + } + + // Pixel data has been written, need to let Cairo know. + cairo_surface_mark_dirty(surface); + + libraw_dcraw_clear_mem(image); + libraw_close(iprc); + return surface; +} + +#endif // HAVE_LIBRAW --------------------------------------------------------- +#ifdef HAVE_LIBRSVG // -------------------------------------------------------- + +#ifdef FIV_RSVG_DEBUG +#include +#include +#endif + +// FIXME: librsvg rasterizes filters, so this method isn't fully appropriate. +static cairo_surface_t * +open_librsvg(const gchar *data, gsize len, const gchar *path, GError **error) +{ + GFile *base_file = g_file_new_for_path(path); + GInputStream *is = g_memory_input_stream_new_from_data(data, len, NULL); + RsvgHandle *handle = rsvg_handle_new_from_stream_sync( + is, base_file, RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA, NULL, error); + g_object_unref(base_file); + g_object_unref(is); + if (!handle) + return NULL; + + // TODO(p): Acquire this from somewhere else. + rsvg_handle_set_dpi(handle, 96); + + double w = 0, h = 0; +#if LIBRSVG_CHECK_VERSION(2, 51, 0) + if (!rsvg_handle_get_intrinsic_size_in_pixels(handle, &w, &h)) { +#else + RsvgDimensionData dd = {}; + rsvg_handle_get_dimensions(handle, &dd); + if ((w = dd.width) <= 0 || (h = dd.height) <= 0) { +#endif + RsvgRectangle viewbox = {}; + gboolean has_viewport = FALSE; + rsvg_handle_get_intrinsic_dimensions( + handle, NULL, NULL, NULL, NULL, &has_viewport, &viewbox); + if (!has_viewport) { + set_error(error, "cannot compute pixel dimensions"); + g_object_unref(handle); + return NULL; + } + + w = viewbox.width; + h = viewbox.height; + } + + cairo_rectangle_t extents = { + .x = 0, .y = 0, .width = ceil(w), .height = ceil(h)}; + cairo_surface_t *surface = + cairo_recording_surface_create(CAIRO_CONTENT_COLOR_ALPHA, &extents); + +#ifdef FIV_RSVG_DEBUG + cairo_device_t *script = cairo_script_create("cairo.script"); + cairo_surface_t *tee = + cairo_script_surface_create_for_target(script, surface); + cairo_t *cr = cairo_create(tee); + cairo_device_destroy(script); + cairo_surface_destroy(tee); +#else + cairo_t *cr = cairo_create(surface); +#endif + + RsvgRectangle viewport = {.x = 0, .y = 0, .width = w, .height = h}; + if (!rsvg_handle_render_document(handle, cr, &viewport, error)) { + cairo_surface_destroy(surface); + cairo_destroy(cr); + g_object_unref(handle); + return NULL; + } + + cairo_destroy(cr); + g_object_unref(handle); + +#ifdef FIV_RSVG_DEBUG + cairo_surface_t *svg = cairo_svg_surface_create("cairo.svg", w, h); + cr = cairo_create(svg); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + cairo_surface_destroy(svg); + + cairo_surface_t *png = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w * 10, h * 10); + cr = cairo_create(png); + cairo_scale(cr, 10, 10); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + cairo_surface_write_to_png(png, "cairo.png"); + cairo_surface_destroy(png); +#endif + return surface; +} + +#endif // HAVE_LIBRSVG -------------------------------------------------------- +#ifdef HAVE_XCURSOR //--------------------------------------------------------- + +// fmemopen is part of POSIX-1.2008, this exercise is technically unnecessary. +// libXcursor checks for EOF rather than -1, it may eat your hamster. +struct fiv_io_xcursor { + XcursorFile parent; + unsigned char *data; + long position, len; +}; + +static int +fiv_io_xcursor_read(XcursorFile *file, unsigned char *buf, int len) +{ + struct fiv_io_xcursor *fix = (struct fiv_io_xcursor *) file; + if (fix->position < 0 || fix->position > fix->len) { + errno = EOVERFLOW; + return -1; + } + long n = MIN(fix->len - fix->position, len); + if (n > G_MAXINT) { + errno = EIO; + return -1; + } + memcpy(buf, fix->data + fix->position, n); + fix->position += n; + return n; +} + +static int +fiv_io_xcursor_write(G_GNUC_UNUSED XcursorFile *file, + G_GNUC_UNUSED unsigned char *buf, G_GNUC_UNUSED int len) +{ + errno = EBADF; + return -1; +} + +static int +fiv_io_xcursor_seek(XcursorFile *file, long offset, int whence) +{ + struct fiv_io_xcursor *fix = (struct fiv_io_xcursor *) file; + switch (whence) { + case SEEK_SET: + fix->position = offset; + break; + case SEEK_CUR: + fix->position += offset; + break; + case SEEK_END: + fix->position = fix->len + offset; + break; + default: + errno = EINVAL; + return -1; + } + // This is technically too late for fseek(), but libXcursor doesn't care. + if (fix->position < 0) { + errno = EINVAL; + return -1; + } + return fix->position; +} + +static const XcursorFile fiv_io_xcursor_adaptor = { + .closure = NULL, + .read = fiv_io_xcursor_read, + .write = fiv_io_xcursor_write, + .seek = fiv_io_xcursor_seek, +}; + +static cairo_surface_t * +open_xcursor(const gchar *data, gsize len, GError **error) +{ + if (len > G_MAXLONG) { + set_error(error, "size overflow"); + return NULL; + } + + struct fiv_io_xcursor file = { + .parent = fiv_io_xcursor_adaptor, + .data = (unsigned char *) data, + .position = 0, + .len = len, + }; + + XcursorImages *images = XcursorXcFileLoadAllImages(&file.parent); + if (!images) { + set_error(error, "general failure"); + return NULL; + } + + // Interpret cursors as animated pages. + cairo_surface_t *pages = NULL, *frames_head = NULL, *frames_tail = NULL; + + // XXX: Assuming that all "nominal sizes" have the same dimensions. + XcursorDim last_nominal = -1; + for (int i = 0; i < images->nimage; i++) { + XcursorImage *image = images->images[i]; + + // The library automatically byte swaps in _XcursorReadImage(). + cairo_surface_t *surface = cairo_image_surface_create_for_data( + (unsigned char *) image->pixels, CAIRO_FORMAT_ARGB32, + image->width, image->height, image->width * sizeof *image->pixels); + cairo_surface_set_user_data(surface, &fiv_io_key_frame_duration, + (void *) (intptr_t) image->delay, NULL); + + if (pages && image->size == last_nominal) { + cairo_surface_set_user_data( + surface, &fiv_io_key_frame_previous, frames_tail, NULL); + cairo_surface_set_user_data(frames_tail, &fiv_io_key_frame_next, + surface, (cairo_destroy_func_t) cairo_surface_destroy); + } else if (frames_head) { + cairo_surface_set_user_data( + frames_head, &fiv_io_key_frame_previous, frames_tail, NULL); + + cairo_surface_set_user_data(frames_head, &fiv_io_key_page_next, + surface, (cairo_destroy_func_t) cairo_surface_destroy); + cairo_surface_set_user_data( + surface, &fiv_io_key_page_previous, frames_head, NULL); + frames_head = surface; + } else { + pages = frames_head = surface; + } + + frames_tail = surface; + last_nominal = image->size; + } + if (!pages) { + XcursorImagesDestroy(images); + return NULL; + } + + // Wrap around animations in the last page. + cairo_surface_set_user_data( + frames_head, &fiv_io_key_frame_previous, frames_tail, NULL); + + // There is no need to copy data, assign it to the surface. + static cairo_user_data_key_t key = {}; + cairo_surface_set_user_data( + pages, &key, images, (cairo_destroy_func_t) XcursorImagesDestroy); + return pages; +} + +#endif // HAVE_XCURSOR -------------------------------------------------------- +#ifdef HAVE_LIBWEBP //--------------------------------------------------------- + +static cairo_surface_t * +load_libwebp_nonanimated( + WebPDecoderConfig *config, const WebPData *wd, GError **error) +{ + cairo_surface_t *surface = cairo_image_surface_create( + config->input.has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, + config->input.width, config->input.height); + cairo_status_t surface_status = cairo_surface_status(surface); + if (surface_status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(surface_status)); + cairo_surface_destroy(surface); + return NULL; + } + + config->options.use_threads = true; + + config->output.width = config->input.width; + config->output.height = config->input.height; + config->output.is_external_memory = true; + config->output.u.RGBA.rgba = cairo_image_surface_get_data(surface); + config->output.u.RGBA.stride = cairo_image_surface_get_stride(surface); + config->output.u.RGBA.size = + config->output.u.RGBA.stride * cairo_image_surface_get_height(surface); + if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + config->output.colorspace = MODE_bgrA; + else + config->output.colorspace = MODE_Argb; + + VP8StatusCode err = 0; + if ((err = WebPDecode(wd->bytes, wd->size, config))) { + set_error(error, "WebP decoding error"); + cairo_surface_destroy(surface); + return NULL; + } + + cairo_surface_mark_dirty(surface); + return surface; +} + +static cairo_surface_t * +load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info, + int *last_timestamp, GError **error) +{ + uint8_t *buf = NULL; + int timestamp = 0; + if (!WebPAnimDecoderGetNext(dec, &buf, ×tamp)) { + set_error(error, "WebP decoding error"); + return NULL; + } + + bool is_opaque = (info->bgcolor & 0xFF) == 0xFF; + uint64_t area = info->canvas_width * info->canvas_height; + cairo_surface_t *surface = cairo_image_surface_create( + is_opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, + info->canvas_width, info->canvas_height); + + cairo_status_t surface_status = cairo_surface_status(surface); + if (surface_status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(surface_status)); + cairo_surface_destroy(surface); + return NULL; + } + + uint32_t *dst = (uint32_t *) cairo_image_surface_get_data(surface); + if (G_BYTE_ORDER == G_LITTLE_ENDIAN) { + memcpy(dst, buf, area * sizeof *dst); + } else { + uint32_t *src = (uint32_t *) buf; + for (uint64_t i = 0; i < area; i++) + *dst++ = GUINT32_FROM_LE(*src++); + } + + cairo_surface_mark_dirty(surface); + + // This API is confusing and awkward. + cairo_surface_set_user_data(surface, &fiv_io_key_frame_duration, + (void *) (intptr_t) (timestamp - *last_timestamp), NULL); + *last_timestamp = timestamp; + return surface; +} + +static cairo_surface_t * +load_libwebp_animated(const WebPData *wd, GError **error) +{ + WebPAnimDecoderOptions options = {}; + WebPAnimDecoderOptionsInit(&options); + options.use_threads = true; + options.color_mode = MODE_bgrA; + + WebPAnimInfo info = {}; + WebPAnimDecoder *dec = WebPAnimDecoderNew(wd, &options); + WebPAnimDecoderGetInfo(dec, &info); + + cairo_surface_t *frames = NULL, *frames_tail = NULL; + if (info.canvas_width > INT_MAX || info.canvas_height > INT_MAX) { + set_error(error, "image dimensions overflow"); + goto fail; + } + + int last_timestamp = 0; + while (WebPAnimDecoderHasMoreFrames(dec)) { + cairo_surface_t *surface = + load_libwebp_frame(dec, &info, &last_timestamp, error); + if (!surface) { + g_clear_pointer(&frames, cairo_surface_destroy); + goto fail; + } + + if (frames_tail) + cairo_surface_set_user_data(frames_tail, &fiv_io_key_frame_next, + surface, (cairo_destroy_func_t) cairo_surface_destroy); + else + frames = surface; + + cairo_surface_set_user_data( + surface, &fiv_io_key_frame_previous, frames_tail, NULL); + frames_tail = surface; + } + + if (frames) { + cairo_surface_set_user_data( + frames, &fiv_io_key_frame_previous, frames_tail, NULL); + } else { + set_error(error, "the animation has no frames"); + g_clear_pointer(&frames, cairo_surface_destroy); + } + +fail: + WebPAnimDecoderDelete(dec); + return frames; +} + +static cairo_surface_t * +open_libwebp(const gchar *data, gsize len, const gchar *path, GError **error) +{ + // It is wholly zero-initialized by libwebp. + WebPDecoderConfig config = {}; + if (!WebPInitDecoderConfig(&config)) { + set_error(error, "libwebp version mismatch"); + return NULL; + } + + // TODO(p): Differentiate between a bad WebP, and not a WebP. + VP8StatusCode err = 0; + WebPData wd = {.bytes = (const uint8_t *) data, .size = len}; + if ((err = WebPGetFeatures(wd.bytes, wd.size, &config.input))) { + set_error(error, "WebP decoding error"); + return NULL; + } + + cairo_surface_t *result = config.input.has_animation + ? load_libwebp_animated(&wd, error) + : load_libwebp_nonanimated(&config, &wd, error); + if (!result) + goto fail; + + // Of course everything has to use a different abstraction. + WebPDemuxer *demux = WebPDemux(&wd); + if (!demux) { + g_warning("%s: %s", path, "demux failure"); + goto fail; + } + + // Releasing the demux chunk iterator is actually a no-op. + WebPChunkIterator chunk_iter = {}; + uint32_t flags = WebPDemuxGetI(demux, WEBP_FF_FORMAT_FLAGS); + if ((flags & EXIF_FLAG) && + WebPDemuxGetChunk(demux, "EXIF", 1, &chunk_iter)) { + cairo_surface_set_user_data(result, &fiv_io_key_exif, + g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), + (cairo_destroy_func_t) g_bytes_unref); + WebPDemuxReleaseChunkIterator(&chunk_iter); + } + if ((flags & ICCP_FLAG) && + WebPDemuxGetChunk(demux, "ICCP", 1, &chunk_iter)) { + cairo_surface_set_user_data(result, &fiv_io_key_icc, + g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), + (cairo_destroy_func_t) g_bytes_unref); + WebPDemuxReleaseChunkIterator(&chunk_iter); + } + if ((flags & XMP_FLAG) && + WebPDemuxGetChunk(demux, "XMP ", 1, &chunk_iter)) { + cairo_surface_set_user_data(result, &fiv_io_key_xmp, + g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), + (cairo_destroy_func_t) g_bytes_unref); + WebPDemuxReleaseChunkIterator(&chunk_iter); + } + if (flags & ANIMATION_FLAG) { + cairo_surface_set_user_data(result, &fiv_io_key_loops, + (void *) (uintptr_t) WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT), + NULL); + } + + WebPDemuxDelete(demux); + +fail: + WebPFreeDecBuffer(&config.output); + return result; +} + +#endif // HAVE_LIBWEBP -------------------------------------------------------- +#ifdef HAVE_LIBHEIF //--------------------------------------------------------- + +static cairo_surface_t * +load_libheif_image(struct heif_image_handle *handle, GError **error) +{ + cairo_surface_t *surface = NULL; + int has_alpha = heif_image_handle_has_alpha_channel(handle); + int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle); + if (bit_depth < 0) { + set_error(error, "undefined bit depth"); + goto fail; + } + + // Setting `convert_hdr_to_8bit` seems to be a no-op for RGBA32/64. + struct heif_decoding_options *opts = heif_decoding_options_alloc(); + + // TODO(p): We can get 16-bit depth, in reality most likely 10-bit. + struct heif_image *image = NULL; + struct heif_error err = heif_decode_image(handle, &image, + heif_colorspace_RGB, heif_chroma_interleaved_RGBA, opts); + if (err.code != heif_error_Ok) { + set_error(error, err.message); + goto fail_decode; + } + + int w = heif_image_get_width(image, heif_channel_interleaved); + int h = heif_image_get_height(image, heif_channel_interleaved); + + surface = cairo_image_surface_create( + has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, w, h); + cairo_status_t surface_status = cairo_surface_status(surface); + if (surface_status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(surface_status)); + cairo_surface_destroy(surface); + surface = NULL; + goto fail_process; + } + + // As of writing, the library is using 16-byte alignment, unlike Cairo. + int src_stride = 0; + const uint8_t *src = heif_image_get_plane_readonly( + image, heif_channel_interleaved, &src_stride); + int dst_stride = cairo_image_surface_get_stride(surface); + const uint8_t *dst = cairo_image_surface_get_data(surface); + + for (int y = 0; y < h; y++) { + uint32_t *dstp = (uint32_t *) (dst + dst_stride * y); + const uint32_t *srcp = (const uint32_t *) (src + src_stride * y); + for (int x = 0; x < w; x++) { + uint32_t rgba = g_ntohl(srcp[x]); + *dstp++ = rgba << 24 | rgba >> 8; + } + } + + // TODO(p): Test real behaviour on real transparent images. + if (has_alpha && !heif_image_handle_is_premultiplied_alpha(handle)) { + for (int y = 0; y < h; y++) { + uint32_t *dstp = (uint32_t *) (dst + dst_stride * y); + for (int x = 0; x < w; x++) { + uint32_t argb = dstp[x], a = argb >> 24; + dstp[x] = a << 24 | + PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 | + PREMULTIPLY8(a, 0xFF & (argb >> 8)) << 8 | + PREMULTIPLY8(a, 0xFF & argb); + } + } + } + + heif_item_id exif_id = 0; + if (heif_image_handle_get_list_of_metadata_block_IDs( + handle, "Exif", &exif_id, 1)) { + size_t exif_len = heif_image_handle_get_metadata_size(handle, exif_id); + void *exif = g_malloc0(exif_len); + err = heif_image_handle_get_metadata(handle, exif_id, exif); + if (err.code) { + g_warning("%s", err.message); + g_free(exif); + } else { + cairo_surface_set_user_data(surface, &fiv_io_key_exif, + g_bytes_new_take(exif, exif_len), + (cairo_destroy_func_t) g_bytes_unref); + } + } + + // https://loc.gov/preservation/digital/formats/fdd/fdd000526.shtml#factors + if (heif_image_handle_get_color_profile_type(handle) == + heif_color_profile_type_prof) { + size_t icc_len = heif_image_handle_get_raw_color_profile_size(handle); + void *icc = g_malloc0(icc_len); + err = heif_image_handle_get_raw_color_profile(handle, icc); + if (err.code) { + g_warning("%s", err.message); + g_free(icc); + } else { + cairo_surface_set_user_data(surface, &fiv_io_key_icc, + g_bytes_new_take(icc, icc_len), + (cairo_destroy_func_t) g_bytes_unref); + } + } + + cairo_surface_mark_dirty(surface); + +fail_process: + heif_image_release(image); +fail_decode: + heif_decoding_options_free(opts); +fail: + return surface; +} + +static void +load_libheif_aux_images(const gchar *path, struct heif_image_handle *top, + cairo_surface_t **result, cairo_surface_t **result_tail) +{ + // Include the depth image, we have no special processing for it now. + int filter = LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA; + + int n = heif_image_handle_get_number_of_auxiliary_images(top, filter); + heif_item_id *ids = g_malloc0_n(n, sizeof *ids); + n = heif_image_handle_get_list_of_auxiliary_image_IDs(top, filter, ids, n); + for (int i = 0; i < n; i++) { + struct heif_image_handle *handle = NULL; + struct heif_error err = + heif_image_handle_get_auxiliary_image_handle(top, ids[i], &handle); + if (err.code != heif_error_Ok) { + g_warning("%s: %s", path, err.message); + continue; + } + + GError *e = NULL; + if (!try_append_page( + load_libheif_image(handle, &e), result, result_tail)) { + g_warning("%s: %s", path, e->message); + g_error_free(e); + } + + heif_image_handle_release(handle); + } + + g_free(ids); +} + +static cairo_surface_t * +open_libheif(const gchar *data, gsize len, const gchar *path, GError **error) +{ + // libheif will throw C++ exceptions on allocation failures. + // The library is generally awful through and through. + struct heif_context *ctx = heif_context_alloc(); + cairo_surface_t *result = NULL, *result_tail = NULL; + + struct heif_error err; + err = heif_context_read_from_memory_without_copy(ctx, data, len, NULL); + if (err.code != heif_error_Ok) { + set_error(error, err.message); + goto fail_read; + } + + int n = heif_context_get_number_of_top_level_images(ctx); + heif_item_id *ids = g_malloc0_n(n, sizeof *ids); + n = heif_context_get_list_of_top_level_image_IDs(ctx, ids, n); + for (int i = 0; i < n; i++) { + struct heif_image_handle *handle = NULL; + err = heif_context_get_image_handle(ctx, ids[i], &handle); + if (err.code != heif_error_Ok) { + g_warning("%s: %s", path, err.message); + continue; + } + + GError *e = NULL; + if (!try_append_page( + load_libheif_image(handle, &e), &result, &result_tail)) { + g_warning("%s: %s", path, e->message); + g_error_free(e); + } + + // TODO(p): Possibly add thumbnail images as well. + load_libheif_aux_images(path, handle, &result, &result_tail); + heif_image_handle_release(handle); + } + if (!result) { + g_clear_pointer(&result, cairo_surface_destroy); + set_error(error, "empty or unsupported image"); + } + + g_free(ids); +fail_read: + heif_context_free(ctx); + return result; +} + +#endif // HAVE_LIBHEIF -------------------------------------------------------- +#ifdef HAVE_LIBTIFF //--------------------------------------------------------- + +struct fiv_io_tiff { + unsigned char *data; + gchar *error; + + // No, libtiff, the offset is not supposed to be unsigned (also see: + // man 0p sys_types.h), but at least it's fewer cases for us to care about. + toff_t position, len; +}; + +static tsize_t +fiv_io_tiff_read(thandle_t h, tdata_t buf, tsize_t len) +{ + struct fiv_io_tiff *io = h; + if (len < 0) { + // What the FUCK! This argument is not supposed to be signed! + // How many mistakes can you make in such a basic API? + errno = EOWNERDEAD; + return -1; + } + if (io->position > io->len) { + errno = EOVERFLOW; + return -1; + } + toff_t n = MIN(io->len - io->position, (toff_t) len); + if (n > TIFF_TMSIZE_T_MAX) { + errno = EIO; + return -1; + } + memcpy(buf, io->data + io->position, n); + io->position += n; + return n; +} + +static tsize_t +fiv_io_tiff_write(G_GNUC_UNUSED thandle_t h, + G_GNUC_UNUSED tdata_t buf, G_GNUC_UNUSED tsize_t len) +{ + errno = EBADF; + return -1; +} + +static toff_t +fiv_io_tiff_seek(thandle_t h, toff_t offset, int whence) +{ + struct fiv_io_tiff *io = h; + switch (whence) { + case SEEK_SET: + io->position = offset; + break; + case SEEK_CUR: + io->position += offset; + break; + case SEEK_END: + io->position = io->len + offset; + break; + default: + errno = EINVAL; + return -1; + } + return io->position; +} + +static int +fiv_io_tiff_close(G_GNUC_UNUSED thandle_t h) +{ + return 0; +} + +static toff_t +fiv_io_tiff_size(thandle_t h) +{ + return ((struct fiv_io_tiff *) h)->len; +} + +static void +fiv_io_tiff_error( + thandle_t h, const char *module, const char *format, va_list ap) +{ + struct fiv_io_tiff *io = h; + gchar *message = g_strdup_vprintf(format, ap); + if (io->error) + // I'm not sure if two errors can ever come in a succession, + // but make sure to log them in any case. + g_warning("tiff: %s: %s", module, message); + else + io->error = g_strconcat(module, ": ", message, NULL); + g_free(message); +} + +static void +fiv_io_tiff_warning(G_GNUC_UNUSED thandle_t h, + const char *module, const char *format, va_list ap) +{ + gchar *message = g_strdup_vprintf(format, ap); + g_debug("tiff: %s: %s", module, message); + g_free(message); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static cairo_surface_t * +load_libtiff_directory(TIFF *tiff, GError **error) +{ + char emsg[1024] = ""; + if (!TIFFRGBAImageOK(tiff, emsg)) { + set_error(error, emsg); + return NULL; + } + + // TODO(p): Are there cases where we might not want to "stop on error"? + TIFFRGBAImage image; + if (!TIFFRGBAImageBegin(&image, tiff, 1 /* stop on error */, emsg)) { + set_error(error, emsg); + return NULL; + } + + cairo_surface_t *surface = NULL; + if (image.width > G_MAXINT || image.height >= G_MAXINT || + G_MAXUINT32 / image.width < image.height) { + set_error(error, "image dimensions too large"); + goto fail; + } + + surface = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, image.width, image.height); + + image.req_orientation = ORIENTATION_LEFTTOP; + uint32_t *raster = (uint32_t *) cairo_image_surface_get_data(surface); + if (!TIFFRGBAImageGet(&image, raster, image.width, image.height)) { + g_clear_pointer(&surface, cairo_surface_destroy); + goto fail; + } + + // Needs to be converted from ABGR to alpha-premultiplied ARGB for Cairo. + for (uint32_t i = image.width * image.height; i--;) { + uint32_t pixel = raster[i], + a = TIFFGetA(pixel), + b = TIFFGetB(pixel) * a / 255, + g = TIFFGetG(pixel) * a / 255, + r = TIFFGetR(pixel) * a / 255; + raster[i] = a << 24 | r << 16 | g << 8 | b; + } + + cairo_surface_mark_dirty(surface); + // XXX: The whole file is essentially an Exif, any ideas? + + const uint32_t meta_len = 0; + const void *meta = NULL; + if (TIFFGetField(tiff, TIFFTAG_ICCPROFILE, &meta_len, &meta)) { + cairo_surface_set_user_data(surface, &fiv_io_key_icc, + g_bytes_new(meta, meta_len), (cairo_destroy_func_t) g_bytes_unref); + } + if (TIFFGetField(tiff, TIFFTAG_XMLPACKET, &meta_len, &meta)) { + cairo_surface_set_user_data(surface, &fiv_io_key_xmp, + g_bytes_new(meta, meta_len), (cairo_destroy_func_t) g_bytes_unref); + } + + // Don't ask. The API is high, alright, I'm just not sure about the level. + uint16_t orientation = 0; + if (TIFFGetField(tiff, TIFFTAG_ORIENTATION, &orientation)) { + if (orientation == 5 || orientation == 7) + cairo_surface_set_user_data( + surface, &fiv_io_key_orientation, (void *) (uintptr_t) 5, NULL); + if (orientation == 6 || orientation == 8) + cairo_surface_set_user_data( + surface, &fiv_io_key_orientation, (void *) (uintptr_t) 7, NULL); + } + +fail: + TIFFRGBAImageEnd(&image); + // TODO(p): It's possible to implement ClipPath easily with Cairo. + return surface; +} + +static cairo_surface_t * +open_libtiff(const gchar *data, gsize len, const gchar *path, GError **error) +{ + // Both kinds of handlers are called, redirect everything. + TIFFErrorHandler eh = TIFFSetErrorHandler(NULL); + TIFFErrorHandler wh = TIFFSetWarningHandler(NULL); + TIFFErrorHandlerExt ehe = TIFFSetErrorHandlerExt(fiv_io_tiff_error); + TIFFErrorHandlerExt whe = TIFFSetWarningHandlerExt(fiv_io_tiff_warning); + struct fiv_io_tiff h = { + .data = (unsigned char *) data, + .position = 0, + .len = len, + }; + + cairo_surface_t *result = NULL, *result_tail = NULL; + TIFF *tiff = TIFFClientOpen(path, "rm" /* Avoid mmap. */, &h, + fiv_io_tiff_read, fiv_io_tiff_write, fiv_io_tiff_seek, + fiv_io_tiff_close, fiv_io_tiff_size, NULL, NULL); + if (!tiff) + goto fail; + + // In Nikon NEF files, IFD0 is a tiny uncompressed thumbnail with SubIFDs-- + // two of them JPEGs, the remaining one is raw. libtiff cannot read either + // of those better versions. + // + // TODO(p): If NewSubfileType is ReducedImage, and it has SubIFDs compressed + // as old JPEG (6), decode JPEGInterchangeFormat/JPEGInterchangeFormatLength + // with libjpeg-turbo and insert them as the starting pages. + // + // This is not possible with libtiff directly, because TIFFSetSubDirectory() + // requires an ImageLength tag that's missing, and TIFFReadCustomDirectory() + // takes a privately defined struct that cannot be omitted. + // + // TODO(p): Samsung Android DNGs also claim to be TIFF/EP, but use a smaller + // uncompressed YCbCr image. Apple ProRAW uses the new JPEG Compression (7), + // with a weird Orientation. It also uses that value for its raw data. + uint32_t subtype = 0; + uint16_t subifd_count = 0; + const uint64_t *subifd_offsets = NULL; + if (TIFFGetField(tiff, TIFFTAG_SUBFILETYPE, &subtype) && + (subtype & FILETYPE_REDUCEDIMAGE) && + TIFFGetField(tiff, TIFFTAG_SUBIFD, &subifd_count, &subifd_offsets) && + subifd_count > 0 && subifd_offsets) { + } + + do { + // We inform about unsupported directories, but do not fail on them. + GError *err = NULL; + if (!try_append_page( + load_libtiff_directory(tiff, &err), &result, &result_tail)) { + g_warning("%s: %s", path, err->message); + g_error_free(err); + } + } while (TIFFReadDirectory(tiff)); + TIFFClose(tiff); + +fail: + if (h.error) { + g_clear_pointer(&result, cairo_surface_destroy); + set_error(error, h.error); + g_free(h.error); + } else if (!result) { + set_error(error, "empty or unsupported image"); + } + + TIFFSetErrorHandlerExt(ehe); + TIFFSetWarningHandlerExt(whe); + TIFFSetErrorHandler(eh); + TIFFSetWarningHandler(wh); + return result; +} + +#endif // HAVE_LIBTIFF -------------------------------------------------------- +#ifdef HAVE_GDKPIXBUF // ------------------------------------------------------ + +static cairo_surface_t * +open_gdkpixbuf(const gchar *data, gsize len, GError **error) +{ + // gdk-pixbuf controls the playback itself, there is no reliable method of + // extracting individual frames (due to loops). + GInputStream *is = g_memory_input_stream_new_from_data(data, len, NULL); + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_stream(is, NULL, error); + g_object_unref(is); + if (!pixbuf) + return NULL; + + cairo_surface_t *surface = + gdk_cairo_surface_create_from_pixbuf(pixbuf, 1, NULL); + + const char *orientation = gdk_pixbuf_get_option(pixbuf, "orientation"); + if (orientation && strlen(orientation) == 1) { + int n = *orientation - '0'; + if (n >= 1 && n <= 8) + cairo_surface_set_user_data( + surface, &fiv_io_key_orientation, (void *) (uintptr_t) n, NULL); + } + + const char *icc_profile = gdk_pixbuf_get_option(pixbuf, "icc-profile"); + if (icc_profile) { + gsize out_len = 0; + guchar *raw = g_base64_decode(icc_profile, &out_len); + if (raw) { + cairo_surface_set_user_data(surface, &fiv_io_key_icc, + g_bytes_new_take(raw, out_len), + (cairo_destroy_func_t) g_bytes_unref); + } + } + + g_object_unref(pixbuf); + return surface; +} + +#endif // HAVE_GDKPIXBUF ------------------------------------------------------ + +cairo_user_data_key_t fiv_io_key_exif; +cairo_user_data_key_t fiv_io_key_orientation; +cairo_user_data_key_t fiv_io_key_icc; +cairo_user_data_key_t fiv_io_key_xmp; + +cairo_user_data_key_t fiv_io_key_frame_next; +cairo_user_data_key_t fiv_io_key_frame_previous; +cairo_user_data_key_t fiv_io_key_frame_duration; +cairo_user_data_key_t fiv_io_key_loops; + +cairo_user_data_key_t fiv_io_key_page_next; +cairo_user_data_key_t fiv_io_key_page_previous; + +cairo_surface_t * +fiv_io_open(const gchar *path, GError **error) +{ + // TODO(p): Don't always load everything into memory, test type first, + // so that we can reject non-pictures early. Wuffs only needs the first + // 16 bytes to make a guess right now. + // + // LibRaw poses an issue--there is no good registry for identification + // of supported files. Many of them are compliant TIFF files. + // The only good filtering method for RAWs are currently file extensions + // extracted from shared-mime-info. + // + // SVG is also problematic, an unbounded search for its root element. + // But problematic files can be assumed to be crafted. + // + // gdk-pixbuf exposes its detection data through gdk_pixbuf_get_formats(). + // This may also be unbounded, as per format_check(). + gchar *data = NULL; + gsize len = 0; + if (!g_file_get_contents(path, &data, &len, error)) + return NULL; + + cairo_surface_t *surface = fiv_io_open_from_data(data, len, path, error); + free(data); + return surface; +} + +cairo_surface_t * +fiv_io_open_from_data( + const char *data, size_t len, const gchar *path, GError **error) +{ + wuffs_base__slice_u8 prefix = + wuffs_base__make_slice_u8((uint8_t *) data, len); + + cairo_surface_t *surface = NULL; + switch (wuffs_base__magic_number_guess_fourcc(prefix)) { + case WUFFS_BASE__FOURCC__BMP: + // Note that BMP can redirect into another format, + // which is so far unsupported here. + surface = open_wuffs_using( + wuffs_bmp__decoder__alloc_as__wuffs_base__image_decoder, data, len, + error); + break; + case WUFFS_BASE__FOURCC__GIF: + surface = open_wuffs_using( + wuffs_gif__decoder__alloc_as__wuffs_base__image_decoder, data, len, + error); + break; + case WUFFS_BASE__FOURCC__PNG: + surface = open_wuffs_using( + wuffs_png__decoder__alloc_as__wuffs_base__image_decoder, data, len, + error); + break; + case WUFFS_BASE__FOURCC__JPEG: + surface = open_libjpeg_turbo(data, len, error); + break; + default: +#ifdef HAVE_LIBRAW // --------------------------------------------------------- + if ((surface = open_libraw(data, len, error))) + break; + + // TODO(p): We should try to pass actual processing errors through, + // notably only continue with LIBRAW_FILE_UNSUPPORTED. + if (error) { + g_debug("%s", (*error)->message); + g_clear_error(error); + } +#endif // HAVE_LIBRAW --------------------------------------------------------- +#ifdef HAVE_LIBRSVG // -------------------------------------------------------- + if ((surface = open_librsvg(data, len, path, error))) + break; + + // XXX: It doesn't look like librsvg can return sensible errors. + if (error) { + g_debug("%s", (*error)->message); + g_clear_error(error); + } +#endif // HAVE_LIBRSVG -------------------------------------------------------- +#ifdef HAVE_XCURSOR //--------------------------------------------------------- + if ((surface = open_xcursor(data, len, error))) + break; + if (error) { + g_debug("%s", (*error)->message); + g_clear_error(error); + } +#endif // HAVE_XCURSOR -------------------------------------------------------- +#ifdef HAVE_LIBWEBP //--------------------------------------------------------- + // TODO(p): https://github.com/google/wuffs/commit/4c04ac1 + if ((surface = open_libwebp(data, len, path, error))) + break; + if (error) { + g_debug("%s", (*error)->message); + g_clear_error(error); + } +#endif // HAVE_LIBWEBP -------------------------------------------------------- +#ifdef HAVE_LIBHEIF //--------------------------------------------------------- + if ((surface = open_libheif(data, len, path, error))) + break; + if (error) { + g_debug("%s", (*error)->message); + g_clear_error(error); + } +#endif // HAVE_LIBHEIF -------------------------------------------------------- +#ifdef HAVE_LIBTIFF //--------------------------------------------------------- + // This needs to be positioned after LibRaw. + if ((surface = open_libtiff(data, len, path, error))) + break; + if (error) { + g_debug("%s", (*error)->message); + g_clear_error(error); + } +#endif // HAVE_LIBTIFF -------------------------------------------------------- +#ifdef HAVE_GDKPIXBUF // ------------------------------------------------------ + // This is only used as a last resort, the rest above is special-cased. + if ((surface = open_gdkpixbuf(data, len, error))) + break; + if (error && (*error)->code != GDK_PIXBUF_ERROR_UNKNOWN_TYPE) + break; + + if (error) { + g_debug("%s", (*error)->message); + g_clear_error(error); + } +#endif // HAVE_GDKPIXBUF ------------------------------------------------------ + + set_error(error, "unsupported file type"); + } + + // gdk-pixbuf only gives out this single field--cater to its limitations, + // since we'd really like to have it. + // TODO(p): The Exif orientation should be ignored in JPEG-XL at minimum. + GBytes *exif = NULL; + gsize exif_len = 0; + gconstpointer exif_data = NULL; + if (surface && + (exif = cairo_surface_get_user_data(surface, &fiv_io_key_exif)) && + (exif_data = g_bytes_get_data(exif, &exif_len))) { + cairo_surface_set_user_data(surface, &fiv_io_key_orientation, + (void *) (uintptr_t) fiv_io_exif_orientation(exif_data, exif_len), + NULL); + } + return surface; +} + +// --- Export ------------------------------------------------------------------ +#ifdef HAVE_LIBWEBP + +static WebPData +encode_lossless_webp(cairo_surface_t *surface) +{ + cairo_format_t format = cairo_image_surface_get_format(surface); + int w = cairo_image_surface_get_width(surface); + int h = cairo_image_surface_get_height(surface); + if (format != CAIRO_FORMAT_ARGB32 && + format != CAIRO_FORMAT_RGB24) { + cairo_surface_t *converted = + cairo_image_surface_create((format = CAIRO_FORMAT_ARGB32), w, h); + cairo_t *cr = cairo_create(converted); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + cairo_paint(cr); + cairo_destroy(cr); + surface = converted; + } else { + surface = cairo_surface_reference(surface); + } + + WebPConfig config = {}; + WebPPicture picture = {}; + if (!WebPConfigInit(&config) || + !WebPConfigLosslessPreset(&config, 6) || + !WebPPictureInit(&picture)) + goto fail; + + config.thread_level = true; + if (!WebPValidateConfig(&config)) + goto fail; + + picture.use_argb = true; + picture.width = w; + picture.height = h; + if (!WebPPictureAlloc(&picture)) + goto fail; + + // Cairo uses a similar internal format, so we should be able to + // copy it over and fix up the minor differences. + // This is written to be easy to follow rather than fast. + int stride = cairo_image_surface_get_stride(surface); + if (picture.argb_stride != w || + picture.argb_stride * (int) sizeof *picture.argb != stride || + INT_MAX / picture.argb_stride < h) + goto fail_compatibility; + + uint32_t *argb = + memcpy(picture.argb, cairo_image_surface_get_data(surface), stride * h); + if (format == CAIRO_FORMAT_ARGB32) + for (int i = h * picture.argb_stride; i-- > 0; argb++) + *argb = wuffs_base__color_u32_argb_premul__as__color_u32_argb_nonpremul(*argb); + else + for (int i = h * picture.argb_stride; i-- > 0; argb++) + *argb |= 0xFF000000; + + WebPMemoryWriter writer = {}; + WebPMemoryWriterInit(&writer); + picture.writer = WebPMemoryWrite; + picture.custom_ptr = &writer; + if (!WebPEncode(&config, &picture)) + g_debug("WebPEncode: %d\n", picture.error_code); + +fail_compatibility: + WebPPictureFree(&picture); +fail: + cairo_surface_destroy(surface); + return (WebPData) {.bytes = writer.mem, .size = writer.size}; +} + +static gboolean +encode_webp_image(WebPMux *mux, cairo_surface_t *frame) +{ + WebPData bitstream = encode_lossless_webp(frame); + gboolean ok = WebPMuxSetImage(mux, &bitstream, true) == WEBP_MUX_OK; + WebPDataClear(&bitstream); + return ok; +} + +static gboolean +encode_webp_animation(WebPMux *mux, cairo_surface_t *page) +{ + gboolean ok = TRUE; + for (cairo_surface_t *frame = page; ok && frame; frame = + cairo_surface_get_user_data(frame, &fiv_io_key_frame_next)) { + WebPMuxFrameInfo info = { + .bitstream = encode_lossless_webp(frame), + .duration = (intptr_t) cairo_surface_get_user_data( + frame, &fiv_io_key_frame_duration), + .id = WEBP_CHUNK_ANMF, + .dispose_method = WEBP_MUX_DISPOSE_NONE, + .blend_method = WEBP_MUX_NO_BLEND, + }; + ok = WebPMuxPushFrame(mux, &info, true) == WEBP_MUX_OK; + WebPDataClear(&info.bitstream); + } + WebPMuxAnimParams params = { + .bgcolor = 0x00000000, // BGRA, curiously. + .loop_count = (uintptr_t) + cairo_surface_get_user_data(page, &fiv_io_key_loops), + }; + return ok && WebPMuxSetAnimationParams(mux, ¶ms) == WEBP_MUX_OK; +} + +static gboolean +transfer_metadata(WebPMux *mux, const char *fourcc, cairo_surface_t *page, + const cairo_user_data_key_t *kind) +{ + GBytes *data = cairo_surface_get_user_data(page, kind); + if (!data) + return TRUE; + + gsize len = 0; + gconstpointer p = g_bytes_get_data(data, &len); + return WebPMuxSetChunk(mux, fourcc, &(WebPData) {.bytes = p, .size = len}, + false) == WEBP_MUX_OK; +} + +gboolean +fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, const gchar *path, + GError **error) +{ + g_return_val_if_fail(page != NULL, FALSE); + g_return_val_if_fail(path != NULL, FALSE); + + gboolean ok = TRUE; + WebPMux *mux = WebPMuxNew(); + if (frame) + ok = encode_webp_image(mux, frame); + else if (!cairo_surface_get_user_data(page, &fiv_io_key_frame_next)) + ok = encode_webp_image(mux, page); + else + ok = encode_webp_animation(mux, page); + + ok = ok && transfer_metadata(mux, "EXIF", page, &fiv_io_key_exif); + ok = ok && transfer_metadata(mux, "ICCP", page, &fiv_io_key_icc); + ok = ok && transfer_metadata(mux, "XMP ", page, &fiv_io_key_xmp); + + WebPData assembled = {}; + WebPDataInit(&assembled); + if (!(ok = ok && WebPMuxAssemble(mux, &assembled) == WEBP_MUX_OK)) + set_error(error, "encoding failed"); + else + ok = g_file_set_contents( + path, (const gchar *) assembled.bytes, assembled.size, error); + + WebPMuxDelete(mux); + WebPDataClear(&assembled); + return ok; +} + +#endif // HAVE_LIBWEBP +// --- Metadata ---------------------------------------------------------------- + +FivIoOrientation +fiv_io_exif_orientation(const guint8 *tiff, gsize len) +{ + // libtiff also knows how to do this, but it's not a lot of code. + // The "Orientation" tag/field is part of Baseline TIFF 6.0 (1992), + // it just so happens that Exif is derived from this format. + // There is no other meaningful placement for this than right in IFD0, + // describing the main image. + const uint8_t *end = tiff + len, + le[4] = {'I', 'I', 42, 0}, + be[4] = {'M', 'M', 0, 42}; + + uint16_t (*u16)(const uint8_t *) = NULL; + uint32_t (*u32)(const uint8_t *) = NULL; + if (tiff + 8 > end) { + return FivIoOrientationUnknown; + } else if (!memcmp(tiff, le, sizeof le)) { + u16 = wuffs_base__peek_u16le__no_bounds_check; + u32 = wuffs_base__peek_u32le__no_bounds_check; + } else if (!memcmp(tiff, be, sizeof be)) { + u16 = wuffs_base__peek_u16be__no_bounds_check; + u32 = wuffs_base__peek_u32be__no_bounds_check; + } else { + return FivIoOrientationUnknown; + } + + const uint8_t *ifd0 = tiff + u32(tiff + 4); + if (ifd0 + 2 > end) + return FivIoOrientationUnknown; + + uint16_t fields = u16(ifd0); + enum { BYTE = 1, ASCII, SHORT, LONG, RATIONAL, + SBYTE, UNDEFINED, SSHORT, SLONG, SRATIONAL, FLOAT, DOUBLE }; + enum { Orientation = 274 }; + for (const guint8 *p = ifd0 + 2; fields-- && p + 12 <= end; p += 12) { + uint16_t tag = u16(p), type = u16(p + 2), value16 = u16(p + 8); + uint32_t count = u32(p + 4); + if (tag == Orientation && type == SHORT && count == 1 && + value16 >= 1 && value16 <= 8) + return value16; + } + return FivIoOrientationUnknown; +} + +gboolean +fiv_io_save_metadata(cairo_surface_t *page, const gchar *path, GError **error) +{ + g_return_val_if_fail(page != NULL, FALSE); + + FILE *fp = fopen(path, "wb"); + if (!fp) { + g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), + "%s: %s", path, g_strerror(errno)); + return FALSE; + } + + // This does not constitute a valid JPEG codestream--it's a TEM marker + // (standalone) with trailing nonsense. + fprintf(fp, "\xFF\001Exiv2"); + + GBytes *data = NULL; + gsize len = 0; + gconstpointer p = NULL; + + // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 + // I don't care if Exiv2 supports it this way. + if ((data = cairo_surface_get_user_data(page, &fiv_io_key_exif)) && + (p = g_bytes_get_data(data, &len))) { + while (len) { + gsize chunk = MIN(len, 0xFFFF - 2 - 6); + uint8_t header[10] = "\xFF\xE1\000\000Exif\000\000"; + header[2] = (chunk + 2 + 6) >> 8; + header[3] = (chunk + 2 + 6); + + fwrite(header, 1, sizeof header, fp); + fwrite(p, 1, chunk, fp); + + len -= chunk; + p += chunk; + } + } + + // https://www.color.org/specification/ICC1v43_2010-12.pdf B.4 + if ((data = cairo_surface_get_user_data(page, &fiv_io_key_icc)) && + (p = g_bytes_get_data(data, &len))) { + gsize limit = 0xFFFF - 2 - 12; + uint8_t current = 0, total = (len + limit - 1) / limit; + while (len) { + gsize chunk = MIN(len, limit); + uint8_t header[18] = "\xFF\xE2\000\000ICC_PROFILE\000\000\000"; + header[2] = (chunk + 2 + 12 + 2) >> 8; + header[3] = (chunk + 2 + 12 + 2); + header[16] = ++current; + header[17] = total; + + fwrite(header, 1, sizeof header, fp); + fwrite(p, 1, chunk, fp); + + len -= chunk; + p += chunk; + } + } + + // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 + // If the main segment overflows, then it's a sign of bad luck, + // because 1.1.3.1 is way too complex. + if ((data = cairo_surface_get_user_data(page, &fiv_io_key_xmp)) && + (p = g_bytes_get_data(data, &len))) { + while (len) { + gsize chunk = MIN(len, 0xFFFF - 2 - 29); + uint8_t header[33] = + "\xFF\xE1\000\000http://ns.adobe.com/xap/1.0/\000"; + header[2] = (chunk + 2 + 29) >> 8; + header[3] = (chunk + 2 + 29); + + fwrite(header, 1, sizeof header, fp); + fwrite(p, 1, chunk, fp); + break; + } + } + + fprintf(fp, "\xFF\xD9"); + if (ferror(fp)) { + g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), + "%s: %s", path, g_strerror(errno)); + fclose(fp); + return FALSE; + } + if (fclose(fp)) { + g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), + "%s: %s", path, g_strerror(errno)); + return FALSE; + } + return TRUE; +} + +// --- Thumbnails -------------------------------------------------------------- + +GType +fiv_io_thumbnail_size_get_type(void) +{ + static gsize guard; + if (g_once_init_enter(&guard)) { +#define XX(name, value, dir) {FIV_IO_THUMBNAIL_SIZE_ ## name, \ + "FIV_IO_THUMBNAIL_SIZE_" #name, #name}, + static const GEnumValue values[] = {FIV_IO_THUMBNAIL_SIZES(XX) {}}; +#undef XX + GType type = g_enum_register_static( + g_intern_static_string("FivIoThumbnailSize"), values); + g_once_init_leave(&guard, type); + } + return guard; +} + +#define XX(name, value, dir) {value, dir}, +FivIoThumbnailSizeInfo + fiv_io_thumbnail_sizes[FIV_IO_THUMBNAIL_SIZE_COUNT] = { + FIV_IO_THUMBNAIL_SIZES(XX)}; +#undef XX + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#ifndef __linux__ +#define st_mtim st_mtimespec +#endif // ! __linux__ + +static int // tri-state +check_spng_thumbnail_texts(struct spng_text *texts, uint32_t texts_len, + const gchar *target, time_t mtime) +{ + // May contain Thumb::Image::Width Thumb::Image::Height, + // but those aren't interesting currently (would be for fast previews). + bool need_uri = true, need_mtime = true; + for (uint32_t i = 0; i < texts_len; i++) { + struct spng_text *text = texts + i; + if (!strcmp(text->keyword, "Thumb::URI")) { + need_uri = false; + if (strcmp(target, text->text)) + return false; + } + if (!strcmp(text->keyword, "Thumb::MTime")) { + need_mtime = false; + if (atol(text->text) != mtime) + return false; + } + } + return need_uri || need_mtime ? -1 : true; +} + +static int // tri-state +check_spng_thumbnail(spng_ctx *ctx, const gchar *target, time_t mtime, int *err) +{ + uint32_t texts_len = 0; + if ((*err = spng_get_text(ctx, NULL, &texts_len))) + return false; + + int result = false; + struct spng_text *texts = g_malloc0_n(texts_len, sizeof *texts); + if (!(*err = spng_get_text(ctx, texts, &texts_len))) + result = check_spng_thumbnail_texts(texts, texts_len, target, mtime); + g_free(texts); + return result; +} + +static cairo_surface_t * +read_spng_thumbnail( + const gchar *path, const gchar *uri, time_t mtime, GError **error) +{ + FILE *fp; + cairo_surface_t *result = NULL; + if (!(fp = fopen(path, "rb"))) { + set_error(error, g_strerror(errno)); + return NULL; + } + + errno = 0; + spng_ctx *ctx = spng_ctx_new(0); + if (!ctx) { + set_error(error, g_strerror(errno)); + goto fail_init; + } + + int err; + size_t size = 0; + if ((err = spng_set_png_file(ctx, fp)) || + (err = spng_set_image_limits(ctx, INT16_MAX, INT16_MAX)) || + (err = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &size))) { + set_error(error, spng_strerror(err)); + goto fail; + } + if (check_spng_thumbnail(ctx, uri, mtime, &err) == false) { + set_error(error, err ? spng_strerror(err) : "mismatch"); + goto fail; + } + + struct spng_ihdr ihdr = {}; + spng_get_ihdr(ctx, &ihdr); + cairo_surface_t *surface = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, ihdr.width, ihdr.height); + + cairo_status_t surface_status = cairo_surface_status(surface); + if (surface_status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(surface_status)); + goto fail_data; + } + + uint32_t *data = (uint32_t *) cairo_image_surface_get_data(surface); + g_assert((size_t) cairo_image_surface_get_stride(surface) * + cairo_image_surface_get_height(surface) == size); + + cairo_surface_flush(surface); + if ((err = spng_decode_image(ctx, data, size, SPNG_FMT_RGBA8, + SPNG_DECODE_TRNS | SPNG_DECODE_GAMMA))) { + set_error(error, spng_strerror(err)); + goto fail_data; + } + + // The specification does not say where the required metadata should be, + // it could very well be broken up into two parts. + if (check_spng_thumbnail(ctx, uri, mtime, &err) != true) { + set_error( + error, err ? spng_strerror(err) : "mismatch or not a thumbnail"); + goto fail_data; + } + + // pixman can be mildly abused to do this operation, but it won't be faster. + struct spng_trns trns = {}; + if (ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA || + ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA || + !spng_get_trns(ctx, &trns)) { + for (size_t i = size / sizeof *data; i--; ) { + const uint8_t *unit = (const uint8_t *) &data[i]; + uint32_t a = unit[3], + b = PREMULTIPLY8(a, unit[2]), + g = PREMULTIPLY8(a, unit[1]), + r = PREMULTIPLY8(a, unit[0]); + data[i] = a << 24 | r << 16 | g << 8 | b; + } + } else { + for (size_t i = size / sizeof *data; i--; ) { + uint32_t rgba = g_ntohl(data[i]); + data[i] = rgba << 24 | rgba >> 8; + } + } + + cairo_surface_mark_dirty((result = surface)); + +fail_data: + if (!result) + cairo_surface_destroy(surface); +fail: + spng_ctx_free(ctx); +fail_init: + fclose(fp); + return result; +} + +cairo_surface_t * +fiv_io_lookup_thumbnail(GFile *target, FivIoThumbnailSize size) +{ + g_return_val_if_fail(size >= FIV_IO_THUMBNAIL_SIZE_MIN && + size <= FIV_IO_THUMBNAIL_SIZE_MAX, NULL); + + // Local files only, at least for now. + gchar *path = g_file_get_path(target); + if (!path) + return NULL; + + GStatBuf st = {}; + int err = g_stat(path, &st); + g_free(path); + if (err) + return NULL; + + gchar *uri = g_file_get_uri(target); + gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1); + gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache"); + + // The lookup sequence is: nominal..max, then mirroring back to ..min. + cairo_surface_t *result = NULL; + GError *error = NULL; + for (int i = 0; i < FIV_IO_THUMBNAIL_SIZE_COUNT; i++) { + int use = size + i; + if (use > FIV_IO_THUMBNAIL_SIZE_MAX) + use = FIV_IO_THUMBNAIL_SIZE_MAX - i; + + gchar *path = g_strdup_printf("%s/thumbnails/%s/%s.png", cache_dir, + fiv_io_thumbnail_sizes[use].thumbnail_spec_name, sum); + result = read_spng_thumbnail(path, uri, st.st_mtim.tv_sec, &error); + if (error) { + g_debug("%s: %s", path, error->message); + g_clear_error(&error); + } + g_free(path); + if (result) + break; + } + + g_free(cache_dir); + g_free(sum); + g_free(uri); + return result; +} + +int +fiv_io_filecmp(GFile *location1, GFile *location2) +{ + if (g_file_has_prefix(location1, location2)) + return +1; + if (g_file_has_prefix(location2, location1)) + return -1; + + gchar *name1 = g_file_get_parse_name(location1); + gchar *name2 = g_file_get_parse_name(location2); + int result = g_utf8_collate(name1, name2); + g_free(name1); + g_free(name2); + return result; +} diff --git a/fiv-io.h b/fiv-io.h new file mode 100644 index 0000000..5fbe276 --- /dev/null +++ b/fiv-io.h @@ -0,0 +1,122 @@ +// +// fiv-io.h: image operations +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#pragma once + +#include +#include +#include + +extern const char *fiv_io_supported_media_types[]; + +char **fiv_io_all_supported_media_types(void); + +// Userdata are typically attached to all Cairo surfaces in an animation. + +/// GBytes with plain Exif/TIFF data. +extern cairo_user_data_key_t fiv_io_key_exif; +/// FivIoOrientation, as a uintptr_t. +extern cairo_user_data_key_t fiv_io_key_orientation; +/// GBytes with plain ICC profile data. +extern cairo_user_data_key_t fiv_io_key_icc; +/// GBytes with plain XMP data. +extern cairo_user_data_key_t fiv_io_key_xmp; + +/// The next frame in a sequence, as a surface, in a chain, pre-composited. +/// There is no wrap-around. +extern cairo_user_data_key_t fiv_io_key_frame_next; +/// The previous frame in a sequence, as a surface, in a chain, pre-composited. +/// This is a weak pointer that wraps around, and needn't be present +/// for static images. +extern cairo_user_data_key_t fiv_io_key_frame_previous; +/// Frame duration in milliseconds as an intptr_t. +extern cairo_user_data_key_t fiv_io_key_frame_duration; +/// How many times to repeat the animation, or zero for +inf, as a uintptr_t. +extern cairo_user_data_key_t fiv_io_key_loops; + +/// The first frame of the next page, as a surface, in a chain. +/// There is no wrap-around. +extern cairo_user_data_key_t fiv_io_key_page_next; +/// The first frame of the previous page, as a surface, in a chain. +/// There is no wrap-around. This is a weak pointer. +extern cairo_user_data_key_t fiv_io_key_page_previous; + +cairo_surface_t *fiv_io_open(const gchar *path, GError **error); +cairo_surface_t *fiv_io_open_from_data( + const char *data, size_t len, const gchar *path, GError **error); + +int fiv_io_filecmp(GFile *f1, GFile *f2); + +// --- Export ------------------------------------------------------------------ + +/// Requires libwebp. +/// If no exact frame is specified, this potentially creates an animation. +gboolean fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, + const gchar *path, GError **error); + +// --- Metadata ---------------------------------------------------------------- + +// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6 +typedef enum _FivIoOrientation { + FivIoOrientationUnknown = 0, + FivIoOrientation0 = 1, + FivIoOrientationMirror0 = 2, + FivIoOrientation180 = 3, + FivIoOrientationMirror180 = 4, + FivIoOrientationMirror270 = 5, + FivIoOrientation90 = 6, + FivIoOrientationMirror90 = 7, + FivIoOrientation270 = 8 +} FivIoOrientation; + +FivIoOrientation fiv_io_exif_orientation(const guint8 *exif, gsize len); + +/// Save metadata attached by this module in Exiv2 format. +gboolean fiv_io_save_metadata( + cairo_surface_t *page, const gchar *path, GError **error); + +// --- Thumbnails -------------------------------------------------------------- + +// And this is how you avoid glib-mkenums. +typedef enum _FivIoThumbnailSize { +#define FIV_IO_THUMBNAIL_SIZES(XX) \ + XX(SMALL, 128, "normal") \ + XX(NORMAL, 256, "large") \ + XX(LARGE, 512, "x-large") \ + XX(HUGE, 1024, "xx-large") +#define XX(name, value, dir) FIV_IO_THUMBNAIL_SIZE_ ## name, + FIV_IO_THUMBNAIL_SIZES(XX) +#undef XX + FIV_IO_THUMBNAIL_SIZE_COUNT, + + FIV_IO_THUMBNAIL_SIZE_MIN = 0, + FIV_IO_THUMBNAIL_SIZE_MAX = FIV_IO_THUMBNAIL_SIZE_COUNT - 1 +} FivIoThumbnailSize; + +GType fiv_io_thumbnail_size_get_type(void) G_GNUC_CONST; +#define FIV_TYPE_IO_THUMBNAIL_SIZE (fiv_io_thumbnail_size_get_type()) + +typedef struct _FivIoThumbnailSizeInfo { + int size; ///< Nominal size in pixels + const char *thumbnail_spec_name; ///< thumbnail-spec directory name +} FivIoThumbnailSizeInfo; + +extern FivIoThumbnailSizeInfo + fiv_io_thumbnail_sizes[FIV_IO_THUMBNAIL_SIZE_COUNT]; + +cairo_surface_t *fiv_io_lookup_thumbnail( + GFile *target, FivIoThumbnailSize size); diff --git a/fiv-sidebar.c b/fiv-sidebar.c new file mode 100644 index 0000000..5a01194 --- /dev/null +++ b/fiv-sidebar.c @@ -0,0 +1,433 @@ +// +// fiv-sidebar.c: molesting GtkPlacesSidebar +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#include + +#include "fiv-io.h" // fiv_io_filecmp +#include "fiv-sidebar.h" + +struct _FivSidebar { + GtkScrolledWindow parent_instance; + GtkPlacesSidebar *places; + GtkWidget *toolbar; + GtkWidget *listbox; + GFile *location; +}; + +G_DEFINE_TYPE(FivSidebar, fiv_sidebar, GTK_TYPE_SCROLLED_WINDOW) + +G_DEFINE_QUARK(fiv-sidebar-location-quark, fiv_sidebar_location) + +enum { + OPEN_LOCATION, + LAST_SIGNAL, +}; + +// Globals are, sadly, the canonical way of storing signal numbers. +static guint sidebar_signals[LAST_SIGNAL]; + +static void +fiv_sidebar_dispose(GObject *gobject) +{ + FivSidebar *self = FIV_SIDEBAR(gobject); + g_clear_object(&self->location); + + G_OBJECT_CLASS(fiv_sidebar_parent_class)->dispose(gobject); +} + +static void +fiv_sidebar_class_init(FivSidebarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->dispose = fiv_sidebar_dispose; + + // You're giving me no choice, Adwaita. + // Your style is hardcoded to match against the class' CSS name. + // And I need replicate the internal widget structure. + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + gtk_widget_class_set_css_name(widget_class, "placessidebar"); + + // TODO(p): Consider a return value, and using it. + sidebar_signals[OPEN_LOCATION] = + g_signal_new("open_location", G_TYPE_FROM_CLASS(klass), 0, 0, + NULL, NULL, NULL, G_TYPE_NONE, + 2, G_TYPE_FILE, GTK_TYPE_PLACES_OPEN_FLAGS); +} + +static gboolean +on_rowlabel_query_tooltip(GtkWidget *widget, + G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y, + G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip) +{ + GtkLabel *label = GTK_LABEL(widget); + if (!pango_layout_is_ellipsized(gtk_label_get_layout(label))) + return FALSE; + + gtk_tooltip_set_text(tooltip, gtk_label_get_text(label)); + return TRUE; +} + +static GtkWidget * +create_row(GFile *file, const char *icon_name) +{ + // TODO(p): Handle errors better. + GFileInfo *info = + g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); + if (!info) + return NULL; + + const char *name = g_file_info_get_display_name(info); + GtkWidget *rowbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + + GtkWidget *rowimage = + gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU); + gtk_style_context_add_class( + gtk_widget_get_style_context(rowimage), "sidebar-icon"); + gtk_container_add(GTK_CONTAINER(rowbox), rowimage); + + GtkWidget *rowlabel = gtk_label_new(name); + gtk_label_set_ellipsize(GTK_LABEL(rowlabel), PANGO_ELLIPSIZE_END); + gtk_widget_set_has_tooltip(rowlabel, TRUE); + g_signal_connect(rowlabel, "query-tooltip", + G_CALLBACK(on_rowlabel_query_tooltip), NULL); + gtk_style_context_add_class( + gtk_widget_get_style_context(rowlabel), "sidebar-label"); + gtk_container_add(GTK_CONTAINER(rowbox), rowlabel); + + GtkWidget *revealer = gtk_revealer_new(); + gtk_revealer_set_reveal_child( + GTK_REVEALER(revealer), TRUE); + gtk_revealer_set_transition_type( + GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_NONE); + gtk_container_add(GTK_CONTAINER(revealer), rowbox); + + GtkWidget *row = gtk_list_box_row_new(); + g_object_set_qdata_full(G_OBJECT(row), fiv_sidebar_location_quark(), + g_object_ref(file), (GDestroyNotify) g_object_unref); + gtk_container_add(GTK_CONTAINER(row), revealer); + gtk_widget_show_all(row); + return row; +} + +static gint +listbox_compare( + GtkListBoxRow *row1, GtkListBoxRow *row2, G_GNUC_UNUSED gpointer user_data) +{ + return fiv_io_filecmp( + g_object_get_qdata(G_OBJECT(row1), fiv_sidebar_location_quark()), + g_object_get_qdata(G_OBJECT(row2), fiv_sidebar_location_quark())); +} + +static void +update_location(FivSidebar *self, GFile *location) +{ + if (location) { + g_clear_object(&self->location); + self->location = g_object_ref(location); + } + + gtk_places_sidebar_set_location(self->places, self->location); + gtk_container_foreach(GTK_CONTAINER(self->listbox), + (GtkCallback) gtk_widget_destroy, NULL); + g_return_if_fail(self->location != NULL); + + GFile *iter = g_object_ref(self->location); + while (TRUE) { + GFile *parent = g_file_get_parent(iter); + g_object_unref(iter); + if (!(iter = parent)) + break; + + gtk_list_box_prepend(GTK_LIST_BOX(self->listbox), + create_row(parent, "go-up-symbolic")); + } + + // Other options are "folder-{visiting,open}-symbolic", though the former + // is mildly inappropriate (means: open in another window). + gtk_container_add(GTK_CONTAINER(self->listbox), + create_row(self->location, "circle-filled-symbolic")); + + GFileEnumerator *enumerator = g_file_enumerate_children(self->location, + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME + "," G_FILE_ATTRIBUTE_STANDARD_NAME + "," G_FILE_ATTRIBUTE_STANDARD_TYPE + "," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (!enumerator) + return; + + // TODO(p): gtk_list_box_set_filter_func(), or even use a model, + // which could be shared with FivBrowser. + while (TRUE) { + GFileInfo *info = NULL; + GFile *child = NULL; + if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) || + !info) + break; + + if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY && + !g_file_info_get_is_hidden(info)) + gtk_container_add(GTK_CONTAINER(self->listbox), + create_row(child, "go-down-symbolic")); + } + g_object_unref(enumerator); +} + +static void +on_open_breadcrumb( + G_GNUC_UNUSED GtkListBox *listbox, GtkListBoxRow *row, gpointer user_data) +{ + FivSidebar *self = FIV_SIDEBAR(user_data); + GFile *location = + g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark()); + g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, + location, GTK_PLACES_OPEN_NORMAL); +} + +static void +on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location, + GtkPlacesOpenFlags flags, gpointer user_data) +{ + FivSidebar *self = FIV_SIDEBAR(user_data); + g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, location, flags); + + // Deselect the item in GtkPlacesSidebar, if unsuccessful. + update_location(self, NULL); +} + +static void +complete_path(GFile *location, GtkListStore *model) +{ + // TODO(p): Do not enter directories unless followed by '/'. + // This information has already been stripped from `location`. + GFile *parent = G_FILE_TYPE_DIRECTORY == + g_file_query_file_type(location, G_FILE_QUERY_INFO_NONE, NULL) + ? g_object_ref(location) + : g_file_get_parent(location); + if (!parent) + return; + + GFileEnumerator *enumerator = g_file_enumerate_children(parent, + G_FILE_ATTRIBUTE_STANDARD_NAME + "," G_FILE_ATTRIBUTE_STANDARD_TYPE + "," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (!enumerator) + goto fail_enumerator; + + while (TRUE) { + GFileInfo *info = NULL; + GFile *child = NULL; + if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) || + !info) + break; + + if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY || + g_file_info_get_is_hidden(info)) + continue; + + char *parse_name = g_file_get_parse_name(child); + if (!g_str_has_suffix(parse_name, G_DIR_SEPARATOR_S)) { + char *save = parse_name; + parse_name = g_strdup_printf("%s%c", parse_name, G_DIR_SEPARATOR); + g_free(save); + } + gtk_list_store_insert_with_values(model, NULL, -1, 0, parse_name, -1); + g_free(parse_name); + } + + g_object_unref(enumerator); +fail_enumerator: + g_object_unref(parent); +} + +static GFile * +resolve_location(FivSidebar *self, const char *text) +{ + // Relative paths produce invalid GFile objects with this function. + // And even if they didn't, we have our own root for them. + GFile *file = g_file_parse_name(text); + if (g_uri_is_valid(text, G_URI_FLAGS_PARSE_RELAXED, NULL) || + g_file_peek_path(file)) + return file; + + GFile *absolute = + g_file_get_child_for_display_name(self->location, text, NULL); + if (!absolute) + return file; + + g_object_unref(file); + return absolute; +} + +static void +on_enter_location_changed(GtkEntry *entry, gpointer user_data) +{ + FivSidebar *self = FIV_SIDEBAR(user_data); + const char *text = gtk_entry_get_text(entry); + GFile *location = resolve_location(self, text); + + // Don't touch the network anywhere around here, URIs are a no-no. + GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(entry)); + if (!g_file_peek_path(location) || g_file_query_exists(location, NULL)) + gtk_style_context_remove_class(style, GTK_STYLE_CLASS_WARNING); + else + gtk_style_context_add_class(style, GTK_STYLE_CLASS_WARNING); + + // XXX: For some reason, this jumps around with longer lists. + GtkEntryCompletion *completion = gtk_entry_get_completion(entry); + GtkTreeModel *model = gtk_entry_completion_get_model(completion); + gtk_list_store_clear(GTK_LIST_STORE(model)); + if (g_file_peek_path(location)) + complete_path(location, GTK_LIST_STORE(model)); + g_object_unref(location); +} + +static void +on_show_enter_location( + G_GNUC_UNUSED GtkPlacesSidebar *sidebar, G_GNUC_UNUSED gpointer user_data) +{ + FivSidebar *self = FIV_SIDEBAR(user_data); + GtkWidget *dialog = gtk_dialog_new_with_buttons("Enter location", + GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(self))), + GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL | + GTK_DIALOG_USE_HEADER_BAR, + "_Open", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_CANCEL, NULL); + + GtkListStore *model = gtk_list_store_new(1, G_TYPE_STRING); + gtk_tree_sortable_set_sort_column_id( + GTK_TREE_SORTABLE(model), 0, GTK_SORT_ASCENDING); + + GtkEntryCompletion *completion = gtk_entry_completion_new(); + gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model)); + gtk_entry_completion_set_text_column(completion, 0); + // TODO(p): Complete ~ paths so that they start with ~, then we can filter. + gtk_entry_completion_set_match_func( + completion, (GtkEntryCompletionMatchFunc) gtk_true, NULL, NULL); + g_object_unref(model); + + GtkWidget *entry = gtk_entry_new(); + gtk_entry_set_completion(GTK_ENTRY(entry), completion); + gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); + g_signal_connect(entry, "changed", + G_CALLBACK(on_enter_location_changed), self); + + GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + gtk_container_add(GTK_CONTAINER(content), entry); + gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); + gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE); + gtk_window_set_default_size(GTK_WINDOW(dialog), 800, -1); + + GdkGeometry geometry = {.max_width = G_MAXSHORT, .max_height = -1}; + gtk_window_set_geometry_hints( + GTK_WINDOW(dialog), NULL, &geometry, GDK_HINT_MAX_SIZE); + gtk_widget_show_all(dialog); + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + const char *text = gtk_entry_get_text(GTK_ENTRY(entry)); + GFile *location = resolve_location(self, text); + g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, + location, GTK_PLACES_OPEN_NORMAL); + g_object_unref(location); + } + gtk_widget_destroy(dialog); + g_object_unref(completion); + + // Deselect the item in GtkPlacesSidebar, if unsuccessful. + update_location(self, NULL); +} + +static void +fiv_sidebar_init(FivSidebar *self) +{ + // TODO(p): Transplant functionality from the shitty GtkPlacesSidebar. + // We cannot reasonably place any new items within its own GtkListBox, + // so we need to replicate the style hierarchy to some extent. + self->places = GTK_PLACES_SIDEBAR(gtk_places_sidebar_new()); + gtk_places_sidebar_set_show_recent(self->places, FALSE); + gtk_places_sidebar_set_show_trash(self->places, FALSE); + gtk_places_sidebar_set_open_flags(self->places, + GTK_PLACES_OPEN_NORMAL | GTK_PLACES_OPEN_NEW_WINDOW); + g_signal_connect(self->places, "open-location", + G_CALLBACK(on_open_location), self); + + gtk_places_sidebar_set_show_enter_location(self->places, TRUE); + g_signal_connect(self->places, "show-enter-location", + G_CALLBACK(on_show_enter_location), self); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->places), + GTK_POLICY_NEVER, GTK_POLICY_NEVER); + + // None of GtkActionBar, GtkToolbar, .inline-toolbar is appropriate. + // It is either side-favouring borders or excess button padding. + self->toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); + gtk_style_context_add_class( + gtk_widget_get_style_context(self->toolbar), GTK_STYLE_CLASS_TOOLBAR); + + self->listbox = gtk_list_box_new(); + gtk_list_box_set_selection_mode( + GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE); + g_signal_connect(self->listbox, "row-activated", + G_CALLBACK(on_open_breadcrumb), self); + gtk_list_box_set_sort_func( + GTK_LIST_BOX(self->listbox), listbox_compare, self, NULL); + + // Fill up what would otherwise be wasted space, + // as it is in the examples of Nautilus and Thunar. + GtkWidget *superbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add( + GTK_CONTAINER(superbox), GTK_WIDGET(self->places)); + gtk_container_add( + GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL)); + gtk_container_add( + GTK_CONTAINER(superbox), self->toolbar); + gtk_container_add( + GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL)); + gtk_container_add( + GTK_CONTAINER(superbox), self->listbox); + gtk_container_add(GTK_CONTAINER(self), superbox); + + gtk_scrolled_window_set_policy( + GTK_SCROLLED_WINDOW(self), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)), + GTK_STYLE_CLASS_SIDEBAR); + gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)), + "fiv"); +} + +// --- Public interface -------------------------------------------------------- + +void +fiv_sidebar_set_location(FivSidebar *self, GFile *location) +{ + g_return_if_fail(FIV_IS_SIDEBAR(self)); + update_location(self, location); +} + +void +fiv_sidebar_show_enter_location(FivSidebar *self) +{ + g_return_if_fail(FIV_IS_SIDEBAR(self)); + g_signal_emit_by_name(self->places, "show-enter-location"); +} + +GtkBox * +fiv_sidebar_get_toolbar(FivSidebar *self) +{ + g_return_val_if_fail(FIV_IS_SIDEBAR(self), NULL); + return GTK_BOX(self->toolbar); +} diff --git a/fiv-sidebar.h b/fiv-sidebar.h new file mode 100644 index 0000000..8a3f14a --- /dev/null +++ b/fiv-sidebar.h @@ -0,0 +1,27 @@ +// +// fiv-sidebar.h: molesting GtkPlacesSidebar +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#pragma once + +#include + +#define FIV_TYPE_SIDEBAR (fiv_sidebar_get_type()) +G_DECLARE_FINAL_TYPE(FivSidebar, fiv_sidebar, FIV, SIDEBAR, GtkScrolledWindow) + +void fiv_sidebar_set_location(FivSidebar *self, GFile *location); +void fiv_sidebar_show_enter_location(FivSidebar *self); +GtkBox *fiv_sidebar_get_toolbar(FivSidebar *self); diff --git a/fiv-view.c b/fiv-view.c new file mode 100644 index 0000000..e064b56 --- /dev/null +++ b/fiv-view.c @@ -0,0 +1,918 @@ +// +// fiv-view.c: fast image viewer - view widget +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#include "config.h" + +#include +#include + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif // GDK_WINDOWING_X11 +#ifdef GDK_WINDOWING_QUARTZ +#include +#endif // GDK_WINDOWING_QUARTZ + +#include "fiv-io.h" +#include "fiv-view.h" + +struct _FivView { + GtkWidget parent_instance; + cairo_surface_t *image; ///< The loaded image (sequence) + cairo_surface_t *page; ///< Current page within image, weak + cairo_surface_t *frame; ///< Current frame within page, weak + FivIoOrientation orientation; ///< Current page orientation + bool filter; ///< Smooth scaling toggle + bool scale_to_fit; ///< Image no larger than the allocation + double scale; ///< Scaling factor + + int remaining_loops; ///< Greater than zero if limited + gint64 frame_time; ///< Current frame's start, µs precision + gulong frame_update_connection; ///< GdkFrameClock::update +}; + +G_DEFINE_TYPE(FivView, fiv_view, GTK_TYPE_WIDGET) + +static FivIoOrientation view_left[9] = { + [FivIoOrientationUnknown] = FivIoOrientationUnknown, + [FivIoOrientation0] = FivIoOrientation270, + [FivIoOrientationMirror0] = FivIoOrientationMirror270, + [FivIoOrientation180] = FivIoOrientation90, + [FivIoOrientationMirror180] = FivIoOrientationMirror90, + [FivIoOrientationMirror270] = FivIoOrientationMirror180, + [FivIoOrientation90] = FivIoOrientation0, + [FivIoOrientationMirror90] = FivIoOrientationMirror0, + [FivIoOrientation270] = FivIoOrientation180 +}; + +static FivIoOrientation view_mirror[9] = { + [FivIoOrientationUnknown] = FivIoOrientationUnknown, + [FivIoOrientation0] = FivIoOrientationMirror0, + [FivIoOrientationMirror0] = FivIoOrientation0, + [FivIoOrientation180] = FivIoOrientationMirror180, + [FivIoOrientationMirror180] = FivIoOrientation180, + [FivIoOrientationMirror270] = FivIoOrientation270, + [FivIoOrientation90] = FivIoOrientationMirror270, + [FivIoOrientationMirror90] = FivIoOrientation90, + [FivIoOrientation270] = FivIoOrientationMirror270 +}; + +static FivIoOrientation view_right[9] = { + [FivIoOrientationUnknown] = FivIoOrientationUnknown, + [FivIoOrientation0] = FivIoOrientation90, + [FivIoOrientationMirror0] = FivIoOrientationMirror90, + [FivIoOrientation180] = FivIoOrientation270, + [FivIoOrientationMirror180] = FivIoOrientationMirror270, + [FivIoOrientationMirror270] = FivIoOrientationMirror0, + [FivIoOrientation90] = FivIoOrientation180, + [FivIoOrientationMirror90] = FivIoOrientationMirror180, + [FivIoOrientation270] = FivIoOrientation0 +}; + +enum { + PROP_SCALE = 1, + PROP_SCALE_TO_FIT, + PROP_FILTER, + N_PROPERTIES +}; + +static GParamSpec *view_properties[N_PROPERTIES]; + +static void +fiv_view_finalize(GObject *gobject) +{ + FivView *self = FIV_VIEW(gobject); + cairo_surface_destroy(self->image); + + G_OBJECT_CLASS(fiv_view_parent_class)->finalize(gobject); +} + +static void +fiv_view_get_property( + GObject *object, guint property_id, GValue *value, GParamSpec *pspec) +{ + FivView *self = FIV_VIEW(object); + switch (property_id) { + case PROP_SCALE: + g_value_set_double(value, self->scale); + break; + case PROP_SCALE_TO_FIT: + g_value_set_boolean(value, self->scale_to_fit); + break; + case PROP_FILTER: + g_value_set_boolean(value, self->filter); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + +static void +get_surface_dimensions(FivView *self, double *width, double *height) +{ + *width = *height = 0; + if (!self->image) + return; + + cairo_rectangle_t extents = {}; + switch (cairo_surface_get_type(self->page)) { + case CAIRO_SURFACE_TYPE_IMAGE: + switch (self->orientation) { + case FivIoOrientation90: + case FivIoOrientationMirror90: + case FivIoOrientation270: + case FivIoOrientationMirror270: + *width = cairo_image_surface_get_height(self->page); + *height = cairo_image_surface_get_width(self->page); + break; + default: + *width = cairo_image_surface_get_width(self->page); + *height = cairo_image_surface_get_height(self->page); + } + return; + case CAIRO_SURFACE_TYPE_RECORDING: + if (!cairo_recording_surface_get_extents(self->page, &extents)) { + cairo_recording_surface_ink_extents(self->page, + &extents.x, &extents.y, &extents.width, &extents.height); + } + + *width = extents.width; + *height = extents.height; + return; + default: + g_assert_not_reached(); + } +} + +static void +get_display_dimensions(FivView *self, int *width, int *height) +{ + double w, h; + get_surface_dimensions(self, &w, &h); + + *width = ceil(w * self->scale); + *height = ceil(h * self->scale); +} + +static cairo_matrix_t +get_orientation_matrix(FivIoOrientation o, double width, double height) +{ + cairo_matrix_t matrix = {}; + cairo_matrix_init_identity(&matrix); + switch (o) { + case FivIoOrientation90: + cairo_matrix_rotate(&matrix, -M_PI_2); + cairo_matrix_translate(&matrix, -width, 0); + break; + case FivIoOrientation180: + cairo_matrix_scale(&matrix, -1, -1); + cairo_matrix_translate(&matrix, -width, -height); + break; + case FivIoOrientation270: + cairo_matrix_rotate(&matrix, +M_PI_2); + cairo_matrix_translate(&matrix, 0, -height); + break; + case FivIoOrientationMirror0: + cairo_matrix_scale(&matrix, -1, +1); + cairo_matrix_translate(&matrix, -width, 0); + break; + case FivIoOrientationMirror90: + cairo_matrix_rotate(&matrix, +M_PI_2); + cairo_matrix_scale(&matrix, -1, +1); + cairo_matrix_translate(&matrix, -width, -height); + break; + case FivIoOrientationMirror180: + cairo_matrix_scale(&matrix, +1, -1); + cairo_matrix_translate(&matrix, 0, -height); + break; + case FivIoOrientationMirror270: + cairo_matrix_rotate(&matrix, -M_PI_2); + cairo_matrix_scale(&matrix, -1, +1); + default: + break; + } + return matrix; +} + +static void +fiv_view_get_preferred_height(GtkWidget *widget, gint *minimum, gint *natural) +{ + FivView *self = FIV_VIEW(widget); + if (self->scale_to_fit) { + double sw, sh; + get_surface_dimensions(self, &sw, &sh); + *natural = ceil(sh); + *minimum = 1; + } else { + int dw, dh; + get_display_dimensions(self, &dw, &dh); + *minimum = *natural = dh; + } +} + +static void +fiv_view_get_preferred_width(GtkWidget *widget, gint *minimum, gint *natural) +{ + FivView *self = FIV_VIEW(widget); + if (self->scale_to_fit) { + double sw, sh; + get_surface_dimensions(self, &sw, &sh); + *natural = ceil(sw); + *minimum = 1; + } else { + int dw, dh; + get_display_dimensions(self, &dw, &dh); + *minimum = *natural = dw; + } +} + +static void +fiv_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation) +{ + GTK_WIDGET_CLASS(fiv_view_parent_class)->size_allocate(widget, allocation); + + FivView *self = FIV_VIEW(widget); + if (!self->image || !self->scale_to_fit) + return; + + double w, h; + get_surface_dimensions(self, &w, &h); + + self->scale = 1; + if (ceil(w * self->scale) > allocation->width) + self->scale = allocation->width / w; + if (ceil(h * self->scale) > allocation->height) + self->scale = allocation->height / h; + g_object_notify_by_pspec(G_OBJECT(widget), view_properties[PROP_SCALE]); +} + +static void +fiv_view_realize(GtkWidget *widget) +{ + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + + GdkWindowAttr attributes = { + .window_type = GDK_WINDOW_CHILD, + .x = allocation.x, + .y = allocation.y, + .width = allocation.width, + .height = allocation.height, + + // Input-only would presumably also work (as in GtkPathBar, e.g.), + // but it merely seems to involve more work. + .wclass = GDK_INPUT_OUTPUT, + + // Assuming here that we can't ask for a higher-precision Visual + // than what we get automatically. + .visual = gtk_widget_get_visual(widget), + .event_mask = gtk_widget_get_events(widget) | GDK_SCROLL_MASK | + GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK, + }; + + // We need this window to receive input events at all. + GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget), + &attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); + + // Without the following call, or the rendering mode set to "recording", + // RGB30 degrades to RGB24, because gdk_window_begin_paint_internal() + // creates backing stores using cairo_content_t constants. + // + // It completely breaks the Quartz backend, so limit it to X11. +#ifdef GDK_WINDOWING_X11 + // FIXME: This causes some flicker while scrolling, because it disables + // double buffering, see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560 + // + // If GTK+'s OpenGL integration fails to deliver, we need to use the window + // directly, sidestepping the toolkit entirely. + if (GDK_IS_X11_WINDOW(window)) + gdk_window_ensure_native(window); +#endif // GDK_WINDOWING_X11 + + gtk_widget_register_window(widget, window); + gtk_widget_set_window(widget, window); + gtk_widget_set_realized(widget, TRUE); +} + +static gboolean +fiv_view_draw(GtkWidget *widget, cairo_t *cr) +{ + // Placed here due to our using a native GdkWindow on X11, + // which makes the widget have no double buffering or default background. + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + gtk_render_background(gtk_widget_get_style_context(widget), cr, 0, 0, + allocation.width, allocation.height); + + FivView *self = FIV_VIEW(widget); + if (!self->image || + !gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget))) + return TRUE; + + int w, h; + double sw, sh; + get_display_dimensions(self, &w, &h); + get_surface_dimensions(self, &sw, &sh); + + double x = 0; + double y = 0; + if (w < allocation.width) + x = round((allocation.width - w) / 2.); + if (h < allocation.height) + y = round((allocation.height - h) / 2.); + + // FIXME: Recording surfaces do not work well with CAIRO_SURFACE_TYPE_XLIB, + // we always get a shitty pixmap, where transparency contains junk. + if (cairo_surface_get_type(self->frame) == CAIRO_SURFACE_TYPE_RECORDING) { + cairo_surface_t *image = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + cairo_t *tcr = cairo_create(image); + cairo_scale(tcr, self->scale, self->scale); + cairo_set_source_surface(tcr, self->frame, 0, 0); + cairo_paint(tcr); + cairo_destroy(tcr); + + cairo_set_source_surface(cr, image, x, y); + cairo_paint(cr); + cairo_surface_destroy(image); + return TRUE; + } + + // XXX: The rounding together with padding may result in up to + // a pixel's worth of made-up picture data. + cairo_rectangle(cr, x, y, w, h); + cairo_clip(cr); + + cairo_translate(cr, x, y); + cairo_scale(cr, self->scale, self->scale); + cairo_set_source_surface(cr, self->frame, 0, 0); + + cairo_matrix_t matrix = get_orientation_matrix(self->orientation, sw, sh); + cairo_pattern_t *pattern = cairo_get_source(cr); + cairo_pattern_set_matrix(pattern, &matrix); + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD); + + // TODO(p): Prescale it ourselves to an off-screen bitmap, gamma-correctly. + if (self->filter) + cairo_pattern_set_filter(pattern, CAIRO_FILTER_GOOD); + else + cairo_pattern_set_filter(pattern, CAIRO_FILTER_NEAREST); + +#ifdef GDK_WINDOWING_QUARTZ + // Not supported there. Acts a bit like repeating, but weirdly offset. + if (GDK_IS_QUARTZ_WINDOW(gtk_widget_get_window(widget))) + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_NONE); +#endif // GDK_WINDOWING_QUARTZ + + cairo_paint(cr); + return TRUE; +} + +static gboolean +fiv_view_button_press_event(GtkWidget *widget, GdkEventButton *event) +{ + GTK_WIDGET_CLASS(fiv_view_parent_class)->button_press_event(widget, event); + + if (event->button == GDK_BUTTON_PRIMARY && + gtk_widget_get_focus_on_click(widget)) + gtk_widget_grab_focus(widget); + + // TODO(p): Use for left button scroll drag, which may rather be a gesture. + return FALSE; +} + +#define SCALE_STEP 1.4 + +static gboolean +set_scale_to_fit(FivView *self, bool scale_to_fit) +{ + self->scale_to_fit = scale_to_fit; + + gtk_widget_queue_resize(GTK_WIDGET(self)); + g_object_notify_by_pspec( + G_OBJECT(self), view_properties[PROP_SCALE_TO_FIT]); + return TRUE; +} + +static gboolean +set_scale(FivView *self, double scale) +{ + self->scale = scale; + g_object_notify_by_pspec( + G_OBJECT(self), view_properties[PROP_SCALE]); + return set_scale_to_fit(self, false); +} + +static gboolean +fiv_view_scroll_event(GtkWidget *widget, GdkEventScroll *event) +{ + FivView *self = FIV_VIEW(widget); + if (!self->image) + return FALSE; + if (event->state & gtk_accelerator_get_default_mod_mask()) + return FALSE; + + switch (event->direction) { + case GDK_SCROLL_UP: + return set_scale(self, self->scale * SCALE_STEP); + case GDK_SCROLL_DOWN: + return set_scale(self, self->scale / SCALE_STEP); + default: + // For some reason, we can also get GDK_SCROLL_SMOOTH. + // Left/right are good to steal from GtkScrolledWindow for consistency. + return TRUE; + } +} + +static void +stop_animating(FivView *self) +{ + GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self)); + if (!clock || !self->frame_update_connection) + return; + + g_signal_handler_disconnect(clock, self->frame_update_connection); + gdk_frame_clock_end_updating(clock); + + self->frame_time = 0; + self->frame_update_connection = 0; + self->remaining_loops = 0; +} + +static gboolean +advance_frame(FivView *self) +{ + cairo_surface_t *next = + cairo_surface_get_user_data(self->frame, &fiv_io_key_frame_next); + if (next) { + self->frame = next; + } else { + if (self->remaining_loops && !--self->remaining_loops) + return FALSE; + + self->frame = self->page; + } + return TRUE; +} + +static gboolean +advance_animation(FivView *self, GdkFrameClock *clock) +{ + gint64 now = gdk_frame_clock_get_frame_time(clock); + while (true) { + // TODO(p): See if infinite frames can actually happen, and how. + intptr_t duration = (intptr_t) cairo_surface_get_user_data( + self->frame, &fiv_io_key_frame_duration); + if (duration < 0) + return FALSE; + + // Do not busy loop. GIF timings are given in hundredths of a second. + // Note that browsers seem to do [< 10] => 100: + // https://bugs.webkit.org/show_bug.cgi?id=36082 + if (duration == 0) + duration = gdk_frame_timings_get_refresh_interval( + gdk_frame_clock_get_current_timings(clock)) / 1000; + if (duration == 0) + duration = 1; + + gint64 then = self->frame_time + duration * 1000; + if (then > now) + return TRUE; + if (!advance_frame(self)) + return FALSE; + + self->frame_time = then; + gtk_widget_queue_draw(GTK_WIDGET(self)); + } +} + +static void +on_frame_clock_update(GdkFrameClock *clock, gpointer user_data) +{ + FivView *self = FIV_VIEW(user_data); + if (!advance_animation(self, clock)) + stop_animating(self); +} + +static void +start_animating(FivView *self) +{ + stop_animating(self); + + GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self)); + if (!clock || !self->image || + !cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next)) + return; + + self->frame_time = gdk_frame_clock_get_frame_time(clock); + self->frame_update_connection = g_signal_connect( + clock, "update", G_CALLBACK(on_frame_clock_update), self); + self->remaining_loops = + (uintptr_t) cairo_surface_get_user_data(self->page, &fiv_io_key_loops); + + gdk_frame_clock_begin_updating(clock); +} + +static void +switch_page(FivView *self, cairo_surface_t *page) +{ + self->frame = self->page = page; + if ((self->orientation = (uintptr_t) cairo_surface_get_user_data( + self->page, &fiv_io_key_orientation)) == FivIoOrientationUnknown) + self->orientation = FivIoOrientation0; + + start_animating(self); + gtk_widget_queue_resize(GTK_WIDGET(self)); +} + +static void +fiv_view_map(GtkWidget *widget) +{ + GTK_WIDGET_CLASS(fiv_view_parent_class)->map(widget); + + // Loading before mapping will fail to obtain a GdkFrameClock. + start_animating(FIV_VIEW(widget)); +} + +void +fiv_view_unmap(GtkWidget *widget) +{ + stop_animating(FIV_VIEW(widget)); + GTK_WIDGET_CLASS(fiv_view_parent_class)->unmap(widget); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +show_error_dialog(GtkWindow *parent, GError *error) +{ + GtkWidget *dialog = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + g_error_free(error); +} + +static GtkWindow * +get_toplevel(GtkWidget *widget) +{ + if (GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget)))) + return GTK_WINDOW(widget); + return NULL; +} + +static void +on_draw_page(G_GNUC_UNUSED GtkPrintOperation *operation, + GtkPrintContext *context, G_GNUC_UNUSED int page_nr, FivView *self) +{ + double surface_width_px = 0, surface_height_px = 0; + get_surface_dimensions(self, &surface_width_px, &surface_height_px); + + // Any DPI will be wrong, unless we import that information from the image. + double scale = 1 / 96.; + double w = surface_width_px * scale, h = surface_height_px * scale; + + // Scale down to fit the print area, taking care to not divide by zero. + double areaw = gtk_print_context_get_width(context); + double areah = gtk_print_context_get_height(context); + scale *= fmin((areaw < w) ? areaw / w : 1, (areah < h) ? areah / h : 1); + + cairo_t *cr = gtk_print_context_get_cairo_context(context); + cairo_scale(cr, scale, scale); + cairo_set_source_surface(cr, self->frame, 0, 0); + cairo_matrix_t matrix = get_orientation_matrix( + self->orientation, surface_width_px, surface_height_px); + cairo_pattern_set_matrix(cairo_get_source(cr), &matrix); + cairo_paint(cr); +} + +static gboolean +print(FivView *self) +{ + GtkPrintOperation *print = gtk_print_operation_new(); + gtk_print_operation_set_n_pages(print, 1); + gtk_print_operation_set_embed_page_setup(print, TRUE); + gtk_print_operation_set_unit(print, GTK_UNIT_INCH); + gtk_print_operation_set_job_name(print, "Image"); + g_signal_connect(print, "draw-page", G_CALLBACK(on_draw_page), self); + + static GtkPrintSettings *settings = NULL; + if (settings != NULL) + gtk_print_operation_set_print_settings(print, settings); + + GError *error = NULL; + GtkWindow *window = get_toplevel(GTK_WIDGET(self)); + GtkPrintOperationResult res = gtk_print_operation_run( + print, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, window, &error); + if (res == GTK_PRINT_OPERATION_RESULT_APPLY) { + if (settings != NULL) + g_object_unref(settings); + settings = g_object_ref(gtk_print_operation_get_print_settings(print)); + } + if (error) + show_error_dialog(window, error); + + g_object_unref(print); + return TRUE; +} + +static gboolean +save_as(FivView *self, gboolean frame) +{ + GtkWindow *window = get_toplevel(GTK_WIDGET(self)); + GtkWidget *dialog = gtk_file_chooser_dialog_new( + frame ? "Save frame as" : "Save page as", + window, GTK_FILE_CHOOSER_ACTION_SAVE, + "_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL); + + GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog); + + // TODO(p): Consider a hard dependency on libwebp, or clean this up. +#ifdef HAVE_LIBWEBP + // This is the best general format: supports lossless encoding, animations, + // alpha channel, and Exif and ICC profile metadata. + // PNG is another viable option, but sPNG can't do APNG, Wuffs can't save, + // and libpng is a pain in the arse. + GtkFileFilter *webp_filter = gtk_file_filter_new(); + gtk_file_filter_add_mime_type(webp_filter, "image/webp"); + gtk_file_filter_add_pattern(webp_filter, "*.webp"); + gtk_file_filter_set_name(webp_filter, "Lossless WebP"); + gtk_file_chooser_add_filter(chooser, webp_filter); + + // TODO(p): Derive it from the currently displayed filename, + // and set the directory to the same place. + gtk_file_chooser_set_current_name( + chooser, frame ? "frame.webp" : "page.webp"); +#endif // HAVE_LIBWEBP + + // The format is supported by Exiv2 and ExifTool. + // This is mostly a developer tool. + GtkFileFilter *exv_filter = gtk_file_filter_new(); + gtk_file_filter_add_mime_type(exv_filter, "image/x-exv"); + gtk_file_filter_add_pattern(exv_filter, "*.exv"); + gtk_file_filter_set_name(exv_filter, "Exiv2 metadata"); + gtk_file_chooser_add_filter(chooser, exv_filter); + + switch (gtk_dialog_run(GTK_DIALOG(dialog))) { + gchar *path; + case GTK_RESPONSE_ACCEPT: + path = gtk_file_chooser_get_filename(chooser); + + GError *error = NULL; +#ifdef HAVE_LIBWEBP + if (gtk_file_chooser_get_filter(chooser) == webp_filter) + fiv_io_save(self->page, frame ? self->frame : NULL, path, &error); + else +#endif // HAVE_LIBWEBP + fiv_io_save_metadata(self->page, path, &error); + if (error) + show_error_dialog(window, error); + g_free(path); + + // Fall-through. + default: + gtk_widget_destroy(dialog); + // Fall-through. + case GTK_RESPONSE_NONE: + return TRUE; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static inline gboolean +command(FivView *self, FivViewCommand command) +{ + fiv_view_command(self, command); + return TRUE; +} + +static gboolean +fiv_view_key_press_event(GtkWidget *widget, GdkEventKey *event) +{ + FivView *self = FIV_VIEW(widget); + if (!self->image) + return FALSE; + + // It should not matter that GDK_KEY_plus involves holding Shift. + guint state = event->state & gtk_accelerator_get_default_mod_mask() & + ~GDK_SHIFT_MASK; + + // The standard, intuitive bindings. + if (state == GDK_CONTROL_MASK) { + switch (event->keyval) { + case GDK_KEY_0: + return command(self, FIV_VIEW_COMMAND_ZOOM_1); + case GDK_KEY_plus: + return command(self, FIV_VIEW_COMMAND_ZOOM_IN); + case GDK_KEY_minus: + return command(self, FIV_VIEW_COMMAND_ZOOM_OUT); + case GDK_KEY_p: + return command(self, FIV_VIEW_COMMAND_PRINT); + case GDK_KEY_s: + return command(self, FIV_VIEW_COMMAND_SAVE_PAGE); + case GDK_KEY_S: + return save_as(self, TRUE); + } + } + if (state != 0) + return FALSE; + + switch (event->keyval) { + case GDK_KEY_1: + case GDK_KEY_2: + case GDK_KEY_3: + case GDK_KEY_4: + case GDK_KEY_5: + case GDK_KEY_6: + case GDK_KEY_7: + case GDK_KEY_8: + case GDK_KEY_9: + return set_scale(self, event->keyval - GDK_KEY_0); + case GDK_KEY_plus: + return command(self, FIV_VIEW_COMMAND_ZOOM_IN); + case GDK_KEY_minus: + return command(self, FIV_VIEW_COMMAND_ZOOM_OUT); + + case GDK_KEY_x: // Inspired by gThumb. + return set_scale_to_fit(self, !self->scale_to_fit); + + case GDK_KEY_i: + self->filter = !self->filter; + g_object_notify_by_pspec( + G_OBJECT(self), view_properties[PROP_FILTER]); + gtk_widget_queue_draw(widget); + return TRUE; + + case GDK_KEY_less: + return command(self, FIV_VIEW_COMMAND_ROTATE_LEFT); + case GDK_KEY_equal: + return command(self, FIV_VIEW_COMMAND_MIRROR); + case GDK_KEY_greater: + return command(self, FIV_VIEW_COMMAND_ROTATE_RIGHT); + + case GDK_KEY_bracketleft: + return command(self, FIV_VIEW_COMMAND_PAGE_PREVIOUS); + case GDK_KEY_bracketright: + return command(self, FIV_VIEW_COMMAND_PAGE_NEXT); + + case GDK_KEY_braceleft: + return command(self, FIV_VIEW_COMMAND_FRAME_PREVIOUS); + case GDK_KEY_braceright: + return command(self, FIV_VIEW_COMMAND_FRAME_NEXT); + } + return FALSE; +} + +static void +fiv_view_class_init(FivViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->finalize = fiv_view_finalize; + object_class->get_property = fiv_view_get_property; + + view_properties[PROP_SCALE] = g_param_spec_double( + "scale", "Scale", "Zoom level", + 0, G_MAXDOUBLE, 1.0, G_PARAM_READABLE); + view_properties[PROP_SCALE_TO_FIT] = g_param_spec_boolean( + "scale-to-fit", "Scale to fit", "Scale images down to fit the window", + TRUE, G_PARAM_READABLE); + view_properties[PROP_FILTER] = g_param_spec_boolean( + "filter", "Use filtering", "Scale images smoothly", + TRUE, G_PARAM_READABLE); + g_object_class_install_properties( + object_class, N_PROPERTIES, view_properties); + + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + widget_class->get_preferred_height = fiv_view_get_preferred_height; + widget_class->get_preferred_width = fiv_view_get_preferred_width; + widget_class->size_allocate = fiv_view_size_allocate; + widget_class->map = fiv_view_map; + widget_class->unmap = fiv_view_unmap; + widget_class->realize = fiv_view_realize; + widget_class->draw = fiv_view_draw; + widget_class->button_press_event = fiv_view_button_press_event; + widget_class->scroll_event = fiv_view_scroll_event; + widget_class->key_press_event = fiv_view_key_press_event; + + // TODO(p): Later override "screen_changed", recreate Pango layouts there, + // if we get to have any, or otherwise reflect DPI changes. + gtk_widget_class_set_css_name(widget_class, "fiv-view"); +} + +static void +fiv_view_init(FivView *self) +{ + gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); + + self->filter = true; + self->scale = 1.0; +} + +// --- Picture loading --------------------------------------------------------- + +// TODO(p): Progressive picture loading, or at least async/cancellable. +gboolean +fiv_view_open(FivView *self, const gchar *path, GError **error) +{ + cairo_surface_t *surface = fiv_io_open(path, error); + if (!surface) + return FALSE; + if (self->image) + cairo_surface_destroy(self->image); + + self->frame = self->page = NULL; + self->image = surface; + switch_page(self, self->image); + set_scale_to_fit(self, true); + return TRUE; +} + +static void +page_step(FivView *self, int step) +{ + cairo_user_data_key_t *key = + step < 0 ? &fiv_io_key_page_previous : &fiv_io_key_page_next; + cairo_surface_t *page = cairo_surface_get_user_data(self->page, key); + if (page) + switch_page(self, page); +} + +static void +frame_step(FivView *self, int step) +{ + stop_animating(self); + cairo_user_data_key_t *key = + step < 0 ? &fiv_io_key_frame_previous : &fiv_io_key_frame_next; + if (!step || !(self->frame = cairo_surface_get_user_data(self->frame, key))) + self->frame = self->page; + gtk_widget_queue_draw(GTK_WIDGET(self)); +} + +void +fiv_view_command(FivView *self, FivViewCommand command) +{ + g_return_if_fail(FIV_IS_VIEW(self)); + + GtkWidget *widget = GTK_WIDGET(self); + if (!self->image) + return; + + switch (command) { + break; case FIV_VIEW_COMMAND_ROTATE_LEFT: + self->orientation = view_left[self->orientation]; + gtk_widget_queue_resize(widget); + break; case FIV_VIEW_COMMAND_MIRROR: + self->orientation = view_mirror[self->orientation]; + gtk_widget_queue_resize(widget); + break; case FIV_VIEW_COMMAND_ROTATE_RIGHT: + self->orientation = view_right[self->orientation]; + gtk_widget_queue_resize(widget); + + break; case FIV_VIEW_COMMAND_PAGE_FIRST: + switch_page(self, self->image); + break; case FIV_VIEW_COMMAND_PAGE_PREVIOUS: + page_step(self, -1); + break; case FIV_VIEW_COMMAND_PAGE_NEXT: + page_step(self, +1); + break; case FIV_VIEW_COMMAND_PAGE_LAST: + for (cairo_surface_t *s = self->page; + (s = cairo_surface_get_user_data(s, &fiv_io_key_page_next)); ) + self->page = s; + switch_page(self, self->page); + + break; case FIV_VIEW_COMMAND_FRAME_FIRST: + frame_step(self, 0); + break; case FIV_VIEW_COMMAND_FRAME_PREVIOUS: + frame_step(self, -1); + break; case FIV_VIEW_COMMAND_FRAME_NEXT: + frame_step(self, +1); + + break; case FIV_VIEW_COMMAND_PRINT: + print(self); + break; case FIV_VIEW_COMMAND_SAVE_PAGE: + save_as(self, FALSE); + + break; case FIV_VIEW_COMMAND_ZOOM_IN: + set_scale(self, self->scale * SCALE_STEP); + break; case FIV_VIEW_COMMAND_ZOOM_OUT: + set_scale(self, self->scale / SCALE_STEP); + break; case FIV_VIEW_COMMAND_ZOOM_1: + set_scale(self, 1.0); + } +} diff --git a/fiv-view.h b/fiv-view.h new file mode 100644 index 0000000..92c89e9 --- /dev/null +++ b/fiv-view.h @@ -0,0 +1,52 @@ +// +// fiv-view.h: fast image viewer - view widget +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#pragma once + +#include + +#define FIV_TYPE_VIEW (fiv_view_get_type()) +G_DECLARE_FINAL_TYPE(FivView, fiv_view, FIV, VIEW, GtkWidget) + +/// Try to open the given file, synchronously, to be displayed by the widget. +gboolean fiv_view_open(FivView *self, const gchar *path, GError **error); + +typedef enum _FivViewCommand { + FIV_VIEW_COMMAND_ROTATE_LEFT = 1, + FIV_VIEW_COMMAND_MIRROR, + FIV_VIEW_COMMAND_ROTATE_RIGHT, + + FIV_VIEW_COMMAND_PAGE_FIRST, + FIV_VIEW_COMMAND_PAGE_PREVIOUS, + FIV_VIEW_COMMAND_PAGE_NEXT, + FIV_VIEW_COMMAND_PAGE_LAST, + + FIV_VIEW_COMMAND_FRAME_FIRST, + FIV_VIEW_COMMAND_FRAME_PREVIOUS, + FIV_VIEW_COMMAND_FRAME_NEXT, + // Going to the end frame makes no sense, wrap around if needed. + + FIV_VIEW_COMMAND_PRINT, + FIV_VIEW_COMMAND_SAVE_PAGE, + + FIV_VIEW_COMMAND_ZOOM_IN, + FIV_VIEW_COMMAND_ZOOM_OUT, + FIV_VIEW_COMMAND_ZOOM_1 +} FivViewCommand; + +/// Execute a user action. +void fiv_view_command(FivView *self, FivViewCommand command); diff --git a/meson.build b/meson.build index 6ed86d2..0cabcc4 100644 --- a/meson.build +++ b/meson.build @@ -1,3 +1,4 @@ +# vim: noet ts=4 sts=4 sw=4: project('fastiv', 'c', default_options : ['c_std=gnu99', 'warning_level=2'], version : '0.1.0') @@ -65,13 +66,13 @@ resources = gnome.compile_resources('resources', c_name : 'resources', ) -exe = executable('fastiv', 'fastiv.c', 'fastiv-view.c', 'fastiv-io.c', - 'fastiv-browser.c', 'fastiv-sidebar.c', 'xdg.c', resources, +exe = executable('fastiv', 'fastiv.c', 'fiv-view.c', 'fiv-io.c', + 'fiv-browser.c', 'fiv-sidebar.c', 'xdg.c', resources, install : true, dependencies : [dependencies]) if gdkpixbuf.found() - executable('io-benchmark', 'fastiv-io-benchmark.c', 'fastiv-io.c', 'xdg.c', + executable('io-benchmark', 'fiv-io-benchmark.c', 'fiv-io.c', 'xdg.c', build_by_default : false, dependencies : [dependencies, gdkpixbuf]) endif -- cgit v1.2.3-70-g09d2