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