aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules5
-rw-r--r--LICENSE2
-rw-r--r--README.adoc55
-rw-r--r--docs/fiv.adoc41
-rw-r--r--docs/fiv.html43
-rw-r--r--docs/fiv.webpbin155162 -> 198174 bytes
-rw-r--r--docs/stylesheet.css3
-rw-r--r--fiv-browser.c487
-rw-r--r--fiv-browser.h2
-rw-r--r--fiv-collection.c8
-rw-r--r--fiv-context-menu.c57
-rw-r--r--fiv-context-menu.h3
-rw-r--r--fiv-io-cmm.c462
-rw-r--r--fiv-io-model.c742
-rw-r--r--fiv-io-model.h72
-rw-r--r--fiv-io.c2629
-rw-r--r--fiv-io.h273
-rw-r--r--fiv-jpegcrop.c3
-rwxr-xr-xfiv-reverse-search9
-rw-r--r--fiv-reverse-search.desktop.in10
-rw-r--r--fiv-sidebar.c90
-rw-r--r--fiv-sidebar.h3
-rw-r--r--fiv-thumbnail.c619
-rw-r--r--fiv-thumbnail.h12
-rwxr-xr-xfiv-update-desktop-files.in10
-rw-r--r--fiv-view.c627
-rw-r--r--fiv.c1309
-rw-r--r--fiv.gschema.xml11
-rw-r--r--fiv.wxs.in71
-rw-r--r--meson.build174
-rw-r--r--meson_options.txt2
-rwxr-xr-xmsys2-configure.sh (renamed from msys2-cross-configure.sh)73
-rwxr-xr-xmsys2-install.sh (renamed from msys2-cross-install.sh)12
-rwxr-xr-xmsys2-package.sh34
-rw-r--r--resources/cross-large-symbolic.svg4
-rw-r--r--resources/resources.gresource.xml2
-rw-r--r--resources/text-symbolic.svg150
m---------submodules/liberty0
m---------submodules/wuffs-mirror-release-c0
-rw-r--r--subprojects/libjpegqs.wrap8
-rw-r--r--subprojects/packagefiles/libjpegqs/meson.build4
-rwxr-xr-xtiff-tables.awk20
-rw-r--r--tiff-tables.db46
-rw-r--r--tiffer.h356
-rw-r--r--tools/benchmark-io.c (renamed from fiv-io-benchmark.c)35
-rw-r--r--tools/bmffinfo.c142
-rw-r--r--tools/hotpixels.c210
-rw-r--r--tools/info.c286
-rw-r--r--tools/info.h1021
-rw-r--r--tools/jpeginfo.c610
-rw-r--r--tools/rawinfo.c175
-rw-r--r--tools/tiffinfo.c79
-rw-r--r--tools/webpinfo.c133
m---------wuffs-mirror-release-c0
-rw-r--r--xdg.c3
55 files changed, 7405 insertions, 3832 deletions
diff --git a/.gitmodules b/.gitmodules
index c6b083b..d955ecc 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "wuffs-mirror-release-c"]
- path = wuffs-mirror-release-c
+ path = submodules/wuffs-mirror-release-c
url = https://github.com/google/wuffs-mirror-release-c
+[submodule "liberty"]
+ path = submodules/liberty
+ url = https://git.janouch.name/p/liberty.git
diff --git a/LICENSE b/LICENSE
index 837a7f9..96d7caf 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
diff --git a/README.adoc b/README.adoc
index 218ba82..9ca9e65 100644
--- a/README.adoc
+++ b/README.adoc
@@ -2,7 +2,7 @@ fiv
===
'fiv' is a slightly unconventional, general-purpose image browser and viewer
-for Linux (that said, macOS and Windows ports are possible).
+for Linux and Windows (macOS still has major issues).
image::docs/fiv.webp["Screenshot of both the browser and the viewer"]
@@ -13,7 +13,7 @@ Features
photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf
modules manage to load.
- Employs high-performance file format libraries: Wuffs and libjpeg-turbo.
- - Makes use of 30-bit X.org visuals, whenever it's possible and appropriate.
+ - Can make use of 30-bit X.org visuals, under certain conditions.
- Has a notion of pages, and tries to load all included content within files.
- Can keep the zoom and position when browsing, to help with comparing
zoomed-in images.
@@ -33,41 +33,60 @@ Not necessarily in this order.
Packages
--------
-Regular releases are sporadic. git master should be stable enough. You can get
-a package with the latest development version from Archlinux's AUR.
+Regular releases are sporadic. git master should be stable enough.
+You can get a package with the latest development version using Arch Linux's
+https://aur.archlinux.org/packages/fiv-git[AUR],
+or as a https://git.janouch.name/p/nixexprs[Nix derivation].
Building and Running
--------------------
-Build-only dependencies: Meson, pkg-config, asciidoctor or asciidoc +
+Build-only dependencies:
+ Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) +
Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info,
-libturbojpeg, libwebp +
-Optional dependencies: lcms2, LibRaw, librsvg-2.0, xcursor, libheif, libtiff,
-ExifTool, resvg (unstable API, needs to be requested explicitly)
+ libturbojpeg, libwebp, libepoxy, librsvg-2.0 (for icons) +
+Optional dependencies: lcms2, Little CMS fast float plugin,
+ LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool,
+ resvg (unstable API, needs to be requested explicitly) +
+Runtime dependencies for reverse image search:
+ xdg-utils, cURL, jq
$ git clone --recursive https://git.janouch.name/p/fiv.git
- $ meson builddir
+ $ cd fiv
+ $ meson setup builddir
$ cd builddir
$ meson compile
+ $ meson devenv fiv
-Considering the vast amount of dynamically-linked dependencies, do not attempt
-direct installations via `ninja install`. To test the program:
+The lossless JPEG cropper and reverse image search are intended to be invoked
+from a file manager context menu.
- $ meson devenv fiv
+For proper integration, you will need to install the application. On Debian,
+you can get a quick and dirty installation package for testing purposes using:
-The lossless JPEG cropper is intended to be invoked from a context menu.
+ $ meson compile deb
+ # dpkg -i fiv-*.deb
Windows
~~~~~~~
'fiv' can be cross-compiled for Windows, provided that you install a bunch of
-dependencies listed at the beginning of 'msys2-cross-configure.sh',
-plus rsvg-convert from librsvg2, and icotool from icoutils.
+dependencies listed at the beginning of 'msys2-configure.sh',
+plus rsvg-convert from librsvg2, icotool from icoutils, and msitools ≥ 0.102.
Beware that the build will take up about a gigabyte of disk space.
- $ sh -e msys2-cross-configure.sh builddir
- $ meson install -C builddir
+ $ sh -e msys2-configure.sh builddir
+ $ meson compile package -C builddir
If everything succeeds, you will find a portable build of the application
-in the 'builddir/package' subdirectory. Keep your expectations low.
+in the 'builddir/package' subdirectory, and a very basic MSI installation
+package in 'builddir'.
+
+Faster colour management
+^^^^^^^^^^^^^^^^^^^^^^^^
+To get the Little CMS fast float plugin, you'll have to enter MSYS2 and
+https://www.msys2.org/wiki/Creating-Packages/#re-building-a-package[rebuild]
+_mingw-w64-lcms2_ with the following change:
+
+ sed -i 's/meson setup /&-Dfastfloat=true /' PKGCONFIG
Documentation
-------------
diff --git a/docs/fiv.adoc b/docs/fiv.adoc
index f7f992e..0188086 100644
--- a/docs/fiv.adoc
+++ b/docs/fiv.adoc
@@ -26,6 +26,19 @@ the _User Guide_.
Options
-------
+*--browse*::
+ When an image is passed, start in browsing mode, and preselect that
+ image in its containing directory. This is used by *fiv*'s inode/directory
+ handler to implement the "Open Containing Folder" feature of certain
+ applications.
+
+*--collection*::
+ Always put arguments in a virtual directory, even when only one is passed.
+ Implies *--browse*.
+
+*--help-all*::
+ Show the full list of options, including those provided by GTK+.
+
*--invalidate-cache*::
Invalidate the wide thumbnail cache, removing thumbnails for files that can
no longer be found.
@@ -35,18 +48,11 @@ Options
the list of MIME types within *fiv*'s desktop file when the list
of GdkPixbuf loaders changes.
-*--browse*::
- When an image is passed, start in browsing mode, and preselect that
- image in its containing directory. This is used by *fiv*'s inode/directory
- handler to implement the "Open Containing Folder" feature of certain
- applications.
-
-*--thumbnail*=_SIZE_::
- Generate thumbnails for the first argument, in all sizes not exceeding
- _SIZE_, and present the largest of them on the standard output
- in an application-specific bitmap format. Available sizes follow directory
- names in the _Thumbnail Managing Standard_.
+*-V*, *--version*::
+ Output version information and exit.
+Internal options
+~~~~~~~~~~~~~~~~
*--extract-thumbnail*::
Present any embedded thumbnail of the first argument on the standard output
in an application-specific bitmap format. When both *--thumbnail*
@@ -54,11 +60,16 @@ Options
exiting early if successful. This is used to enhance responsivity
of thumbnail procurement.
-*-V*, *--version*::
- Output version information and exit.
+*--thumbnail*=_SIZE_::
+ Generate wide thumbnails for the first argument, in all sizes not exceeding
+ _SIZE_, and present the largest of them on the standard output
+ in an application-specific bitmap format. Available sizes follow directory
+ names in the _Thumbnail Managing Standard_.
-*--help-all*::
- Show the full list of options, including those provided by GTK+.
+*--thumbnail-for-search*=_SIZE_::
+ Transform the first argument to a widely supported image file format,
+ and present it on the standard output. The image will be downscaled as
+ necessary so as to not exceed _SIZE_ (see *--thumbnail*).
Reporting bugs
--------------
diff --git a/docs/fiv.html b/docs/fiv.html
index 5a9b918..a458882 100644
--- a/docs/fiv.html
+++ b/docs/fiv.html
@@ -17,9 +17,9 @@ q:lang(en):after { content: "’"; }
<span id="author">Přemysl Eric Janouch</span><br>
<span id="email"><a href="mailto:p@janouch.name">p@janouch.name</a></span><br>
<span id="revnumber">version 0.0.0,</span>
-<span id="revdate">2022-07-31</span>
+<span id="revdate">2023-04-17</span>
-<p class="figure"><img src="fiv.webp" alt="fiv's browser and viewer">
+<p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes">
<h2>Introduction</h2>
@@ -33,30 +33,31 @@ and page up/down buttons in mind, such as SteelSeries Sensei series. Ozone Neon
series may also be mapped this way. Your experience may be degraded with other
kinds of devices.
-<p>Controls should generally be accessible through the keyboard. Pressing Ctrl+?
-will give you a convenient overview of all shortcuts. In addition to these,
-remember that you may often use Ctrl+Tab and F6 to navigate to different groups
-of widgets.
+<p>Controls should generally be accessible through the keyboard. Pressing
+<kbd>Ctrl</kbd>&#8239;+&#8239;<kbd>?</kbd> will give you a convenient overview
+of all shortcuts. In addition to these, remember that you may often use
+<kbd>Ctrl</kbd>&#8239;+&#8239;<kbd>Tab</kbd> and <kbd>F6</kbd> to navigate to
+different groups of widgets.
<h2>Browser</h2>
<p><i>fiv</i> normally starts in a file browser view. On the left side of the
window, you'll find your GTK+ bookmarks, mounted locations as recognized by
-GVfs, an item for entering arbitrary filesystem paths or URIs, view controls,
-and finally breadcrumbs leading to the currently opened directory, as well as
-its descendants.
+GVfs, an item for entering arbitrary filesystem paths or URIs, and finally
+breadcrumbs leading to the currently opened directory, as well as descendants
+of it. At the top, there is a toolbar with view controls.
-<p>You can open items in a new window either by middle clicking on them either
-directly, or with the Ctrl key pressed down. Right clicking the directory view
-offers a context menu for opening files, or even the directory itself,
-in a different application.
+<p>You can open items in a new window either by middle clicking on them, or with
+the left mouse button while holding the <kbd>Ctrl</kbd> key.
+Right clicking the directory view offers a context menu for opening files,
+or even the directory itself, in a different application.
<h2>Viewer</h2>
<p>The image viewer may be both entered (so long as you have a file selected)
-and exited using the Enter key. This way you may easily switch between the two
-modes. When using the mouse, the forwards and backwards buttons will fulfill
-the same function.
+and exited using the <kbd>Enter</kbd> key. This way you may easily switch
+between the two modes. When using the mouse, the forwards and backwards buttons
+will fulfill the same function.
<p>Double clicking the image switches full-screen view, and the mouse wheel
adjusts the zoom level.
@@ -94,14 +95,8 @@ rm -rf ~/.cache/thumbnails/wide-*
<h2>Configuration</h2>
-<p>The few configuration options <i>fiv</i> has can be adjusted using
-<i>dconf-editor</i>, which can be launched in the appropriate location from
-within the application by pressing Ctrl+,. For command line usage, there is
-the <i>gsettings</i> utility:
-
-<pre>
-gsettings list-recursively name.janouch.fiv
-</pre>
+<p>To adjust the few configuration options of <i>fiv</i>,
+press <kbd>Ctrl</kbd>&#8239;+&#8239;<kbd>,</kbd> to open <i>Preferences</i>.
<p>To make your changes take effect, restart <i>fiv</i>.
diff --git a/docs/fiv.webp b/docs/fiv.webp
index ef47681..249afe1 100644
--- a/docs/fiv.webp
+++ b/docs/fiv.webp
Binary files differ
diff --git a/docs/stylesheet.css b/docs/stylesheet.css
index b4b5b2d..daf49f9 100644
--- a/docs/stylesheet.css
+++ b/docs/stylesheet.css
@@ -5,4 +5,5 @@ h2 { padding-top: .67em; border-top: 1px solid silver; }
p { line-height: 1.5; } .figure { text-align: center; } img { max-width: 100%; }
q { font-style: normal; } .details { border-bottom: 1px solid silver; }
.details br { display: none; } .details br + span:before { content: " — "; }
-pre { padding: 0 1em; }
+pre { padding: 0 1em; } kbd { border: solid #ccc; border-radius: .25em;
+ border-width: 1px 2px 2px 1px; padding: 0 .25em; font-family: inherit; }
diff --git a/fiv-browser.c b/fiv-browser.c
index 192c3bc..c9963f4 100644
--- a/fiv-browser.c
+++ b/fiv-browser.c
@@ -1,7 +1,7 @@
//
// fiv-browser.c: filesystem browsing widget
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -17,9 +17,6 @@
#include "config.h"
-#include <math.h>
-#include <pixman.h>
-
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
@@ -27,11 +24,16 @@
#ifdef GDK_WINDOWING_QUARTZ
#include <gdk/gdkquartz.h>
#endif // GDK_WINDOWING_QUARTZ
+#include <pixman.h>
+
+#include <math.h>
+#include <stdlib.h>
#include "fiv-browser.h"
#include "fiv-collection.h"
#include "fiv-context-menu.h"
#include "fiv-io.h"
+#include "fiv-io-model.h"
#include "fiv-thumbnail.h"
// --- Widget ------------------------------------------------------------------
@@ -45,9 +47,12 @@
// │ n │ ┊ glow border │ n ┊
// │ g ╰───────────────────╯ g ╰┄┄┄┄┄
// │ s p a c i n g
+// │ l a b e l
+// │ s p a c i n g
// │ ╭┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄
//
// The glow is actually a glowing margin, the border is rendered in two parts.
+// When labels are hidden, the surrounding spacing is collapsed.
//
typedef struct entry Entry;
@@ -72,8 +77,10 @@ struct _FivBrowser {
int item_height; ///< Thumbnail height in pixels
int item_spacing; ///< Space between items in pixels
+ gboolean show_labels; ///< Show labels underneath items
+
FivIoModel *model; ///< Filesystem model
- GArray *entries; ///< []Entry
+ GPtrArray *entries; ///< []*Entry
GArray *layouted_rows; ///< []Row
const Entry *selected; ///< Selected entry or NULL
@@ -85,7 +92,8 @@ struct _FivBrowser {
Thumbnailer *thumbnailers; ///< Parallelized thumbnailers
size_t thumbnailers_len; ///< Thumbnailers array size
- GQueue thumbnailers_queue; ///< Queued up Entry pointers
+ GQueue thumbnailers_queue_1; ///< Queued up Entry pointers, hi-prio
+ GQueue thumbnailers_queue_2; ///< Queued up Entry pointers, lo-prio
GdkCursor *pointer; ///< Cached pointer cursor
cairo_pattern_t *glow; ///< CAIRO_FORMAT_A8 mask for corners
@@ -100,29 +108,40 @@ struct _FivBrowser {
/// The "last modified" timestamp of source images for thumbnails.
static cairo_user_data_key_t fiv_browser_key_mtime_msec;
+/// The original file size of source images for thumbnails.
+static cairo_user_data_key_t fiv_browser_key_filesize;
struct entry {
- gchar *uri; ///< GIO URI
- gchar *target_uri; ///< GIO URI for any target
- gint64 mtime_msec; ///< Modification time in milliseconds
+ FivIoModelEntry *e; ///< Reference to model entry
cairo_surface_t *thumbnail; ///< Prescaled thumbnail
GIcon *icon; ///< If no thumbnail, use this icon
+
+ gboolean removed; ///< Model announced removal
};
+static Entry *
+entry_new(FivIoModelEntry *e)
+{
+ Entry *self = g_slice_alloc0(sizeof *self);
+ self->e = e;
+ return self;
+}
+
static void
-entry_free(Entry *self)
+entry_destroy(Entry *self)
{
- g_free(self->uri);
- g_free(self->target_uri);
+ fiv_io_model_entry_unref(self->e);
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
g_clear_object(&self->icon);
+ g_slice_free1(sizeof *self, self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct item {
const Entry *entry;
- int x_offset; ///< Offset within the row
+ PangoLayout *label; ///< Label
+ int x_offset; ///< X offset within the row
};
struct row {
@@ -135,11 +154,34 @@ struct row {
static void
row_free(Row *self)
{
+ for (gsize i = 0; i < self->len; i++)
+ g_clear_object(&self->items[i].label);
g_free(self->items);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+static double
+row_subheight(const FivBrowser *self, const Row *row)
+{
+ if (!self->show_labels)
+ return 0;
+
+ // If we didn't ellipsize labels, this should be made to account
+ // for vertical centering as well.
+ int tallest_label = 0;
+ for (gsize i = 0; i < row->len; i++) {
+ PangoRectangle ink = {}, logical = {};
+ pango_layout_get_extents(row->items[i].label, &ink, &logical);
+
+ int height = (logical.y + logical.height) / PANGO_SCALE;
+ if (tallest_label < height)
+ tallest_label = height;
+ }
+
+ return self->item_spacing + tallest_label;
+}
+
static void
append_row(FivBrowser *self, int *y, int x, GArray *items_array)
{
@@ -154,6 +196,7 @@ append_row(FivBrowser *self, int *y, int x, GArray *items_array)
// Not trying to pack them vertically, but this would be the place to do it.
*y += self->item_height;
*y += self->item_border_y;
+ *y += row_subheight(self, &row);
}
static int
@@ -173,7 +216,7 @@ relayout(FivBrowser *self, int width)
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);
+ const Entry *entry = self->entries->pdata[i];
if (!entry->thumbnail)
continue;
@@ -189,8 +232,29 @@ relayout(FivBrowser *self, int width)
x = 0;
}
- g_array_append_val(items,
- ((Item) {.entry = entry, .x_offset = x + self->item_border_x}));
+ PangoLayout *label = NULL;
+ if (self->show_labels) {
+ label = gtk_widget_create_pango_layout(
+ widget, entry->e->display_name);
+ pango_layout_set_width(
+ label, (width - 2 * self->glow_w) * PANGO_SCALE);
+ pango_layout_set_alignment(label, PANGO_ALIGN_CENTER);
+ pango_layout_set_wrap(label, PANGO_WRAP_WORD_CHAR);
+ pango_layout_set_ellipsize(label, PANGO_ELLIPSIZE_END);
+
+#if PANGO_VERSION_CHECK(1, 44, 0)
+ PangoAttrList *attrs = pango_attr_list_new();
+ pango_attr_list_insert(attrs, pango_attr_insert_hyphens_new(FALSE));
+ pango_layout_set_attributes(label, attrs);
+ pango_attr_list_unref (attrs);
+#endif
+ }
+
+ g_array_append_val(items, ((Item) {
+ .entry = entry,
+ .label = label,
+ .x_offset = x + self->item_border_x,
+ }));
x += width;
if (max_width < width)
@@ -212,14 +276,13 @@ relayout(FivBrowser *self, int width)
gtk_adjustment_set_page_size(self->hadjustment, width);
}
if (self->vadjustment) {
+ int height = gtk_widget_get_allocated_height(widget);
gtk_adjustment_set_lower(self->vadjustment, 0);
- gtk_adjustment_set_upper(self->vadjustment, total_height);
+ gtk_adjustment_set_upper(self->vadjustment, MAX(height, total_height));
gtk_adjustment_set_step_increment(self->vadjustment,
self->item_height + self->item_spacing + 2 * self->item_border_y);
- gtk_adjustment_set_page_increment(
- self->vadjustment, gtk_widget_get_allocated_height(widget) * 0.9);
- gtk_adjustment_set_page_size(
- self->vadjustment, gtk_widget_get_allocated_height(widget));
+ gtk_adjustment_set_page_increment(self->vadjustment, height * 0.9);
+ gtk_adjustment_set_page_size(self->vadjustment, height);
}
return total_height;
}
@@ -342,7 +405,7 @@ draw_row(FivBrowser *self, cairo_t *cr, const Row *row)
// Performance optimization--specifically targeting the checkerboard.
if (cairo_image_surface_get_format(item->entry->thumbnail) !=
- CAIRO_FORMAT_RGB24) {
+ CAIRO_FORMAT_RGB24 || item->entry->removed) {
gtk_render_background(style, cr, border.left, border.top,
extents.width, extents.height);
}
@@ -358,12 +421,49 @@ draw_row(FivBrowser *self, cairo_t *cr, const Row *row)
cairo_mask_surface(
cr, item->entry->thumbnail, border.left, border.top);
} else {
+ // Distinguish removed items by rendering them only faintly.
+ if (item->entry->removed)
+ cairo_push_group(cr);
+
cairo_set_source_surface(
cr, item->entry->thumbnail, border.left, border.top);
cairo_paint(cr);
- // Here, we could consider multiplying
+ // Here, we could also consider multiplying
// the whole rectangle with the selection color.
+ if (item->entry->removed) {
+ cairo_pop_group_to_source(cr);
+ cairo_paint_with_alpha(cr, 0.25);
+ }
+ }
+
+ // This rendition is about the best I could come up with.
+ // It might be possible to use more such emblems with entries,
+ // though they would deserve some kind of a blur-glow.
+ if (item->entry->removed) {
+ int size = 32;
+ cairo_surface_t *cross = gtk_icon_theme_load_surface(
+ gtk_icon_theme_get_default(), "cross-large-symbolic",
+ size, gtk_widget_get_scale_factor(GTK_WIDGET(self)),
+ gtk_widget_get_window(GTK_WIDGET(self)),
+ GTK_ICON_LOOKUP_FORCE_SYMBOLIC, NULL);
+ if (cross) {
+ cairo_set_source_rgb(cr, 1, 0, 0);
+ cairo_mask_surface(cr, cross,
+ border.left + extents.width - size - size / 4,
+ border.top + extents.height - size - size / 4);
+ cairo_surface_destroy(cross);
+ }
+ }
+
+ if (self->show_labels) {
+ gtk_style_context_save(style);
+ gtk_style_context_add_class(style, "label");
+ gtk_render_layout(style, cr, -border.left,
+ border.top + extents.height + self->item_border_y +
+ self->item_spacing,
+ item->label);
+ gtk_style_context_restore(style);
}
cairo_restore(cr);
@@ -440,51 +540,81 @@ rescale_thumbnail(cairo_surface_t *thumbnail, double row_height)
return scaled;
}
-static char *
+static const char *
entry_system_wide_uri(const Entry *self)
{
// "recent" and "trash", e.g., also have "standard::target-uri" set,
// but we'd like to avoid saving their thumbnails.
- if (self->target_uri && fiv_collection_uri_matches(self->uri))
- return self->target_uri;
+ if (self->e->target_uri && fiv_collection_uri_matches(self->e->uri))
+ return self->e->target_uri;
- return self->uri;
+ return self->e->uri;
}
static void
-entry_add_thumbnail(gpointer data, gpointer user_data)
+entry_set_surface_user_data(const Entry *self)
{
- Entry *self = data;
- g_clear_object(&self->icon);
- g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
+ // This choice of mtime favours unnecessary thumbnail reloading
+ // over retaining stale data (consider both calling functions).
+ cairo_surface_set_user_data(self->thumbnail,
+ &fiv_browser_key_mtime_msec, (void *) (intptr_t) self->e->mtime_msec,
+ NULL);
+ cairo_surface_set_user_data(self->thumbnail,
+ &fiv_browser_key_filesize, (void *) (uintptr_t) self->e->filesize,
+ NULL);
+}
- FivBrowser *browser = FIV_BROWSER(user_data);
+static cairo_surface_t *
+entry_lookup_thumbnail(Entry *self, FivBrowser *browser)
+{
cairo_surface_t *cached =
- g_hash_table_lookup(browser->thumbnail_cache, self->uri);
+ g_hash_table_lookup(browser->thumbnail_cache, self->e->uri);
if (cached &&
- (intptr_t) cairo_surface_get_user_data(
- cached, &fiv_browser_key_mtime_msec) == self->mtime_msec) {
- self->thumbnail = cairo_surface_reference(cached);
+ (intptr_t) cairo_surface_get_user_data(cached,
+ &fiv_browser_key_mtime_msec) == (intptr_t) self->e->mtime_msec &&
+ (uintptr_t) cairo_surface_get_user_data(cached,
+ &fiv_browser_key_filesize) == (uintptr_t) self->e->filesize) {
// TODO(p): If this hit is low-quality, see if a high-quality thumbnail
// hasn't been produced without our knowledge (avoid launching a minion
// unnecessarily; we might also shift the concern there).
- } else {
- cairo_surface_t *found = fiv_thumbnail_lookup(
- entry_system_wide_uri(self), self->mtime_msec, browser->item_size);
- self->thumbnail = rescale_thumbnail(found, browser->item_height);
+ return cairo_surface_reference(cached);
+ }
+
+ cairo_surface_t *found = fiv_thumbnail_lookup(
+ entry_system_wide_uri(self), self->e->mtime_msec, self->e->filesize,
+ browser->item_size);
+ return rescale_thumbnail(found, browser->item_height);
+}
+
+static void
+entry_add_thumbnail(gpointer data, gpointer user_data)
+{
+ Entry *self = data;
+ FivBrowser *browser = FIV_BROWSER(user_data);
+ if (self->removed) {
+ // Keep whatever size of thumbnail we had at the time up until reload.
+ // g_file_query_info() fails for removed files, so keep the icon, too.
+ if (self->icon) {
+ g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
+ } else {
+ self->thumbnail =
+ rescale_thumbnail(self->thumbnail, browser->item_height);
+ }
+ return;
}
- if (self->thumbnail) {
- // This choice of mtime favours unnecessary thumbnail reloading.
- cairo_surface_set_user_data(self->thumbnail,
- &fiv_browser_key_mtime_msec, (void *) (intptr_t) self->mtime_msec,
- NULL);
+ g_clear_object(&self->icon);
+ g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
+
+ if ((self->thumbnail = entry_lookup_thumbnail(self, browser))) {
+ // Yes, this is a pointless action in case it's been found in the cache.
+ entry_set_surface_user_data(self);
return;
}
// Fall back to symbolic icons, though there's only so much we can do
// in parallel--GTK+ isn't thread-safe.
- GFile *file = g_file_new_for_uri(self->uri);
+ GFile *file = g_file_new_for_uri(self->e->uri);
GFileInfo *info = g_file_query_info(file,
G_FILE_ATTRIBUTE_STANDARD_NAME
"," G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON,
@@ -513,11 +643,15 @@ materialize_icon(FivBrowser *self, Entry *entry)
// 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);
+ GtkIconTheme *theme = gtk_icon_theme_get_default();
+ GtkIconInfo *icon_info = gtk_icon_theme_lookup_by_gicon(theme, entry->icon,
+ self->item_height / 2, GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
+ if (!icon_info) {
+ // This icon is included within GTK+.
+ icon_info = gtk_icon_theme_lookup_icon(theme, "text-x-generic",
+ self->item_height / 2, GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
+ }
if (!icon_info)
return;
@@ -547,26 +681,38 @@ materialize_icon(FivBrowser *self, Entry *entry)
}
static void
+reload_one_thumbnail_finish(FivBrowser *self, Entry *entry)
+{
+ if (!entry->removed && entry->thumbnail) {
+ g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri),
+ cairo_surface_reference(entry->thumbnail));
+ }
+
+ materialize_icon(self, entry);
+}
+
+static void
+reload_one_thumbnail(FivBrowser *self, Entry *entry)
+{
+ entry_add_thumbnail(entry, self);
+ reload_one_thumbnail_finish(self, entry);
+
+ gtk_widget_queue_resize(GTK_WIDGET(self));
+}
+
+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_push(pool, self->entries->pdata[i], NULL);
g_thread_pool_free(pool, FALSE, TRUE);
// Once a URI disappears from the model, its thumbnail is forgotten.
g_hash_table_remove_all(self->thumbnail_cache);
-
- for (guint i = 0; i < self->entries->len; i++) {
- Entry *entry = &g_array_index(self->entries, Entry, i);
- if (entry->thumbnail) {
- g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->uri),
- cairo_surface_reference(entry->thumbnail));
- }
-
- materialize_icon(self, entry);
- }
+ for (guint i = 0; i < self->entries->len; i++)
+ reload_one_thumbnail_finish(self, self->entries->pdata[i]);
gtk_widget_queue_resize(GTK_WIDGET(self));
}
@@ -597,15 +743,11 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry)
if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) {
cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq,
(void *) (intptr_t) 1, NULL);
- g_queue_push_tail(&self->thumbnailers_queue, entry);
+ g_queue_push_tail(&self->thumbnailers_queue_2, entry);
}
- // This choice of mtime favours unnecessary thumbnail reloading
- // over retaining stale data.
- cairo_surface_set_user_data(entry->thumbnail,
- &fiv_browser_key_mtime_msec, (void *) (intptr_t) entry->mtime_msec,
- NULL);
- g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->uri),
+ entry_set_surface_user_data(entry);
+ g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->e->uri),
cairo_surface_reference(entry->thumbnail));
}
@@ -655,13 +797,21 @@ on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data)
thumbnailer_next(t);
}
+// TODO(p): Try to keep the minions alive (stdout will be a problem).
static gboolean
thumbnailer_next(Thumbnailer *t)
{
- // TODO(p): Try to keep the minions alive (stdout will be a problem).
+ // Already have something to do, not a failure.
+ if (t->target)
+ return TRUE;
+
+ // They could have been removed via post-reload changes in the model.
FivBrowser *self = t->self;
- if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue)))
- return FALSE;
+ do {
+ if (!(t->target = g_queue_pop_head(&self->thumbnailers_queue_1)) &&
+ !(t->target = g_queue_pop_head(&self->thumbnailers_queue_2)))
+ return FALSE;
+ } while (t->target->removed);
// Case analysis:
// - We haven't found any thumbnail for the entry at all
@@ -698,7 +848,8 @@ thumbnailer_next(Thumbnailer *t)
static void
thumbnailers_abort(FivBrowser *self)
{
- g_queue_clear(&self->thumbnailers_queue);
+ g_queue_clear(&self->thumbnailers_queue_1);
+ g_queue_clear(&self->thumbnailers_queue_2);
for (size_t i = 0; i < self->thumbnailers_len; i++) {
Thumbnailer *t = self->thumbnailers + i;
@@ -714,32 +865,35 @@ thumbnailers_abort(FivBrowser *self)
}
static void
-thumbnailers_start(FivBrowser *self)
+thumbnailers_enqueue(FivBrowser *self, Entry *entry)
{
- thumbnailers_abort(self);
- if (!self->model)
- return;
-
- GQueue lq = G_QUEUE_INIT;
- for (guint i = 0; i < self->entries->len; i++) {
- Entry *entry = &g_array_index(self->entries, Entry, i);
+ if (!entry->removed) {
if (entry->icon)
- g_queue_push_tail(&self->thumbnailers_queue, entry);
+ g_queue_push_tail(&self->thumbnailers_queue_1, entry);
else if (cairo_surface_get_user_data(
entry->thumbnail, &fiv_thumbnail_key_lq))
- g_queue_push_tail(&lq, entry);
- }
- while (!g_queue_is_empty(&lq)) {
- g_queue_push_tail_link(
- &self->thumbnailers_queue, g_queue_pop_head_link(&lq));
+ g_queue_push_tail(&self->thumbnailers_queue_2, entry);
}
+}
+static void
+thumbnailers_deploy(FivBrowser *self)
+{
for (size_t i = 0; i < self->thumbnailers_len; i++) {
if (!thumbnailer_next(self->thumbnailers + i))
break;
}
}
+static void
+thumbnailers_restart(FivBrowser *self)
+{
+ thumbnailers_abort(self);
+ for (guint i = 0; i < self->entries->len; i++)
+ thumbnailers_enqueue(self, self->entries->pdata[i]);
+ thumbnailers_deploy(self);
+}
+
// --- Boilerplate -------------------------------------------------------------
G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0,
@@ -747,6 +901,7 @@ G_DEFINE_TYPE_EXTENDED(FivBrowser, fiv_browser, GTK_TYPE_WIDGET, 0,
enum {
PROP_THUMBNAIL_SIZE = 1,
+ PROP_SHOW_LABELS,
N_PROPERTIES,
// These are overriden, we do not register them.
@@ -799,7 +954,7 @@ fiv_browser_finalize(GObject *gobject)
{
FivBrowser *self = FIV_BROWSER(gobject);
thumbnailers_abort(self);
- g_array_free(self->entries, TRUE);
+ g_ptr_array_free(self->entries, TRUE);
g_array_free(self->layouted_rows, TRUE);
if (self->model) {
g_signal_handlers_disconnect_by_data(self->model, self);
@@ -827,6 +982,9 @@ fiv_browser_get_property(
case PROP_THUMBNAIL_SIZE:
g_value_set_enum(value, self->item_size);
break;
+ case PROP_SHOW_LABELS:
+ g_value_set_boolean(value, self->show_labels);
+ break;
case PROP_HADJUSTMENT:
g_value_set_object(value, self->hadjustment);
break;
@@ -856,7 +1014,7 @@ set_item_size(FivBrowser *self, FivThumbnailSize size)
g_hash_table_remove_all(self->thumbnail_cache);
reload_thumbnails(self);
- thumbnailers_start(self);
+ thumbnailers_restart(self);
g_object_notify_by_pspec(
G_OBJECT(self), browser_properties[PROP_THUMBNAIL_SIZE]);
@@ -872,6 +1030,13 @@ fiv_browser_set_property(
case PROP_THUMBNAIL_SIZE:
set_item_size(self, g_value_get_enum(value));
break;
+ case PROP_SHOW_LABELS:
+ if (self->show_labels != g_value_get_boolean(value)) {
+ self->show_labels = g_value_get_boolean(value);
+ gtk_widget_queue_resize(GTK_WIDGET(self));
+ g_object_notify_by_pspec(object, pspec);
+ }
+ break;
case PROP_HADJUSTMENT:
if (replace_adjustment(
self, &self->hadjustment, g_value_get_object(value)))
@@ -1042,7 +1207,7 @@ fiv_browser_draw(GtkWidget *widget, cairo_t *cr)
static gboolean
open_entry(GtkWidget *self, const Entry *entry, gboolean new_window)
{
- GFile *location = g_file_new_for_uri(entry->uri);
+ GFile *location = g_file_new_for_uri(entry->e->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);
@@ -1052,7 +1217,9 @@ open_entry(GtkWidget *self, const Entry *entry, gboolean new_window)
static void
show_context_menu(GtkWidget *widget, GFile *file)
{
- gtk_menu_popup_at_pointer(fiv_context_menu_new(widget, file), NULL);
+ GtkMenu *menu = fiv_context_menu_new(widget, file);
+ if (menu)
+ gtk_menu_popup_at_pointer(menu, NULL);
}
static void
@@ -1112,7 +1279,7 @@ fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
// no matter what its new location is.
gdk_window_set_cursor(gtk_widget_get_window(widget), NULL);
- GFile *file = g_file_new_for_uri(entry->uri);
+ GFile *file = g_file_new_for_uri(entry->e->uri);
show_context_menu(widget, file);
g_object_unref(file);
return GDK_EVENT_STOP;
@@ -1256,8 +1423,8 @@ fiv_browser_drag_data_get(GtkWidget *widget,
{
FivBrowser *self = FIV_BROWSER(widget);
if (self->selected) {
- (void) gtk_selection_data_set_uris(
- data, (gchar *[]) {entry_system_wide_uri(self->selected), NULL});
+ (void) gtk_selection_data_set_uris(data, (gchar *[])
+ {(gchar *) entry_system_wide_uri(self->selected), NULL});
}
}
@@ -1283,12 +1450,13 @@ scroll_to_row(FivBrowser *self, const Row *row)
double y1 = gtk_adjustment_get_value(self->vadjustment);
double ph = gtk_adjustment_get_page_size(self->vadjustment);
+ double sh = self->item_border_y + row_subheight(self, row);
if (row->y_offset < y1) {
gtk_adjustment_set_value(
self->vadjustment, row->y_offset - self->item_border_y);
- } else if (row->y_offset + self->item_height > y1 + ph) {
- gtk_adjustment_set_value(self->vadjustment,
- row->y_offset - ph + self->item_height + self->item_border_y);
+ } else if (row->y_offset + self->item_height + sh > y1 + ph) {
+ gtk_adjustment_set_value(
+ self->vadjustment, row->y_offset - ph + self->item_height + sh);
}
}
@@ -1404,6 +1572,14 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
switch ((event->state & gtk_accelerator_get_default_mod_mask())) {
case 0:
switch (event->keyval) {
+ case GDK_KEY_Delete:
+ if (self->selected) {
+ GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget));
+ GFile *file = g_file_new_for_uri(self->selected->e->uri);
+ fiv_context_menu_remove(window, file);
+ g_object_unref(file);
+ }
+ return GDK_EVENT_STOP;
case GDK_KEY_Return:
if (self->selected)
return open_entry(widget, self->selected, FALSE);
@@ -1433,7 +1609,7 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
case GDK_KEY_Return:
if (self->selected) {
GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget));
- fiv_context_menu_information(window, self->selected->uri);
+ fiv_context_menu_information(window, self->selected->e->uri);
}
return GDK_EVENT_STOP;
}
@@ -1459,20 +1635,16 @@ 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)
+
+ // TODO(p): Consider getting rid of tooltips altogether.
+ if (self->show_labels)
return FALSE;
- GFile *file = g_file_new_for_uri(entry->uri);
- GFileInfo *info =
- g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
- G_FILE_QUERY_INFO_NONE, NULL, NULL);
- g_object_unref(file);
- if (!info)
+ const Entry *entry = entry_at(self, x, y);
+ if (!entry)
return FALSE;
- gtk_tooltip_set_text(tooltip, g_file_info_get_display_name(info));
- g_object_unref(info);
+ gtk_tooltip_set_text(tooltip, entry->e->display_name);
return TRUE;
}
@@ -1486,7 +1658,7 @@ fiv_browser_popup_menu(GtkWidget *widget)
GFile *file = NULL;
GdkRectangle rect = {};
if (self->selected) {
- file = g_file_new_for_uri(self->selected->uri);
+ file = g_file_new_for_uri(self->selected->e->uri);
rect = entry_rect(self, self->selected);
rect.x += rect.width / 2;
rect.y += rect.height / 2;
@@ -1524,7 +1696,7 @@ on_long_press(GtkGestureLongPress *lp, gdouble x, gdouble y, gpointer user_data)
// It might also be possible to have long-press just select items,
// and show some kind of toolbar with available actions.
- GFile *file = g_file_new_for_uri(entry->uri);
+ GFile *file = g_file_new_for_uri(entry->e->uri);
gtk_menu_popup_at_rect(fiv_context_menu_new(widget, file), window,
&(GdkRectangle) {.x = x, .y = y}, GDK_GRAVITY_NORTH_WEST,
GDK_GRAVITY_NORTH_WEST, event);
@@ -1553,6 +1725,13 @@ fiv_browser_style_updated(GtkWidget *widget)
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);
+ // XXX: Right now, specifying custom fonts within our CSS pseudo-regions
+ // has no effect, so it might be appropriate to also add .label/.symbolic
+ // classes here, remember the resulting GTK_STYLE_PROPERTY_FONT,
+ // and apply them in relayout() with pango_layout_set_font_description().
+ // There is virtually nothing to be gained from this flexibility, though.
+ // XXX: We should also invoke relayout() here, because different states
+ // might theoretically use different fonts.
gtk_style_context_restore(style);
self->glow_w = (margin.left + margin.right) / 2;
@@ -1634,6 +1813,9 @@ fiv_browser_class_init(FivBrowserClass *klass)
"thumbnail-size", "Thumbnail size", "The thumbnail height to use",
FIV_TYPE_THUMBNAIL_SIZE, FIV_THUMBNAIL_SIZE_NORMAL,
G_PARAM_READWRITE);
+ browser_properties[PROP_SHOW_LABELS] = g_param_spec_boolean(
+ "show-labels", "Show labels", "Whether to show filename labels",
+ FALSE, G_PARAM_READWRITE);
g_object_class_install_properties(
object_class, N_PROPERTIES, browser_properties);
@@ -1686,8 +1868,8 @@ 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->entries =
+ g_ptr_array_new_with_free_func((GDestroyNotify) entry_destroy);
self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row));
g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free);
abort_button_tracking(self);
@@ -1700,9 +1882,11 @@ fiv_browser_init(FivBrowser *self)
g_malloc0_n(self->thumbnailers_len, sizeof *self->thumbnailers);
for (size_t i = 0; i < self->thumbnailers_len; i++)
self->thumbnailers[i].self = self;
- g_queue_init(&self->thumbnailers_queue);
+ g_queue_init(&self->thumbnailers_queue_1);
+ g_queue_init(&self->thumbnailers_queue_2);
set_item_size(self, FIV_THUMBNAIL_SIZE_NORMAL);
+ self->show_labels = FALSE;
self->glow_padded = cairo_pattern_create_rgba(0, 0, 0, 0);
self->glow = cairo_pattern_create_rgba(0, 0, 0, 0);
@@ -1721,35 +1905,80 @@ fiv_browser_init(FivBrowser *self)
// --- Public interface --------------------------------------------------------
-// TODO(p): Later implement any arguments of this FivIoModel signal.
static void
-on_model_files_changed(FivIoModel *model, FivBrowser *self)
+on_model_reloaded(FivIoModel *model, FivBrowser *self)
{
g_return_if_fail(model == self->model);
gchar *selected_uri = NULL;
if (self->selected)
- selected_uri = g_strdup(self->selected->uri);
+ selected_uri = g_strdup(self->selected->e->uri);
thumbnailers_abort(self);
- g_array_set_size(self->entries, 0);
g_array_set_size(self->layouted_rows, 0);
+ g_ptr_array_set_size(self->entries, 0);
gsize len = 0;
- const FivIoModelEntry *files = fiv_io_model_get_files(self->model, &len);
+ FivIoModelEntry *const *files = fiv_io_model_get_files(self->model, &len);
for (gsize i = 0; i < len; i++) {
- Entry e = {.thumbnail = NULL,
- .uri = g_strdup(files[i].uri),
- .target_uri = g_strdup(files[i].target_uri),
- .mtime_msec = files[i].mtime_msec};
- g_array_append_val(self->entries, e);
+ g_ptr_array_add(
+ self->entries, entry_new(fiv_io_model_entry_ref(files[i])));
}
fiv_browser_select(self, selected_uri);
g_free(selected_uri);
+ // Restarting thumbnailers is critical, because they keep Entry pointers.
reload_thumbnails(self);
- thumbnailers_start(self);
+ thumbnailers_restart(self);
+}
+
+static void
+on_model_changed(FivIoModel *model, FivIoModelEntry *old, FivIoModelEntry *new,
+ FivBrowser *self)
+{
+ g_return_if_fail(model == self->model);
+
+ // Add new entries to the end, so as to not disturb the layout.
+ if (!old) {
+ Entry *entry = entry_new(fiv_io_model_entry_ref(new));
+ g_ptr_array_add(self->entries, entry);
+
+ reload_one_thumbnail(self, entry);
+ thumbnailers_enqueue(self, entry);
+ thumbnailers_deploy(self);
+ return;
+ }
+
+ Entry *found = NULL;
+ for (guint i = 0; i < self->entries->len; i++) {
+ Entry *entry = self->entries->pdata[i];
+ if (entry->e == old) {
+ found = entry;
+ break;
+ }
+ }
+ if (!found)
+ return;
+
+ // Rename entries in place, so as to not disturb the layout.
+ // XXX: This behaves differently from FivIoModel, and by extension fiv.c.
+ if (new) {
+ fiv_io_model_entry_unref(found->e);
+ found->e = fiv_io_model_entry_ref(new);
+ found->removed = FALSE;
+
+ // TODO(p): If there is a URI mismatch, don't reload thumbnails,
+ // so that there's no jumping around. Or, a bit more properly,
+ // move the thumbnail cache entry to the new URI.
+ reload_one_thumbnail(self, found);
+ // TODO(p): Rather cancel the entry in any running thumbnailer,
+ // remove it from queues, and _enqueue() + _deploy().
+ thumbnailers_restart(self);
+ } else {
+ found->removed = TRUE;
+ gtk_widget_queue_draw(GTK_WIDGET(self));
+ }
}
GtkWidget *
@@ -1760,9 +1989,11 @@ fiv_browser_new(FivIoModel *model)
FivBrowser *self = g_object_new(FIV_TYPE_BROWSER, NULL);
self->model = g_object_ref(model);
- g_signal_connect(
- self->model, "files-changed", G_CALLBACK(on_model_files_changed), self);
- on_model_files_changed(self->model, self);
+ g_signal_connect(self->model, "reloaded",
+ G_CALLBACK(on_model_reloaded), self);
+ g_signal_connect(self->model, "files-changed",
+ G_CALLBACK(on_model_changed), self);
+ on_model_reloaded(self->model, self);
return GTK_WIDGET(self);
}
@@ -1791,8 +2022,8 @@ fiv_browser_select(FivBrowser *self, const char *uri)
return;
for (guint i = 0; i < self->entries->len; i++) {
- const Entry *entry = &g_array_index(self->entries, Entry, i);
- if (!g_strcmp0(entry->uri, uri)) {
+ const Entry *entry = self->entries->pdata[i];
+ if (!g_strcmp0(entry->e->uri, uri)) {
self->selected = entry;
scroll_to_selection(self);
break;
diff --git a/fiv-browser.h b/fiv-browser.h
index 0a93721..701cb50 100644
--- a/fiv-browser.h
+++ b/fiv-browser.h
@@ -17,7 +17,7 @@
#pragma once
-#include "fiv-io.h"
+#include "fiv-io-model.h"
#include <gtk/gtk.h>
diff --git a/fiv-collection.c b/fiv-collection.c
index 13548b9..6878898 100644
--- a/fiv-collection.c
+++ b/fiv-collection.c
@@ -528,12 +528,16 @@ fiv_collection_file_query_info(GFile *file, const char *attributes,
g_file_info_set_name(info, basename);
g_free(basename);
- if ((name = g_file_info_get_display_name(info))) {
+ if (g_file_info_has_attribute(
+ info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME) &&
+ (name = g_file_info_get_display_name(info))) {
gchar *prefixed = get_prefixed_name(self, name);
g_file_info_set_display_name(info, prefixed);
g_free(prefixed);
}
- if ((name = g_file_info_get_edit_name(info))) {
+ if (g_file_info_has_attribute(
+ info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME) &&
+ (name = g_file_info_get_edit_name(info))) {
gchar *prefixed = get_prefixed_name(self, name);
g_file_info_set_edit_name(info, prefixed);
g_free(prefixed);
diff --git a/fiv-context-menu.c b/fiv-context-menu.c
index b5dafc4..16460b6 100644
--- a/fiv-context-menu.c
+++ b/fiv-context-menu.c
@@ -1,7 +1,7 @@
//
// fiv-context-menu.c: popup menu
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -328,6 +328,17 @@ open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
}
static void
+show_error_dialog(GtkWindow *parent, GError *error)
+{
+ GtkWidget *dialog =
+ gtk_message_dialog_new(GTK_WINDOW(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 void
open_context_launch(GtkWidget *widget, OpenContext *self)
{
GdkAppLaunchContext *context =
@@ -342,8 +353,9 @@ open_context_launch(GtkWidget *widget, OpenContext *self)
(void) g_app_info_set_as_last_used_for_type(
self->app_info, self->content_type, NULL);
} else {
- g_warning("%s", error->message);
- g_error_free(error);
+ GtkWindow *window = g_weak_ref_get(&self->window);
+ show_error_dialog(window, error);
+ g_clear_object(&window);
}
g_list_free(files);
g_object_unref(context);
@@ -396,6 +408,15 @@ on_chooser_activate(GtkMenuItem *item, gpointer user_data)
GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window,
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type);
g_clear_object(&window);
+
+#if 0
+ // This exists as a concept in mimeapps.list, but GNOME infuriatingly
+ // infers it from the last used application if missing.
+ gtk_app_chooser_widget_set_show_default(
+ GTK_APP_CHOOSER_WIDGET(gtk_app_chooser_dialog_get_widget(
+ GTK_APP_CHOOSER_DIALOG(dialog))), TRUE);
+#endif
+
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);
@@ -414,6 +435,24 @@ on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
g_free(uri);
}
+void
+fiv_context_menu_remove(GtkWindow *parent, GFile *file)
+{
+ // TODO(p): Use g_file_trash_async(), for which we need a task manager.
+ GError *error = NULL;
+ if (!g_file_trash(file, NULL, &error))
+ show_error_dialog(parent, error);
+}
+
+static void
+on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
+{
+ OpenContext *ctx = user_data;
+ GtkWindow *window = g_weak_ref_get(&ctx->window);
+ fiv_context_menu_remove(window, ctx->file);
+ g_clear_object(&window);
+}
+
static gboolean
destroy_widget_idle_source_func(GtkWidget *widget)
{
@@ -494,11 +533,21 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
ctx, open_context_unref, 0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
+ // TODO(p): Can we avoid using the "trash" string constant for this check?
+ if (!g_file_has_uri_scheme(file, "trash")) {
+ gtk_menu_shell_append(
+ GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
+
+ item = gtk_menu_item_new_with_mnemonic("Move to _Trash");
+ g_signal_connect_data(item, "activate", G_CALLBACK(on_trash_activate),
+ g_rc_box_acquire(ctx), open_context_unref, 0);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
+ }
if (type == G_FILE_TYPE_REGULAR) {
gtk_menu_shell_append(
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
- item = gtk_menu_item_new_with_mnemonic("_Information...");
+ item = gtk_menu_item_new_with_mnemonic("_Information");
g_signal_connect_data(item, "activate", G_CALLBACK(on_info_activate),
g_rc_box_acquire(ctx), open_context_unref, 0);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
diff --git a/fiv-context-menu.h b/fiv-context-menu.h
index 8da58c1..34374aa 100644
--- a/fiv-context-menu.h
+++ b/fiv-context-menu.h
@@ -1,7 +1,7 @@
//
// fiv-context-menu.h: popup menu
//
-// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -18,4 +18,5 @@
#include <gtk/gtk.h>
void fiv_context_menu_information(GtkWindow *parent, const char *uri);
+void fiv_context_menu_remove(GtkWindow *parent, GFile *file);
GtkMenu *fiv_context_menu_new(GtkWidget *widget, GFile *file);
diff --git a/fiv-io-cmm.c b/fiv-io-cmm.c
new file mode 100644
index 0000000..b131acf
--- /dev/null
+++ b/fiv-io-cmm.c
@@ -0,0 +1,462 @@
+//
+// fiv-io-cmm.c: colour management
+//
+// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
+//
+// 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 <glib.h>
+#include <stdbool.h>
+
+#include "fiv-io.h"
+
+// Colour management must be handled before RGB conversions.
+// TODO(p): Make it also possible to use Skia's skcms.
+#ifdef HAVE_LCMS2
+#include <lcms2.h>
+#endif // HAVE_LCMS2
+#ifdef HAVE_LCMS2_FAST_FLOAT
+#include <lcms2_fast_float.h>
+#endif // HAVE_LCMS2_FAST_FLOAT
+
+// --- CMM-independent transforms ----------------------------------------------
+
+// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
+// ARGB/BGRA/XRGB/BGRX.
+static void
+trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len)
+{
+ // This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms.
+ // It will typically produce horribly oversaturated results.
+ // 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;
+ }
+}
+
+// From libwebp, verified to exactly match [x * a / 255].
+#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
+
+void
+fiv_io_premultiply_argb32(FivIoImage *image)
+{
+ if (image->format != CAIRO_FORMAT_ARGB32)
+ return;
+
+ for (uint32_t y = 0; y < image->height; y++) {
+ uint32_t *dstp = (uint32_t *) (image->data + image->stride * y);
+ for (uint32_t x = 0; x < image->width; 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);
+ }
+ }
+}
+
+// --- Profiles ----------------------------------------------------------------
+#ifdef HAVE_LCMS2
+
+struct _FivIoProfile {
+ FivIoCmm *cmm;
+ cmsHPROFILE profile;
+};
+
+GBytes *
+fiv_io_profile_to_bytes(FivIoProfile *profile)
+{
+ cmsUInt32Number len = 0;
+ (void) cmsSaveProfileToMem(profile, NULL, &len);
+ gchar *data = g_malloc0(len);
+ if (!cmsSaveProfileToMem(profile, data, &len)) {
+ g_free(data);
+ return NULL;
+ }
+ return g_bytes_new_take(data, len);
+}
+
+static FivIoProfile *
+fiv_io_profile_new(FivIoCmm *cmm, cmsHPROFILE profile)
+{
+ FivIoProfile *self = g_new0(FivIoProfile, 1);
+ self->cmm = g_object_ref(cmm);
+ self->profile = profile;
+ return self;
+}
+
+void
+fiv_io_profile_free(FivIoProfile *self)
+{
+ cmsCloseProfile(self->profile);
+ g_clear_object(&self->cmm);
+ g_free(self);
+}
+
+#else // ! HAVE_LCMS2
+
+GBytes *fiv_io_profile_to_bytes(FivIoProfile *) { return NULL; }
+void fiv_io_profile_free(FivIoProfile *) {}
+
+#endif // ! HAVE_LCMS2
+// --- Contexts ----------------------------------------------------------------
+#ifdef HAVE_LCMS2
+
+struct _FivIoCmm {
+ GObject parent_instance;
+ cmsContext context;
+
+ // https://github.com/mm2/Little-CMS/issues/430
+ gboolean broken_premul;
+};
+
+G_DEFINE_TYPE(FivIoCmm, fiv_io_cmm, G_TYPE_OBJECT)
+
+static void
+fiv_io_cmm_finalize(GObject *gobject)
+{
+ FivIoCmm *self = FIV_IO_CMM(gobject);
+ cmsDeleteContext(self->context);
+
+ G_OBJECT_CLASS(fiv_io_cmm_parent_class)->finalize(gobject);
+}
+
+static void
+fiv_io_cmm_class_init(FivIoCmmClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ object_class->finalize = fiv_io_cmm_finalize;
+}
+
+static void
+fiv_io_cmm_init(FivIoCmm *self)
+{
+ self->context = cmsCreateContext(NULL, self);
+#ifdef HAVE_LCMS2_FAST_FLOAT
+ if (cmsPluginTHR(self->context, cmsFastFloatExtensions()))
+ self->broken_premul = LCMS_VERSION <= 2160;
+#endif // HAVE_LCMS2_FAST_FLOAT
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+FivIoCmm *
+fiv_io_cmm_get_default(void)
+{
+ static gsize initialization_value = 0;
+ static FivIoCmm *default_ = NULL;
+ if (g_once_init_enter(&initialization_value)) {
+ gsize setup_value = 1;
+ default_ = g_object_new(FIV_TYPE_IO_CMM, NULL);
+ g_once_init_leave(&initialization_value, setup_value);
+ }
+ return default_;
+}
+
+FivIoProfile *
+fiv_io_cmm_get_profile(FivIoCmm *self, const void *data, size_t len)
+{
+ g_return_val_if_fail(self != NULL, NULL);
+
+ return fiv_io_profile_new(self,
+ cmsOpenProfileFromMemTHR(self->context, data, len));
+}
+
+FivIoProfile *
+fiv_io_cmm_get_profile_sRGB(FivIoCmm *self)
+{
+ g_return_val_if_fail(self != NULL, NULL);
+
+ return fiv_io_profile_new(self,
+ cmsCreate_sRGBProfileTHR(self->context));
+}
+
+FivIoProfile *
+fiv_io_cmm_get_profile_parametric(FivIoCmm *self,
+ double gamma, double whitepoint[2], double primaries[6])
+{
+ g_return_val_if_fail(self != NULL, NULL);
+
+ const cmsCIExyY cmsWP = {whitepoint[0], whitepoint[1], 1.0};
+ const cmsCIExyYTRIPLE cmsP = {
+ {primaries[0], primaries[1], 1.0},
+ {primaries[2], primaries[3], 1.0},
+ {primaries[4], primaries[5], 1.0},
+ };
+
+ cmsToneCurve *curve = cmsBuildGamma(self->context, gamma);
+ if (!curve)
+ return NULL;
+
+ cmsHPROFILE profile = cmsCreateRGBProfileTHR(self->context,
+ &cmsWP, &cmsP, (cmsToneCurve *[3]){curve, curve, curve});
+ cmsFreeToneCurve(curve);
+ return fiv_io_profile_new(self, profile);
+}
+
+#else // ! HAVE_LCMS2
+
+FivIoCmm *
+fiv_io_cmm_get_default()
+{
+ return NULL;
+}
+
+FivIoProfile *
+fiv_io_cmm_get_profile(FivIoCmm *, const void *, size_t)
+{
+ return NULL;
+}
+
+FivIoProfile *
+fiv_io_cmm_get_profile_sRGB(FivIoCmm *)
+{
+ return NULL;
+}
+
+FivIoProfile *
+fiv_io_cmm_get_profile_parametric(FivIoCmm *, double, double[2], double[6])
+{
+ return NULL;
+}
+
+#endif // ! HAVE_LCMS2
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+FivIoProfile *
+fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma)
+{
+ return fiv_io_cmm_get_profile_parametric(self, gamma,
+ (double[2]){0.3127, 0.3290},
+ (double[6]){0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600});
+}
+
+FivIoProfile *
+fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes)
+{
+ gsize len = 0;
+ gconstpointer p = g_bytes_get_data(bytes, &len);
+ return fiv_io_cmm_get_profile(self, p, len);
+}
+
+// --- Image loading -----------------------------------------------------------
+#ifdef HAVE_LCMS2
+
+// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
+#define FIV_IO_PROFILE_ARGB32 \
+ (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
+#define FIV_IO_PROFILE_4X16LE \
+ (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
+
+void
+fiv_io_cmm_cmyk(FivIoCmm *self,
+ FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
+{
+ g_return_if_fail(target == NULL || self != NULL);
+
+ cmsHTRANSFORM transform = NULL;
+ if (source && target) {
+ transform = cmsCreateTransformTHR(self->context,
+ source->profile, TYPE_CMYK_8_REV,
+ target->profile, FIV_IO_PROFILE_ARGB32, INTENT_PERCEPTUAL, 0);
+ }
+ if (transform) {
+ cmsDoTransform(
+ transform, image->data, image->data, image->width * image->height);
+ cmsDeleteTransform(transform);
+ return;
+ }
+ trivial_cmyk_to_host_byte_order_argb(
+ image->data, image->width * image->height);
+}
+
+static bool
+fiv_io_cmm_rgb_direct(FivIoCmm *self, unsigned char *data, int w, int h,
+ FivIoProfile *source, FivIoProfile *target,
+ uint32_t source_format, uint32_t target_format)
+{
+ g_return_val_if_fail(target == NULL || self != NULL, false);
+
+ // TODO(p): We should make this optional.
+ FivIoProfile *src_fallback = NULL;
+ if (target && !source)
+ source = src_fallback = fiv_io_cmm_get_profile_sRGB(self);
+
+ cmsHTRANSFORM transform = NULL;
+ if (source && target) {
+ transform = cmsCreateTransformTHR(self->context,
+ source->profile, source_format,
+ target->profile, target_format, INTENT_PERCEPTUAL, 0);
+ }
+ if (transform) {
+ cmsDoTransform(transform, data, data, w * h);
+ cmsDeleteTransform(transform);
+ }
+ if (src_fallback)
+ fiv_io_profile_free(src_fallback);
+ return transform != NULL;
+}
+
+static void
+fiv_io_cmm_xrgb32(FivIoCmm *self,
+ FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
+{
+ fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
+ source, target, FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32);
+}
+
+void
+fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
+ int w, int h, FivIoProfile *source, FivIoProfile *target)
+{
+ fiv_io_cmm_rgb_direct(self, data, w, h, source, target,
+ FIV_IO_PROFILE_4X16LE, FIV_IO_PROFILE_4X16LE);
+}
+
+#else // ! HAVE_LCMS2
+
+void
+fiv_io_cmm_cmyk(FivIoCmm *, FivIoImage *image, FivIoProfile *, FivIoProfile *)
+{
+ trivial_cmyk_to_host_byte_order_argb(
+ image->data, image->width * image->height);
+}
+
+static void
+fiv_io_cmm_xrgb32(FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *)
+{
+}
+
+void
+fiv_io_cmm_4x16le_direct(
+ FivIoCmm *, unsigned char *, int, int, FivIoProfile *, FivIoProfile *)
+{
+}
+
+#endif // ! HAVE_LCMS2
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#if defined HAVE_LCMS2 && LCMS_VERSION >= 2130
+
+#define FIV_IO_PROFILE_ARGB32_PREMUL \
+ (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8_PREMUL : TYPE_ARGB_8_PREMUL)
+
+static void
+fiv_io_cmm_argb32(FivIoCmm *self, FivIoImage *image,
+ FivIoProfile *source, FivIoProfile *target)
+{
+ g_return_if_fail(image->format == CAIRO_FORMAT_ARGB32);
+
+ // TODO: With self->broken_premul,
+ // this probably also needs to be wrapped in un-premultiplication.
+ fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
+ source, target,
+ FIV_IO_PROFILE_ARGB32_PREMUL, FIV_IO_PROFILE_ARGB32_PREMUL);
+}
+
+void
+fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
+ FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
+{
+ g_return_if_fail(target == NULL || self != NULL);
+
+ if (image->format != CAIRO_FORMAT_ARGB32) {
+ fiv_io_cmm_xrgb32(self, image, source, target);
+ } else if (!target || self->broken_premul) {
+ fiv_io_cmm_xrgb32(self, image, source, target);
+ fiv_io_premultiply_argb32(image);
+ } else if (!fiv_io_cmm_rgb_direct(self, image->data,
+ image->width, image->height, source, target,
+ FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32_PREMUL)) {
+ g_debug("failed to create a premultiplying transform");
+ fiv_io_premultiply_argb32(image);
+ }
+}
+
+#else // ! HAVE_LCMS2 || LCMS_VERSION < 2130
+
+static void
+fiv_io_cmm_argb32(FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *)
+{
+ // TODO(p): Unpremultiply, transform, repremultiply. Or require lcms2>=2.13.
+}
+
+void
+fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
+ FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
+{
+ fiv_io_cmm_xrgb32(self, image, source, target);
+ fiv_io_premultiply_argb32(image);
+}
+
+#endif // ! HAVE_LCMS2 || LCMS_VERSION < 2130
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+void
+fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
+ void (*frame_cb) (FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *))
+{
+ FivIoProfile *source = NULL;
+ if (page->icc)
+ source = fiv_io_cmm_get_profile_from_bytes(self, page->icc);
+
+ // TODO(p): All animations need to be composited in a linear colour space.
+ for (FivIoImage *frame = page; frame != NULL; frame = frame->frame_next)
+ frame_cb(self, frame, source, target);
+
+ if (source)
+ fiv_io_profile_free(source);
+}
+
+void
+fiv_io_cmm_any(FivIoCmm *self,
+ FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
+{
+ // TODO(p): Ensure we do colour management early enough, so that
+ // no avoidable increase of quantization error occurs beforehands,
+ // and also for correct alpha compositing.
+ switch (image->format) {
+ break; case CAIRO_FORMAT_RGB24:
+ fiv_io_cmm_xrgb32(self, image, source, target);
+ break; case CAIRO_FORMAT_ARGB32:
+ fiv_io_cmm_argb32(self, image, source, target);
+ break; default:
+ g_debug("CM attempted on an unsupported surface format");
+ }
+}
+
+// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
+FivIoImage *
+fiv_io_cmm_finish(FivIoCmm *self, FivIoImage *image, FivIoProfile *target)
+{
+ if (!target)
+ return image;
+
+ for (FivIoImage *page = image; page != NULL; page = page->page_next)
+ fiv_io_cmm_page(self, page, target, fiv_io_cmm_any);
+ return image;
+}
diff --git a/fiv-io-model.c b/fiv-io-model.c
new file mode 100644
index 0000000..3309702
--- /dev/null
+++ b/fiv-io-model.c
@@ -0,0 +1,742 @@
+//
+// fiv-io-model.c: filesystem
+//
+// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
+//
+// 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 "fiv-io.h"
+#include "fiv-io-model.h"
+#include "xdg.h"
+
+GType
+fiv_io_model_sort_get_type(void)
+{
+ static gsize guard;
+ if (g_once_init_enter(&guard)) {
+#define XX(name) {FIV_IO_MODEL_SORT_ ## name, \
+ "FIV_IO_MODEL_SORT_" #name, #name},
+ static const GEnumValue values[] = {FIV_IO_MODEL_SORTS(XX) {}};
+#undef XX
+ GType type = g_enum_register_static(
+ g_intern_static_string("FivIoModelSort"), values);
+ g_once_init_leave(&guard, type);
+ }
+ return guard;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+G_DEFINE_BOXED_TYPE(FivIoModelEntry, fiv_io_model_entry,
+ fiv_io_model_entry_ref, fiv_io_model_entry_unref)
+
+FivIoModelEntry *
+fiv_io_model_entry_ref(FivIoModelEntry *self)
+{
+ return g_rc_box_acquire(self);
+}
+
+void
+fiv_io_model_entry_unref(FivIoModelEntry *self)
+{
+ g_rc_box_release(self);
+}
+
+static size_t
+entry_strsize(const char *string)
+{
+ if (!string)
+ return 0;
+
+ return strlen(string) + 1;
+}
+
+static char *
+entry_strappend(char **p, const char *string, size_t size)
+{
+ if (!string)
+ return NULL;
+
+ char *destination = memcpy(*p, string, size);
+ *p += size;
+ return destination;
+}
+
+// See model_load_attributes for a (superset of a) list of required attributes.
+static FivIoModelEntry *
+entry_new(GFile *file, GFileInfo *info)
+{
+ gchar *uri = g_file_get_uri(file);
+ const gchar *target_uri = g_file_info_get_attribute_string(
+ info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
+ const gchar *display_name = g_file_info_get_display_name(info);
+
+ // TODO(p): Make it possible to use g_utf8_collate_key() instead,
+ // which does not use natural sorting.
+ gchar *parse_name = g_file_get_parse_name(file);
+ gchar *collate_key = g_utf8_collate_key_for_filename(parse_name, -1);
+ g_free(parse_name);
+
+ // The entries are immutable. Packing them into the structure
+ // should help memory usage as well as performance.
+ size_t size_uri = entry_strsize(uri);
+ size_t size_target_uri = entry_strsize(target_uri);
+ size_t size_display_name = entry_strsize(display_name);
+ size_t size_collate_key = entry_strsize(collate_key);
+
+ FivIoModelEntry *entry = g_rc_box_alloc0(sizeof *entry +
+ size_uri +
+ size_target_uri +
+ size_display_name +
+ size_collate_key);
+
+ gchar *p = (gchar *) entry + sizeof *entry;
+ entry->uri = entry_strappend(&p, uri, size_uri);
+ entry->target_uri = entry_strappend(&p, target_uri, size_target_uri);
+ entry->display_name = entry_strappend(&p, display_name, size_display_name);
+ entry->collate_key = entry_strappend(&p, collate_key, size_collate_key);
+
+ entry->filesize = (guint64) g_file_info_get_size(info);
+
+ GDateTime *mtime = g_file_info_get_modification_date_time(info);
+ if (mtime) {
+ entry->mtime_msec = g_date_time_to_unix(mtime) * 1000 +
+ g_date_time_get_microsecond(mtime) / 1000;
+ g_date_time_unref(mtime);
+ }
+
+ g_free(uri);
+ g_free(collate_key);
+ return entry;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct _FivIoModel {
+ GObject parent_instance;
+ GPatternSpec **supported_patterns;
+
+ GFile *directory; ///< Currently loaded directory
+ GFileMonitor *monitor; ///< "directory" monitoring
+ GPtrArray *subdirs; ///< "directory" contents
+ GPtrArray *files; ///< "directory" contents
+
+ FivIoModelSort sort_field; ///< How to sort
+ gboolean sort_descending; ///< Whether to sort in reverse
+ gboolean filtering; ///< Only show non-hidden, supported
+};
+
+G_DEFINE_TYPE(FivIoModel, fiv_io_model, G_TYPE_OBJECT)
+
+enum {
+ PROP_FILTERING = 1,
+ PROP_SORT_FIELD,
+ PROP_SORT_DESCENDING,
+ N_PROPERTIES
+};
+
+static GParamSpec *model_properties[N_PROPERTIES];
+
+enum {
+ RELOADED,
+ FILES_CHANGED,
+ SUBDIRECTORIES_CHANGED,
+ LAST_SIGNAL,
+};
+
+// Globals are, sadly, the canonical way of storing signal numbers.
+static guint model_signals[LAST_SIGNAL];
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static GPtrArray *
+model_entry_array_new(void)
+{
+ return g_ptr_array_new_with_free_func(
+ (GDestroyNotify) fiv_io_model_entry_unref);
+}
+
+#if !GLIB_CHECK_VERSION(2, 70, 0)
+#define g_pattern_spec_match g_pattern_match
+#endif
+
+static gboolean
+model_supports(FivIoModel *self, const char *filename)
+{
+ gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
+ if (!utf8)
+ return FALSE;
+
+ gchar *lc = g_utf8_strdown(utf8, -1);
+ gsize lc_length = strlen(lc);
+ gchar *reversed = g_utf8_strreverse(lc, lc_length);
+ g_free(utf8);
+
+ // fnmatch() uses the /locale encoding/, and isn't present on Windows.
+ // TODO(p): Consider using g_file_info_get_display_name() for direct UTF-8.
+ gboolean result = FALSE;
+ for (GPatternSpec **p = self->supported_patterns; *p; p++)
+ if ((result = g_pattern_spec_match(*p, lc_length, lc, reversed)))
+ break;
+
+ g_free(lc);
+ g_free(reversed);
+ return result;
+}
+
+static inline int
+model_compare_entries(FivIoModel *self,
+ const FivIoModelEntry *entry1, GFile *file1,
+ const FivIoModelEntry *entry2, GFile *file2)
+{
+ if (g_file_has_prefix(file1, file2))
+ return +1;
+ if (g_file_has_prefix(file2, file1))
+ return -1;
+
+ int result = 0;
+ switch (self->sort_field) {
+ case FIV_IO_MODEL_SORT_MTIME:
+ result -= entry1->mtime_msec < entry2->mtime_msec;
+ result += entry1->mtime_msec > entry2->mtime_msec;
+ if (result != 0)
+ break;
+
+ // Fall-through
+ case FIV_IO_MODEL_SORT_NAME:
+ case FIV_IO_MODEL_SORT_COUNT:
+ result = strcmp(entry1->collate_key, entry2->collate_key);
+ }
+ return self->sort_descending ? -result : +result;
+}
+
+static gint
+model_compare(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+ const FivIoModelEntry *entry1 = *(const FivIoModelEntry **) a;
+ const FivIoModelEntry *entry2 = *(const FivIoModelEntry **) b;
+ GFile *file1 = g_file_new_for_uri(entry1->uri);
+ GFile *file2 = g_file_new_for_uri(entry2->uri);
+ int result = model_compare_entries(user_data, entry1, file1, entry2, file2);
+ g_object_unref(file1);
+ g_object_unref(file2);
+ return result;
+}
+
+static const char *model_load_attributes =
+ G_FILE_ATTRIBUTE_STANDARD_TYPE ","
+ G_FILE_ATTRIBUTE_STANDARD_NAME ","
+ G_FILE_ATTRIBUTE_STANDARD_SIZE ","
+ G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
+ G_FILE_ATTRIBUTE_STANDARD_TARGET_URI ","
+ G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
+ G_FILE_ATTRIBUTE_TIME_MODIFIED ","
+ G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC;
+
+static GPtrArray *
+model_decide_placement(
+ FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files)
+{
+ if (self->filtering &&
+ g_file_info_has_attribute(info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
+ g_file_info_get_is_hidden(info))
+ return NULL;
+ if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
+ return subdirs;
+ if (!self->filtering ||
+ model_supports(self, g_file_info_get_name(info)))
+ return files;
+ return NULL;
+}
+
+static gboolean
+model_reload_to(FivIoModel *self, GFile *directory,
+ GPtrArray *subdirs, GPtrArray *files, GError **error)
+{
+ if (subdirs)
+ g_ptr_array_set_size(subdirs, 0);
+ if (files)
+ g_ptr_array_set_size(files, 0);
+
+ GFileEnumerator *enumerator = g_file_enumerate_children(
+ directory, model_load_attributes, G_FILE_QUERY_INFO_NONE, NULL, error);
+ if (!enumerator)
+ return FALSE;
+
+ GFileInfo *info = NULL;
+ GFile *child = NULL;
+ GError *e = NULL;
+ while (TRUE) {
+ if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, &e) &&
+ e) {
+ g_warning("%s", e->message);
+ g_clear_error(&e);
+ continue;
+ }
+ if (!info)
+ break;
+
+ GPtrArray *target =
+ model_decide_placement(self, info, subdirs, files);
+ if (target)
+ g_ptr_array_add(target, entry_new(child, info));
+ }
+ g_object_unref(enumerator);
+
+ if (subdirs)
+ g_ptr_array_sort_with_data(subdirs, model_compare, self);
+ if (files)
+ g_ptr_array_sort_with_data(files, model_compare, self);
+ return TRUE;
+}
+
+static gboolean
+model_reload(FivIoModel *self, GError **error)
+{
+ // Note that this will clear all entries on failure.
+ gboolean result = model_reload_to(
+ self, self->directory, self->subdirs, self->files, error);
+ g_signal_emit(self, model_signals[RELOADED], 0);
+ return result;
+}
+
+static void
+model_resort(FivIoModel *self)
+{
+ g_ptr_array_sort_with_data(self->subdirs, model_compare, self);
+ g_ptr_array_sort_with_data(self->files, model_compare, self);
+ g_signal_emit(self, model_signals[RELOADED], 0);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static gint
+model_find(const GPtrArray *target, GFile *file, FivIoModelEntry **entry)
+{
+ for (guint i = 0; i < target->len; i++) {
+ FivIoModelEntry *e = target->pdata[i];
+ GFile *f = g_file_new_for_uri(e->uri);
+ gboolean match = g_file_equal(f, file);
+ g_object_unref(f);
+ if (match) {
+ *entry = e;
+ return i;
+ }
+ }
+ return -1;
+}
+
+enum monitor_event {
+ MONITOR_NONE,
+ MONITOR_CHANGING,
+ MONITOR_RENAMING,
+ MONITOR_REMOVING,
+ MONITOR_ADDING
+};
+
+static void
+monitor_apply(enum monitor_event event, GPtrArray *target, int index,
+ FivIoModelEntry *new_entry)
+{
+ g_return_if_fail(event != MONITOR_CHANGING || index >= 0);
+
+ if (event == MONITOR_RENAMING && index < 0)
+ // The file used to be filtered out but isn't anymore.
+ event = MONITOR_ADDING;
+ else if (!new_entry && index >= 0)
+ // The file wasn't filtered out but now it is.
+ event = MONITOR_REMOVING;
+
+ if (event == MONITOR_CHANGING) {
+ fiv_io_model_entry_unref(target->pdata[index]);
+ target->pdata[index] = fiv_io_model_entry_ref(new_entry);
+ }
+ if (event == MONITOR_REMOVING || event == MONITOR_RENAMING)
+ g_ptr_array_remove_index(target, index);
+ if (event == MONITOR_RENAMING || event == MONITOR_ADDING)
+ g_ptr_array_add(target, fiv_io_model_entry_ref(new_entry));
+}
+
+static void
+on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
+ GFile *other_file, GFileMonitorEvent event_type, gpointer user_data)
+{
+ FivIoModel *self = user_data;
+
+ FivIoModelEntry *old_entry = NULL;
+ gint files_index = model_find(self->files, file, &old_entry);
+ gint subdirs_index = model_find(self->subdirs, file, &old_entry);
+
+ enum monitor_event event = MONITOR_NONE;
+ GFile *new_entry_file = NULL;
+ switch (event_type) {
+ case G_FILE_MONITOR_EVENT_CHANGED:
+ case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
+ event = MONITOR_CHANGING;
+ new_entry_file = file;
+ break;
+ case G_FILE_MONITOR_EVENT_RENAMED:
+ event = MONITOR_RENAMING;
+ new_entry_file = other_file;
+ break;
+ case G_FILE_MONITOR_EVENT_DELETED:
+ case G_FILE_MONITOR_EVENT_MOVED_OUT:
+ event = MONITOR_REMOVING;
+ break;
+ case G_FILE_MONITOR_EVENT_CREATED:
+ case G_FILE_MONITOR_EVENT_MOVED_IN:
+ event = MONITOR_ADDING;
+ old_entry = NULL;
+ new_entry_file = file;
+ break;
+
+ case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
+ // TODO(p): Figure out if we can't make use of _CHANGES_DONE_HINT.
+ case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
+ case G_FILE_MONITOR_EVENT_UNMOUNTED:
+ // TODO(p): Figure out how to handle _UNMOUNTED sensibly.
+ case G_FILE_MONITOR_EVENT_MOVED:
+ return;
+ }
+
+ FivIoModelEntry *new_entry = NULL;
+ GPtrArray *new_target = NULL;
+ if (new_entry_file) {
+ GError *error = NULL;
+ GFileInfo *info = g_file_query_info(new_entry_file,
+ model_load_attributes, G_FILE_QUERY_INFO_NONE, NULL, &error);
+ if (error) {
+ g_debug("monitor: %s", error->message);
+ g_error_free(error);
+ goto run;
+ }
+
+ if ((new_target =
+ model_decide_placement(self, info, self->subdirs, self->files)))
+ new_entry = entry_new(new_entry_file, info);
+ g_object_unref(info);
+
+ if ((files_index != -1 && new_target == self->subdirs) ||
+ (subdirs_index != -1 && new_target == self->files)) {
+ g_debug("monitor: ignoring transfer between files and subdirs");
+ goto out;
+ }
+ }
+
+run:
+ // Keep a reference alive so that signal handlers see the new arrays.
+ if (old_entry)
+ fiv_io_model_entry_ref(old_entry);
+
+ if (files_index != -1 || new_target == self->files) {
+ monitor_apply(event, self->files, files_index, new_entry);
+ g_signal_emit(self, model_signals[FILES_CHANGED],
+ 0, old_entry, new_entry);
+ }
+ if (subdirs_index != -1 || new_target == self->subdirs) {
+ monitor_apply(event, self->subdirs, subdirs_index, new_entry);
+ g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED],
+ 0, old_entry, new_entry);
+ }
+
+ // NOTE: It would make sense to do
+ // g_ptr_array_sort_with_data(self->{files,subdirs}, model_compare, self);
+ // but then the iteration behaviour of fiv.c would differ from what's shown
+ // in the browser. Perhaps we need to use an index-based, fully-synchronized
+ // interface similar to GListModel::items-changed.
+
+ if (old_entry)
+ fiv_io_model_entry_unref(old_entry);
+out:
+ if (new_entry)
+ fiv_io_model_entry_unref(new_entry);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// This would be more efficient iteratively, but it's not that important.
+static GFile *
+model_last_deep_subdirectory(FivIoModel *self, GFile *directory)
+{
+ GFile *result = NULL;
+ GPtrArray *subdirs = model_entry_array_new();
+ if (!model_reload_to(self, directory, subdirs, NULL, NULL))
+ goto out;
+
+ if (subdirs->len) {
+ FivIoModelEntry *entry = g_ptr_array_index(subdirs, subdirs->len - 1);
+ GFile *last = g_file_new_for_uri(entry->uri);
+ result = model_last_deep_subdirectory(self, last);
+ g_object_unref(last);
+ } else {
+ result = g_object_ref(directory);
+ }
+
+out:
+ g_ptr_array_free(subdirs, TRUE);
+ return result;
+}
+
+GFile *
+fiv_io_model_get_previous_directory(FivIoModel *self)
+{
+ g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
+
+ GFile *parent_directory = g_file_get_parent(self->directory);
+ if (!parent_directory)
+ return NULL;
+
+ GFile *result = NULL;
+ GPtrArray *subdirs = model_entry_array_new();
+ if (!model_reload_to(self, parent_directory, subdirs, NULL, NULL))
+ goto out;
+
+ for (gsize i = 0; i < subdirs->len; i++) {
+ FivIoModelEntry *entry = g_ptr_array_index(subdirs, i);
+ GFile *file = g_file_new_for_uri(entry->uri);
+ if (g_file_equal(file, self->directory)) {
+ g_object_unref(file);
+ break;
+ }
+
+ g_clear_object(&result);
+ result = file;
+ }
+ if (result) {
+ GFile *last = model_last_deep_subdirectory(self, result);
+ g_object_unref(result);
+ result = last;
+ } else {
+ result = g_object_ref(parent_directory);
+ }
+
+out:
+ g_object_unref(parent_directory);
+ g_ptr_array_free(subdirs, TRUE);
+ return result;
+}
+
+// This would be more efficient iteratively, but it's not that important.
+static GFile *
+model_next_directory_within_parents(FivIoModel *self, GFile *directory)
+{
+ GFile *parent_directory = g_file_get_parent(directory);
+ if (!parent_directory)
+ return NULL;
+
+ GFile *result = NULL;
+ GPtrArray *subdirs = model_entry_array_new();
+ if (!model_reload_to(self, parent_directory, subdirs, NULL, NULL))
+ goto out;
+
+ gboolean found_self = FALSE;
+ for (gsize i = 0; i < subdirs->len; i++) {
+ FivIoModelEntry *entry = g_ptr_array_index(subdirs, i);
+ result = g_file_new_for_uri(entry->uri);
+ if (found_self)
+ goto out;
+
+ found_self = g_file_equal(result, directory);
+ g_clear_object(&result);
+ }
+ if (!result)
+ result = model_next_directory_within_parents(self, parent_directory);
+
+out:
+ g_object_unref(parent_directory);
+ g_ptr_array_free(subdirs, TRUE);
+ return result;
+}
+
+GFile *
+fiv_io_model_get_next_directory(FivIoModel *self)
+{
+ g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
+
+ if (self->subdirs->len) {
+ FivIoModelEntry *entry = g_ptr_array_index(self->subdirs, 0);
+ return g_file_new_for_uri(entry->uri);
+ }
+
+ return model_next_directory_within_parents(self, self->directory);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+fiv_io_model_finalize(GObject *gobject)
+{
+ FivIoModel *self = FIV_IO_MODEL(gobject);
+ for (GPatternSpec **p = self->supported_patterns; *p; p++)
+ g_pattern_spec_free(*p);
+ g_free(self->supported_patterns);
+
+ g_clear_object(&self->directory);
+ g_clear_object(&self->monitor);
+ g_ptr_array_free(self->subdirs, TRUE);
+ g_ptr_array_free(self->files, TRUE);
+
+ G_OBJECT_CLASS(fiv_io_model_parent_class)->finalize(gobject);
+}
+
+static void
+fiv_io_model_get_property(
+ GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
+{
+ FivIoModel *self = FIV_IO_MODEL(object);
+ switch (property_id) {
+ case PROP_FILTERING:
+ g_value_set_boolean(value, self->filtering);
+ break;
+ case PROP_SORT_FIELD:
+ g_value_set_enum(value, self->sort_field);
+ break;
+ case PROP_SORT_DESCENDING:
+ g_value_set_boolean(value, self->sort_descending);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ }
+}
+
+static void
+fiv_io_model_set_property(
+ GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
+{
+ FivIoModel *self = FIV_IO_MODEL(object);
+ switch (property_id) {
+ case PROP_FILTERING:
+ if (self->filtering != g_value_get_boolean(value)) {
+ self->filtering = !self->filtering;
+ g_object_notify_by_pspec(object, model_properties[property_id]);
+ (void) model_reload(self, NULL /* error */);
+ }
+ break;
+ case PROP_SORT_FIELD:
+ if ((int) self->sort_field != g_value_get_enum(value)) {
+ self->sort_field = g_value_get_enum(value);
+ g_object_notify_by_pspec(object, model_properties[property_id]);
+ model_resort(self);
+ }
+ break;
+ case PROP_SORT_DESCENDING:
+ if (self->sort_descending != g_value_get_boolean(value)) {
+ self->sort_descending = !self->sort_descending;
+ g_object_notify_by_pspec(object, model_properties[property_id]);
+ model_resort(self);
+ }
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ }
+}
+
+static void
+fiv_io_model_class_init(FivIoModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ object_class->get_property = fiv_io_model_get_property;
+ object_class->set_property = fiv_io_model_set_property;
+ object_class->finalize = fiv_io_model_finalize;
+
+ model_properties[PROP_FILTERING] = g_param_spec_boolean(
+ "filtering", "Filtering", "Only show non-hidden, supported entries",
+ TRUE, G_PARAM_READWRITE);
+ model_properties[PROP_SORT_FIELD] = g_param_spec_enum(
+ "sort-field", "Sort field", "Sort order",
+ FIV_TYPE_IO_MODEL_SORT, FIV_IO_MODEL_SORT_NAME, G_PARAM_READWRITE);
+ model_properties[PROP_SORT_DESCENDING] = g_param_spec_boolean(
+ "sort-descending", "Sort descending", "Use reverse sort order",
+ FALSE, G_PARAM_READWRITE);
+ g_object_class_install_properties(
+ object_class, N_PROPERTIES, model_properties);
+
+ // All entries might have changed.
+ model_signals[RELOADED] =
+ g_signal_new("reloaded", G_TYPE_FROM_CLASS(klass), 0, 0,
+ NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+ model_signals[FILES_CHANGED] =
+ g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 2, FIV_TYPE_IO_MODEL_ENTRY, FIV_TYPE_IO_MODEL_ENTRY);
+ model_signals[SUBDIRECTORIES_CHANGED] =
+ g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 2, FIV_TYPE_IO_MODEL_ENTRY, FIV_TYPE_IO_MODEL_ENTRY);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+fiv_io_model_init(FivIoModel *self)
+{
+ self->filtering = TRUE;
+
+ char **types = fiv_io_all_supported_media_types();
+ char **globs = extract_mime_globs((const char **) types);
+ g_strfreev(types);
+
+ gsize n = g_strv_length(globs);
+ self->supported_patterns =
+ g_malloc0_n(n + 1, sizeof *self->supported_patterns);
+ while (n--)
+ self->supported_patterns[n] = g_pattern_spec_new(globs[n]);
+ g_strfreev(globs);
+
+ self->files = model_entry_array_new();
+ self->subdirs = model_entry_array_new();
+}
+
+gboolean
+fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error)
+{
+ g_return_val_if_fail(FIV_IS_IO_MODEL(self), FALSE);
+ g_return_val_if_fail(G_IS_FILE(directory), FALSE);
+
+ g_clear_object(&self->directory);
+ g_clear_object(&self->monitor);
+ self->directory = g_object_ref(directory);
+
+ GError *e = NULL;
+ if ((self->monitor = g_file_monitor_directory(
+ directory, G_FILE_MONITOR_WATCH_MOVES, NULL, &e))) {
+ g_signal_connect(self->monitor, "changed",
+ G_CALLBACK(on_monitor_changed), self);
+ } else {
+ g_debug("directory monitoring failed: %s", e->message);
+ g_error_free(e);
+ }
+ return model_reload(self, error);
+}
+
+GFile *
+fiv_io_model_get_location(FivIoModel *self)
+{
+ g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
+ return self->directory;
+}
+
+FivIoModelEntry *const *
+fiv_io_model_get_files(FivIoModel *self, gsize *len)
+{
+ *len = self->files->len;
+ return (FivIoModelEntry *const *) self->files->pdata;
+}
+
+FivIoModelEntry *const *
+fiv_io_model_get_subdirs(FivIoModel *self, gsize *len)
+{
+ *len = self->subdirs->len;
+ return (FivIoModelEntry *const *) self->subdirs->pdata;
+}
diff --git a/fiv-io-model.h b/fiv-io-model.h
new file mode 100644
index 0000000..c785130
--- /dev/null
+++ b/fiv-io-model.h
@@ -0,0 +1,72 @@
+//
+// fiv-io-model.h: filesystem
+//
+// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
+//
+// 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 <gio/gio.h>
+#include <glib.h>
+
+// Avoid glib-mkenums.
+typedef enum _FivIoModelSort {
+#define FIV_IO_MODEL_SORTS(XX) \
+ XX(NAME) \
+ XX(MTIME)
+#define XX(name) FIV_IO_MODEL_SORT_ ## name,
+ FIV_IO_MODEL_SORTS(XX)
+#undef XX
+ FIV_IO_MODEL_SORT_COUNT
+} FivIoModelSort;
+
+GType fiv_io_model_sort_get_type(void) G_GNUC_CONST;
+#define FIV_TYPE_IO_MODEL_SORT (fiv_io_model_sort_get_type())
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+typedef struct {
+ const char *uri; ///< GIO URI
+ const char *target_uri; ///< GIO URI for any target
+ const char *display_name; ///< Label for the file
+ const char *collate_key; ///< Collate key for the filename
+ guint64 filesize; ///< Filesize in bytes
+ gint64 mtime_msec; ///< Modification time in milliseconds
+} FivIoModelEntry;
+
+GType fiv_io_model_entry_get_type(void) G_GNUC_CONST;
+#define FIV_TYPE_IO_MODEL_ENTRY (fiv_io_model_entry_get_type())
+
+FivIoModelEntry *fiv_io_model_entry_ref(FivIoModelEntry *self);
+void fiv_io_model_entry_unref(FivIoModelEntry *self);
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define FIV_TYPE_IO_MODEL (fiv_io_model_get_type())
+G_DECLARE_FINAL_TYPE(FivIoModel, fiv_io_model, FIV, IO_MODEL, GObject)
+
+/// Loads a directory. Clears itself even on failure.
+gboolean fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error);
+
+/// Returns the current location as a GFile.
+/// There is no ownership transfer, and the object may be NULL.
+GFile *fiv_io_model_get_location(FivIoModel *self);
+
+/// Returns the previous VFS directory in order, or NULL.
+GFile *fiv_io_model_get_previous_directory(FivIoModel *self);
+/// Returns the next VFS directory in order, or NULL.
+GFile *fiv_io_model_get_next_directory(FivIoModel *self);
+
+FivIoModelEntry *const *fiv_io_model_get_files(FivIoModel *self, gsize *len);
+FivIoModelEntry *const *fiv_io_model_get_subdirs(FivIoModel *self, gsize *len);
diff --git a/fiv-io.c b/fiv-io.c
index 547727d..2ea06cf 100644
--- a/fiv-io.c
+++ b/fiv-io.c
@@ -1,7 +1,7 @@
//
// fiv-io.c: image operations
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -19,30 +19,32 @@
#include <errno.h>
#include <math.h>
+#include <setjmp.h>
+#include <stdio.h>
#include <cairo.h>
#include <glib.h>
+#include <jpeglib.h>
#include <turbojpeg.h>
#include <webp/decode.h>
#include <webp/demux.h>
#include <webp/encode.h>
#include <webp/mux.h>
-
#ifdef HAVE_JPEG_QS
-#include <setjmp.h>
-#include <stdio.h>
-
-#include <jpeglib.h>
#include <libjpegqs.h>
#endif // HAVE_JPEG_QS
-// Colour management must be handled before RGB conversions.
-#ifdef HAVE_LCMS2
-#include <lcms2.h>
-#endif // HAVE_LCMS2
+#define TIFF_TABLES_CONSTANTS_ONLY
+#include "tiff-tables.h"
+#include "tiffer.h"
#ifdef HAVE_LIBRAW
#include <libraw.h>
+#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
+#define LIBRAW_OPIONS_NO_MEMERR_CALLBACK 0
+#else
+#define rawparams params
+#endif
#endif // HAVE_LIBRAW
#ifdef HAVE_RESVG
#include <resvg.h>
@@ -77,7 +79,7 @@
#define WUFFS_CONFIG__MODULE__PNG
#define WUFFS_CONFIG__MODULE__TGA
#define WUFFS_CONFIG__MODULE__ZLIB
-#include "wuffs-mirror-release-c/release/c/wuffs-v0.3.c"
+#include "submodules/wuffs-mirror-release-c/release/c/wuffs-v0.3.c"
#include "fiv-io.h"
@@ -118,23 +120,28 @@ const char *fiv_io_supported_media_types[] = {
gchar **
fiv_io_all_supported_media_types(void)
{
+ GHashTable *unique =
+ g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
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));
+ if (g_hash_table_insert(unique, g_strdup(*p), NULL))
+ 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);
+ if (g_hash_table_insert(unique, *p, NULL))
+ g_ptr_array_add(types, g_strdup(*p));
g_free(subtypes);
}
g_slist_free(formats);
#endif // HAVE_GDKPIXBUF
+ g_hash_table_unref(unique);
g_ptr_array_add(types, NULL);
- return (char **) g_ptr_array_free(types, FALSE);
+ return (gchar **) g_ptr_array_free(types, FALSE);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -168,305 +175,117 @@ add_warning(const FivIoOpenContext *ctx, const char *format, ...)
va_end(ap);
}
-static bool
-try_append_page(cairo_surface_t *surface, cairo_surface_t **result,
- cairo_surface_t **result_tail)
-{
- if (!surface)
- return false;
+// --- Images ------------------------------------------------------------------
- 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;
+FivIoImage *
+fiv_io_image_new(cairo_format_t format, uint32_t width, uint32_t height)
+{
+ // CAIRO_STRIDE_ALIGNMENT is 4 bytes, we only use multiples.
+ size_t unit = 0;
+ switch (format) {
+ case CAIRO_FORMAT_RGB24:
+ case CAIRO_FORMAT_RGB30:
+ case CAIRO_FORMAT_ARGB32:
+ unit = 4;
+ break;
+#if CAIRO_VERSION >= 11702
+ case CAIRO_FORMAT_RGB96F:
+ unit = 12;
+ break;
+ case CAIRO_FORMAT_RGBA128F:
+ unit = 16;
+ break;
+#endif
+ default:
+ return NULL;
}
- return true;
-}
-// --- Colour management -------------------------------------------------------
+ uint8_t *data = g_try_malloc0(unit * width * height);
+ if (!data)
+ return NULL;
-FivIoProfile
-fiv_io_profile_new(const void *data, size_t len)
-{
-#ifdef HAVE_LCMS2
- return cmsOpenProfileFromMem(data, len);
-#else
- (void) data;
- (void) len;
- return NULL;
-#endif
+ FivIoImage *image = g_rc_box_new0(FivIoImage);
+ image->data = data;
+ image->format = format;
+ image->width = width;
+ image->stride = width * unit;
+ image->height = height;
+ return image;
}
-FivIoProfile
-fiv_io_profile_new_sRGB(void)
+FivIoImage *
+fiv_io_image_ref(FivIoImage *self)
{
-#ifdef HAVE_LCMS2
- return cmsCreate_sRGBProfile();
-#else
- return NULL;
-#endif
+ return g_rc_box_acquire(self);
}
-FivIoProfile
-fiv_io_profile_new_sRGB_gamma(double gamma)
+static void
+fiv_io_image_finalize(FivIoImage *image)
{
-#ifdef HAVE_LCMS2
- // TODO(p): Make sure to use the library in a thread-safe manner.
- cmsContext context = NULL;
+ g_free(image->data);
- static const cmsCIExyY D65 = {0.3127, 0.3290, 1.0};
- static const cmsCIExyYTRIPLE primaries = {
- {0.6400, 0.3300, 1.0}, {0.3000, 0.6000, 1.0}, {0.1500, 0.0600, 1.0}};
- cmsToneCurve *curve = cmsBuildGamma(context, gamma);
- if (!curve)
- return NULL;
+ g_bytes_unref(image->exif);
+ g_bytes_unref(image->icc);
+ g_bytes_unref(image->xmp);
+ g_bytes_unref(image->thum);
- cmsHPROFILE profile = cmsCreateRGBProfileTHR(
- context, &D65, &primaries, (cmsToneCurve *[3]){curve, curve, curve});
- cmsFreeToneCurve(curve);
- return profile;
-#else
- (void) gamma;
- return NULL;
-#endif
-}
+ if (image->text)
+ g_hash_table_unref(image->text);
-static FivIoProfile
-fiv_io_profile_new_from_bytes(GBytes *bytes)
-{
- gsize len = 0;
- gconstpointer p = g_bytes_get_data(bytes, &len);
- return fiv_io_profile_new(p, len);
-}
+ if (image->render)
+ image->render->destroy(image->render);
-static GBytes *
-fiv_io_profile_to_bytes(FivIoProfile profile)
-{
-#ifdef HAVE_LCMS2
- cmsUInt32Number len = 0;
- (void) cmsSaveProfileToMem(profile, NULL, &len);
- gchar *data = g_malloc0(len);
- if (!cmsSaveProfileToMem(profile, data, &len)) {
- g_free(data);
- return NULL;
- }
- return g_bytes_new_take(data, len);
-#else
- (void) profile;
- return NULL;
-#endif
+ if (image->page_next)
+ fiv_io_image_unref(image->page_next);
+ if (image->frame_next)
+ fiv_io_image_unref(image->frame_next);
}
void
-fiv_io_profile_free(FivIoProfile self)
+fiv_io_image_unref(FivIoImage *self)
{
-#ifdef HAVE_LCMS2
- cmsCloseProfile(self);
-#else
- (void) self;
-#endif
+ g_rc_box_release_full(self, (GDestroyNotify) fiv_io_image_finalize);
}
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
-#define FIV_IO_LCMS2_ARGB32 \
- (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
-#define FIV_IO_LCMS2_4X16LE \
- (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
-
-// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
-// ARGB/BGRA/XRGB/BGRX.
-static void
-trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len)
-{
- // This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms.
- // It will typically produce horribly oversaturated results.
- // 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
-fiv_io_profile_cmyk(
- cairo_surface_t *surface, FivIoProfile source, FivIoProfile target)
+cairo_surface_t *
+fiv_io_image_to_surface_noref(const FivIoImage *image)
{
- unsigned char *data = cairo_image_surface_get_data(surface);
- int w = cairo_image_surface_get_width(surface);
- int h = cairo_image_surface_get_height(surface);
-
-#ifndef HAVE_LCMS2
- (void) source;
- (void) target;
-#else
- cmsHTRANSFORM transform = NULL;
- if (source && target) {
- transform = cmsCreateTransform(source, TYPE_CMYK_8_REV, target,
- FIV_IO_LCMS2_ARGB32, INTENT_PERCEPTUAL, 0);
- }
- if (transform) {
- cmsDoTransform(transform, data, data, w * h);
- cmsDeleteTransform(transform);
- return;
- }
-#endif
- trivial_cmyk_to_host_byte_order_argb(data, w * h);
+ return cairo_image_surface_create_for_data(
+ image->data, image->format, image->width, image->height, image->stride);
}
-static void
-fiv_io_profile_xrgb32_direct(unsigned char *data, int w, int h,
- FivIoProfile source, FivIoProfile target)
-{
-#ifndef HAVE_LCMS2
- (void) data;
- (void) w;
- (void) h;
- (void) source;
- (void) target;
-#else
- // TODO(p): We should make this optional.
- cmsHPROFILE src_fallback = NULL;
- if (target && !source)
- source = src_fallback = cmsCreate_sRGBProfile();
-
- cmsHTRANSFORM transform = NULL;
- if (source && target) {
- transform = cmsCreateTransform(source, FIV_IO_LCMS2_ARGB32, target,
- FIV_IO_LCMS2_ARGB32, INTENT_PERCEPTUAL, 0);
- }
- if (transform) {
- cmsDoTransform(transform, data, data, w * h);
- cmsDeleteTransform(transform);
- }
- if (src_fallback)
- cmsCloseProfile(src_fallback);
-#endif
-}
-
-static void
-fiv_io_profile_xrgb32(
- cairo_surface_t *surface, FivIoProfile source, FivIoProfile target)
+cairo_surface_t *
+fiv_io_image_to_surface(FivIoImage *image)
{
- unsigned char *data = cairo_image_surface_get_data(surface);
- int w = cairo_image_surface_get_width(surface);
- int h = cairo_image_surface_get_height(surface);
- fiv_io_profile_xrgb32_direct(data, w, h, source, target);
-}
+ // TODO(p): Remove this shortcut eventually. And the function.
+ if (!image)
+ return NULL;
-static void
-fiv_io_profile_4x16le_direct(
- unsigned char *data, int w, int h, FivIoProfile source, FivIoProfile target)
-{
-#ifndef HAVE_LCMS2
- (void) data;
- (void) w;
- (void) h;
- (void) source;
- (void) target;
-#else
- // TODO(p): We should make this optional.
- cmsHPROFILE src_fallback = NULL;
- if (target && !source)
- source = src_fallback = cmsCreate_sRGBProfile();
-
- cmsHTRANSFORM transform = NULL;
- if (source && target) {
- transform = cmsCreateTransform(source, FIV_IO_LCMS2_4X16LE, target,
- FIV_IO_LCMS2_4X16LE, INTENT_PERCEPTUAL, 0);
- }
- if (transform) {
- cmsDoTransform(transform, data, data, w * h);
- cmsDeleteTransform(transform);
- }
- if (src_fallback)
- cmsCloseProfile(src_fallback);
-#endif
+ static cairo_user_data_key_t key_image;
+ cairo_surface_t *surface = cairo_image_surface_create_for_data(
+ image->data, image->format, image->width, image->height, image->stride);
+ cairo_surface_set_user_data(surface, &key_image,
+ image, (cairo_destroy_func_t) fiv_io_image_unref);
+ return surface;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static void
-fiv_io_profile_xrgb32_page(cairo_surface_t *page, FivIoProfile target)
-{
- GBytes *bytes = NULL;
- FivIoProfile source = NULL;
- if ((bytes = cairo_surface_get_user_data(page, &fiv_io_key_icc)))
- source = fiv_io_profile_new_from_bytes(bytes);
-
- // TODO(p): All animations need to be composited in a linear colour space.
- for (cairo_surface_t *frame = page; frame != NULL;
- frame = cairo_surface_get_user_data(frame, &fiv_io_key_frame_next))
- fiv_io_profile_xrgb32(frame, source, target);
-
- if (source)
- fiv_io_profile_free(source);
-}
-
-// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
-static cairo_surface_t *
-fiv_io_profile_finalize(cairo_surface_t *image, FivIoProfile target)
-{
- if (!image || !target)
- return image;
-
- for (cairo_surface_t *page = image; page != NULL;
- page = cairo_surface_get_user_data(page, &fiv_io_key_page_next)) {
- // TODO(p): 1. un/premultiply ARGB, 2. do colour management
- // early enough, so that no avoidable increase of quantization error
- // occurs beforehands, and also for correct alpha compositing.
- // FIXME: This assumes that if the first frame is opaque, they all are.
- if (cairo_image_surface_get_format(page) == CAIRO_FORMAT_RGB24)
- fiv_io_profile_xrgb32_page(page, target);
- }
- return image;
-}
-
-static void
-fiv_io_premultiply_argb32(cairo_surface_t *surface)
+static bool
+try_append_page(
+ FivIoImage *image, FivIoImage **result, FivIoImage **result_tail)
{
- int w = cairo_image_surface_get_width(surface);
- int h = cairo_image_surface_get_height(surface);
- unsigned char *data = cairo_image_surface_get_data(surface);
- int stride = cairo_image_surface_get_stride(surface);
- if (cairo_image_surface_get_format(surface) != CAIRO_FORMAT_ARGB32)
- return;
+ if (!image)
+ return false;
- for (int y = 0; y < h; y++) {
- uint32_t *dstp = (uint32_t *) (data + 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);
- }
+ if (*result) {
+ (*result_tail)->page_next = image;
+ image->page_previous = *result_tail;
+ *result_tail = image;
+ } else {
+ *result = *result_tail = image;
}
-}
-
-static void
-fiv_io_premultiply_argb32_page(cairo_surface_t *page)
-{
- for (cairo_surface_t *frame = page; frame != NULL;
- frame = cairo_surface_get_user_data(frame, &fiv_io_key_frame_next))
- fiv_io_premultiply_argb32(frame);
+ return true;
}
// --- Wuffs -------------------------------------------------------------------
@@ -562,11 +381,12 @@ struct load_wuffs_frame_context {
GBytes *meta_iccp; ///< Reference-counted ICC profile
GBytes *meta_xmp; ///< Reference-counted XMP
- FivIoProfile target; ///< Target device profile, if any
- FivIoProfile source; ///< Source colour profile, if any
+ FivIoCmm *cmm; ///< CMM context, if any
+ FivIoProfile *target; ///< Target device profile, if any
+ FivIoProfile *source; ///< Source colour profile, if any
- cairo_surface_t *result; ///< The resulting surface (referenced)
- cairo_surface_t *result_tail; ///< The final animation frame
+ FivIoImage *result; ///< The resulting image (referenced)
+ FivIoImage *result_tail; ///< The final animation frame
};
static bool
@@ -593,66 +413,58 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
decode_format = CAIRO_FORMAT_ARGB32;
unsigned char *targetbuf = NULL;
- cairo_surface_t *surface =
- cairo_image_surface_create(decode_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));
+ FivIoImage *image =
+ fiv_io_image_new(decode_format, ctx->width, ctx->height);
+ if (!image) {
+ set_error(error, "image allocation failure");
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);
+ // There is no padding with ARGB/BGR/XRGB/BGRX.
+ // This function does not support a stride different from the width,
+ // maybe Wuffs internals do not either.
wuffs_base__pixel_buffer pb = {0};
if (ctx->expand_16_float || ctx->pack_16_10) {
- uint32_t targetbuf_size = ctx->height * ctx->width * 8;
+ uint32_t targetbuf_size = image->height * image->width * 8;
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)));
+ wuffs_base__make_slice_u8(
+ image->data, image->stride * image->height));
}
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));
- // The PNG decoder, at minimum, will flush any pixel data, so use them.
- if (status.repr != wuffs_base__suspension__short_read)
- goto fail;
+ // The PNG decoder, at minimum, will flush any pixel data upon
+ // finding out that the input is truncated, so accept whatever we get.
}
if (ctx->target) {
if (ctx->expand_16_float || ctx->pack_16_10) {
- fiv_io_profile_4x16le_direct(
+ fiv_io_cmm_4x16le_direct(ctx->cmm,
targetbuf, ctx->width, ctx->height, ctx->source, ctx->target);
// The first one premultiplies below, the second doesn't need to.
} else {
- fiv_io_profile_xrgb32_direct(surface_data, ctx->width, ctx->height,
- ctx->source, ctx->target);
- fiv_io_premultiply_argb32(surface);
+ fiv_io_cmm_argb32_premultiply(
+ ctx->cmm, image, ctx->source, ctx->target);
}
}
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 *out = (float *) image->data;
+ for (uint32_t y = 0; y < image->height; y++) {
+ for (uint32_t x = 0; x < image->width; x++) {
float b = *in++ / 65535., g = *in++ / 65535.,
r = *in++ / 65535., a = *in++ / 65535.;
*out++ = r * a;
@@ -664,9 +476,9 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
} 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 *out = (uint32_t *) image->data;
+ for (uint32_t y = 0; y < image->height; y++) {
+ for (uint32_t x = 0; x < image->width; x++) {
uint32_t b = *in++, g = *in++, r = *in++, X = *in++;
*out++ = (X >> 14) << 30 |
(r >> 6) << 20 | (g >> 6) << 10 | (b >> 6);
@@ -674,19 +486,17 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
}
}
- // 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);
+ // Copy the previous frame to a new image.
+ FivIoImage *prev = ctx->result_tail, *canvas = fiv_io_image_new(
+ prev->format, prev->width, prev->height);
+ if (!canvas) {
+ set_error(error, "image allocation failure");
+ goto fail;
+ }
+
+ memcpy(canvas->data, prev->data, prev->stride * prev->height);
// Apply that frame's disposal method.
// XXX: We do not expect opaque pictures to receive holes this way.
@@ -703,7 +513,9 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
b = (uint8_t) (bg) / 255. / a;
}
- cairo_t *cr = cairo_create(canvas);
+ cairo_surface_t *surface = fiv_io_image_to_surface_noref(canvas);
+ cairo_t *cr = cairo_create(surface);
+ cairo_surface_destroy(surface);
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,
@@ -733,46 +545,41 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
? CAIRO_OPERATOR_SOURCE
: CAIRO_OPERATOR_OVER);
+ surface = fiv_io_image_to_surface_noref(image);
cairo_set_source_surface(cr, surface, 0, 0);
+ cairo_surface_destroy(surface);
cairo_paint(cr);
cairo_destroy(cr);
- cairo_surface_destroy(surface);
- surface = canvas;
+
+ fiv_io_image_unref(image);
+ image = 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);
+ image->exif = g_bytes_ref(ctx->meta_exif);
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);
+ image->icc = g_bytes_ref(ctx->meta_iccp);
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);
+ image->xmp = g_bytes_ref(ctx->meta_xmp);
- 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);
+ image->loops = wuffs_base__image_decoder__num_animation_loops(ctx->dec);
+ image->frame_duration = wuffs_base__frame_config__duration(&fc) /
+ WUFFS_BASE__FLICKS_PER_MILLISECOND;
- cairo_surface_set_user_data(
- surface, &fiv_io_key_frame_previous, ctx->result_tail, NULL);
+ image->frame_previous = ctx->result_tail;
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);
+ ctx->result_tail->frame_next = image;
else
- ctx->result = surface;
+ ctx->result = image;
- ctx->result_tail = surface;
+ ctx->result_tail = image;
ctx->last_fc = fc;
g_free(targetbuf);
return wuffs_base__status__is_ok(&status);
fail:
- cairo_surface_destroy(surface);
- g_clear_pointer(&ctx->result, cairo_surface_destroy);
+ g_clear_pointer(&image, fiv_io_image_unref);
+ g_clear_pointer(&ctx->result, fiv_io_image_unref);
ctx->result_tail = NULL;
g_free(targetbuf);
return false;
@@ -781,12 +588,13 @@ fail:
// 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 *
+static FivIoImage *
open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
const FivIoOpenContext *ioctx, GError **error)
{
struct load_wuffs_frame_context ctx = {
- .dec = dec, .src = &src, .target = ioctx->screen_profile};
+ .dec = dec, .src = &src,
+ .cmm = ioctx->cmm, .target = ioctx->screen_profile};
// TODO(p): PNG text chunks, like we do with PNG thumbnails.
// TODO(p): See if something could and should be done about
@@ -870,9 +678,11 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
// TODO(p): Improve our simplistic PNG handling of: gAMA, cHRM, sRGB.
if (ctx.target) {
if (ctx.meta_iccp)
- ctx.source = fiv_io_profile_new_from_bytes(ctx.meta_iccp);
+ ctx.source = fiv_io_cmm_get_profile_from_bytes(
+ ctx.cmm, ctx.meta_iccp);
else if (isfinite(gamma) && gamma > 0)
- ctx.source = fiv_io_profile_new_sRGB_gamma(gamma);
+ ctx.source = fiv_io_cmm_get_profile_sRGB_gamma(
+ ctx.cmm, gamma);
}
// Wuffs maps tRNS to BGRA in `decoder.decode_trns?`, we should be fine.
@@ -943,8 +753,7 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
// 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);
+ ctx.result->frame_previous = ctx.result_tail;
fail:
free(ctx.workbuf.ptr);
@@ -955,7 +764,7 @@ fail:
return ctx.result;
}
-static cairo_surface_t *
+static FivIoImage *
open_wuffs_using(wuffs_base__image_decoder *(*allocate)(),
const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
{
@@ -965,11 +774,11 @@ open_wuffs_using(wuffs_base__image_decoder *(*allocate)(),
return NULL;
}
- cairo_surface_t *surface =
+ FivIoImage *image =
open_wuffs(dec, wuffs_base__ptr_u8__reader((uint8_t *) data, len, TRUE),
ctx, error);
free(dec);
- return surface;
+ return image;
}
// --- Wuffs for PNG thumbnails ------------------------------------------------
@@ -1003,7 +812,7 @@ pull_metadata_kvp(wuffs_png__decoder *dec, wuffs_base__io_buffer *src,
}
// An uncomplicated variant of fiv_io_open(), might be up for refactoring.
-cairo_surface_t *
+FivIoImage *
fiv_io_open_png_thumbnail(const char *path, GError **error)
{
wuffs_png__decoder dec = {};
@@ -1026,7 +835,7 @@ fiv_io_open_png_thumbnail(const char *path, GError **error)
wuffs_base__image_config cfg = {};
wuffs_base__slice_u8 workbuf = {};
- cairo_surface_t *surface = NULL;
+ FivIoImage *image = NULL;
bool success = false;
GHashTable *texts =
@@ -1068,23 +877,19 @@ fiv_io_open_png_thumbnail(const char *path, GError **error)
}
}
- surface = cairo_image_surface_create(
+ image = fiv_io_image_new(
wuffs_base__image_config__first_frame_is_opaque(&cfg)
? CAIRO_FORMAT_RGB24
: 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));
+ if (!image) {
+ set_error(error, "image allocation failure");
goto fail;
}
wuffs_base__pixel_buffer pb = {};
status = wuffs_base__pixel_buffer__set_from_slice(&pb, &cfg.pixcfg,
- wuffs_base__make_slice_u8(cairo_image_surface_get_data(surface),
- cairo_image_surface_get_stride(surface) *
- cairo_image_surface_get_height(surface)));
+ wuffs_base__make_slice_u8(image->data, image->stride * image->height));
if (!wuffs_base__status__is_ok(&status)) {
set_error(error, wuffs_base__status__message(&status));
goto fail;
@@ -1117,49 +922,220 @@ fiv_io_open_png_thumbnail(const char *path, GError **error)
g_assert(key == NULL);
- cairo_surface_mark_dirty(surface);
- cairo_surface_set_user_data(surface, &fiv_io_key_text,
- g_hash_table_ref(texts), (cairo_destroy_func_t) g_hash_table_unref);
+ image->text = g_hash_table_ref(texts);
success = true;
fail:
if (!success)
- g_clear_pointer(&surface, cairo_surface_destroy);
+ g_clear_pointer(&image, fiv_io_image_unref);
free(workbuf.ptr);
g_free(data);
g_hash_table_unref(texts);
- return surface;
+ return image;
+}
+
+// --- Multi-Picture Format ----------------------------------------------------
+
+static uint32_t
+parse_mpf_mpentry(const uint8_t *p, const struct tiffer *T)
+{
+ uint32_t attrs = T->un->u32(p);
+ uint32_t offset = T->un->u32(p + 8);
+
+ enum {
+ TypeBaselineMPPrimaryImage = 0x030000,
+ TypeLargeThumbnailVGA = 0x010001,
+ TypeLargeThumbnailFullHD = 0x010002,
+ TypeMultiFrameImagePanorama = 0x020001,
+ TypeMultiFrameImageDisparity = 0x020002,
+ TypeMultiFrameImageMultiAngle = 0x020003,
+ TypeUndefined = 0x000000,
+ };
+ switch (attrs & 0xFFFFFF) {
+ case TypeLargeThumbnailVGA:
+ case TypeLargeThumbnailFullHD:
+ // Wasted cycles.
+ case TypeUndefined:
+ // Apple uses this for HDR and depth maps (same and lower resolution).
+ // TODO(p): It would be nice to be able to view them.
+ return 0;
+ }
+
+ // Don't report non-JPEGs, even though they're unlikely.
+ if (((attrs >> 24) & 0x7) != 0)
+ return 0;
+
+ return offset;
+}
+
+static uint32_t *
+parse_mpf_index_entries(const struct tiffer *T, struct tiffer_entry *entry)
+{
+ uint32_t count = entry->remaining_count / 16;
+ uint32_t *offsets = g_malloc0_n(sizeof *offsets, count + 1), *out = offsets;
+ for (uint32_t i = 0; i < count; i++) {
+ // 5.2.3.3.3. Individual Image Data Offset
+ uint32_t offset = parse_mpf_mpentry(entry->p + i * 16, T);
+ if (offset)
+ *out++ = offset;
+ }
+ return offsets;
+}
+
+static uint32_t *
+parse_mpf_index_ifd(struct tiffer *T)
+{
+ struct tiffer_entry entry = {};
+ while (tiffer_next_entry(T, &entry)) {
+ // 5.2.3.3. MP Entry
+ if (entry.tag == MPF_MPEntry && entry.type == TIFFER_UNDEFINED &&
+ !(entry.remaining_count % 16)) {
+ return parse_mpf_index_entries(T, &entry);
+ }
+ }
+ return NULL;
+}
+
+static bool
+parse_mpf(
+ GPtrArray *individuals, const uint8_t *mpf, size_t len, size_t total_len)
+{
+ struct tiffer T;
+ if (!tiffer_init(&T, mpf, len) || !tiffer_next_ifd(&T))
+ return false;
+
+ // First image: IFD0 is Index IFD, any IFD1 is Attribute IFD.
+ // Other images: IFD0 is Attribute IFD, there is no Index IFD.
+ uint32_t *offsets = parse_mpf_index_ifd(&T);
+ if (offsets) {
+ for (const uint32_t *o = offsets; *o; o++)
+ if (*o <= total_len)
+ g_ptr_array_add(individuals, (gpointer) mpf + *o);
+ free(offsets);
+ }
+ return true;
}
// --- JPEG --------------------------------------------------------------------
-static GBytes *
-parse_jpeg_metadata(cairo_surface_t *surface, const char *data, gsize len)
+struct exif_profile {
+ double whitepoint[2]; ///< TIFF_WhitePoint
+ double primaries[6]; ///< TIFF_PrimaryChromaticities
+ enum Exif_ColorSpace colorspace; ///< Exif_ColorSpace
+ double gamma; ///< Exif_Gamma
+
+ bool have_whitepoint;
+ bool have_primaries;
+ bool have_colorspace;
+ bool have_gamma;
+};
+
+static bool
+parse_exif_profile_reals(
+ const struct tiffer *T, struct tiffer_entry *entry, double *out)
+{
+ while (tiffer_real(T, entry, out++))
+ if (!tiffer_next_value(entry))
+ return false;
+ return true;
+}
+
+static void
+parse_exif_profile_subifd(
+ struct exif_profile *params, const struct tiffer *T, uint32_t offset)
+{
+ struct tiffer subT = {};
+ if (!tiffer_subifd(T, offset, &subT))
+ return;
+
+ struct tiffer_entry entry = {};
+ while (tiffer_next_entry(&subT, &entry)) {
+ int64_t value = 0;
+ if (G_UNLIKELY(entry.tag == Exif_ColorSpace) &&
+ entry.type == TIFFER_SHORT && entry.remaining_count == 1 &&
+ tiffer_integer(&subT, &entry, &value)) {
+ params->have_colorspace = true;
+ params->colorspace = value;
+ } else if (G_UNLIKELY(entry.tag == Exif_Gamma) &&
+ entry.type == TIFFER_RATIONAL && entry.remaining_count == 1 &&
+ tiffer_real(&subT, &entry, &params->gamma)) {
+ params->have_gamma = true;
+ }
+ }
+}
+
+static FivIoProfile *
+parse_exif_profile(FivIoCmm *cmm, const void *data, size_t len)
+{
+ struct tiffer T = {};
+ if (!tiffer_init(&T, (const uint8_t *) data, len) || !tiffer_next_ifd(&T))
+ return NULL;
+
+ struct exif_profile params = {};
+ struct tiffer_entry entry = {};
+ while (tiffer_next_entry(&T, &entry)) {
+ int64_t offset = 0;
+ if (G_UNLIKELY(entry.tag == TIFF_ExifIFDPointer) &&
+ entry.type == TIFFER_LONG && entry.remaining_count == 1 &&
+ tiffer_integer(&T, &entry, &offset) &&
+ offset >= 0 && offset <= UINT32_MAX) {
+ parse_exif_profile_subifd(&params, &T, offset);
+ } else if (G_UNLIKELY(entry.tag == TIFF_WhitePoint) &&
+ entry.type == TIFFER_RATIONAL &&
+ entry.remaining_count == G_N_ELEMENTS(params.whitepoint)) {
+ params.have_whitepoint =
+ parse_exif_profile_reals(&T, &entry, params.whitepoint);
+ } else if (G_UNLIKELY(entry.tag == TIFF_PrimaryChromaticities) &&
+ entry.type == TIFFER_RATIONAL &&
+ entry.remaining_count == G_N_ELEMENTS(params.primaries)) {
+ params.have_primaries =
+ parse_exif_profile_reals(&T, &entry, params.primaries);
+ }
+ }
+ if (!params.have_colorspace)
+ return NULL;
+
+ // If sRGB is claimed, assume all parameters are standard.
+ if (params.colorspace == Exif_ColorSpace_sRGB)
+ return fiv_io_cmm_get_profile_sRGB(cmm);
+
+ // AdobeRGB Nikon JPEGs provide all of these.
+ if (params.colorspace != Exif_ColorSpace_Uncalibrated ||
+ !params.have_gamma ||
+ !params.have_whitepoint ||
+ !params.have_primaries)
+ return NULL;
+
+ return fiv_io_cmm_get_profile_parametric(cmm,
+ params.gamma, params.whitepoint, params.primaries);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct jpeg_metadata {
+ GByteArray *exif; ///< Exif buffer or NULL
+ GByteArray *icc; ///< ICC profile buffer or NULL
+ GPtrArray *mpf; ///< Multi-Picture Format or NULL
+ int width; ///< Image width
+ int height; ///< Image height
+};
+
+static void
+parse_jpeg_metadata(const char *data, size_t len, struct jpeg_metadata *meta)
{
// 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,
+ SOF0 = 0xC0, SOF1, SOF2, SOF3, DHT, SOF5, SOF6, SOF7,
+ JPG, SOF9, SOF10, SOF11, DAC, SOF13, SOF14, SOF15,
+ RST0, RST1, RST2, RST3, RST4, RST5, RST6, RST7,
+ SOI, EOI, SOS, DQT, DNL, DRI, DHP, EXP,
+ APP0, APP1, APP2, APP3, APP4, APP5, APP6, APP7,
};
- 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.
@@ -1188,155 +1164,127 @@ parse_jpeg_metadata(cairo_surface_t *surface, const char *data, gsize len)
if (G_UNLIKELY((p += length) > end))
break;
+ switch (marker) {
+ case SOF0:
+ case SOF1:
+ case SOF2:
+ case SOF3:
+ case SOF5:
+ case SOF6:
+ case SOF7:
+ case SOF9:
+ case SOF10:
+ case SOF11:
+ case SOF13:
+ case SOF14:
+ case SOF15:
+ if (length >= 5) {
+ meta->width = (payload[3] << 8) + payload[4];
+ meta->height = (payload[1] << 8) + payload[2];
+ }
+ }
+
// 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) {
+ // XXX: Thumbnails may in practice overflow into follow-up segments.
+ if (meta->exif && marker == APP1 && p - payload >= 6 &&
+ !memcmp(payload, "Exif\0", 5) && !meta->exif->len) {
payload += 6;
- g_byte_array_append(exif, payload, p - payload);
+ g_byte_array_append(meta->exif, payload, p - payload);
}
// https://www.color.org/specification/ICC1v43_2010-12.pdf B.4
- if (marker == APP2 && p - payload >= 14 &&
+ if (meta->icc && 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);
+ g_byte_array_append(meta->icc, payload, p - payload);
icc_done = payload[-1] == icc_sequence;
}
+ // CIPA DC-007-2021 (Multi-Picture Format) 5.2
+ // https://www.cipa.jp/e/std/std-sec.html
+ if (meta->mpf && marker == APP2 && p - payload >= 8 &&
+ !memcmp(payload, "MPF\0", 4) && !meta->mpf->len) {
+ payload += 4;
+ parse_mpf(meta->mpf, payload, p - payload, end - payload);
+ }
+
// 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);
+ if (meta->icc && !icc_done)
+ g_byte_array_set_size(meta->icc, 0);
+}
+
+static FivIoImage *open_libjpeg_turbo(
+ const char *data, gsize len, const FivIoOpenContext *ctx, GError **error);
+
+static void
+load_jpeg_finalize(FivIoImage *image, bool cmyk,
+ const FivIoOpenContext *ctx, const char *data, size_t len)
+{
+ struct jpeg_metadata meta = {
+ .exif = g_byte_array_new(),
+ .icc = g_byte_array_new(),
+ .mpf = g_ptr_array_new(),
+ };
+
+ parse_jpeg_metadata(data, len, &meta);
+
+ if (!ctx->first_frame_only) {
+ // XXX: This is ugly, as it relies on just the first individual image
+ // having any follow-up entries (as it should be).
+ FivIoImage *image_tail = image;
+ for (guint i = 0; i < meta.mpf->len; i++) {
+ const char *jpeg = meta.mpf->pdata[i];
+ GError *error = NULL;
+ if (!try_append_page(
+ open_libjpeg_turbo(jpeg, len - (jpeg - data), ctx, &error),
+ &image, &image_tail)) {
+ add_warning(ctx, "MPF image %d: %s", i + 2, error->message);
+ g_error_free(error);
+ }
+ }
+ }
+ g_ptr_array_free(meta.mpf, TRUE);
+
+ if (meta.exif->len)
+ image->exif = g_byte_array_free_to_bytes(meta.exif);
else
- g_byte_array_free(exif, TRUE);
+ g_byte_array_free(meta.exif, TRUE);
GBytes *icc_profile = NULL;
- if (icc_done)
- cairo_surface_set_user_data(surface, &fiv_io_key_icc,
- (icc_profile = g_byte_array_free_to_bytes(icc)),
- (cairo_destroy_func_t) g_bytes_unref);
+ if (meta.icc->len)
+ image->icc = icc_profile = g_byte_array_free_to_bytes(meta.icc);
else
- g_byte_array_free(icc, TRUE);
- return icc_profile;
-}
+ g_byte_array_free(meta.icc, TRUE);
-static void
-load_jpeg_finalize(cairo_surface_t *surface, bool cmyk,
- FivIoProfile destination, const char *data, size_t len)
-{
- GBytes *icc_profile = parse_jpeg_metadata(surface, data, len);
- FivIoProfile source = NULL;
- if (icc_profile)
- source = fiv_io_profile_new(
+ FivIoProfile *source = NULL;
+ if (icc_profile && ctx->cmm)
+ source = fiv_io_cmm_get_profile(ctx->cmm,
g_bytes_get_data(icc_profile, NULL), g_bytes_get_size(icc_profile));
+ else if (image->exif && ctx->cmm)
+ source = parse_exif_profile(ctx->cmm,
+ g_bytes_get_data(image->exif, NULL), g_bytes_get_size(image->exif));
if (cmyk)
- fiv_io_profile_cmyk(surface, source, destination);
+ fiv_io_cmm_cmyk(ctx->cmm, image, source, ctx->screen_profile);
else
- fiv_io_profile_xrgb32(surface, source, destination);
+ fiv_io_cmm_any(ctx->cmm, image, source, ctx->screen_profile);
if (source)
fiv_io_profile_free(source);
-
- // Pixel data has been written, need to let Cairo know.
- cairo_surface_mark_dirty(surface);
-}
-
-static cairo_surface_t *
-open_libjpeg_turbo(
- const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
-{
- // Note that there doesn't seem to be much of a point in using this
- // simplified API anymore, because JPEG-QS needs the original libjpeg API.
- // It's just more or less duplicated code which won't compile with
- // the slow version of the library.
- 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;
- }
-
- bool use_cmyk = colorspace == TJCS_CMYK || colorspace == TJCS_YCCK;
- int pixel_format = use_cmyk
- ? TJPF_CMYK
- : (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRX : TJPF_XRGB);
-
- // The limit of Cairo/pixman is 32767. but JPEG can go as high as 65535.
- // Prevent Cairo from throwing an error, and make use of libjpeg's scaling.
- // gdk-pixbuf circumvents this check, producing unrenderable surfaces.
- const int max = 32767;
-
- int nfs = 0;
- tjscalingfactor *fs = tjGetScalingFactors(&nfs), f = {0, 1};
- if (fs && (width > max || height > max)) {
- for (int i = 0; i < nfs; i++) {
- if (TJSCALED(width, fs[i]) <= max &&
- TJSCALED(height, fs[i]) <= max &&
- fs[i].num * f.denom > f.num * fs[i].denom)
- f = fs[i];
- }
-
- add_warning(ctx,
- "the image is too large, and had to be scaled by %d/%d",
- f.num, f.denom);
- width = TJSCALED(width, f);
- height = TJSCALED(height, f);
- }
-
- cairo_surface_t *surface =
- cairo_image_surface_create(CAIRO_FORMAT_RGB24, 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) {
- add_warning(ctx, "%s", tjGetErrorStr2(dec));
- } else {
- set_error(error, tjGetErrorStr2(dec));
- cairo_surface_destroy(surface);
- tjDestroy(dec);
- return NULL;
- }
- }
-
- load_jpeg_finalize(surface, use_cmyk, ctx->screen_profile, data, len);
- tjDestroy(dec);
- return surface;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-#ifdef HAVE_JPEG_QS
-
struct libjpeg_error_mgr {
struct jpeg_error_mgr pub;
jmp_buf buf;
GError **error;
+ const FivIoOpenContext *ctx;
};
static void
@@ -1349,17 +1297,27 @@ libjpeg_error_exit(j_common_ptr cinfo)
longjmp(err->buf, 1);
}
-static cairo_surface_t *
-open_libjpeg_enhanced(
- const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
+static void
+libjpeg_output_message(j_common_ptr cinfo)
+{
+ struct libjpeg_error_mgr *err = (struct libjpeg_error_mgr *) cinfo->err;
+ char buf[JMSG_LENGTH_MAX] = "";
+ (*cinfo->err->format_message)(cinfo, buf);
+ add_warning(err->ctx, "%s", buf);
+}
+
+static FivIoImage *
+load_libjpeg_turbo(const char *data, gsize len, const FivIoOpenContext *ctx,
+ void (*loop)(struct jpeg_decompress_struct *, JSAMPARRAY), GError **error)
{
- cairo_surface_t *volatile surface = NULL;
+ FivIoImage *volatile image = NULL;
- struct libjpeg_error_mgr jerr = {.error = error};
+ struct libjpeg_error_mgr jerr = {.error = error, .ctx = ctx};
struct jpeg_decompress_struct cinfo = {.err = jpeg_std_error(&jerr.pub)};
jerr.pub.error_exit = libjpeg_error_exit;
+ jerr.pub.output_message = libjpeg_output_message;
if (setjmp(jerr.buf)) {
- g_clear_pointer(&surface, cairo_surface_destroy);
+ g_clear_pointer(&image, fiv_io_image_unref);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
@@ -1367,6 +1325,8 @@ open_libjpeg_enhanced(
jpeg_create_decompress(&cinfo);
jpeg_mem_src(&cinfo, (const unsigned char *) data, len);
(void) jpeg_read_header(&cinfo, true);
+ // TODO(p): With newer libjpeg-turbo, if cinfo.data_precision is 12 or 16,
+ // try to load it with higher precision.
bool use_cmyk = cinfo.jpeg_color_space == JCS_CMYK ||
cinfo.jpeg_color_space == JCS_YCCK;
@@ -1381,45 +1341,98 @@ open_libjpeg_enhanced(
int width = cinfo.output_width;
int height = cinfo.output_height;
- surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, 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));
+ // The limit of Cairo/pixman is 32767. but JPEG can go as high as 65535.
+ // Prevent Cairo from throwing an error, and make use of libjpeg's scaling.
+ // gdk-pixbuf circumvents this check, producing unrenderable surfaces.
+ const int max = 32767;
+
+ int nfs = 0;
+ tjscalingfactor *fs = tjGetScalingFactors(&nfs), f = {0, 1};
+ if (fs && (width > max || height > max)) {
+ for (int i = 0; i < nfs; i++) {
+ if (TJSCALED(width, fs[i]) <= max &&
+ TJSCALED(height, fs[i]) <= max &&
+ fs[i].num * f.denom > f.num * fs[i].denom)
+ f = fs[i];
+ }
+
+ add_warning(ctx,
+ "the image is too large, and had to be scaled by %d/%d",
+ f.num, f.denom);
+ width = TJSCALED(width, f);
+ height = TJSCALED(height, f);
+ cinfo.scale_num = f.num;
+ cinfo.scale_denom = f.denom;
+ }
+
+ image = fiv_io_image_new(CAIRO_FORMAT_RGB24, width, height);
+ if (!image) {
+ set_error(error, "image allocation failure");
longjmp(jerr.buf, 1);
}
- unsigned char *surface_data = cairo_image_surface_get_data(surface);
- int surface_stride = cairo_image_surface_get_stride(surface);
JSAMPARRAY lines = (*cinfo.mem->alloc_small)(
(j_common_ptr) &cinfo, JPOOL_IMAGE, sizeof *lines * height);
for (int i = 0; i < height; i++)
- lines[i] = surface_data + i * surface_stride;
+ lines[i] = image->data + i * image->stride;
+
+ // Slightly unfortunate generalization.
+ loop(&cinfo, lines);
+
+ load_jpeg_finalize(image, use_cmyk, ctx, data, len);
+ jpeg_destroy_decompress(&cinfo);
+ return image;
+}
+
+static void
+load_libjpeg_simple(
+ struct jpeg_decompress_struct *cinfo, JSAMPARRAY lines)
+{
+ (void) jpeg_start_decompress(cinfo);
+ while (cinfo->output_scanline < cinfo->output_height)
+ (void) jpeg_read_scanlines(cinfo, lines + cinfo->output_scanline,
+ cinfo->output_height - cinfo->output_scanline);
+ (void) jpeg_finish_decompress(cinfo);
+}
+
+#ifdef HAVE_JPEG_QS
+static void
+load_libjpeg_enhanced(
+ struct jpeg_decompress_struct *cinfo, JSAMPARRAY lines)
+{
// Go for the maximum quality setting.
jpegqs_control_t opts = {
- .flags = JPEGQS_DIAGONALS | JPEGQS_JOINT_YUV | JPEGQS_UPSAMPLE_UV,
+ .flags = JPEGQS_DIAGONALS | JPEGQS_JOINT_YUV,
.threads = g_get_num_processors(),
.niter = 3,
};
- (void) jpegqs_start_decompress(&cinfo, &opts);
- while (cinfo.output_scanline < cinfo.output_height)
- (void) jpeg_read_scanlines(&cinfo, lines + cinfo.output_scanline,
- cinfo.output_height - cinfo.output_scanline);
- if (cinfo.out_color_space == JCS_CMYK)
- trivial_cmyk_to_host_byte_order_argb(
- surface_data, cinfo.output_width * cinfo.output_height);
- (void) jpegqs_finish_decompress(&cinfo);
+ // Waiting for https://github.com/ilyakurdyukov/jpeg-quantsmooth/issues/28
+#if LIBJPEG_TURBO_VERSION_NUMBER < 2001090
+ opts.flags |= JPEGQS_UPSAMPLE_UV;
+#endif
- load_jpeg_finalize(surface, use_cmyk, ctx->screen_profile, data, len);
- jpeg_destroy_decompress(&cinfo);
- return surface;
+ (void) jpegqs_start_decompress(cinfo, &opts);
+ while (cinfo->output_scanline < cinfo->output_height)
+ (void) jpeg_read_scanlines(cinfo, lines + cinfo->output_scanline,
+ cinfo->output_height - cinfo->output_scanline);
+ (void) jpegqs_finish_decompress(cinfo);
}
#else
-#define open_libjpeg_enhanced open_libjpeg_turbo
+#define load_libjpeg_enhanced load_libjpeg_simple
#endif
+static FivIoImage *
+open_libjpeg_turbo(
+ const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
+{
+ return load_libjpeg_turbo(data, len, ctx,
+ ctx->enhance ? load_libjpeg_enhanced : load_libjpeg_simple,
+ error);
+}
+
// --- WebP --------------------------------------------------------------------
static const char *
@@ -1447,17 +1460,15 @@ load_libwebp_error(VP8StatusCode err)
}
}
-static cairo_surface_t *
+static FivIoImage *
load_libwebp_nonanimated(WebPDecoderConfig *config, const WebPData *wd,
const FivIoOpenContext *ctx, GError **error)
{
- cairo_surface_t *surface = cairo_image_surface_create(
+ FivIoImage *image = fiv_io_image_new(
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);
+ if (!image) {
+ set_error(error, "image allocation failure");
return NULL;
}
@@ -1466,10 +1477,9 @@ load_libwebp_nonanimated(WebPDecoderConfig *config, const WebPData *wd,
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);
+ config->output.u.RGBA.rgba = image->data;
+ config->output.u.RGBA.stride = image->stride;
+ config->output.u.RGBA.size = config->output.u.RGBA.stride * image->height;
bool premultiply = !ctx->screen_profile;
if (G_BYTE_ORDER == G_LITTLE_ENDIAN)
@@ -1480,43 +1490,34 @@ load_libwebp_nonanimated(WebPDecoderConfig *config, const WebPData *wd,
WebPIDecoder *idec = WebPIDecode(NULL, 0, config);
if (!idec) {
set_error(error, "WebP decoding error");
- cairo_surface_destroy(surface);
+ fiv_io_image_unref(image);
return NULL;
}
VP8StatusCode err = WebPIUpdate(idec, wd->bytes, wd->size);
- cairo_surface_mark_dirty(surface);
int x = 0, y = 0, w = 0, h = 0;
(void) WebPIDecodedArea(idec, &x, &y, &w, &h);
WebPIDelete(idec);
if (err == VP8_STATUS_OK)
- return surface;
+ return image;
if (err != VP8_STATUS_SUSPENDED) {
g_set_error(error, FIV_IO_ERROR, FIV_IO_ERROR_OPEN, "%s: %s",
"WebP decoding error", load_libwebp_error(err));
- cairo_surface_destroy(surface);
+ fiv_io_image_unref(image);
return NULL;
}
add_warning(ctx, "image file is truncated");
if (config->input.has_alpha)
- return surface;
+ return image;
// Always use transparent black, rather than opaque black.
- cairo_surface_t *masked = cairo_image_surface_create(
- CAIRO_FORMAT_ARGB32, config->input.width, config->input.height);
- cairo_t *cr = cairo_create(masked);
- cairo_set_source_surface(cr, surface, 0, 0);
- cairo_rectangle(cr, x, y, w, h);
- cairo_clip(cr);
- cairo_paint(cr);
- cairo_destroy(cr);
- cairo_surface_destroy(surface);
- return masked;
+ image->format = CAIRO_FORMAT_ARGB32;
+ return image;
}
-static cairo_surface_t *
+static FivIoImage *
load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info,
int *last_timestamp, GError **error)
{
@@ -1527,38 +1528,39 @@ load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info,
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,
+ FivIoImage *image = fiv_io_image_new(CAIRO_FORMAT_RGB24,
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);
+ if (!image) {
+ set_error(error, "image allocation failure");
return NULL;
}
- uint32_t *dst = (uint32_t *) cairo_image_surface_get_data(surface);
+ uint32_t *dst = (uint32_t *) image->data;
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++);
+ const uint32_t *src = (const uint32_t *) buf;
+ for (uint64_t i = 0; i < area; i++) {
+ uint32_t value = *src++;
+ *dst++ = GUINT32_FROM_LE(value);
+ }
}
- cairo_surface_mark_dirty(surface);
+ // info->bgcolor is not reliable.
+ for (const uint32_t *p = dst, *end = dst + area; p < end; p++)
+ if ((~*p & 0xff000000)) {
+ image->format = CAIRO_FORMAT_ARGB32;
+ break;
+ }
// This API is confusing and awkward.
- cairo_surface_set_user_data(surface, &fiv_io_key_frame_duration,
- (void *) (intptr_t) (timestamp - *last_timestamp), NULL);
+ image->frame_duration = timestamp - *last_timestamp;
*last_timestamp = timestamp;
- return surface;
+ return image;
}
-static cairo_surface_t *
+static FivIoImage *
load_libwebp_animated(
const WebPData *wd, const FivIoOpenContext *ctx, GError **error)
{
@@ -1572,7 +1574,7 @@ load_libwebp_animated(
WebPAnimDecoder *dec = WebPAnimDecoderNew(wd, &options);
WebPAnimDecoderGetInfo(dec, &info);
- cairo_surface_t *frames = NULL, *frames_tail = NULL;
+ FivIoImage *frames = NULL, *frames_tail = NULL;
if (info.canvas_width > INT_MAX || info.canvas_height > INT_MAX) {
set_error(error, "image dimensions overflow");
goto fail;
@@ -1580,30 +1582,27 @@ load_libwebp_animated(
int last_timestamp = 0;
while (WebPAnimDecoderHasMoreFrames(dec)) {
- cairo_surface_t *surface =
+ FivIoImage *image =
load_libwebp_frame(dec, &info, &last_timestamp, error);
- if (!surface) {
- g_clear_pointer(&frames, cairo_surface_destroy);
+ if (!image) {
+ g_clear_pointer(&frames, fiv_io_image_unref);
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);
+ frames_tail->frame_next = image;
else
- frames = surface;
+ frames = image;
- cairo_surface_set_user_data(
- surface, &fiv_io_key_frame_previous, frames_tail, NULL);
- frames_tail = surface;
+ image->frame_previous = frames_tail;
+ frames_tail = image;
}
if (frames) {
- cairo_surface_set_user_data(
- frames, &fiv_io_key_frame_previous, frames_tail, NULL);
+ frames->frame_previous = frames_tail;
} else {
set_error(error, "the animation has no frames");
- g_clear_pointer(&frames, cairo_surface_destroy);
+ g_clear_pointer(&frames, fiv_io_image_unref);
}
fail:
@@ -1611,7 +1610,7 @@ fail:
return frames;
}
-static cairo_surface_t *
+static FivIoImage *
open_libwebp(
const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
{
@@ -1631,7 +1630,7 @@ open_libwebp(
return NULL;
}
- cairo_surface_t *result = config.input.has_animation
+ FivIoImage *result = config.input.has_animation
? load_libwebp_animated(&wd, ctx, error)
: load_libwebp_nonanimated(&config, &wd, ctx, error);
if (!result)
@@ -1646,90 +1645,321 @@ open_libwebp(
}
// Releasing the demux chunk iterator is actually a no-op.
- // TODO(p): Avoid copy-pasting the chunk transfer code.
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);
+ result->exif =
+ g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size);
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);
+ result->icc =
+ g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size);
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);
+ result->xmp =
+ g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size);
WebPDemuxReleaseChunkIterator(&chunk_iter);
}
if (WebPDemuxGetChunk(demux, "THUM", 1, &chunk_iter)) {
- cairo_surface_set_user_data(result, &fiv_io_key_thum,
- g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size),
- (cairo_destroy_func_t) g_bytes_unref);
+ result->thum =
+ g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size);
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);
- }
+ if (flags & ANIMATION_FLAG)
+ result->loops = WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT);
WebPDemuxDelete(demux);
- if (ctx->screen_profile) {
- fiv_io_profile_xrgb32_page(result, ctx->screen_profile);
- fiv_io_premultiply_argb32_page(result);
- }
+ if (ctx->screen_profile)
+ fiv_io_cmm_argb32_premultiply_page(
+ ctx->cmm, result, ctx->screen_profile);
fail:
WebPFreeDecBuffer(&config.output);
return result;
}
-// --- Optional dependencies ---------------------------------------------------
+// --- TIFF/EP + DNG -----------------------------------------------------------
+// In Nikon NEF files, which claim to be TIFF/EP-compatible, IFD0 is a tiny
+// uncompressed thumbnail with SubIFDs that, aside from raw sensor data,
+// typically contain a nearly full-size JPEG preview.
+//
+// LibRaw takes too long a time to render something that will never be as good
+// as that large preview--e.g., due to exposure correction or denoising.
+// While since version 0.21.0 the library provides an API that would allow us
+// to extract the JPEG, a little bit of custom processing won't hurt either.
+// TODO(p): Though it can also extract thumbnails from many more formats,
+// so maybe keep this code as a fallback for old or missing LibRaw.
+//
+// Note that libtiff can only read the horrible IFD0 thumbnail.
+// (TIFFSetSubDirectory() requires an ImageLength tag that's missing from JPEG
+// SubIFDs, and TIFFReadCustomDirectory() takes a privately defined struct that
+// may not be omitted.)
-#ifdef HAVE_LIBRAW // ---------------------------------------------------------
+static bool
+tiffer_find(const struct tiffer *self, uint16_t tag, struct tiffer_entry *entry)
+{
+ // Note that we could employ binary search, because tags must be ordered:
+ // - TIFF 6.0: Sort Order
+ // - ISO/DIS 12234-2: 4.1.2, 5.1
+ // - CIPA DC-007-2009 (Multi-Picture Format): 5.2.3., 5.2.4.
+ // - CIPA DC-008-2019 (Exif 2.32): 4.6.2.
+ // However, it doesn't seem to warrant the ugly code.
+ struct tiffer T = *self;
+ while (tiffer_next_entry(&T, entry)) {
+ if (entry->tag == tag)
+ return true;
+ }
+ *entry = (struct tiffer_entry) {};
+ return false;
+}
-static cairo_surface_t *
-open_libraw(const char *data, gsize len, GError **error)
+static bool
+tiffer_find_integer(const struct tiffer *self, uint16_t tag, int64_t *i)
{
- // 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");
+ struct tiffer_entry entry = {};
+ return tiffer_find(self, tag, &entry) && tiffer_integer(self, &entry, i);
+}
+
+// In case of failure, an entry with a zero "remaining_count" is returned.
+static struct tiffer_entry
+tiff_ep_subifds_init(const struct tiffer *T)
+{
+ struct tiffer_entry entry = {};
+ (void) tiffer_find(T, TIFF_SubIFDs, &entry);
+ return entry;
+}
+
+static bool
+tiff_ep_subifds_next(
+ const struct tiffer *T, struct tiffer_entry *subifds, struct tiffer *subT)
+{
+ // XXX: Except for a zero "remaining_count", all conditions are errors,
+ // and should perhaps be reported.
+ int64_t offset = 0;
+ if (!tiffer_integer(T, subifds, &offset) ||
+ offset < 0 || offset > UINT32_MAX || !tiffer_subifd(T, offset, subT))
+ return false;
+
+ (void) tiffer_next_value(subifds);
+ return true;
+}
+
+static bool
+tiff_ep_find_main(const struct tiffer *T, struct tiffer *outputT)
+{
+ // This is a mandatory field.
+ int64_t type = 0;
+ if (!tiffer_find_integer(T, TIFF_NewSubfileType, &type))
+ return false;
+
+ // This is the main image.
+ // (See DNG rather than ISO/DIS 12234-2 for values.)
+ if (type == 0) {
+ *outputT = *T;
+ return true;
+ }
+
+ struct tiffer_entry subifds = tiff_ep_subifds_init(T);
+ struct tiffer subT = {};
+ while (tiff_ep_subifds_next(T, &subifds, &subT))
+ if (tiff_ep_find_main(&subT, outputT))
+ return true;
+ return false;
+}
+
+struct tiff_ep_jpeg {
+ const uint8_t *jpeg; ///< JPEG data stream
+ size_t jpeg_length; ///< JPEG data stream length
+ int64_t pixels; ///< Number of pixels in the JPEG
+};
+
+static void
+tiff_ep_find_jpeg_evaluate(const struct tiffer *T, struct tiff_ep_jpeg *out)
+{
+ // This is a mandatory field.
+ int64_t compression = 0;
+ if (!tiffer_find_integer(T, TIFF_Compression, &compression))
+ return;
+
+ uint16_t tag_pointer = 0, tag_length = 0;
+ switch (compression) {
+ // This is how Exif specifies it, which doesn't follow TIFF 6.0.
+ case TIFF_Compression_JPEG:
+ tag_pointer = TIFF_JPEGInterchangeFormat;
+ tag_length = TIFF_JPEGInterchangeFormatLength;
+ break;
+ // Theoretically, there may be more strips, but this is not expected.
+ case TIFF_Compression_JPEGDatastream:
+ tag_pointer = TIFF_StripOffsets;
+ tag_length = TIFF_StripByteCounts;
+ break;
+ default:
+ return;
+ }
+
+ int64_t ipointer = 0, ilength = 0;
+ if (!tiffer_find_integer(T, tag_pointer, &ipointer) || ipointer <= 0 ||
+ !tiffer_find_integer(T, tag_length, &ilength) || ilength <= 0 ||
+ ipointer > T->end - T->begin ||
+ T->end - T->begin - ipointer < ilength)
+ return;
+
+ // Note that to get the largest JPEG,
+ // we don't need to descend into Exif thumbnails.
+ // TODO(p): Consider DNG 1.2.0.0 PreviewColorSpace.
+ // But first, try to find some real-world files with it.
+ const uint8_t *jpeg = T->begin + ipointer;
+ size_t jpeg_length = ilength;
+
+ struct jpeg_metadata meta = {};
+ parse_jpeg_metadata((const char *) jpeg, jpeg_length, &meta);
+ int64_t pixels = meta.width * meta.height;
+ if (pixels > out->pixels) {
+ out->jpeg = jpeg;
+ out->jpeg_length = jpeg_length;
+ out->pixels = pixels;
+ }
+}
+
+static bool
+tiff_ep_find_jpeg(const struct tiffer *T, struct tiff_ep_jpeg *out)
+{
+ // This is a mandatory field.
+ int64_t type = 0;
+ if (!tiffer_find_integer(T, TIFF_NewSubfileType, &type))
+ return false;
+
+ // This is a thumbnail of the main image.
+ // (See DNG rather than ISO/DIS 12234-2 for values.)
+ if (type == 1)
+ tiff_ep_find_jpeg_evaluate(T, out);
+
+ struct tiffer_entry subifds = tiff_ep_subifds_init(T);
+ struct tiffer subT = {};
+ while (tiff_ep_subifds_next(T, &subifds, &subT))
+ if (!tiff_ep_find_jpeg(&subT, out))
+ return false;
+ return true;
+}
+
+static FivIoImage *
+load_tiff_ep(
+ const struct tiffer *T, const FivIoOpenContext *ctx, GError **error)
+{
+ // ISO/DIS 12234-2 is a fuck-up that says this should be in "IFD0",
+ // but it might have intended to say "all top-level IFDs".
+ // The DNG specification shares the same problem.
+ //
+ // In any case, chained TIFFs are relatively rare.
+ struct tiffer_entry entry = {};
+ bool is_tiffep = tiffer_find(T, TIFF_TIFF_EPStandardID, &entry) &&
+ entry.type == TIFFER_BYTE && entry.remaining_count == 4 &&
+ entry.p[0] == 1 && !entry.p[1] && !entry.p[2] && !entry.p[3];
+
+ // Apple ProRAW, e.g., does not claim TIFF/EP compatibility,
+ // but we should still be able to make sense of it.
+ bool is_supported_dng = tiffer_find(T, TIFF_DNGBackwardVersion, &entry) &&
+ entry.type == TIFFER_BYTE && entry.remaining_count == 4 &&
+ entry.p[0] == 1 && entry.p[1] <= 6 && !entry.p[2] && !entry.p[3];
+ if (!is_tiffep && !is_supported_dng) {
+ set_error(error, "not a supported TIFF/EP or DNG image");
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
+ struct tiffer fullT = {};
+ if (!tiff_ep_find_main(T, &fullT)) {
+ set_error(error, "could not find a main image");
+ return NULL;
+ }
- // 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.
+ int64_t width = 0, height = 0;
+ if (!tiffer_find_integer(&fullT, TIFF_ImageWidth, &width) ||
+ !tiffer_find_integer(&fullT, TIFF_ImageLength, &height) ||
+ width <= 0 || height <= 0) {
+ set_error(error, "missing or invalid main image dimensions");
+ return NULL;
+ }
- int err = 0;
- if ((err = libraw_open_buffer(iprc, (void *) data, len))) {
- set_error(error, libraw_strerror(err));
- libraw_close(iprc);
+ struct tiff_ep_jpeg out = {};
+ if (!tiff_ep_find_jpeg(T, &out)) {
+ set_error(error, "error looking for a full-size JPEG preview");
+ return NULL;
+ }
+
+ // Nikon NEFs seem to generally have a preview above 99 percent,
+ // (though some of them may not even reach 50 percent).
+ // Be a bit more generous than that with our crop tolerance.
+ // TODO(p): Also take into account DNG DefaultCropSize, if present.
+ if (out.pixels / ((double) width * height) < 0.95) {
+ set_error(error, "could not find a large enough JPEG preview");
+ return NULL;
+ }
+
+ FivIoImage *image = open_libjpeg_turbo(
+ (const char *) out.jpeg, out.jpeg_length, ctx, error);
+ if (!image)
+ return NULL;
+
+ // Note that Exif may override this later in fiv_io_open_from_data().
+ // TODO(p): Try to use the Orientation field nearest to the target IFD.
+ // IFD0 just happens to be fine for Nikon NEF.
+ int64_t orientation = 0;
+ if (tiffer_find_integer(T, TIFF_Orientation, &orientation) &&
+ orientation >= 1 && orientation <= 8) {
+ image->orientation = orientation;
+ }
+
+ // XXX: AdobeRGB Nikon NEFs can only be distinguished by a ColorSpace tag
+ // from within their MakerNote.
+ return image;
+}
+
+static FivIoImage *
+open_tiff_ep(
+ const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
+{
+ // -Wunused-function, we might want to give this its own compile unit.
+ (void) tiffer_real;
+
+ struct tiffer T = {};
+ if (!tiffer_init(&T, (const uint8_t *) data, len)) {
+ set_error(error, "not a TIFF file");
return NULL;
}
- // TODO(p): Do we need to check iprc->idata.raw_count? Maybe for TIFFs?
+ FivIoImage *result = NULL, *result_tail = NULL;
+ while (tiffer_next_ifd(&T)) {
+ if (!try_append_page(
+ load_tiff_ep(&T, ctx, error), &result, &result_tail)) {
+ g_clear_pointer(&result, fiv_io_image_unref);
+ return NULL;
+ }
+ if (ctx->first_frame_only)
+ break;
+
+ // TODO(p): Try to adjust tiffer so that this isn't necessary.
+ struct tiffer_entry dummy = {};
+ while (tiffer_next_entry(&T, &dummy))
+ ;
+ }
+ return result;
+}
+
+// --- Optional dependencies ---------------------------------------------------
+
+#ifdef HAVE_LIBRAW // ---------------------------------------------------------
+
+static FivIoImage *
+load_libraw(libraw_data_t *iprc, GError **error)
+{
+ int err = 0;
if ((err = libraw_unpack(iprc))) {
set_error(error, libraw_strerror(err));
- libraw_close(iprc);
return NULL;
}
@@ -1737,7 +1967,6 @@ open_libraw(const char *data, gsize len, GError **error)
// 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
@@ -1745,7 +1974,6 @@ open_libraw(const char *data, gsize len, GError **error)
// 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;
}
@@ -1754,7 +1982,6 @@ open_libraw(const char *data, gsize len, GError **error)
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;
}
@@ -1762,26 +1989,18 @@ open_libraw(const char *data, gsize len, GError **error)
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);
+ FivIoImage *I =
+ fiv_io_image_new(CAIRO_FORMAT_RGB24, image->width, image->height);
+ if (!I) {
+ set_error(error, "image allocation failure");
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);
+ uint32_t *pixels = (uint32_t *) I->data;
unsigned char *p = image->data;
for (ushort y = 0; y < image->height; y++) {
for (ushort x = 0; x < image->width; x++) {
@@ -1791,12 +2010,55 @@ open_libraw(const char *data, gsize len, GError **error)
}
}
- // Pixel data has been written, need to let Cairo know.
- cairo_surface_mark_dirty(surface);
-
libraw_dcraw_clear_mem(image);
+ return I;
+}
+
+static FivIoImage *
+open_libraw(
+ const char *data, gsize len, const FivIoOpenContext *ctx, 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;
+ }
+
+ // 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;
+ FivIoImage *result = NULL, *result_tail = NULL;
+ if ((err = libraw_open_buffer(iprc, (const void *) data, len))) {
+ set_error(error, libraw_strerror(err));
+ goto out;
+ }
+ if (!try_append_page(load_libraw(iprc, error), &result, &result_tail) ||
+ ctx->first_frame_only)
+ goto out;
+
+ for (unsigned i = 1; i < iprc->idata.raw_count; i++) {
+ iprc->rawparams.shot_select = i;
+
+ // This library is terrible, we need to start again.
+ if ((err = libraw_open_buffer(iprc, (const void *) data, len))) {
+ set_error(error, libraw_strerror(err));
+ g_clear_pointer(&result, fiv_io_image_unref);
+ goto out;
+ }
+ if (!try_append_page(load_libraw(iprc, error), &result, &result_tail)) {
+ g_clear_pointer(&result, fiv_io_image_unref);
+ goto out;
+ }
+ }
+
+out:
libraw_close(iprc);
- return surface;
+ return fiv_io_cmm_finish(ctx->cmm, result, ctx->screen_profile);
}
#endif // HAVE_LIBRAW ---------------------------------------------------------
@@ -1810,16 +2072,16 @@ typedef struct {
} FivIoRenderClosureResvg;
static void
-load_resvg_destroy(void *closure)
+load_resvg_destroy(FivIoRenderClosure *closure)
{
- FivIoRenderClosureResvg *self = closure;
+ FivIoRenderClosureResvg *self = (void *) closure;
resvg_tree_destroy(self->tree);
g_free(self);
}
-static cairo_surface_t *
-load_resvg_render_internal(
- FivIoRenderClosureResvg *self, double scale, GError **error)
+static FivIoImage *
+load_resvg_render_internal(FivIoRenderClosureResvg *self, double scale,
+ FivIoCmm *cmm, FivIoProfile *target, GError **error)
{
double w = ceil(self->width * scale), h = ceil(self->height * scale);
if (w > SHRT_MAX || h > SHRT_MAX) {
@@ -1827,38 +2089,37 @@ load_resvg_render_internal(
return NULL;
}
- cairo_surface_t *surface =
- cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 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);
+ FivIoImage *image = fiv_io_image_new(CAIRO_FORMAT_ARGB32, w, h);
+ if (!image) {
+ set_error(error, "image allocation failure");
return NULL;
}
- uint32_t *pixels = (uint32_t *) cairo_image_surface_get_data(surface);
+ uint32_t *pixels = (uint32_t *) image->data;
+#if RESVG_MAJOR_VERSION == 0 && RESVG_MINOR_VERSION < 33
resvg_fit_to fit_to = {
scale == 1 ? RESVG_FIT_TO_TYPE_ORIGINAL : RESVG_FIT_TO_TYPE_ZOOM,
scale};
resvg_render(self->tree, fit_to, resvg_transform_identity(),
- cairo_image_surface_get_width(surface),
- cairo_image_surface_get_height(surface), (char *) pixels);
+ image->width, image->height, (char *) pixels);
+#else
+ resvg_render(self->tree, (resvg_transform) {.a = scale, .d = scale},
+ image->width, image->height, (char *) pixels);
+#endif
- // TODO(p): Also apply colour management, we'll need to un-premultiply.
for (int i = 0; i < w * h; i++) {
uint32_t rgba = g_ntohl(pixels[i]);
pixels[i] = rgba << 24 | rgba >> 8;
}
-
- cairo_surface_mark_dirty(surface);
- return surface;
+ return fiv_io_cmm_finish(cmm, image, target);
}
-static cairo_surface_t *
-load_resvg_render(FivIoRenderClosure *closure, double scale)
+static FivIoImage *
+load_resvg_render(FivIoRenderClosure *closure,
+ FivIoCmm *cmm, FivIoProfile *target, double scale)
{
FivIoRenderClosureResvg *self = (FivIoRenderClosureResvg *) closure;
- return load_resvg_render_internal(self, scale, NULL);
+ return load_resvg_render_internal(self, scale, cmm, target, NULL);
}
static const char *
@@ -1882,7 +2143,7 @@ load_resvg_error(int err)
}
}
-static cairo_surface_t *
+static FivIoImage *
open_resvg(
const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
{
@@ -1911,19 +2172,20 @@ open_resvg(
FivIoRenderClosureResvg *closure = g_malloc0(sizeof *closure);
closure->parent.render = load_resvg_render;
+ closure->parent.destroy = load_resvg_destroy;
closure->tree = tree;
closure->width = size.width;
closure->height = size.height;
- cairo_surface_t *surface = load_resvg_render_internal(closure, 1., error);
- if (!surface) {
- load_resvg_destroy(closure);
+ FivIoImage *image = load_resvg_render_internal(
+ closure, 1., ctx->cmm, ctx->screen_profile, error);
+ if (!image) {
+ load_resvg_destroy(&closure->parent);
return NULL;
}
- cairo_surface_set_user_data(
- surface, &fiv_io_key_render, closure, load_resvg_destroy);
- return surface;
+ image->render = &closure->parent;
+ return image;
}
#endif // HAVE_RESVG ----------------------------------------------------------
@@ -1937,43 +2199,54 @@ typedef struct {
} FivIoRenderClosureLibrsvg;
static void
-load_librsvg_destroy(void *closure)
+load_librsvg_destroy(FivIoRenderClosure *closure)
{
- FivIoRenderClosureLibrsvg *self = closure;
+ FivIoRenderClosureLibrsvg *self = (void *) closure;
g_object_unref(self->handle);
g_free(self);
}
-static cairo_surface_t *
-load_librsvg_render(FivIoRenderClosure *closure, double scale)
+static FivIoImage *
+load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
+ FivIoCmm *cmm, FivIoProfile *target, GError **error)
{
- FivIoRenderClosureLibrsvg *self = (FivIoRenderClosureLibrsvg *) closure;
RsvgRectangle viewport = {.x = 0, .y = 0,
.width = self->width * scale, .height = self->height * scale};
- cairo_surface_t *surface = cairo_image_surface_create(
+ FivIoImage *image = fiv_io_image_new(
CAIRO_FORMAT_ARGB32, ceil(viewport.width), ceil(viewport.height));
+ if (!image) {
+ set_error(error, "image allocation failure");
+ return NULL;
+ }
- GError *error = NULL;
+ cairo_surface_t *surface = fiv_io_image_to_surface_noref(image);
cairo_t *cr = cairo_create(surface);
- (void) rsvg_handle_render_document(self->handle, cr, &viewport, &error);
+ cairo_surface_destroy(surface);
+ gboolean success =
+ rsvg_handle_render_document(self->handle, cr, &viewport, error);
+ cairo_status_t status = cairo_status(cr);
cairo_destroy(cr);
- if (error) {
- g_debug("%s", error->message);
- g_error_free(error);
- cairo_surface_destroy(surface);
+ if (!success) {
+ fiv_io_image_unref(image);
return NULL;
}
-
- cairo_status_t surface_status = cairo_surface_status(surface);
- if (surface_status != CAIRO_STATUS_SUCCESS) {
- g_debug("%s", cairo_status_to_string(surface_status));
- cairo_surface_destroy(surface);
+ if (status != CAIRO_STATUS_SUCCESS) {
+ set_error(error, cairo_status_to_string(status));
+ fiv_io_image_unref(image);
return NULL;
}
- return surface;
+ return fiv_io_cmm_finish(cmm, image, target);
}
-static cairo_surface_t *
+static FivIoImage *
+load_librsvg_render(FivIoRenderClosure *closure,
+ FivIoCmm *cmm, FivIoProfile *target, double scale)
+{
+ FivIoRenderClosureLibrsvg *self = (FivIoRenderClosureLibrsvg *) closure;
+ return load_librsvg_render_internal(self, scale, cmm, target, NULL);
+}
+
+static FivIoImage *
open_librsvg(
const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
{
@@ -2010,32 +2283,24 @@ open_librsvg(
h = viewbox.height;
}
- // librsvg rasterizes filters, so this method isn't fully appropriate.
- // It might be worth removing altogether.
- 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);
-
- cairo_t *cr = cairo_create(surface);
- 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);
-
FivIoRenderClosureLibrsvg *closure = g_malloc0(sizeof *closure);
closure->parent.render = load_librsvg_render;
+ closure->parent.destroy = load_librsvg_destroy;
closure->handle = handle;
closure->width = w;
closure->height = h;
- cairo_surface_set_user_data(
- surface, &fiv_io_key_render, closure, load_librsvg_destroy);
- return surface;
+
+ // librsvg rasterizes filters, so rendering to a recording Cairo surface
+ // has been abandoned.
+ FivIoImage *image = load_librsvg_render_internal(
+ closure, 1., ctx->cmm, ctx->screen_profile, error);
+ if (!image) {
+ load_librsvg_destroy(&closure->parent);
+ return NULL;
+ }
+
+ image->render = &closure->parent;
+ return image;
}
#endif // HAVE_LIBRSVG --------------------------------------------------------
@@ -2108,8 +2373,9 @@ static const XcursorFile fiv_io_xcursor_adaptor = {
.seek = fiv_io_xcursor_seek,
};
-static cairo_surface_t *
-open_xcursor(const char *data, gsize len, GError **error)
+static FivIoImage *
+open_xcursor(
+ const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
{
if (len > G_MAXLONG) {
set_error(error, "size overflow");
@@ -2130,54 +2396,46 @@ open_xcursor(const char *data, gsize len, GError **error)
}
// Interpret cursors as animated pages.
- cairo_surface_t *pages = NULL, *frames_head = NULL, *frames_tail = NULL;
+ FivIoImage *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];
+ FivIoImage *I =
+ fiv_io_image_new(CAIRO_FORMAT_ARGB32, image->width, image->height);
+ if (!I) {
+ add_warning(ctx, "%s", "image allocation failure");
+ break;
+ }
+
// 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);
+ memcpy(I->data, image->pixels, I->stride * I->height);
+ I->frame_duration = image->delay;
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);
+ I->frame_previous = frames_tail;
+ frames_tail->frame_next = I;
} 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;
+ frames_head->frame_previous = frames_tail;
+
+ frames_head->page_next = I;
+ I->page_previous = frames_head;
+ frames_head = I;
} else {
- pages = frames_head = surface;
+ pages = frames_head = I;
}
- frames_tail = surface;
+ frames_tail = I;
last_nominal = image->size;
}
- if (!pages) {
- XcursorImagesDestroy(images);
+ XcursorImagesDestroy(images);
+ if (!pages)
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);
+ frames_head->frame_previous = frames_tail;
// Do not bother doing colour correction, there is no correct rendering.
return pages;
@@ -2186,10 +2444,10 @@ open_xcursor(const char *data, gsize len, GError **error)
#endif // HAVE_XCURSOR --------------------------------------------------------
#ifdef HAVE_LIBHEIF //---------------------------------------------------------
-static cairo_surface_t *
+static FivIoImage *
load_libheif_image(struct heif_image_handle *handle, GError **error)
{
- cairo_surface_t *surface = NULL;
+ FivIoImage *I = 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) {
@@ -2212,13 +2470,10 @@ load_libheif_image(struct heif_image_handle *handle, GError **error)
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(
+ I = fiv_io_image_new(
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;
+ if (!I) {
+ set_error(error, "image allocation failure");
goto fail_process;
}
@@ -2226,11 +2481,8 @@ load_libheif_image(struct heif_image_handle *handle, GError **error)
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);
+ uint32_t *dstp = (uint32_t *) (I->data + I->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]);
@@ -2240,7 +2492,7 @@ load_libheif_image(struct heif_image_handle *handle, GError **error)
// TODO(p): Test real behaviour on real transparent images.
if (has_alpha && !heif_image_handle_is_premultiplied_alpha(handle))
- fiv_io_premultiply_argb32(surface);
+ fiv_io_premultiply_argb32(I);
heif_item_id exif_id = 0;
if (heif_image_handle_get_list_of_metadata_block_IDs(
@@ -2252,9 +2504,7 @@ load_libheif_image(struct heif_image_handle *handle, GError **error)
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);
+ I->exif = g_bytes_new_take(exif, exif_len);
}
}
@@ -2268,26 +2518,22 @@ load_libheif_image(struct heif_image_handle *handle, GError **error)
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);
+ I->icc = g_bytes_new_take(icc, icc_len);
}
}
- cairo_surface_mark_dirty(surface);
-
fail_process:
heif_image_release(image);
fail_decode:
heif_decoding_options_free(opts);
fail:
- return surface;
+ return I;
}
static void
load_libheif_aux_images(const FivIoOpenContext *ioctx,
- struct heif_image_handle *top, cairo_surface_t **result,
- cairo_surface_t **result_tail)
+ struct heif_image_handle *top,
+ FivIoImage **result, FivIoImage **result_tail)
{
// Include the depth image, we have no special processing for it now.
int filter = LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA;
@@ -2317,14 +2563,14 @@ load_libheif_aux_images(const FivIoOpenContext *ioctx,
g_free(ids);
}
-static cairo_surface_t *
+static FivIoImage *
open_libheif(
const char *data, gsize len, const FivIoOpenContext *ioctx, 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;
+ FivIoImage *result = NULL, *result_tail = NULL;
struct heif_error err;
err = heif_context_read_from_memory_without_copy(ctx, data, len, NULL);
@@ -2356,14 +2602,14 @@ open_libheif(
heif_image_handle_release(handle);
}
if (!result) {
- g_clear_pointer(&result, cairo_surface_destroy);
+ g_clear_pointer(&result, fiv_io_image_unref);
set_error(error, "empty or unsupported image");
}
g_free(ids);
fail_read:
heif_context_free(ctx);
- return fiv_io_profile_finalize(result, ioctx->screen_profile);
+ return fiv_io_cmm_finish(ioctx->cmm, result, ioctx->screen_profile);
}
#endif // HAVE_LIBHEIF --------------------------------------------------------
@@ -2470,7 +2716,7 @@ fiv_io_tiff_warning(G_GNUC_UNUSED thandle_t h,
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static cairo_surface_t *
+static FivIoImage *
load_libtiff_directory(TIFF *tiff, GError **error)
{
char emsg[1024] = "";
@@ -2486,22 +2732,26 @@ load_libtiff_directory(TIFF *tiff, GError **error)
return NULL;
}
- cairo_surface_t *surface = NULL;
+ FivIoImage *I = 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(image.alpha != EXTRASAMPLE_UNSPECIFIED
+ I = fiv_io_image_new(image.alpha != EXTRASAMPLE_UNSPECIFIED
? CAIRO_FORMAT_ARGB32
: CAIRO_FORMAT_RGB24,
image.width, image.height);
+ if (!I) {
+ set_error(error, "image allocation failure");
+ goto fail;
+ }
image.req_orientation = ORIENTATION_LEFTTOP;
- uint32_t *raster = (uint32_t *) cairo_image_surface_get_data(surface);
+ uint32_t *raster = (uint32_t *) I->data;
if (!TIFFRGBAImageGet(&image, raster, image.width, image.height)) {
- g_clear_pointer(&surface, cairo_surface_destroy);
+ g_clear_pointer(&I, fiv_io_image_unref);
goto fail;
}
@@ -2513,40 +2763,36 @@ load_libtiff_directory(TIFF *tiff, GError **error)
}
// It seems that neither GIMP nor Photoshop use unassociated alpha.
if (image.alpha == EXTRASAMPLE_UNASSALPHA)
- fiv_io_premultiply_argb32(surface);
+ fiv_io_premultiply_argb32(I);
- cairo_surface_mark_dirty(surface);
// XXX: The whole file is essentially an Exif, any ideas?
+ // TODO(p): TIFF has a number of fields that an ICC profile can be
+ // constructed from--it's not a good idea to blindly default to sRGB
+ // if we don't find an ICC profile.
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);
- }
+ if (TIFFGetField(tiff, TIFFTAG_ICCPROFILE, &meta_len, &meta))
+ I->icc = g_bytes_new(meta, meta_len);
+ if (TIFFGetField(tiff, TIFFTAG_XMLPACKET, &meta_len, &meta))
+ I->xmp = g_bytes_new(meta, meta_len);
// 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);
+ I->orientation = 5;
if (orientation == 6 || orientation == 8)
- cairo_surface_set_user_data(
- surface, &fiv_io_key_orientation, (void *) (uintptr_t) 7, NULL);
+ I->orientation = 7;
}
fail:
TIFFRGBAImageEnd(&image);
// TODO(p): It's possible to implement ClipPath easily with Cairo.
- return surface;
+ return I;
}
-static cairo_surface_t *
+static FivIoImage *
open_libtiff(
const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
{
@@ -2562,42 +2808,19 @@ open_libtiff(
.len = len,
};
- cairo_surface_t *result = NULL, *result_tail = NULL;
+ FivIoImage *result = NULL, *result_tail = NULL;
TIFF *tiff = TIFFClientOpen(ctx->uri, "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)) {
+ load_libtiff_directory(tiff, &err), &result, &result_tail) &&
+ err) {
add_warning(ctx, "%s", err->message);
g_error_free(err);
}
@@ -2606,7 +2829,7 @@ open_libtiff(
fail:
if (h.error) {
- g_clear_pointer(&result, cairo_surface_destroy);
+ g_clear_pointer(&result, fiv_io_image_unref);
set_error(error, h.error);
g_free(h.error);
} else if (!result) {
@@ -2617,28 +2840,25 @@ fail:
TIFFSetWarningHandlerExt(whe);
TIFFSetErrorHandler(eh);
TIFFSetWarningHandler(wh);
-
- // TODO(p): Colour management even for un/associated alpha channels.
- // Note that TIFF has a number of fields that an ICC profile can be
- // constructed from--it's not a good idea to blindly assume sRGB.
- return fiv_io_profile_finalize(result, ctx->screen_profile);
+ return fiv_io_cmm_finish(ctx->cmm, result, ctx->screen_profile);
}
#endif // HAVE_LIBTIFF --------------------------------------------------------
#ifdef HAVE_GDKPIXBUF // ------------------------------------------------------
-static cairo_surface_t *
+static FivIoImage *
load_gdkpixbuf_argb32_unpremultiplied(GdkPixbuf *pixbuf)
{
int w = gdk_pixbuf_get_width(pixbuf);
int h = gdk_pixbuf_get_height(pixbuf);
- cairo_surface_t *surface =
- cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
+ FivIoImage *image = fiv_io_image_new(CAIRO_FORMAT_ARGB32, w, h);
+ if (!image)
+ return NULL;
guint length = 0;
guchar *src = gdk_pixbuf_get_pixels_with_length(pixbuf, &length);
int src_stride = gdk_pixbuf_get_rowstride(pixbuf);
- uint32_t *dst = (uint32_t *) cairo_image_surface_get_data(surface);
+ uint32_t *dst = (uint32_t *) image->data;
for (int y = 0; y < h; y++) {
const guchar *p = src + y * src_stride;
for (int x = 0; x < w; x++) {
@@ -2646,11 +2866,10 @@ load_gdkpixbuf_argb32_unpremultiplied(GdkPixbuf *pixbuf)
p += 4;
}
}
- cairo_surface_mark_dirty(surface);
- return surface;
+ return image;
}
-static cairo_surface_t *
+static FivIoImage *
open_gdkpixbuf(
const char *data, gsize len, const FivIoOpenContext *ctx, GError **error)
{
@@ -2668,16 +2887,33 @@ open_gdkpixbuf(
gdk_pixbuf_get_n_channels(pixbuf) == 4 &&
gdk_pixbuf_get_bits_per_sample(pixbuf) == 8;
- cairo_surface_t *surface = NULL;
- if (custom_argb32)
- surface = load_gdkpixbuf_argb32_unpremultiplied(pixbuf);
- else
- surface = gdk_cairo_surface_create_from_pixbuf(pixbuf, 1, NULL);
-
- cairo_status_t surface_status = cairo_surface_status(surface);
- if (surface_status != CAIRO_STATUS_SUCCESS) {
- set_error(error, cairo_status_to_string(surface_status));
+ FivIoImage *image = NULL;
+ if (custom_argb32) {
+ image = load_gdkpixbuf_argb32_unpremultiplied(pixbuf);
+ } else if ((image = fiv_io_image_new(CAIRO_FORMAT_ARGB32,
+ gdk_pixbuf_get_width(pixbuf), gdk_pixbuf_get_height(pixbuf)))) {
+ // TODO(p): Ideally, don't go through Cairo at all.
+ cairo_surface_t *surface = fiv_io_image_to_surface_noref(image);
+ cairo_t *cr = cairo_create(surface);
cairo_surface_destroy(surface);
+
+ // Don't depend on GDK being initialized, to speed up thumbnailing
+ // (calling gdk_cairo_surface_create_from_pixbuf() would).
+ gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
+ cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
+ cairo_paint(cr);
+
+ // If the source was opaque, so will be the destination.
+ if (cairo_pattern_get_surface(cairo_get_source(cr), &surface) ==
+ CAIRO_STATUS_SUCCESS) {
+ if (cairo_surface_get_content(surface) == CAIRO_CONTENT_COLOR)
+ image->format = CAIRO_FORMAT_RGB24;
+ }
+ cairo_destroy(cr);
+ }
+
+ if (!image) {
+ set_error(error, "image allocation failure");
g_object_unref(pixbuf);
return NULL;
}
@@ -2686,52 +2922,29 @@ open_gdkpixbuf(
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);
+ image->orientation = n;
}
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);
- }
+ if (raw)
+ image->icc = g_bytes_new_take(raw, out_len);
}
g_object_unref(pixbuf);
- if (custom_argb32) {
- fiv_io_profile_xrgb32_page(surface, ctx->screen_profile);
- fiv_io_premultiply_argb32_page(surface);
- } else {
- surface = fiv_io_profile_finalize(surface, ctx->screen_profile);
- }
- return surface;
+ if (custom_argb32)
+ fiv_io_cmm_argb32_premultiply_page(
+ ctx->cmm, image, ctx->screen_profile);
+ else
+ image = fiv_io_cmm_finish(ctx->cmm, image, ctx->screen_profile);
+ return image;
}
#endif // HAVE_GDKPIXBUF ------------------------------------------------------
-// TODO(p): Check that all cairo_surface_set_user_data() calls succeed.
-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_thum;
-cairo_user_data_key_t fiv_io_key_text;
-
-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_user_data_key_t fiv_io_key_render;
-
-cairo_surface_t *
+FivIoImage *
fiv_io_open(const FivIoOpenContext *ctx, GError **error)
{
// TODO(p): Don't always load everything into memory, test type first,
@@ -2752,56 +2965,69 @@ fiv_io_open(const FivIoOpenContext *ctx, GError **error)
gchar *data = NULL;
gsize len = 0;
- if (!g_file_load_contents(file, NULL, &data, &len, NULL, error))
+ gboolean success =
+ g_file_load_contents(file, NULL, &data, &len, NULL, error);
+ g_object_unref(file);
+ if (!success)
return NULL;
- cairo_surface_t *surface = fiv_io_open_from_data(data, len, ctx, error);
+ FivIoImage *image = fiv_io_open_from_data(data, len, ctx, error);
g_free(data);
- return surface;
+ return image;
}
-cairo_surface_t *
+FivIoImage *
fiv_io_open_from_data(
const char *data, size_t len, const FivIoOpenContext *ctx, GError **error)
{
wuffs_base__slice_u8 prefix =
wuffs_base__make_slice_u8((uint8_t *) data, len);
- cairo_surface_t *surface = NULL;
+ FivIoImage *image = NULL;
switch (wuffs_base__magic_number_guess_fourcc(prefix, true /* closed */)) {
case WUFFS_BASE__FOURCC__BMP:
// Note that BMP can redirect into another format,
// which is so far unsupported here.
- surface = open_wuffs_using(
+ image = open_wuffs_using(
wuffs_bmp__decoder__alloc_as__wuffs_base__image_decoder, data, len,
ctx, error);
break;
case WUFFS_BASE__FOURCC__GIF:
- surface = open_wuffs_using(
+ image = open_wuffs_using(
wuffs_gif__decoder__alloc_as__wuffs_base__image_decoder, data, len,
ctx, error);
break;
case WUFFS_BASE__FOURCC__PNG:
- surface = open_wuffs_using(
+ image = open_wuffs_using(
wuffs_png__decoder__alloc_as__wuffs_base__image_decoder, data, len,
ctx, error);
break;
case WUFFS_BASE__FOURCC__TGA:
- surface = open_wuffs_using(
+ image = open_wuffs_using(
wuffs_tga__decoder__alloc_as__wuffs_base__image_decoder, data, len,
ctx, error);
break;
case WUFFS_BASE__FOURCC__JPEG:
- surface = ctx->enhance
- ? open_libjpeg_enhanced(data, len, ctx, error)
- : open_libjpeg_turbo(data, len, ctx, error);
+ image = open_libjpeg_turbo(data, len, ctx, error);
break;
case WUFFS_BASE__FOURCC__WEBP:
- surface = open_libwebp(data, len, ctx, error);
+ image = open_libwebp(data, len, ctx, error);
break;
default:
+ // Try to extract full-size previews from TIFF/EP-compatible raws,
+ // but allow for running the full render.
+#ifdef HAVE_LIBRAW // ---------------------------------------------------------
+ if (!ctx->enhance) {
+#endif // HAVE_LIBRAW ---------------------------------------------------------
+ if ((image = open_tiff_ep(data, len, ctx, error)))
+ break;
+ if (error) {
+ g_debug("%s", (*error)->message);
+ g_clear_error(error);
+ }
#ifdef HAVE_LIBRAW // ---------------------------------------------------------
- if ((surface = open_libraw(data, len, error)))
+ }
+ if ((image = open_libraw(data, len, ctx, error)))
break;
// TODO(p): We should try to pass actual processing errors through,
@@ -2812,7 +3038,7 @@ fiv_io_open_from_data(
}
#endif // HAVE_LIBRAW ---------------------------------------------------------
#ifdef HAVE_RESVG // ----------------------------------------------------------
- if ((surface = open_resvg(data, len, ctx, error)))
+ if ((image = open_resvg(data, len, ctx, error)))
break;
if (error) {
g_debug("%s", (*error)->message);
@@ -2820,7 +3046,7 @@ fiv_io_open_from_data(
}
#endif // HAVE_RESVG ----------------------------------------------------------
#ifdef HAVE_LIBRSVG // --------------------------------------------------------
- if ((surface = open_librsvg(data, len, ctx, error)))
+ if ((image = open_librsvg(data, len, ctx, error)))
break;
// XXX: It doesn't look like librsvg can return sensible errors.
@@ -2830,7 +3056,7 @@ fiv_io_open_from_data(
}
#endif // HAVE_LIBRSVG --------------------------------------------------------
#ifdef HAVE_XCURSOR //---------------------------------------------------------
- if ((surface = open_xcursor(data, len, error)))
+ if ((image = open_xcursor(data, len, ctx, error)))
break;
if (error) {
g_debug("%s", (*error)->message);
@@ -2838,7 +3064,7 @@ fiv_io_open_from_data(
}
#endif // HAVE_XCURSOR --------------------------------------------------------
#ifdef HAVE_LIBHEIF //---------------------------------------------------------
- if ((surface = open_libheif(data, len, ctx, error)))
+ if ((image = open_libheif(data, len, ctx, error)))
break;
if (error) {
g_debug("%s", (*error)->message);
@@ -2847,7 +3073,7 @@ fiv_io_open_from_data(
#endif // HAVE_LIBHEIF --------------------------------------------------------
#ifdef HAVE_LIBTIFF //---------------------------------------------------------
// This needs to be positioned after LibRaw.
- if ((surface = open_libtiff(data, len, ctx, error)))
+ if ((image = open_libtiff(data, len, ctx, error)))
break;
if (error) {
g_debug("%s", (*error)->message);
@@ -2860,10 +3086,12 @@ fiv_io_open_from_data(
#ifdef HAVE_GDKPIXBUF // ------------------------------------------------------
// This is used as a last resort, the rest above is special-cased.
- if (!surface) {
+ if (!image) {
GError *err = NULL;
- if ((surface = open_gdkpixbuf(data, len, ctx, &err))) {
+ if ((image = open_gdkpixbuf(data, len, ctx, &err))) {
g_clear_error(error);
+ } else if (!err) {
+ // Contrary to documentation, this is a possible outcome (libheif).
} else if (err->code == GDK_PIXBUF_ERROR_UNKNOWN_TYPE) {
g_error_free(err);
} else {
@@ -2876,17 +3104,13 @@ fiv_io_open_from_data(
// 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);
+ if (image && image->exif &&
+ (exif_data = g_bytes_get_data(image->exif, &exif_len))) {
+ image->orientation = fiv_io_exif_orientation(exif_data, exif_len);
}
- return surface;
+ return image;
}
// --- Thumbnail passing utilities ---------------------------------------------
@@ -2956,380 +3180,112 @@ fiv_io_deserialize(GBytes *bytes, guint64 *user_data)
return surface;
}
-// --- Filesystem --------------------------------------------------------------
-
-#include "xdg.h"
-
-static void
-model_entry_finalize(FivIoModelEntry *entry)
-{
- g_free(entry->uri);
- g_free(entry->target_uri);
- g_free(entry->collate_key);
-}
-
-struct _FivIoModel {
- GObject parent_instance;
- GPatternSpec **supported_patterns;
-
- GFile *directory; ///< Currently loaded directory
- GFileMonitor *monitor; ///< "directory" monitoring
- GArray *subdirs; ///< "directory" contents
- GArray *files; ///< "directory" contents
-
- FivIoModelSort sort_field; ///< How to sort
- gboolean sort_descending; ///< Whether to sort in reverse
- gboolean filtering; ///< Only show non-hidden, supported
-};
-
-G_DEFINE_TYPE(FivIoModel, fiv_io_model, G_TYPE_OBJECT)
-
-enum {
- PROP_FILTERING = 1,
- PROP_SORT_FIELD,
- PROP_SORT_DESCENDING,
- N_PROPERTIES
-};
-
-static GParamSpec *model_properties[N_PROPERTIES];
-
-enum {
- FILES_CHANGED,
- SUBDIRECTORIES_CHANGED,
- LAST_SIGNAL,
-};
-
-// Globals are, sadly, the canonical way of storing signal numbers.
-static guint model_signals[LAST_SIGNAL];
-
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static gboolean
-model_supports(FivIoModel *self, const char *filename)
-{
- gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
- if (!utf8)
- return FALSE;
-
- gchar *lc = g_utf8_strdown(utf8, -1);
- gsize lc_length = strlen(lc);
- gchar *reversed = g_utf8_strreverse(lc, lc_length);
- g_free(utf8);
-
- // fnmatch() uses the /locale encoding/, and isn't present on Windows.
- // TODO(p): Consider using g_file_info_get_display_name() for direct UTF-8.
- gboolean result = FALSE;
- for (GPatternSpec **p = self->supported_patterns; *p; p++)
- if ((result = g_pattern_spec_match(*p, lc_length, lc, reversed)))
- break;
-
- g_free(lc);
- g_free(reversed);
- return result;
-}
-
-static inline int
-model_compare_entries(FivIoModel *self,
- const FivIoModelEntry *entry1, GFile *file1,
- const FivIoModelEntry *entry2, GFile *file2)
-{
- if (g_file_has_prefix(file1, file2))
- return +1;
- if (g_file_has_prefix(file2, file1))
- return -1;
-
- int result = 0;
- switch (self->sort_field) {
- case FIV_IO_MODEL_SORT_MTIME:
- result -= entry1->mtime_msec < entry2->mtime_msec;
- result += entry1->mtime_msec > entry2->mtime_msec;
- if (result != 0)
- break;
-
- // Fall-through
- case FIV_IO_MODEL_SORT_NAME:
- case FIV_IO_MODEL_SORT_COUNT:
- result = strcmp(entry1->collate_key, entry2->collate_key);
- }
- return self->sort_descending ? -result : +result;
-}
-
-static gint
-model_compare(gconstpointer a, gconstpointer b, gpointer user_data)
+static cairo_status_t
+write_to_byte_array(
+ void *closure, const unsigned char *data, unsigned int length)
{
- const FivIoModelEntry *entry1 = a;
- const FivIoModelEntry *entry2 = b;
- GFile *file1 = g_file_new_for_uri(entry1->uri);
- GFile *file2 = g_file_new_for_uri(entry2->uri);
- int result = model_compare_entries(user_data, entry1, file1, entry2, file2);
- g_object_unref(file1);
- g_object_unref(file2);
- return result;
+ g_byte_array_append(closure, data, length);
+ return CAIRO_STATUS_SUCCESS;
}
-static void
-model_resort(FivIoModel *self)
+GBytes *
+fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error)
{
- g_array_sort_with_data(self->subdirs, model_compare, self);
- g_array_sort_with_data(self->files, model_compare, self);
-
- g_signal_emit(self, model_signals[FILES_CHANGED], 0);
- g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0);
-}
-
-static gboolean
-model_reload(FivIoModel *self, GError **error)
-{
- g_array_set_size(self->subdirs, 0);
- g_array_set_size(self->files, 0);
-
- GFileEnumerator *enumerator = g_file_enumerate_children(self->directory,
- G_FILE_ATTRIBUTE_STANDARD_TYPE ","
- G_FILE_ATTRIBUTE_STANDARD_NAME ","
- G_FILE_ATTRIBUTE_STANDARD_TARGET_URI ","
- G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
- G_FILE_ATTRIBUTE_TIME_MODIFIED ","
- G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC,
- G_FILE_QUERY_INFO_NONE, NULL, error);
- if (!enumerator) {
- // Note that this has had a side-effect of clearing all entries.
- g_signal_emit(self, model_signals[FILES_CHANGED], 0);
- g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0);
- return FALSE;
- }
-
- GFileInfo *info = NULL;
- GFile *child = NULL;
- GError *e = NULL;
- while (TRUE) {
- if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, &e) &&
- e) {
- g_warning("%s", e->message);
- g_clear_error(&e);
- continue;
- }
-
- if (!info)
- break;
- if (self->filtering && g_file_info_get_is_hidden(info))
- continue;
+ g_return_val_if_fail(
+ surface && cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE,
+ NULL);
- FivIoModelEntry entry = {.uri = g_file_get_uri(child),
- .target_uri = g_strdup(g_file_info_get_attribute_string(
- info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI))};
- GDateTime *mtime = g_file_info_get_modification_date_time(info);
- if (mtime) {
- entry.mtime_msec = g_date_time_to_unix(mtime) * 1000 +
- g_date_time_get_microsecond(mtime) / 1000;
- g_date_time_unref(mtime);
+ cairo_format_t format = cairo_image_surface_get_format(surface);
+ if (format == CAIRO_FORMAT_ARGB32) {
+ const uint32_t *data =
+ (const uint32_t *) cairo_image_surface_get_data(surface);
+
+ bool all_solid = true;
+ for (size_t len = cairo_image_surface_get_width(surface) *
+ cairo_image_surface_get_height(surface); len--; ) {
+ if ((data[len] >> 24) != 0xFF)
+ all_solid = false;
}
+ if (all_solid)
+ format = CAIRO_FORMAT_RGB24;
+ }
+
+ if (format != CAIRO_FORMAT_RGB24) {
+#if CAIRO_HAS_PNG_FUNCTIONS
+ GByteArray *ba = g_byte_array_new();
+ cairo_status_t status =
+ cairo_surface_write_to_png_stream(surface, write_to_byte_array, ba);
+ if (status == CAIRO_STATUS_SUCCESS)
+ return g_byte_array_free_to_bytes(ba);
+ g_byte_array_unref(ba);
+#endif
- gchar *parse_name = g_file_get_parse_name(child);
- // TODO(p): Make it possible to use g_utf8_collate_key() instead,
- // which does not use natural sorting.
- entry.collate_key = g_utf8_collate_key_for_filename(parse_name, -1);
- g_free(parse_name);
-
- const char *name = g_file_info_get_name(info);
- if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
- g_array_append_val(self->subdirs, entry);
- else if (!self->filtering || model_supports(self, name))
- g_array_append_val(self->files, entry);
- else
- model_entry_finalize(&entry);
+ // Last resort: remove transparency by painting over black.
+ cairo_surface_t *converted =
+ cairo_image_surface_create(CAIRO_FORMAT_RGB24,
+ cairo_image_surface_get_width(surface),
+ cairo_image_surface_get_height(surface));
+ cairo_t *cr = cairo_create(converted);
+ cairo_set_source_surface(cr, surface, 0, 0);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+ cairo_paint(cr);
+ cairo_destroy(cr);
+ GBytes *result = fiv_io_serialize_for_search(converted, error);
+ cairo_surface_destroy(converted);
+ return result;
}
- g_object_unref(enumerator);
-
- // We also emit change signals there, indirectly.
- model_resort(self);
- return TRUE;
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static void
-fiv_io_model_finalize(GObject *gobject)
-{
- FivIoModel *self = FIV_IO_MODEL(gobject);
- for (GPatternSpec **p = self->supported_patterns; *p; p++)
- g_pattern_spec_free(*p);
- g_free(self->supported_patterns);
-
- g_clear_object(&self->directory);
- g_clear_object(&self->monitor);
- g_array_free(self->subdirs, TRUE);
- g_array_free(self->files, TRUE);
-
- G_OBJECT_CLASS(fiv_io_model_parent_class)->finalize(gobject);
-}
-static void
-fiv_io_model_get_property(
- GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
-{
- FivIoModel *self = FIV_IO_MODEL(object);
- switch (property_id) {
- case PROP_FILTERING:
- g_value_set_boolean(value, self->filtering);
- break;
- case PROP_SORT_FIELD:
- g_value_set_int(value, self->sort_field);
- break;
- case PROP_SORT_DESCENDING:
- g_value_set_boolean(value, self->sort_descending);
- break;
- default:
- G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ tjhandle enc = tjInitCompress();
+ if (!enc) {
+ set_error(error, tjGetErrorStr2(enc));
+ return NULL;
}
-}
-static void
-fiv_io_model_set_property(
- GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
-{
- FivIoModel *self = FIV_IO_MODEL(object);
- switch (property_id) {
- case PROP_FILTERING:
- if (self->filtering != g_value_get_boolean(value)) {
- self->filtering = !self->filtering;
- g_object_notify_by_pspec(object, model_properties[property_id]);
- (void) model_reload(self, NULL /* error */);
- }
- break;
- case PROP_SORT_FIELD:
- if ((int) self->sort_field != g_value_get_int(value)) {
- self->sort_field = g_value_get_int(value);
- g_object_notify_by_pspec(object, model_properties[property_id]);
- model_resort(self);
- }
- break;
- case PROP_SORT_DESCENDING:
- if (self->sort_descending != g_value_get_boolean(value)) {
- self->sort_descending = !self->sort_descending;
- g_object_notify_by_pspec(object, model_properties[property_id]);
- model_resort(self);
- }
- break;
- default:
- G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ unsigned char *jpeg = NULL;
+ unsigned long length = 0;
+ if (tjCompress2(enc, cairo_image_surface_get_data(surface),
+ cairo_image_surface_get_width(surface),
+ cairo_image_surface_get_stride(surface),
+ cairo_image_surface_get_height(surface),
+ (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRX : TJPF_XRGB),
+ &jpeg, &length, TJSAMP_444, 90, 0)) {
+ set_error(error, tjGetErrorStr2(enc));
+ tjFree(jpeg);
+ tjDestroy(enc);
+ return NULL;
}
-}
-static void
-fiv_io_model_class_init(FivIoModelClass *klass)
-{
- GObjectClass *object_class = G_OBJECT_CLASS(klass);
- object_class->get_property = fiv_io_model_get_property;
- object_class->set_property = fiv_io_model_set_property;
- object_class->finalize = fiv_io_model_finalize;
-
- model_properties[PROP_FILTERING] = g_param_spec_boolean(
- "filtering", "Filtering", "Only show non-hidden, supported entries",
- TRUE, G_PARAM_READWRITE);
- // TODO(p): GObject enumerations are annoying, but this should be one.
- model_properties[PROP_SORT_FIELD] = g_param_spec_int(
- "sort-field", "Sort field", "Sort order",
- FIV_IO_MODEL_SORT_MIN, FIV_IO_MODEL_SORT_MAX,
- FIV_IO_MODEL_SORT_NAME, G_PARAM_READWRITE);
- model_properties[PROP_SORT_DESCENDING] = g_param_spec_boolean(
- "sort-descending", "Sort descending", "Use reverse sort order",
- FALSE, G_PARAM_READWRITE);
- g_object_class_install_properties(
- object_class, N_PROPERTIES, model_properties);
-
- // TODO(p): Arguments something like: index, added, removed.
- model_signals[FILES_CHANGED] =
- g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
- NULL, NULL, NULL, G_TYPE_NONE, 0);
- model_signals[SUBDIRECTORIES_CHANGED] =
- g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
- NULL, NULL, NULL, G_TYPE_NONE, 0);
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static void
-fiv_io_model_init(FivIoModel *self)
-{
- self->filtering = TRUE;
-
- char **types = fiv_io_all_supported_media_types();
- char **globs = extract_mime_globs((const char **) types);
- g_strfreev(types);
-
- gsize n = g_strv_length(globs);
- self->supported_patterns =
- g_malloc0_n(n + 1, sizeof *self->supported_patterns);
- while (n--)
- self->supported_patterns[n] = g_pattern_spec_new(globs[n]);
- g_strfreev(globs);
-
- self->files = g_array_new(FALSE, TRUE, sizeof(FivIoModelEntry));
- self->subdirs = g_array_new(FALSE, TRUE, sizeof(FivIoModelEntry));
- g_array_set_clear_func(
- self->subdirs, (GDestroyNotify) model_entry_finalize);
- g_array_set_clear_func(
- self->files, (GDestroyNotify) model_entry_finalize);
-}
-
-gboolean
-fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error)
-{
- g_return_val_if_fail(FIV_IS_IO_MODEL(self), FALSE);
- g_return_val_if_fail(G_IS_FILE(directory), FALSE);
-
- g_clear_object(&self->directory);
- g_clear_object(&self->monitor);
- self->directory = g_object_ref(directory);
-
- // TODO(p): Process the ::changed signal.
- self->monitor = g_file_monitor_directory(
- directory, G_FILE_MONITOR_WATCH_MOVES, NULL, NULL /* error */);
- return model_reload(self, error);
-}
-
-GFile *
-fiv_io_model_get_location(FivIoModel *self)
-{
- g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
- return self->directory;
-}
-
-const FivIoModelEntry *
-fiv_io_model_get_files(FivIoModel *self, gsize *len)
-{
- *len = self->files->len;
- return (const FivIoModelEntry *) self->files->data;
-}
-
-const FivIoModelEntry *
-fiv_io_model_get_subdirs(FivIoModel *self, gsize *len)
-{
- *len = self->subdirs->len;
- return (const FivIoModelEntry *) self->subdirs->data;
+ tjDestroy(enc);
+ return g_bytes_new_with_free_func(
+ jpeg, length, (GDestroyNotify) tjFree, jpeg);
}
// --- Export ------------------------------------------------------------------
unsigned char *
fiv_io_encode_webp(
- cairo_surface_t *surface, const WebPConfig *config, size_t *len)
+ FivIoImage *image, const WebPConfig *config, size_t *len)
{
- 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);
+ if (image->format != CAIRO_FORMAT_ARGB32 &&
+ image->format != CAIRO_FORMAT_RGB24) {
+ FivIoImage *converted =
+ fiv_io_image_new(CAIRO_FORMAT_ARGB32, image->width, image->height);
+
+ cairo_surface_t *surface = fiv_io_image_to_surface_noref(converted);
+ cairo_t *cr = cairo_create(surface);
+ cairo_surface_destroy(surface);
+
+ surface = fiv_io_image_to_surface_noref(image);
cairo_set_source_surface(cr, surface, 0, 0);
+ cairo_surface_destroy(surface);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr);
cairo_destroy(cr);
- surface = converted;
+ image = converted;
} else {
- surface = cairo_surface_reference(surface);
+ image = fiv_io_image_ref(image);
}
WebPMemoryWriter writer = {};
@@ -3339,27 +3295,26 @@ fiv_io_encode_webp(
goto fail;
picture.use_argb = true;
- picture.width = w;
- picture.height = h;
+ picture.width = image->width;
+ picture.height = image->height;
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)
+ if (picture.argb_stride != (int) image->width ||
+ picture.argb_stride * sizeof *picture.argb != image->stride ||
+ UINT32_MAX / picture.argb_stride < image->height)
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++)
+ memcpy(picture.argb, image->data, image->stride * image->height);
+ if (image->format == CAIRO_FORMAT_ARGB32)
+ for (int i = image->height * 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++)
+ for (int i = image->height * picture.argb_stride; i-- > 0; argb++)
*argb |= 0xFF000000;
// TODO(p): Prevent or propagate VP8_ENC_ERROR_BAD_DIMENSION.
@@ -3371,13 +3326,13 @@ fiv_io_encode_webp(
fail_compatibility:
WebPPictureFree(&picture);
fail:
- cairo_surface_destroy(surface);
+ fiv_io_image_unref(image);
*len = writer.size;
return writer.mem;
}
static WebPData
-encode_lossless_webp(cairo_surface_t *surface)
+encode_lossless_webp(FivIoImage *image)
{
WebPData bitstream = {};
WebPConfig config = {};
@@ -3388,12 +3343,12 @@ encode_lossless_webp(cairo_surface_t *surface)
if (!WebPValidateConfig(&config))
return bitstream;
- bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size);
+ bitstream.bytes = fiv_io_encode_webp(image, &config, &bitstream.size);
return bitstream;
}
static gboolean
-encode_webp_image(WebPMux *mux, cairo_surface_t *frame)
+encode_webp_image(WebPMux *mux, FivIoImage *frame)
{
WebPData bitstream = encode_lossless_webp(frame);
gboolean ok = WebPMuxSetImage(mux, &bitstream, true) == WEBP_MUX_OK;
@@ -3402,15 +3357,13 @@ encode_webp_image(WebPMux *mux, cairo_surface_t *frame)
}
static gboolean
-encode_webp_animation(WebPMux *mux, cairo_surface_t *page)
+encode_webp_animation(WebPMux *mux, FivIoImage *page)
{
gboolean ok = TRUE;
- for (cairo_surface_t *frame = page; ok && frame; frame =
- cairo_surface_get_user_data(frame, &fiv_io_key_frame_next)) {
+ for (FivIoImage *frame = page; ok && frame; frame = frame->frame_next) {
WebPMuxFrameInfo info = {
.bitstream = encode_lossless_webp(frame),
- .duration = (intptr_t) cairo_surface_get_user_data(
- frame, &fiv_io_key_frame_duration),
+ .duration = frame->frame_duration,
.id = WEBP_CHUNK_ANMF,
.dispose_method = WEBP_MUX_DISPOSE_NONE,
.blend_method = WEBP_MUX_NO_BLEND,
@@ -3420,8 +3373,7 @@ encode_webp_animation(WebPMux *mux, cairo_surface_t *page)
}
WebPMuxAnimParams params = {
.bgcolor = 0x00000000, // BGRA, curiously.
- .loop_count = (uintptr_t)
- cairo_surface_get_user_data(page, &fiv_io_key_loops),
+ .loop_count = page->loops,
};
return ok && WebPMuxSetAnimationParams(mux, &params) == WEBP_MUX_OK;
}
@@ -3439,7 +3391,7 @@ set_metadata(WebPMux *mux, const char *fourcc, GBytes *data)
}
gboolean
-fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, FivIoProfile target,
+fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile *target,
const char *path, GError **error)
{
g_return_val_if_fail(page != NULL, FALSE);
@@ -3449,17 +3401,14 @@ fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, FivIoProfile target,
WebPMux *mux = WebPMuxNew();
if (frame)
ok = encode_webp_image(mux, frame);
- else if (!cairo_surface_get_user_data(page, &fiv_io_key_frame_next))
+ else if (!page->frame_next)
ok = encode_webp_image(mux, page);
else
ok = encode_webp_animation(mux, page);
- ok = ok && set_metadata(mux, "EXIF",
- cairo_surface_get_user_data(page, &fiv_io_key_exif));
- ok = ok && set_metadata(mux, "ICCP",
- cairo_surface_get_user_data(page, &fiv_io_key_icc));
- ok = ok && set_metadata(mux, "XMP ",
- cairo_surface_get_user_data(page, &fiv_io_key_xmp));
+ ok = ok && set_metadata(mux, "EXIF", page->exif);
+ ok = ok && set_metadata(mux, "ICCP", page->icc);
+ ok = ok && set_metadata(mux, "XMP ", page->xmp);
GBytes *iccp = NULL;
if (ok && target && (iccp = fiv_io_profile_to_bytes(target)))
@@ -3484,71 +3433,62 @@ fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, FivIoProfile target,
// --- Metadata ----------------------------------------------------------------
void
-fiv_io_orientation_dimensions(cairo_surface_t *surface,
- FivIoOrientation orientation, double *w, double *h)
-{
- cairo_rectangle_t extents = {};
- switch (cairo_surface_get_type(surface)) {
- case CAIRO_SURFACE_TYPE_IMAGE:
- extents.width = cairo_image_surface_get_width(surface);
- extents.height = cairo_image_surface_get_height(surface);
- break;
- case CAIRO_SURFACE_TYPE_RECORDING:
- if (!cairo_recording_surface_get_extents(surface, &extents))
- cairo_recording_surface_ink_extents(surface,
- &extents.x, &extents.y, &extents.width, &extents.height);
- break;
- default:
- g_assert_not_reached();
- }
-
+fiv_io_orientation_dimensions(
+ const FivIoImage *image, FivIoOrientation orientation, double *w, double *h)
+{
switch (orientation) {
case FivIoOrientation90:
case FivIoOrientationMirror90:
case FivIoOrientation270:
case FivIoOrientationMirror270:
- *w = extents.height;
- *h = extents.width;
+ *w = image->height;
+ *h = image->width;
break;
default:
- *w = extents.width;
- *h = extents.height;
+ *w = image->width;
+ *h = image->height;
}
}
cairo_matrix_t
-fiv_io_orientation_apply(cairo_surface_t *surface,
+fiv_io_orientation_apply(const FivIoImage *image,
FivIoOrientation orientation, double *width, double *height)
{
- fiv_io_orientation_dimensions(surface, orientation, width, height);
+ fiv_io_orientation_dimensions(image, orientation, width, height);
+ return fiv_io_orientation_matrix(orientation, *width, *height);
+}
+cairo_matrix_t
+fiv_io_orientation_matrix(
+ FivIoOrientation orientation, double width, double height)
+{
cairo_matrix_t matrix = {};
cairo_matrix_init_identity(&matrix);
switch (orientation) {
case FivIoOrientation90:
cairo_matrix_rotate(&matrix, -M_PI_2);
- cairo_matrix_translate(&matrix, -*width, 0);
+ cairo_matrix_translate(&matrix, -width, 0);
break;
case FivIoOrientation180:
cairo_matrix_scale(&matrix, -1, -1);
- cairo_matrix_translate(&matrix, -*width, -*height);
+ cairo_matrix_translate(&matrix, -width, -height);
break;
case FivIoOrientation270:
cairo_matrix_rotate(&matrix, +M_PI_2);
- cairo_matrix_translate(&matrix, 0, -*height);
+ cairo_matrix_translate(&matrix, 0, -height);
break;
case FivIoOrientationMirror0:
cairo_matrix_scale(&matrix, -1, +1);
- cairo_matrix_translate(&matrix, -*width, 0);
+ 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);
+ cairo_matrix_translate(&matrix, -width, -height);
break;
case FivIoOrientationMirror180:
cairo_matrix_scale(&matrix, +1, -1);
- cairo_matrix_translate(&matrix, 0, -*height);
+ cairo_matrix_translate(&matrix, 0, -height);
break;
case FivIoOrientationMirror270:
cairo_matrix_rotate(&matrix, -M_PI_2);
@@ -3562,49 +3502,28 @@ fiv_io_orientation_apply(cairo_surface_t *surface,
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)
+ struct tiffer T = {};
+ if (!tiffer_init(&T, tiff, len) || !tiffer_next_ifd(&T))
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 (G_UNLIKELY(tag == Orientation && type == SHORT && count == 1 &&
- value16 >= 1 && value16 <= 8))
- return value16;
+ struct tiffer_entry entry = {};
+ while (tiffer_next_entry(&T, &entry)) {
+ int64_t orientation = 0;
+ if (G_UNLIKELY(entry.tag == TIFF_Orientation) &&
+ entry.type == TIFFER_SHORT && entry.remaining_count == 1 &&
+ tiffer_integer(&T, &entry, &orientation) &&
+ orientation >= 1 && orientation <= 8)
+ return orientation;
}
return FivIoOrientationUnknown;
}
gboolean
-fiv_io_save_metadata(cairo_surface_t *page, const char *path, GError **error)
+fiv_io_save_metadata(const FivIoImage *page, const char *path, GError **error)
{
g_return_val_if_fail(page != NULL, FALSE);
@@ -3619,14 +3538,12 @@ fiv_io_save_metadata(cairo_surface_t *page, const char *path, GError **error)
// (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))) {
+ if (page->exif && (p = g_bytes_get_data(page->exif, &len))) {
while (len) {
gsize chunk = MIN(len, 0xFFFF - 2 - 6);
uint8_t header[10] = "\xFF\xE1\000\000Exif\000\000";
@@ -3642,8 +3559,7 @@ fiv_io_save_metadata(cairo_surface_t *page, const char *path, GError **error)
}
// 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))) {
+ if (page->icc && (p = g_bytes_get_data(page->icc, &len))) {
gsize limit = 0xFFFF - 2 - 12;
uint8_t current = 0, total = (len + limit - 1) / limit;
while (len) {
@@ -3665,8 +3581,7 @@ fiv_io_save_metadata(cairo_surface_t *page, const char *path, GError **error)
// 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))) {
+ if (page->xmp && (p = g_bytes_get_data(page->xmp, &len))) {
while (len) {
gsize chunk = MIN(len, 0xFFFF - 2 - 29);
uint8_t header[33] =
diff --git a/fiv-io.h b/fiv-io.h
index 9cbe5d8..a3b482c 100644
--- a/fiv-io.h
+++ b/fiv-io.h
@@ -1,7 +1,7 @@
//
// fiv-io.h: image operations
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -22,18 +22,53 @@
#include <glib.h>
#include <webp/encode.h> // WebPConfig
+typedef enum _FivIoOrientation FivIoOrientation;
+typedef struct _FivIoRenderClosure FivIoRenderClosure;
+typedef struct _FivIoImage FivIoImage;
+typedef struct _FivIoProfile FivIoProfile;
+
// --- Colour management -------------------------------------------------------
+// Note that without a CMM, all FivIoCmm and FivIoProfile will be returned NULL.
+
+GBytes *fiv_io_profile_to_bytes(FivIoProfile *profile);
+void fiv_io_profile_free(FivIoProfile *self);
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define FIV_TYPE_IO_CMM (fiv_io_cmm_get_type())
+G_DECLARE_FINAL_TYPE(FivIoCmm, fiv_io_cmm, FIV, IO_CMM, GObject)
+
+FivIoCmm *fiv_io_cmm_get_default(void);
-// TODO(p): Make it possible to use Skia's skcms,
-// which also supports premultiplied alpha.
-// NOTE: Little CMS 2.13 already supports premultiplied alpha, too.
-typedef void *FivIoProfile;
-FivIoProfile fiv_io_profile_new(const void *data, size_t len);
-FivIoProfile fiv_io_profile_new_sRGB(void);
-void fiv_io_profile_free(FivIoProfile self);
+FivIoProfile *fiv_io_cmm_get_profile(
+ FivIoCmm *self, const void *data, size_t len);
+FivIoProfile *fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes);
+FivIoProfile *fiv_io_cmm_get_profile_sRGB(FivIoCmm *self);
+FivIoProfile *fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma);
+FivIoProfile *fiv_io_cmm_get_profile_parametric(
+ FivIoCmm *self, double gamma, double whitepoint[2], double primaries[6]);
-// From libwebp, verified to exactly match [x * a / 255].
-#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+void fiv_io_premultiply_argb32(FivIoImage *image);
+
+void fiv_io_cmm_cmyk(FivIoCmm *self,
+ FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
+void fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
+ int w, int h, FivIoProfile *source, FivIoProfile *target);
+
+void fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
+ FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
+#define fiv_io_cmm_argb32_premultiply_page(cmm, page, target) \
+ fiv_io_cmm_page((cmm), (page), (target), fiv_io_cmm_argb32_premultiply)
+
+void fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
+ void (*frame_cb) (FivIoCmm *,
+ FivIoImage *, FivIoProfile *, FivIoProfile *));
+void fiv_io_cmm_any(FivIoCmm *self,
+ FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
+FivIoImage *fiv_io_cmm_finish(FivIoCmm *self,
+ FivIoImage *image, FivIoProfile *target);
// --- Loading -----------------------------------------------------------------
@@ -41,135 +76,118 @@ extern const char *fiv_io_supported_media_types[];
gchar **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;
-/// GBytes with a WebP's THUM chunk, used for our thumbnails.
-extern cairo_user_data_key_t fiv_io_key_thum;
-/// GHashTable with key-value pairs from PNG's tEXt, zTXt, iTXt chunks.
-/// Currently only read by fiv_io_open_png_thumbnail().
-extern cairo_user_data_key_t fiv_io_key_text;
-
-/// 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;
-
-typedef struct _FivIoRenderClosure {
+// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
+enum _FivIoOrientation {
+ FivIoOrientationUnknown = 0,
+ FivIoOrientation0 = 1,
+ FivIoOrientationMirror0 = 2,
+ FivIoOrientation180 = 3,
+ FivIoOrientationMirror180 = 4,
+ FivIoOrientationMirror270 = 5,
+ FivIoOrientation90 = 6,
+ FivIoOrientationMirror90 = 7,
+ FivIoOrientation270 = 8
+};
+
+// TODO(p): Maybe make FivIoProfile a referencable type,
+// then loaders could store it in their closures.
+struct _FivIoRenderClosure {
/// The rendering is allowed to fail, returning NULL.
- cairo_surface_t *(*render)(struct _FivIoRenderClosure *, double scale);
-} FivIoRenderClosure;
+ FivIoImage *(*render)(
+ FivIoRenderClosure *, FivIoCmm *, FivIoProfile *, double scale);
+ void (*destroy)(FivIoRenderClosure *);
+};
-/// A FivIoRenderClosure for parametrized re-rendering of vector formats.
-/// This is attached at the page level.
-/// The rendered image will not have this key.
-extern cairo_user_data_key_t fiv_io_key_render;
+// Metadata are typically attached to all Cairo surfaces in an animation.
-typedef struct {
- const char *uri; ///< Source URI
- FivIoProfile screen_profile; ///< Target colour space or NULL
- int screen_dpi; ///< Target DPI
- gboolean enhance; ///< Enhance JPEG (currently)
- gboolean first_frame_only; ///< Only interested in the 1st frame
- GPtrArray *warnings; ///< String vector for non-fatal errors
-} FivIoOpenContext;
+struct _FivIoImage {
+ uint8_t *data; ///< Raw image data
+ cairo_format_t format; ///< Data format
+ uint32_t width; ///< Width of the image in pixels
+ uint32_t stride; ///< Row stride in bytes
+ uint32_t height; ///< Height of the image in pixels
-cairo_surface_t *fiv_io_open(const FivIoOpenContext *ctx, GError **error);
-cairo_surface_t *fiv_io_open_from_data(
- const char *data, size_t len, const FivIoOpenContext *ctx, GError **error);
-cairo_surface_t *fiv_io_open_png_thumbnail(const char *path, GError **error);
+ FivIoOrientation orientation; ///< Orientation to use for display
-// --- Thumbnail passing utilities ---------------------------------------------
+ GBytes *exif; ///< Raw Exif/TIFF segment
+ GBytes *icc; ///< Raw ICC profile data
+ GBytes *xmp; ///< Raw XMP data
+ GBytes *thum; ///< WebP THUM chunk, for our thumbnails
-enum { FIV_IO_SERIALIZE_LOW_QUALITY = 1 << 0 };
+ /// GHashTable with key-value pairs from PNG's tEXt, zTXt, iTXt chunks.
+ /// Currently only read by fiv_io_open_png_thumbnail().
+ GHashTable *text;
-void fiv_io_serialize_to_stdout(cairo_surface_t *surface, guint64 user_data);
-cairo_surface_t *fiv_io_deserialize(GBytes *bytes, guint64 *user_data);
+ /// A FivIoRenderClosure for parametrized re-rendering of vector formats.
+ /// This is attached at the page level.
+ FivIoRenderClosure *render;
-// --- Filesystem --------------------------------------------------------------
+ /// The first frame of the next page, in a chain.
+ /// There is no wrap-around.
+ FivIoImage *page_next;
-typedef enum _FivIoModelSort {
- FIV_IO_MODEL_SORT_NAME,
- FIV_IO_MODEL_SORT_MTIME,
- FIV_IO_MODEL_SORT_COUNT,
+ /// The first frame of the previous page, in a chain.
+ /// There is no wrap-around. This is a weak pointer.
+ FivIoImage *page_previous;
- FIV_IO_MODEL_SORT_MIN = 0,
- FIV_IO_MODEL_SORT_MAX = FIV_IO_MODEL_SORT_COUNT - 1
-} FivIoModelSort;
+ /// The next frame in a sequence, in a chain, pre-composited.
+ /// There is no wrap-around.
+ FivIoImage *frame_next;
-#define FIV_TYPE_IO_MODEL (fiv_io_model_get_type())
-G_DECLARE_FINAL_TYPE(FivIoModel, fiv_io_model, FIV, IO_MODEL, GObject)
+ /// The previous frame in a sequence, in a chain, pre-composited.
+ /// This is a weak pointer that wraps around,
+ /// and needn't be present for static images.
+ FivIoImage *frame_previous;
-/// Loads a directory. Clears itself even on failure.
-gboolean fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error);
+ /// Frame duration in milliseconds.
+ int64_t frame_duration;
-/// Returns the current location as a GFile.
-/// There is no ownership transfer, and the object may be NULL.
-GFile *fiv_io_model_get_location(FivIoModel *self);
+ /// How many times to repeat the animation, or zero for +inf.
+ uint64_t loops;
+};
-typedef struct {
- gchar *uri; ///< GIO URI
- gchar *target_uri; ///< GIO URI for any target
- gchar *collate_key; ///< Collate key for the filename
- gint64 mtime_msec; ///< Modification time in milliseconds
-} FivIoModelEntry;
+FivIoImage *fiv_io_image_ref(FivIoImage *image);
+void fiv_io_image_unref(FivIoImage *image);
-const FivIoModelEntry *fiv_io_model_get_files(FivIoModel *self, gsize *len);
-const FivIoModelEntry *fiv_io_model_get_subdirs(FivIoModel *self, gsize *len);
+/// Analogous to cairo_image_surface_create(). May return NULL.
+FivIoImage *fiv_io_image_new(
+ cairo_format_t format, uint32_t width, uint32_t height);
-// --- Export ------------------------------------------------------------------
+/// Return a new Cairo image surface referencing the same data as the image,
+/// eating the reference to it.
+cairo_surface_t *fiv_io_image_to_surface(FivIoImage *image);
-/// Encodes a Cairo surface as a WebP bitstream, following the configuration.
-/// The result needs to be freed using WebPFree/WebPDataClear().
-unsigned char *fiv_io_encode_webp(
- cairo_surface_t *surface, const WebPConfig *config, size_t *len);
+/// Return a new Cairo image surface referencing the same data as the image,
+/// without eating the image's reference.
+cairo_surface_t *fiv_io_image_to_surface_noref(const FivIoImage *image);
-/// Saves the page as a lossless WebP still picture or animation.
-/// If no exact frame is specified, this potentially creates an animation.
-gboolean fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame,
- FivIoProfile target, const char *path, GError **error);
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-// --- Metadata ----------------------------------------------------------------
+typedef struct {
+ const char *uri; ///< Source URI
+ FivIoCmm *cmm; ///< Colour management module or NULL
+ FivIoProfile *screen_profile; ///< Target colour space or NULL
+ int screen_dpi; ///< Target DPI
+ gboolean enhance; ///< Enhance JPEG (currently)
+ gboolean first_frame_only; ///< Only interested in the 1st frame
+ GPtrArray *warnings; ///< String vector for non-fatal errors
+} FivIoOpenContext;
-// 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;
+FivIoImage *fiv_io_open(const FivIoOpenContext *ctx, GError **error);
+FivIoImage *fiv_io_open_from_data(
+ const char *data, size_t len, const FivIoOpenContext *ctx, GError **error);
-/// Returns a rendering matrix for a surface (user space to pattern space),
+FivIoImage *fiv_io_open_png_thumbnail(const char *path, GError **error);
+
+// --- Metadata ----------------------------------------------------------------
+
+/// Returns a rendering matrix for an image (user space to pattern space),
/// and its target dimensions.
-cairo_matrix_t fiv_io_orientation_apply(cairo_surface_t *surface,
+cairo_matrix_t fiv_io_orientation_apply(const FivIoImage *image,
FivIoOrientation orientation, double *width, double *height);
-void fiv_io_orientation_dimensions(cairo_surface_t *surface,
+cairo_matrix_t fiv_io_orientation_matrix(
+ FivIoOrientation orientation, double width, double height);
+void fiv_io_orientation_dimensions(const FivIoImage *image,
FivIoOrientation orientation, double *width, double *height);
/// Extracts the orientation field from Exif, if there's any.
@@ -177,4 +195,25 @@ 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 char *path, GError **error);
+ const FivIoImage *page, const char *path, GError **error);
+
+// --- Thumbnail passing utilities ---------------------------------------------
+
+enum { FIV_IO_SERIALIZE_LOW_QUALITY = 1 << 0 };
+
+void fiv_io_serialize_to_stdout(cairo_surface_t *surface, guint64 user_data);
+cairo_surface_t *fiv_io_deserialize(GBytes *bytes, guint64 *user_data);
+
+GBytes *fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error);
+
+// --- Export ------------------------------------------------------------------
+
+/// Encodes an image as a WebP bitstream, following the configuration.
+/// The result needs to be freed using WebPFree/WebPDataClear().
+unsigned char *fiv_io_encode_webp(
+ FivIoImage *image, const WebPConfig *config, size_t *len);
+
+/// Saves the page as a lossless WebP still picture or animation.
+/// If no exact frame is specified, this potentially creates an animation.
+gboolean fiv_io_save(FivIoImage *page, FivIoImage *frame,
+ FivIoProfile *target, const char *path, GError **error);
diff --git a/fiv-jpegcrop.c b/fiv-jpegcrop.c
index f19ff74..d8bdb9c 100644
--- a/fiv-jpegcrop.c
+++ b/fiv-jpegcrop.c
@@ -18,6 +18,9 @@
#include <gtk/gtk.h>
#include <turbojpeg.h>
+#include <stdlib.h>
+#include <string.h>
+
#include "config.h"
// --- Utilities ---------------------------------------------------------------
diff --git a/fiv-reverse-search b/fiv-reverse-search
new file mode 100755
index 0000000..5210703
--- /dev/null
+++ b/fiv-reverse-search
@@ -0,0 +1,9 @@
+#!/bin/sh -e
+if [ "$#" -ne 2 ]; then
+ echo "Usage: $0 SEARCH-ENGINE-URI-PREFIX {PATH | URI}" >&2
+ exit 1
+fi
+
+xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
+ | curl --silent --show-error --upload-file - https://transfer.sh/image \
+ | jq --slurp --raw-input --raw-output @uri)"
diff --git a/fiv-reverse-search.desktop.in b/fiv-reverse-search.desktop.in
new file mode 100644
index 0000000..49d5de3
--- /dev/null
+++ b/fiv-reverse-search.desktop.in
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Type=Application
+Name=fiv @NAME@ Reverse Image Search
+GenericName=@NAME@ Reverse Image Search
+Icon=fiv
+Exec=fiv-reverse-search "@URL@" %u
+NoDisplay=true
+Terminal=false
+Categories=Graphics;2DGraphics;
+MimeType=image/png;image/bmp;image/gif;image/x-tga;image/jpeg;image/webp;
diff --git a/fiv-sidebar.c b/fiv-sidebar.c
index fc63a99..900c8a8 100644
--- a/fiv-sidebar.c
+++ b/fiv-sidebar.c
@@ -25,7 +25,6 @@
struct _FivSidebar {
GtkScrolledWindow parent_instance;
GtkPlacesSidebar *places;
- GtkWidget *toolbar;
GtkWidget *listbox;
FivIoModel *model;
};
@@ -78,7 +77,7 @@ fiv_sidebar_class_init(FivSidebarClass *klass)
// 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.
+ // And I need to replicate the internal widget structure.
gtk_widget_class_set_css_name(widget_class, "placessidebar");
// TODO(p): Consider a return value, and using it.
@@ -313,28 +312,9 @@ on_update_task_done(GObject *source_object, G_GNUC_UNUSED GAsyncResult *res,
}
static void
-update_location(FivSidebar *self)
+reload_directories(FivSidebar *self)
{
GFile *location = fiv_io_model_get_location(self->model);
-
- GFile *collection = g_file_new_for_uri(FIV_COLLECTION_SCHEME ":/");
- gtk_places_sidebar_remove_shortcut(self->places, collection);
- if (location && g_file_has_uri_scheme(location, FIV_COLLECTION_SCHEME)) {
- // add_shortcut() asynchronously requests GFileInfo, and only fills in
- // the new row's "uri" data field once that's finished, resulting in
- // the immediate set_location() call below failing to find it.
- gtk_places_sidebar_add_shortcut(self->places, collection);
-
- // Queue up a callback using the same mechanism that GFile uses.
- GTask *task = g_task_new(self, NULL, on_update_task_done, NULL);
- g_task_set_name(task, __func__);
- g_task_set_priority(task, G_PRIORITY_LOW);
- g_task_run_in_thread(task, on_update_task);
- g_object_unref(task);
- }
- g_object_unref(collection);
-
- gtk_places_sidebar_set_location(self->places, location);
gtk_container_foreach(GTK_CONTAINER(self->listbox),
(GtkCallback) gtk_widget_destroy, NULL);
if (!location)
@@ -358,10 +338,10 @@ update_location(FivSidebar *self)
gtk_container_add(GTK_CONTAINER(self->listbox), row);
gsize len = 0;
- const FivIoModelEntry *subdirs =
+ FivIoModelEntry *const *subdirs =
fiv_io_model_get_subdirs(self->model, &len);
for (gsize i = 0; i < len; i++) {
- GFile *file = g_file_new_for_uri(subdirs[i].uri);
+ GFile *file = g_file_new_for_uri(subdirs[i]->uri);
if ((row = create_row(self, file, "go-down-symbolic")))
gtk_container_add(GTK_CONTAINER(self->listbox), row);
g_object_unref(file);
@@ -369,6 +349,41 @@ update_location(FivSidebar *self)
}
static void
+on_model_subdirectories_changed(G_GNUC_UNUSED FivIoModel *model,
+ FivIoModelEntry *old, FivIoModelEntry *new, gpointer user_data)
+{
+ FivSidebar *self = FIV_SIDEBAR(user_data);
+ // TODO(p): Optimize: there's no need to update parent directories.
+ if (!old || !new || strcmp(old->uri, new->uri))
+ reload_directories(self);
+}
+
+static void
+update_location(FivSidebar *self)
+{
+ GFile *location = fiv_io_model_get_location(self->model);
+ GFile *collection = g_file_new_for_uri(FIV_COLLECTION_SCHEME ":/");
+ gtk_places_sidebar_remove_shortcut(self->places, collection);
+ if (location && g_file_has_uri_scheme(location, FIV_COLLECTION_SCHEME)) {
+ // add_shortcut() asynchronously requests GFileInfo, and only fills in
+ // the new row's "uri" data field once that's finished, resulting in
+ // the immediate set_location() call below failing to find it.
+ gtk_places_sidebar_add_shortcut(self->places, collection);
+
+ // Queue up a callback using the same mechanism that GFile uses.
+ GTask *task = g_task_new(self, NULL, on_update_task_done, NULL);
+ g_task_set_name(task, __func__);
+ g_task_set_priority(task, G_PRIORITY_LOW);
+ g_task_run_in_thread(task, on_update_task);
+ g_object_unref(task);
+ }
+ g_object_unref(collection);
+
+ gtk_places_sidebar_set_location(self->places, location);
+ reload_directories(self);
+}
+
+static void
on_open_breadcrumb(
G_GNUC_UNUSED GtkListBox *listbox, GtkListBoxRow *row, gpointer user_data)
{
@@ -418,7 +433,10 @@ complete_path(GFile *location, GtkListStore *model)
!info)
break;
- if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY ||
+ if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY)
+ continue;
+ if (g_file_info_has_attribute(info,
+ G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
g_file_info_get_is_hidden(info))
continue;
@@ -583,12 +601,6 @@ fiv_sidebar_init(FivSidebar *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);
@@ -603,10 +615,6 @@ fiv_sidebar_init(FivSidebar *self)
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);
@@ -634,10 +642,11 @@ fiv_sidebar_new(FivIoModel *model)
gtk_container_set_focus_vadjustment(GTK_CONTAINER(sidebar_port),
gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(self)));
- // TODO(p): There should be an extra signal to watch location changes only.
self->model = g_object_ref(model);
- g_signal_connect_swapped(self->model, "subdirectories-changed",
+ g_signal_connect_swapped(self->model, "reloaded",
G_CALLBACK(update_location), self);
+ g_signal_connect(self->model, "subdirectories-changed",
+ G_CALLBACK(on_model_subdirectories_changed), self);
return GTK_WIDGET(self);
}
@@ -648,10 +657,3 @@ 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
index 2d0888a..0cea059 100644
--- a/fiv-sidebar.h
+++ b/fiv-sidebar.h
@@ -17,7 +17,7 @@
#pragma once
-#include "fiv-io.h"
+#include "fiv-io-model.h"
#include <gtk/gtk.h>
@@ -26,4 +26,3 @@ G_DECLARE_FINAL_TYPE(FivSidebar, fiv_sidebar, FIV, SIDEBAR, GtkScrolledWindow)
GtkWidget *fiv_sidebar_new(FivIoModel *model);
void fiv_sidebar_show_enter_location(FivSidebar *self);
-GtkBox *fiv_sidebar_get_toolbar(FivSidebar *self);
diff --git a/fiv-thumbnail.c b/fiv-thumbnail.c
index f95ed70..fffbac7 100644
--- a/fiv-thumbnail.c
+++ b/fiv-thumbnail.c
@@ -1,7 +1,7 @@
//
// fiv-thumbnail.c: thumbnail management
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -33,6 +33,9 @@
#ifdef HAVE_LIBRAW
#include <libraw.h>
+#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
+#define LIBRAW_OPIONS_NO_MEMERR_CALLBACK 0
+#endif
#endif // HAVE_LIBRAW
// TODO(p): Consider merging back with fiv-io.
@@ -97,7 +100,16 @@ mark_thumbnail_lq(cairo_surface_t *surface)
static gchar *
fiv_thumbnail_get_root(void)
{
+#ifdef G_OS_WIN32
+ // We can do better than GLib with FOLDERID_InternetCache,
+ // and we don't want to place .cache directly in the user's home.
+ // TODO(p): Register this thumbnail path using the installer:
+ // https://learn.microsoft.com/en-us/windows/win32/lwef/disk-cleanup
+ gchar *cache_dir =
+ g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
+#else
gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
+#endif
gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL);
g_free(cache_dir);
return thumbnails_dir;
@@ -122,35 +134,37 @@ might_be_a_thumbnail(const char *path_or_uri)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static cairo_surface_t *
+static FivIoImage *
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
{
+ FivIoCmm *cmm = fiv_io_cmm_get_default();
FivIoOpenContext ctx = {
.uri = g_file_get_uri(target),
- .screen_profile = fiv_io_profile_new_sRGB(),
+ // Remember to synchronize changes with adjust_thumbnail().
+ .cmm = cmm,
+ .screen_profile = fiv_io_cmm_get_profile_sRGB(cmm),
.screen_dpi = 96,
.first_frame_only = TRUE,
// Only using this array as a redirect.
.warnings = g_ptr_array_new_with_free_func(g_free),
};
- cairo_surface_t *surface = fiv_io_open_from_data(
+ FivIoImage *image = fiv_io_open_from_data(
g_bytes_get_data(data, NULL), g_bytes_get_size(data), &ctx, error);
g_free((gchar *) ctx.uri);
g_ptr_array_free(ctx.warnings, TRUE);
if ((*color_managed = !!ctx.screen_profile))
fiv_io_profile_free(ctx.screen_profile);
g_bytes_unref(data);
- return surface;
+ return image;
}
// In principle similar to rescale_thumbnail() from fiv-browser.c.
-static cairo_surface_t *
-adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
+static FivIoImage *
+adjust_thumbnail(FivIoImage *thumbnail, double row_height)
{
// Hardcode orientation.
- FivIoOrientation orientation = (uintptr_t) cairo_surface_get_user_data(
- thumbnail, &fiv_io_key_orientation);
+ FivIoOrientation orientation = thumbnail->orientation;
double w = 0, h = 0;
cairo_matrix_t matrix =
@@ -167,33 +181,46 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
}
// Vector images should not have orientation, this should handle them all.
- FivIoRenderClosure *closure =
- cairo_surface_get_user_data(thumbnail, &fiv_io_key_render);
+ FivIoRenderClosure *closure = thumbnail->render;
if (closure && orientation <= FivIoOrientation0) {
+ // Remember to synchronize changes with render().
+ FivIoCmm *cmm = fiv_io_cmm_get_default();
+ FivIoProfile *screen_profile = fiv_io_cmm_get_profile_sRGB(cmm);
// This API doesn't accept non-uniform scaling; prefer a vertical fit.
- cairo_surface_t *scaled = closure->render(closure, scale_y);
+ FivIoImage *scaled =
+ closure->render(closure, cmm, screen_profile, scale_y);
+ if (screen_profile)
+ fiv_io_profile_free(screen_profile);
if (scaled)
return scaled;
}
- // This will be CAIRO_FORMAT_INVALID with non-image surfaces, which is fine.
- cairo_format_t format = cairo_image_surface_get_format(thumbnail);
- if (format != CAIRO_FORMAT_INVALID &&
- orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
- return cairo_surface_reference(thumbnail);
+ if (orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
+ return fiv_io_image_ref(thumbnail);
+ cairo_format_t format = thumbnail->format;
int projected_width = round(scale_x * w);
int projected_height = round(scale_y * h);
- cairo_surface_t *scaled = cairo_image_surface_create(
+ FivIoImage *scaled = fiv_io_image_new(
(format == CAIRO_FORMAT_RGB24 || format == CAIRO_FORMAT_RGB30)
? CAIRO_FORMAT_RGB24
: CAIRO_FORMAT_ARGB32,
projected_width, projected_height);
+ if (!scaled) {
+ g_warning("image allocation failure");
+ return fiv_io_image_ref(thumbnail);
+ }
+
+ cairo_surface_t *surface = fiv_io_image_to_surface_noref(scaled);
+ cairo_t *cr = cairo_create(surface);
+ cairo_surface_destroy(surface);
- cairo_t *cr = cairo_create(scaled);
cairo_scale(cr, scale_x, scale_y);
- cairo_set_source_surface(cr, thumbnail, 0, 0);
+ surface = fiv_io_image_to_surface_noref(thumbnail);
+ cairo_set_source_surface(cr, surface, 0, 0);
+ cairo_surface_destroy(surface);
+
cairo_pattern_t *pattern = cairo_get_source(cr);
// CAIRO_FILTER_BEST, for some reason, works bad with CAIRO_FORMAT_RGB30.
cairo_pattern_set_filter(pattern, CAIRO_FILTER_GOOD);
@@ -205,9 +232,7 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
// Note that this doesn't get triggered with oversize input surfaces,
// even though nothing will be rendered.
- if (cairo_surface_status(thumbnail) != CAIRO_STATUS_SUCCESS ||
- cairo_surface_status(scaled) != CAIRO_STATUS_SUCCESS ||
- cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
+ if (cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
cairo_status(cr) != CAIRO_STATUS_SUCCESS)
g_warning("thumbnail scaling failed");
@@ -215,149 +240,325 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
return scaled;
}
-static cairo_surface_t *
-orient_thumbnail(cairo_surface_t *surface, FivIoOrientation orientation)
+static FivIoImage *
+orient_thumbnail(FivIoImage *image)
{
- if (!surface || orientation <= FivIoOrientation0)
- return surface;
+ if (image->orientation <= FivIoOrientation0)
+ return image;
double w = 0, h = 0;
cairo_matrix_t matrix =
- fiv_io_orientation_apply(surface, orientation, &w, &h);
- cairo_surface_t *oriented =
- cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
+ fiv_io_orientation_apply(image, image->orientation, &w, &h);
+ FivIoImage *oriented = fiv_io_image_new(image->format, w, h);
+ if (!oriented) {
+ g_warning("image allocation failure");
+ return image;
+ }
- cairo_t *cr = cairo_create(oriented);
+ cairo_surface_t *surface = fiv_io_image_to_surface_noref(oriented);
+ cairo_t *cr = cairo_create(surface);
+ cairo_surface_destroy(surface);
+
+ surface = fiv_io_image_to_surface(image);
cairo_set_source_surface(cr, surface, 0, 0);
+ cairo_surface_destroy(surface);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr);
cairo_destroy(cr);
- cairo_surface_destroy(surface);
return oriented;
}
-cairo_surface_t *
-fiv_thumbnail_extract(GFile *target, FivThumbnailSize max_size, GError **error)
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#ifdef HAVE_LIBRAW
+#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
+
+static int
+extract_libraw_compare(const void *a, const void *b)
{
- const char *path = g_file_peek_path(target);
- if (!path) {
- set_error(error, "thumbnails will only be extracted from local files");
+ const libraw_thumbnail_item_t **t1 = (const libraw_thumbnail_item_t **) a;
+ const libraw_thumbnail_item_t **t2 = (const libraw_thumbnail_item_t **) b;
+ float p1 = (float) (*t1)->twidth * (*t1)->theight;
+ float p2 = (float) (*t2)->twidth * (*t2)->theight;
+ return (p2 < p1) - (p1 < p2);
+}
+
+static gboolean
+extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
+{
+ int count = iprc->thumbs_list.thumbcount;
+ if (count <= 0) {
+ set_error(error, "no thumbnails found");
+ return FALSE;
+ }
+
+ // The old libraw_unpack_thumb() goes for the largest thumbnail,
+ // but we currently want the smallest usable thumbnail. Order them.
+ libraw_thumbnail_item_t **sorted = g_malloc_n(count, sizeof *sorted);
+ for (int i = 0; i < count; i++)
+ sorted[i] = &iprc->thumbs_list.thumblist[i];
+ qsort(sorted, count, sizeof *sorted, extract_libraw_compare);
+
+ // With the raw.pixls.us database, zero dimensions occur in two cases:
+ // - when thumbcount should really be 0,
+ // - with the last, huge JPEG thumbnail in CR3 raws.
+ // The maintainer refuses to change anything about it (#589).
+ int i = 0;
+ while (i < count && (!sorted[i]->twidth || !sorted[i]->theight))
+ i++;
+
+ // Ignore thumbnails whose decoding is likely to be a waste of time.
+ // XXX: This primarily targets the TIFF/EP shortcut code,
+ // because decoding a thumbnail will always be /much/ quicker than a render.
+ // TODO(p): Maybe don't mark raw image thumbnails as low-quality
+ // if they're the right aspect ratio, and of sufficiently large size.
+ // The only downsides to camera-provided thumbnails seem to be cropping,
+ // and when they're decoded incorrectly. Also don't trust tflip.
+ float output_pixels = (float) iprc->sizes.iwidth * iprc->sizes.iheight;
+ // Note that the ratio may even be larger than 1, as seen with CR2 files.
+ while (i < count &&
+ (float) sorted[count - 1]->twidth * sorted[count - 1]->theight >
+ output_pixels * 0.75)
+ count--;
+
+ // The smallest size thumbnail is very often forced to be 4:3,
+ // and the remaining space is filled with black, looking quite wrong.
+ // It isn't really possible to strip those borders, because many are JPEGs.
+ //
+ // Another reason to skip thumbnails of mismatching aspect ratios is
+ // to avoid browser items from jumping around when low-quality thumbnails
+ // get replaced with their final versions.
+ //
+ // Note that some of them actually have borders on all four sides
+ // (Nikon/D50/DSC_5155.NEF, Nikon/D70/20170902_0047.NEF,
+ // Nikon/D70s/RAW_NIKON_D70S.NEF), or even on just one side
+ // (Leica/LEICA M MONOCHROM (Typ 246), Leica/M (Typ 240)).
+ // Another interesting possibility is Sony/DSC-HX99/DSC00001.ARW,
+ // where the correct-ratio thumbnail has borders but the main image doesn't.
+ //
+ // The problematic thumbnail is usually, but not always, sized 160x120,
+ // and some of them may actually be fine.
+ float output_ratio = (float) iprc->sizes.iwidth / iprc->sizes.iheight;
+ while (i < count) {
+ // XXX: tflip is less reliable than libraw_dcraw_make_mem_thumb()
+ // and reading out Orientation from the resulting Exif.
+ float ratio = sorted[i]->tflip == 5 || sorted[i]->tflip == 6
+ ? (float) sorted[i]->theight / sorted[i]->twidth
+ : (float) sorted[i]->twidth / sorted[i]->theight;
+ if (fabsf(ratio - output_ratio) < 0.05)
+ break;
+ i++;
+ }
+
+ // Avoid pink-tinted readouts of CR2 IFD2 (#590).
+ //
+ // This thumbnail can also have a black stripe on the left and the top,
+ // which we should remove if using fixed LibRaw > 0.21.1.
+ if (i < count && iprc->idata.maker_index == LIBRAW_CAMERAMAKER_Canon &&
+ sorted[i]->tformat == LIBRAW_INTERNAL_THUMBNAIL_KODAK_THUMB)
+ i++;
+
+ bool found = i != count;
+ if (found)
+ i = sorted[i] - iprc->thumbs_list.thumblist;
+
+ g_free(sorted);
+ if (!found) {
+ set_error(error, "no suitable thumbnails found");
+ return FALSE;
+ }
+
+ int err = 0;
+ if ((err = libraw_unpack_thumb_ex(iprc, i))) {
+ set_error(error, libraw_strerror(err));
+ return FALSE;
+ }
+ *flip = iprc->thumbs_list.thumblist[i].tflip;
+ return TRUE;
+}
+
+#else // LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
+
+static gboolean
+extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
+{
+ int err = 0;
+ if ((err = libraw_unpack_thumb(iprc))) {
+ set_error(error, libraw_strerror(err));
+ return FALSE;
+ }
+
+ // The main image's "flip" often matches up, but sometimes doesn't, e.g.:
+ // - Phase One/H 25/H25_Outdoor_.IIQ
+ // - Phase One/H 25/H25_IT8.7-2_Card.TIF
+ *flip = iprc->sizes.flip;
+ return TRUE;
+}
+
+#endif // LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
+
+// LibRaw does a weird permutation here, so follow the documentation,
+// which assumes that mirrored orientations never happen.
+static FivIoOrientation
+extract_libraw_unflip(int flip)
+{
+ switch (flip) {
+ break; case 0:
+ return FivIoOrientation0;
+ break; case 3:
+ return FivIoOrientation180;
+ break; case 5:
+ return FivIoOrientation270;
+ break; case 6:
+ return FivIoOrientation90;
+ break; default:
+ return FivIoOrientationUnknown;
+ }
+}
+
+static FivIoImage *
+extract_libraw_bitmap(libraw_processed_image_t *image, int flip, GError **error)
+{
+ // Anything else is extremely rare.
+ if (image->colors != 3 || image->bits != 8) {
+ set_error(error, "unsupported bitmap thumbnail");
return NULL;
}
- GMappedFile *mf = g_mapped_file_new(path, FALSE, error);
- if (!mf)
+ FivIoImage *I = fiv_io_image_new(
+ CAIRO_FORMAT_RGB24, image->width, image->height);
+ if (!I) {
+ set_error(error, "image allocation failure");
return NULL;
+ }
- // Bitmap thumbnails generally need rotating, e.g.:
- // - Hasselblad/H4D-50/2-9-2017_street_0012.fff
- // - OnePlus/One/IMG_20150729_201116.dng (and more DNGs in general)
- // Though it's apparent LibRaw doesn't adjust the thumbnails to match
- // the main image's "flip" field (it just happens to match up often), e.g.:
- // - Phase One/H 25/H25_Outdoor_.IIQ (correct Orientation in IFD0)
- // - Phase One/H 25/H25_IT8.7-2_Card.TIF (correctly missing in IFD0)
- //
- // JPEG thumbnails generally have the right rotation in their Exif, e.g.:
- // - Canon/EOS-1Ds Mark II/RAW_CANON_1DSM2.CR2
- // - Leica/C (Typ 112)/Leica_-_C_(Typ_112)-_3:2.RWL
- // - Nikon/1 S2/RAW_NIKON_1S2.NEF
- // - Panasonic/DMC-FZ18/RAW_PANASONIC_LUMIX_FZ18.RAW
- // - Panasonic/DMC-FZ70/P1000836.RW2
- // - Samsung/NX200/2013-05-08-194524__sam6589.srw
- // - Sony/DSC-HX95/DSC00018.ARW
- //
- // Some files are problematic and we won't bother with special-casing:
- // - Leaf/Aptus 22/L_003172.mos (JPEG)'s thumbnail wrongly contains
- // Exif Orientation 6, and sizes.flip also contains 6.
- // - Nokia/Lumia 1020/RAW_NOKIA_LUMIA_1020.DNG (bitmap) has wrong color.
- // - Ricoh/GXR/R0017428.DNG (JPEG) seems to be plainly invalid.
- FivIoOrientation orientation = FivIoOrientationUnknown;
- cairo_surface_t *surface = NULL;
-#ifndef HAVE_LIBRAW
- // TODO(p): Implement our own thumbnail extractors.
- set_error(error, "unsupported file");
-#else // HAVE_LIBRAW
+ guint32 *out = (guint32 *) I->data;
+ const unsigned char *in = image->data;
+ for (guint64 i = 0; i < (guint64) image->width * image->height; in += 3)
+ out[i++] = in[0] << 16 | in[1] << 8 | in[2];
+
+ I->orientation = extract_libraw_unflip(flip);
+ return I;
+}
+
+static FivIoImage *
+extract_libraw(GFile *target, GMappedFile *mf, GError **error)
+{
+ FivIoImage *I = NULL;
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");
- goto fail;
+ return NULL;
}
int err = 0;
if ((err = libraw_open_buffer(iprc, (void *) g_mapped_file_get_contents(mf),
- g_mapped_file_get_length(mf))) ||
- (err = libraw_unpack_thumb(iprc))) {
+ g_mapped_file_get_length(mf)))) {
set_error(error, libraw_strerror(err));
- goto fail_libraw;
+ goto fail;
}
+ if ((err = libraw_adjust_sizes_info_only(iprc))) {
+ set_error(error, libraw_strerror(err));
+ goto fail;
+ }
+
+ int flip = 0;
+ if (!extract_libraw_unpack(iprc, &flip, error))
+ goto fail;
libraw_processed_image_t *image = libraw_dcraw_make_mem_thumb(iprc, &err);
if (!image) {
set_error(error, libraw_strerror(err));
- goto fail_libraw;
+ goto fail;
}
- gboolean dummy = FALSE;
+ // Bitmap thumbnails generally need rotating, e.g.:
+ // - Hasselblad/H4D-50/2-9-2017_street_0012.fff
+ // - OnePlus/One/IMG_20150729_201116.dng (and more DNGs in general)
+ //
+ // JPEG thumbnails generally have the right rotation in their Exif, e.g.:
+ // - Canon/EOS-1Ds Mark II/RAW_CANON_1DSM2.CR2
+ // - Leica/C (Typ 112)/Leica_-_C_(Typ_112)-_3:2.RWL
+ // - Nikon/1 S2/RAW_NIKON_1S2.NEF
+ // - Panasonic/DMC-FZ18/RAW_PANASONIC_LUMIX_FZ18.RAW
+ // - Panasonic/DMC-FZ70/P1000836.RW2
+ // - Samsung/NX200/2013-05-08-194524__sam6589.srw
+ // - Sony/DSC-HX95/DSC00018.ARW
+ // Note that LibRaw inserts its own Exif segment if it doesn't find one,
+ // and this may differ from flip. It may also be wrong, as in:
+ // - Leaf/Aptus 22/L_003172.mos
+ //
+ // Some files are problematic and we won't bother with special-casing:
+ // - Nokia/Lumia 1020/RAW_NOKIA_LUMIA_1020.DNG (bitmap) has wrong color.
+ // - Ricoh/GXR/R0017428.DNG (JPEG) seems to be plainly invalid.
switch (image->type) {
+ gboolean dummy;
case LIBRAW_IMAGE_JPEG:
- surface = render(
+ I = render(
target, g_bytes_new(image->data, image->data_size), &dummy, error);
- orientation = (int) (intptr_t) cairo_surface_get_user_data(
- surface, &fiv_io_key_orientation);
break;
case LIBRAW_IMAGE_BITMAP:
- // Anything else is extremely rare.
- if (image->colors != 3 || image->bits != 8) {
- set_error(error, "unsupported bitmap thumbnail");
- break;
- }
-
- surface = cairo_image_surface_create(
- CAIRO_FORMAT_RGB24, image->width, image->height);
- guint32 *out = (guint32 *) cairo_image_surface_get_data(surface);
- const unsigned char *in = image->data;
- for (guint64 i = 0; i < image->width * image->height; in += 3)
- out[i++] = in[0] << 16 | in[1] << 8 | in[2];
- cairo_surface_mark_dirty(surface);
-
- // LibRaw actually turns an 8 to 5, so follow the documentation.
- switch (iprc->sizes.flip) {
- break; case 3: orientation = FivIoOrientation180;
- break; case 5: orientation = FivIoOrientation270;
- break; case 6: orientation = FivIoOrientation90;
- }
+ I = extract_libraw_bitmap(image, flip, error);
break;
default:
set_error(error, "unsupported embedded thumbnail");
}
libraw_dcraw_clear_mem(image);
-fail_libraw:
+fail:
libraw_close(iprc);
+ return I;
+}
+
#endif // HAVE_LIBRAW
-fail:
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+cairo_surface_t *
+fiv_thumbnail_extract(GFile *target, FivThumbnailSize max_size, GError **error)
+{
+ const char *path = g_file_peek_path(target);
+ if (!path) {
+ set_error(error, "thumbnails will only be extracted from local files");
+ return NULL;
+ }
+
+ GMappedFile *mf = g_mapped_file_new(path, FALSE, error);
+ if (!mf)
+ return NULL;
+
+ // In this case, g_mapped_file_get_contents() returns NULL, causing issues.
+ if (!g_mapped_file_get_length(mf)) {
+ set_error(error, "empty file");
+ return NULL;
+ }
+
+ FivIoImage *image = NULL;
+#ifdef HAVE_LIBRAW
+ image = extract_libraw(target, mf, error);
+#else // ! HAVE_LIBRAW
+ // TODO(p): Implement our own thumbnail extractors.
+ set_error(error, "unsupported file");
+#endif // ! HAVE_LIBRAW
g_mapped_file_unref(mf);
- // This hardcodes Exif orientation before adjust_thumbnail() might do so,
- // before the early return below.
- surface = orient_thumbnail(surface, orientation);
- if (!surface || max_size < FIV_THUMBNAIL_SIZE_MIN ||
- max_size > FIV_THUMBNAIL_SIZE_MAX)
- return surface;
+ if (!image)
+ return NULL;
+ if (max_size < FIV_THUMBNAIL_SIZE_MIN || max_size > FIV_THUMBNAIL_SIZE_MAX)
+ return fiv_io_image_to_surface(orient_thumbnail(image));
- cairo_surface_t *result =
- adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size);
- cairo_surface_destroy(surface);
- return result;
+ FivIoImage *result =
+ adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
+ fiv_io_image_unref(image);
+ return fiv_io_image_to_surface(result);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static WebPData
-encode_thumbnail(cairo_surface_t *surface)
+encode_thumbnail(FivIoImage *image)
{
WebPData bitstream = {};
WebPConfig config = {};
@@ -369,12 +570,12 @@ encode_thumbnail(cairo_surface_t *surface)
if (!WebPValidateConfig(&config))
return bitstream;
- bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size);
+ bitstream.bytes = fiv_io_encode_webp(image, &config, &bitstream.size);
return bitstream;
}
static void
-save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum)
+save_thumbnail(FivIoImage *thumbnail, const char *path, GString *thum)
{
WebPMux *mux = WebPMuxNew();
WebPData bitstream = encode_thumbnail(thumbnail);
@@ -418,9 +619,33 @@ save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum)
WebPDataClear(&assembled);
}
+cairo_surface_t *
+fiv_thumbnail_produce_for_search(
+ GFile *target, FivThumbnailSize max_size, GError **error)
+{
+ g_return_val_if_fail(max_size >= FIV_THUMBNAIL_SIZE_MIN &&
+ max_size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
+
+ GBytes *data = g_file_load_bytes(target, NULL, NULL, error);
+ if (!data)
+ return NULL;
+
+ gboolean color_managed = FALSE;
+ FivIoImage *image = render(target, data, &color_managed, error);
+ if (!image)
+ return NULL;
+
+ // TODO(p): Might want to keep this a square.
+ FivIoImage *result =
+ adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
+ fiv_io_image_unref(image);
+ return fiv_io_image_to_surface(result);
+}
+
static cairo_surface_t *
produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
{
+ // Note that this comes with a TOCTTOU problem.
goffset filesize = 0;
GFileInfo *info = g_file_query_info(target,
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_SIZE,
@@ -442,21 +667,21 @@ produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
return NULL;
gboolean color_managed = FALSE;
- cairo_surface_t *surface = render(target, data, &color_managed, error);
- if (!surface)
+ FivIoImage *image = render(target, data, &color_managed, error);
+ if (!image)
return NULL;
- cairo_surface_t *result =
- adjust_thumbnail(surface, fiv_thumbnail_sizes[size].size);
- cairo_surface_destroy(surface);
- return result;
+ FivIoImage *result =
+ adjust_thumbnail(image, fiv_thumbnail_sizes[size].size);
+ fiv_io_image_unref(image);
+ return fiv_io_image_to_surface(result);
}
cairo_surface_t *
fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
{
g_return_val_if_fail(max_size >= FIV_THUMBNAIL_SIZE_MIN &&
- max_size <= FIV_THUMBNAIL_SIZE_MAX, FALSE);
+ max_size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
// Don't save thumbnails for FUSE mounts, such as sftp://.
// Moreover, it doesn't make sense to save thumbnails of thumbnails.
@@ -471,6 +696,13 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
return NULL;
}
+ // TODO(p): Use open(O_RDONLY | O_NONBLOCK | _O_BINARY), fstat(),
+ // g_mapped_file_new_from_fd(), and reset the non-blocking flag on the file.
+ if (!S_ISREG(st.st_mode)) {
+ set_error(error, "not a regular file");
+ return NULL;
+ }
+
GError *e = NULL;
GMappedFile *mf = g_mapped_file_new(path, FALSE, &e);
if (!mf) {
@@ -479,12 +711,18 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
return produce_fallback(target, max_size, error);
}
+ // In this case, g_mapped_file_get_bytes() has NULL data, causing issues.
gsize filesize = g_mapped_file_get_length(mf);
+ if (!filesize) {
+ set_error(error, "empty file");
+ return NULL;
+ }
+
gboolean color_managed = FALSE;
- cairo_surface_t *surface =
+ FivIoImage *image =
render(target, g_mapped_file_get_bytes(mf), &color_managed, error);
g_mapped_file_unref(mf);
- if (!surface)
+ if (!image)
return NULL;
// Boilerplate copied from fiv_thumbnail_lookup().
@@ -498,14 +736,12 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
g_string_append_printf(
thum, "%s%c%ld%c", THUMB_MTIME, 0, (long) st.st_mtime, 0);
g_string_append_printf(
- thum, "%s%c%ld%c", THUMB_SIZE, 0, (long) filesize, 0);
+ thum, "%s%c%llu%c", THUMB_SIZE, 0, (unsigned long long) filesize, 0);
- if (cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE) {
- g_string_append_printf(thum, "%s%c%d%c", THUMB_IMAGE_WIDTH, 0,
- cairo_image_surface_get_width(surface), 0);
- g_string_append_printf(thum, "%s%c%d%c", THUMB_IMAGE_HEIGHT, 0,
- cairo_image_surface_get_height(surface), 0);
- }
+ g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_WIDTH, 0,
+ (unsigned) image->width, 0);
+ g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_HEIGHT, 0,
+ (unsigned) image->height, 0);
// Without a CMM, no conversion is attempted.
if (color_managed) {
@@ -513,19 +749,19 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
thum, "%s%c%s%c", THUMB_COLORSPACE, 0, THUMB_COLORSPACE_SRGB, 0);
}
- cairo_surface_t *max_size_surface = NULL;
+ FivIoImage *max_size_image = NULL;
for (int use = max_size; use >= FIV_THUMBNAIL_SIZE_MIN; use--) {
- cairo_surface_t *scaled =
- adjust_thumbnail(surface, fiv_thumbnail_sizes[use].size);
+ FivIoImage *scaled =
+ adjust_thumbnail(image, fiv_thumbnail_sizes[use].size);
gchar *path = g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_dir,
fiv_thumbnail_sizes[use].thumbnail_spec_name, sum);
save_thumbnail(scaled, path, thum);
g_free(path);
- if (!max_size_surface)
- max_size_surface = scaled;
+ if (!max_size_image)
+ max_size_image = scaled;
else
- cairo_surface_destroy(scaled);
+ fiv_io_image_unref(scaled);
}
g_string_free(thum, TRUE);
@@ -533,13 +769,20 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
g_free(thumbnails_dir);
g_free(sum);
g_free(uri);
- cairo_surface_destroy(surface);
- return max_size_surface;
+ fiv_io_image_unref(image);
+ return fiv_io_image_to_surface(max_size_image);
}
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+typedef struct {
+ const char *uri; ///< Target URI
+ time_t mtime; ///< File modification time
+ guint64 size; ///< File size
+} Stat;
+
static bool
-check_wide_thumbnail_texts(GBytes *thum, const char *target, time_t mtime,
- bool *sRGB)
+check_wide_thumbnail_texts(GBytes *thum, const Stat *st, bool *sRGB)
{
gsize len = 0;
const gchar *s = g_bytes_get_data(thum, &len), *end = s + len;
@@ -553,11 +796,14 @@ check_wide_thumbnail_texts(GBytes *thum, const char *target, time_t mtime,
continue;
} else if (!strcmp(key, THUMB_URI)) {
have_uri = true;
- if (strcmp(target, s))
+ if (strcmp(st->uri, s))
return false;
} else if (!strcmp(key, THUMB_MTIME)) {
have_mtime = true;
- if (atol(s) != mtime)
+ if (atol(s) != st->mtime)
+ return false;
+ } else if (!strcmp(key, THUMB_SIZE)) {
+ if (strtoull(s, NULL, 10) != st->size)
return false;
} else if (!strcmp(key, THUMB_COLORSPACE))
*sRGB = !strcmp(s, THUMB_COLORSPACE_SRGB);
@@ -568,30 +814,29 @@ check_wide_thumbnail_texts(GBytes *thum, const char *target, time_t mtime,
}
static cairo_surface_t *
-read_wide_thumbnail(
- const char *path, const char *uri, time_t mtime, GError **error)
+read_wide_thumbnail(const char *path, const Stat *st, GError **error)
{
gchar *thumbnail_uri = g_filename_to_uri(path, NULL, error);
if (!thumbnail_uri)
return NULL;
- cairo_surface_t *surface =
+ FivIoImage *image =
fiv_io_open(&(FivIoOpenContext){.uri = thumbnail_uri}, error);
g_free(thumbnail_uri);
- if (!surface)
+ if (!image)
return NULL;
bool sRGB = false;
- GBytes *thum = cairo_surface_get_user_data(surface, &fiv_io_key_thum);
- if (!thum) {
+ if (!image->thum) {
g_clear_error(error);
set_error(error, "not a thumbnail");
- } else if (!check_wide_thumbnail_texts(thum, uri, mtime, &sRGB)) {
+ } else if (!check_wide_thumbnail_texts(image->thum, st, &sRGB)) {
g_clear_error(error);
set_error(error, "mismatch");
} else {
// TODO(p): Add a function or a non-valueless define to check
// for CMM presence, then remove this ifdef.
+ cairo_surface_t *surface = fiv_io_image_to_surface(image);
#ifdef HAVE_LCMS2
if (!sRGB)
mark_thumbnail_lq(surface);
@@ -599,24 +844,21 @@ read_wide_thumbnail(
return surface;
}
- cairo_surface_destroy(surface);
+ fiv_io_image_unref(image);
return NULL;
}
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
static cairo_surface_t *
-read_png_thumbnail(
- const char *path, const char *uri, time_t mtime, GError **error)
+read_png_thumbnail(const char *path, const Stat *st, GError **error)
{
- cairo_surface_t *surface = fiv_io_open_png_thumbnail(path, error);
- if (!surface)
+ FivIoImage *image = fiv_io_open_png_thumbnail(path, error);
+ if (!image)
return NULL;
- GHashTable *texts = cairo_surface_get_user_data(surface, &fiv_io_key_text);
+ GHashTable *texts = image->text;
if (!texts) {
set_error(error, "not a thumbnail");
- cairo_surface_destroy(surface);
+ fiv_io_image_unref(image);
return NULL;
}
@@ -624,18 +866,27 @@ read_png_thumbnail(
// but those aren't interesting currently (would be for fast previews).
const char *text_uri = g_hash_table_lookup(texts, THUMB_URI);
const char *text_mtime = g_hash_table_lookup(texts, THUMB_MTIME);
- if (!text_uri || strcmp(text_uri, uri) ||
- !text_mtime || atol(text_mtime) != mtime) {
+ const char *text_size = g_hash_table_lookup(texts, THUMB_SIZE);
+ if (!text_uri || strcmp(text_uri, st->uri) ||
+ !text_mtime || atol(text_mtime) != st->mtime) {
set_error(error, "mismatch or not a thumbnail");
- cairo_surface_destroy(surface);
+ fiv_io_image_unref(image);
+ return NULL;
+ }
+ if (text_size && strtoull(text_size, NULL, 10) != st->size) {
+ set_error(error, "file size mismatch");
+ fiv_io_image_unref(image);
return NULL;
}
- return surface;
+ return fiv_io_image_to_surface(image);
}
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
cairo_surface_t *
-fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
+fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, guint64 filesize,
+ FivThumbnailSize size)
{
g_return_val_if_fail(size >= FIV_THUMBNAIL_SIZE_MIN &&
size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
@@ -647,6 +898,7 @@ fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1);
gchar *thumbnails_dir = fiv_thumbnail_get_root();
+ const Stat st = {.uri = uri, .mtime = mtime_msec / 1000, .size = filesize};
// The lookup sequence is: nominal..max, then mirroring back to ..min.
cairo_surface_t *result = NULL;
@@ -659,7 +911,7 @@ fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
const char *name = fiv_thumbnail_sizes[use].thumbnail_spec_name;
gchar *wide = g_strconcat(thumbnails_dir, G_DIR_SEPARATOR_S "wide-",
name, G_DIR_SEPARATOR_S, sum, ".webp", NULL);
- result = read_wide_thumbnail(wide, uri, mtime_msec / 1000, &error);
+ result = read_wide_thumbnail(wide, &st, &error);
if (error) {
g_debug("%s: %s", wide, error->message);
g_clear_error(&error);
@@ -675,7 +927,7 @@ fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
gchar *path = g_strconcat(thumbnails_dir, G_DIR_SEPARATOR_S,
name, G_DIR_SEPARATOR_S, sum, ".png", NULL);
- result = read_png_thumbnail(path, uri, mtime_msec / 1000, &error);
+ result = read_png_thumbnail(path, &st, &error);
if (error) {
g_debug("%s: %s", path, error->message);
g_clear_error(&error);
@@ -708,7 +960,7 @@ print_error(GFile *file, GError *error)
}
static gchar *
-identify_wide_thumbnail(GMappedFile *mf, time_t *mtime, GError **error)
+identify_wide_thumbnail(GMappedFile *mf, Stat *st, GError **error)
{
WebPDemuxer *demux = WebPDemux(&(WebPData) {
.bytes = (const uint8_t *) g_mapped_file_get_contents(mf),
@@ -734,7 +986,9 @@ identify_wide_thumbnail(GMappedFile *mf, time_t *mtime, GError **error)
if (!strcmp(key, THUMB_URI) && !uri)
uri = g_strdup(p);
if (!strcmp(key, THUMB_MTIME))
- *mtime = atol(p);
+ st->mtime = atol(p);
+ if (!strcmp(key, THUMB_SIZE))
+ st->size = strtoull(p, NULL, 10);
key = NULL;
} else {
key = p;
@@ -752,16 +1006,17 @@ static void
check_wide_thumbnail(GFile *thumbnail, GError **error)
{
// Not all errors are enough of a reason for us to delete something.
- GError *tolerable = NULL;
+ GError *tolerable_error = NULL;
const char *path = g_file_peek_path(thumbnail);
- GMappedFile *mf = g_mapped_file_new(path, FALSE, &tolerable);
+ GMappedFile *mf = g_mapped_file_new(path, FALSE, &tolerable_error);
if (!mf) {
- print_error(thumbnail, tolerable);
+ print_error(thumbnail, tolerable_error);
return;
}
- time_t target_mtime = 0;
- gchar *target_uri = identify_wide_thumbnail(mf, &target_mtime, error);
+ // Note that we could enforce the presence of the size field in our spec.
+ Stat target_st = {.uri = NULL, .mtime = 0, .size = G_MAXUINT64};
+ gchar *target_uri = identify_wide_thumbnail(mf, &target_st, error);
g_mapped_file_unref(mf);
if (!target_uri)
return;
@@ -783,26 +1038,32 @@ check_wide_thumbnail(GFile *thumbnail, GError **error)
GFile *target = g_file_new_for_uri(target_uri);
g_free(target_uri);
GFileInfo *info = g_file_query_info(target,
- G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
- G_FILE_QUERY_INFO_NONE, NULL, &tolerable);
+ G_FILE_ATTRIBUTE_STANDARD_NAME ","
+ G_FILE_ATTRIBUTE_STANDARD_SIZE ","
+ G_FILE_ATTRIBUTE_TIME_MODIFIED,
+ G_FILE_QUERY_INFO_NONE, NULL, &tolerable_error);
g_object_unref(target);
- if (g_error_matches(tolerable, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
- g_propagate_error(error, tolerable);
+ if (g_error_matches(tolerable_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+ g_propagate_error(error, tolerable_error);
return;
- } else if (tolerable) {
- print_error(thumbnail, tolerable);
+ } else if (tolerable_error) {
+ print_error(thumbnail, tolerable_error);
return;
}
+ guint64 filesize = g_file_info_get_size(info);
GDateTime *mdatetime = g_file_info_get_modification_date_time(info);
g_object_unref(info);
if (!mdatetime) {
- set_error(&tolerable, "cannot retrieve file modification time");
- print_error(thumbnail, tolerable);
+ set_error(&tolerable_error, "cannot retrieve file modification time");
+ print_error(thumbnail, tolerable_error);
return;
}
- if (g_date_time_to_unix(mdatetime) != target_mtime)
- set_error(error, "mtime mismatch");
+ if (g_date_time_to_unix(mdatetime) != target_st.mtime)
+ set_error(error, "modification time mismatch");
+ else if (target_st.size != G_MAXUINT64 && filesize != target_st.size)
+ set_error(error, "file size mismatch");
+
g_date_time_unref(mdatetime);
}
diff --git a/fiv-thumbnail.h b/fiv-thumbnail.h
index 7f3360a..0d53c01 100644
--- a/fiv-thumbnail.h
+++ b/fiv-thumbnail.h
@@ -1,7 +1,7 @@
//
// fiv-thumbnail.h: thumbnail management
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -21,7 +21,7 @@
#include <gio/gio.h>
#include <glib.h>
-// And this is how you avoid glib-mkenums.
+// Avoid glib-mkenums.
typedef enum _FivThumbnailSize {
#define FIV_THUMBNAIL_SIZES(XX) \
XX(SMALL, 128, "normal") \
@@ -62,10 +62,14 @@ cairo_surface_t *fiv_thumbnail_extract(
cairo_surface_t *fiv_thumbnail_produce(
GFile *target, FivThumbnailSize max_size, GError **error);
+/// Like fiv_thumbnail_produce(), but skips the cache.
+cairo_surface_t *fiv_thumbnail_produce_for_search(
+ GFile *target, FivThumbnailSize max_size, GError **error);
+
/// Retrieves a thumbnail of the most appropriate quality and resolution
/// for the target file.
-cairo_surface_t *fiv_thumbnail_lookup(
- const char *uri, gint64 mtime_msec, FivThumbnailSize size);
+cairo_surface_t *fiv_thumbnail_lookup(const char *uri,
+ gint64 mtime_msec, guint64 filesize, FivThumbnailSize size);
/// Invalidate the wide thumbnail cache. May write to standard streams.
void fiv_thumbnail_invalidate(void);
diff --git a/fiv-update-desktop-files.in b/fiv-update-desktop-files.in
index bbbe9a9..1c8568f 100755
--- a/fiv-update-desktop-files.in
+++ b/fiv-update-desktop-files.in
@@ -1,4 +1,8 @@
#!/bin/sh -e
-sed -i "s|^MimeType=.*|MimeType=$(
- "${DESTDIR:+$DESTDIR/}"'@EXE@' --list-supported-media-types | tr '\n' ';'
-)|" "${DESTDIR:+$DESTDIR/}"'@DESKTOP@'
+fiv=${DESTDIR:+$DESTDIR/}'@FIV@'
+desktopdir=${DESTDIR:+$DESTDIR/}'@DESKTOPDIR@'
+
+types=$("$fiv" --list-supported-media-types | tr '\n' ';')
+for desktop in @DESKTOPS@
+do sed -i "s|^MimeType=.*|MimeType=$types|" "$desktopdir"/"$desktop"
+done
diff --git a/fiv-view.c b/fiv-view.c
index d90af18..fb01b3a 100644
--- a/fiv-view.c
+++ b/fiv-view.c
@@ -1,7 +1,7 @@
//
// fiv-view.c: image viewing widget
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -24,6 +24,7 @@
#include <math.h>
#include <stdbool.h>
+#include <epoxy/gl.h>
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
@@ -63,10 +64,10 @@ struct _FivView {
gchar *messages; ///< Image load information
gchar *uri; ///< Path to the current image (if any)
- cairo_surface_t *image; ///< The loaded image (sequence)
- cairo_surface_t *page; ///< Current page within image, weak
- cairo_surface_t *page_scaled; ///< Current page within image, scaled
- cairo_surface_t *frame; ///< Current frame within page, weak
+ FivIoImage *image; ///< The loaded image (sequence)
+ FivIoImage *page; ///< Current page within image, weak
+ FivIoImage *page_scaled; ///< Current page within image, scaled
+ FivIoImage *frame; ///< Current frame within page, weak
FivIoOrientation orientation; ///< Current page orientation
bool enable_cms : 1; ///< Smooth scaling toggle
bool filter : 1; ///< Smooth scaling toggle
@@ -77,12 +78,16 @@ struct _FivView {
double scale; ///< Scaling factor
double drag_start[2]; ///< Adjustment values for drag origin
- cairo_surface_t *enhance_swap; ///< Quick swap in/out
- FivIoProfile screen_cms_profile; ///< Target colour profile for widget
+ FivIoImage *enhance_swap; ///< Quick swap in/out
+ FivIoProfile *screen_cms_profile; ///< Target colour profile for widget
int remaining_loops; ///< Greater than zero if limited
gint64 frame_time; ///< Current frame's start, µs precision
gulong frame_update_connection; ///< GdkFrameClock::update
+
+ GdkGLContext *gl_context; ///< OpenGL context
+ bool gl_initialized; ///< Objects have been created
+ GLuint gl_program; ///< Linked render program
};
G_DEFINE_TYPE_EXTENDED(FivView, fiv_view, GTK_TYPE_WIDGET, 0,
@@ -110,10 +115,10 @@ static FivIoOrientation view_mirror[9] = {
[FivIoOrientationMirror0] = FivIoOrientation0,
[FivIoOrientation180] = FivIoOrientationMirror180,
[FivIoOrientationMirror180] = FivIoOrientation180,
- [FivIoOrientationMirror270] = FivIoOrientation270,
- [FivIoOrientation90] = FivIoOrientationMirror90,
- [FivIoOrientationMirror90] = FivIoOrientation90,
- [FivIoOrientation270] = FivIoOrientationMirror270,
+ [FivIoOrientationMirror270] = FivIoOrientation90,
+ [FivIoOrientation90] = FivIoOrientationMirror270,
+ [FivIoOrientationMirror90] = FivIoOrientation270,
+ [FivIoOrientation270] = FivIoOrientationMirror90,
};
static FivIoOrientation view_right[9] = {
@@ -161,6 +166,147 @@ enum {
// Globals are, sadly, the canonical way of storing signal numbers.
static guint view_signals[LAST_SIGNAL];
+// --- OpenGL ------------------------------------------------------------------
+// While GTK+ 3 technically still supports legacy desktop OpenGL 2.0[1],
+// we will pick the 3.3 core profile, which is fairly old by now.
+// It doesn't seem to make any sense to go below 3.2.
+//
+// [1] https://stackoverflow.com/a/37923507/76313
+//
+// OpenGL ES
+//
+// Currently, we do not support OpenGL ES at all--it needs its own shaders
+// (if only because of different #version statements), and also further analysis
+// as to what is our minimum version requirement. While GTK+ 3 can again go
+// down as low as OpenGL ES 2.0, this might be too much of a hassle to support.
+//
+// ES can be forced via GDK_GL=gles, if gdk_gl_context_set_required_version()
+// doesn't stand in the way.
+//
+// Let's not forget that this is a desktop image viewer first and foremost.
+
+static const char *
+gl_error_string(GLenum err)
+{
+ switch (err) {
+ case GL_NO_ERROR:
+ return "no error";
+ case GL_CONTEXT_LOST:
+ return "context lost";
+ case GL_INVALID_ENUM:
+ return "invalid enum";
+ case GL_INVALID_VALUE:
+ return "invalid value";
+ case GL_INVALID_OPERATION:
+ return "invalid operation";
+ case GL_INVALID_FRAMEBUFFER_OPERATION:
+ return "invalid framebuffer operation";
+ case GL_OUT_OF_MEMORY:
+ return "out of memory";
+ case GL_STACK_UNDERFLOW:
+ return "stack underflow";
+ case GL_STACK_OVERFLOW:
+ return "stack overflow";
+ default:
+ return NULL;
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static const char *gl_vertex =
+ "#version 330\n"
+ "layout(location = 0) in vec4 position;\n"
+ "out vec2 coordinates;\n"
+ "void main() {\n"
+ "\tcoordinates = position.zw;\n"
+ "\tgl_Position = vec4(position.xy, 0., 1.);\n"
+ "}\n";
+
+static const char *gl_fragment =
+ "#version 330\n"
+ "in vec2 coordinates;\n"
+ "layout(location = 0) out vec4 color;\n"
+ "uniform sampler2D picture;\n"
+ "uniform bool checkerboard;\n"
+ "\n"
+ "vec3 checker() {\n"
+ "\tvec2 xy = gl_FragCoord.xy / 20.;\n"
+ "\tif (checkerboard && (int(floor(xy.x) + floor(xy.y)) & 1) == 0)\n"
+ "\t\treturn vec3(0.98);\n"
+ "\telse\n"
+ "\t\treturn vec3(1.00);\n"
+ "}\n"
+ "\n"
+ "void main() {\n"
+ "\tvec3 c = checker();\n"
+ "\tvec4 t = texture(picture, coordinates);\n"
+ "\t// Premultiplied blending with a solid background.\n"
+ "\t// XXX: This is only correct for linear components.\n"
+ "\tcolor = vec4(c * (1. - t.a) + t.rgb, 1.);\n"
+ "}\n";
+
+static GLuint
+gl_make_shader(int type, const char *glsl)
+{
+ GLuint shader = glCreateShader(type);
+ glShaderSource(shader, 1, &glsl, NULL);
+ glCompileShader(shader);
+
+ GLint status = 0;
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
+ if (!status) {
+ GLint len = 0;
+ glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
+
+ GLchar *buffer = g_malloc0(len + 1);
+ glGetShaderInfoLog(shader, len, NULL, buffer);
+ g_warning("GL shader compilation failed: %s", buffer);
+ g_free(buffer);
+
+ glDeleteShader(shader);
+ return 0;
+ }
+ return shader;
+}
+
+static GLuint
+gl_make_program(void)
+{
+ GLuint vertex = gl_make_shader(GL_VERTEX_SHADER, gl_vertex);
+ GLuint fragment = gl_make_shader(GL_FRAGMENT_SHADER, gl_fragment);
+ if (!vertex || !fragment) {
+ glDeleteShader(vertex);
+ glDeleteShader(fragment);
+ return 0;
+ }
+
+ GLuint program = glCreateProgram();
+ glAttachShader(program, vertex);
+ glAttachShader(program, fragment);
+ glLinkProgram(program);
+ glDeleteShader(vertex);
+ glDeleteShader(fragment);
+
+ GLint status = 0;
+ glGetProgramiv(program, GL_LINK_STATUS, &status);
+ if (!status) {
+ GLint len = 0;
+ glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
+
+ GLchar *buffer = g_malloc0(len + 1);
+ glGetProgramInfoLog(program, len, NULL, buffer);
+ g_warning("GL program linking failed: %s", buffer);
+ g_free(buffer);
+
+ glDeleteProgram(program);
+ return 0;
+ }
+ return program;
+}
+
+// -----------------------------------------------------------------------------
+
static void
on_adjustment_value_changed(
G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data)
@@ -198,12 +344,14 @@ update_adjustments(FivView *self)
if (self->hadjustment) {
gtk_adjustment_configure(self->hadjustment,
- gtk_adjustment_get_value(self->hadjustment), 0, dw,
+ gtk_adjustment_get_value(self->hadjustment),
+ 0, MAX(dw, alloc.width),
alloc.width * 0.1, alloc.width * 0.9, alloc.width);
}
if (self->vadjustment) {
gtk_adjustment_configure(self->vadjustment,
- gtk_adjustment_get_value(self->vadjustment), 0, dh,
+ gtk_adjustment_get_value(self->vadjustment),
+ 0, MAX(dh, alloc.height),
alloc.height * 0.1, alloc.height * 0.9, alloc.height);
}
}
@@ -234,9 +382,9 @@ fiv_view_finalize(GObject *gobject)
{
FivView *self = FIV_VIEW(gobject);
g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free);
- g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
- g_clear_pointer(&self->image, cairo_surface_destroy);
- g_clear_pointer(&self->page_scaled, cairo_surface_destroy);
+ g_clear_pointer(&self->enhance_swap, fiv_io_image_unref);
+ g_clear_pointer(&self->image, fiv_io_image_unref);
+ g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
g_free(self->uri);
g_free(self->messages);
@@ -283,15 +431,13 @@ fiv_view_get_property(
g_value_set_boolean(value, !!self->image);
break;
case PROP_CAN_ANIMATE:
- g_value_set_boolean(value, self->page &&
- cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next));
+ g_value_set_boolean(value, self->page && self->page->frame_next);
break;
case PROP_HAS_PREVIOUS_PAGE:
g_value_set_boolean(value, self->image && self->page != self->image);
break;
case PROP_HAS_NEXT_PAGE:
- g_value_set_boolean(value, self->page &&
- cairo_surface_get_user_data(self->page, &fiv_io_key_page_next));
+ g_value_set_boolean(value, self->page && self->page->page_next);
break;
case PROP_HADJUSTMENT:
@@ -403,21 +549,35 @@ static void
prescale_page(FivView *self)
{
FivIoRenderClosure *closure = NULL;
- if (!self->image || !(closure =
- cairo_surface_get_user_data(self->page, &fiv_io_key_render)))
+ if (!self->image || !(closure = self->page->render))
return;
// TODO(p): Restart the animation. No vector formats currently animate.
g_return_if_fail(!self->frame_update_connection);
+ // Optimization, taking into account the workaround in set_scale().
+ if (!self->page_scaled &&
+ (self->scale == 1 || self->scale == 0.999999999999999))
+ return;
+
// If it fails, the previous frame pointer may become invalid.
- g_clear_pointer(&self->page_scaled, cairo_surface_destroy);
- self->frame = self->page_scaled = closure->render(closure, self->scale);
+ g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
+ self->frame = self->page_scaled = closure->render(closure,
+ self->enable_cms ? fiv_io_cmm_get_default() : NULL,
+ self->enable_cms ? self->screen_cms_profile : NULL, self->scale);
if (!self->page_scaled)
self->frame = self->page;
}
static void
+set_source_image(FivView *self, cairo_t *cr)
+{
+ cairo_surface_t *surface = fiv_io_image_to_surface_noref(self->frame);
+ cairo_set_source_surface(cr, surface, 0, 0);
+ cairo_surface_destroy(surface);
+}
+
+static void
fiv_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation)
{
GTK_WIDGET_CLASS(fiv_view_parent_class)->size_allocate(widget, allocation);
@@ -448,6 +608,27 @@ out:
//
// Note that Wayland does not have any appropriate protocol, as of writing:
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14
+static FivIoProfile *
+monitor_cms_profile(GdkWindow *root, int num)
+{
+ char atom[32] = "";
+ g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num);
+
+ // Sadly, there is no nice GTK+/GDK mechanism to watch this for changes.
+ int format = 0, length = 0;
+ GdkAtom type = GDK_NONE;
+ guchar *data = NULL;
+ FivIoProfile *result = NULL;
+ if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
+ 8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
+ if (format == 8 && length > 0)
+ result = fiv_io_cmm_get_profile(
+ fiv_io_cmm_get_default(), data, length);
+ g_free(data);
+ }
+ return result;
+}
+
static void
reload_screen_cms_profile(FivView *self, GdkWindow *window)
{
@@ -477,6 +658,7 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
GdkDisplay *display = gdk_window_get_display(window);
GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window);
+ GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window));
int num = -1;
for (int i = gdk_display_get_n_monitors(display); num < 0 && i--; )
@@ -485,24 +667,14 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
if (num < 0)
goto out;
- char atom[32] = "";
- g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num);
-
- // Sadly, there is no nice GTK+/GDK mechanism to watch this for changes.
- int format = 0, length = 0;
- GdkAtom type = GDK_NONE;
- guchar *data = NULL;
- GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window));
- if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
- 8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
- if (format == 8 && length > 0)
- self->screen_cms_profile = fiv_io_profile_new(data, length);
- g_free(data);
- }
+ // Cater to xiccd limitations (agalakhov/xiccd#33).
+ if (!(self->screen_cms_profile = monitor_cms_profile(root, num)) && num)
+ self->screen_cms_profile = monitor_cms_profile(root, 0);
out:
if (!self->screen_cms_profile)
- self->screen_cms_profile = fiv_io_profile_new_sRGB();
+ self->screen_cms_profile =
+ fiv_io_cmm_get_profile_sRGB(fiv_io_cmm_get_default());
}
static void
@@ -536,6 +708,9 @@ fiv_view_realize(GtkWidget *widget)
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
+ GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
+ gboolean opengl = g_settings_get_boolean(settings, "opengl");
+
// 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.
@@ -545,19 +720,268 @@ fiv_view_realize(GtkWidget *widget)
// Note that this disables double buffering, and sometimes causes artefacts,
// 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.
- GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
+ // GTK+'s OpenGL integration is terrible, so we may need to use
+ // the X11 subwindow directly, sidestepping the toolkit entirely.
if (GDK_IS_X11_WINDOW(window) &&
g_settings_get_boolean(settings, "native-view-window"))
gdk_window_ensure_native(window);
#endif // GDK_WINDOWING_X11
+ g_object_unref(settings);
gtk_widget_register_window(widget, window);
gtk_widget_set_window(widget, window);
gtk_widget_set_realized(widget, TRUE);
reload_screen_cms_profile(FIV_VIEW(widget), window);
+
+ FivView *self = FIV_VIEW(widget);
+ g_clear_object(&self->gl_context);
+ if (!opengl)
+ return;
+
+ GError *error = NULL;
+ GdkGLContext *gl_context = gdk_window_create_gl_context(window, &error);
+ if (!gl_context) {
+ g_warning("GL: %s", error->message);
+ g_error_free(error);
+ return;
+ }
+
+ gdk_gl_context_set_use_es(gl_context, FALSE);
+ gdk_gl_context_set_required_version(gl_context, 3, 3);
+ gdk_gl_context_set_debug_enabled(gl_context, TRUE);
+
+ if (!gdk_gl_context_realize(gl_context, &error)) {
+ g_warning("GL: %s", error->message);
+ g_error_free(error);
+ g_object_unref(gl_context);
+ return;
+ }
+
+ self->gl_context = gl_context;
+}
+
+static void GLAPIENTRY
+gl_on_message(G_GNUC_UNUSED GLenum source, GLenum type, G_GNUC_UNUSED GLuint id,
+ G_GNUC_UNUSED GLenum severity, G_GNUC_UNUSED GLsizei length,
+ const GLchar *message, G_GNUC_UNUSED const void *user_data)
+{
+ if (type == GL_DEBUG_TYPE_ERROR)
+ g_warning("GL: error: %s", message);
+ else
+ g_debug("GL: %s", message);
+}
+
+static void
+fiv_view_unrealize(GtkWidget *widget)
+{
+ FivView *self = FIV_VIEW(widget);
+ if (self->gl_context) {
+ if (self->gl_initialized) {
+ gdk_gl_context_make_current(self->gl_context);
+ glDeleteProgram(self->gl_program);
+ }
+ if (self->gl_context == gdk_gl_context_get_current())
+ gdk_gl_context_clear_current();
+
+ g_clear_object(&self->gl_context);
+ }
+
+ GTK_WIDGET_CLASS(fiv_view_parent_class)->unrealize(widget);
+}
+
+static bool
+gl_draw(FivView *self, cairo_t *cr)
+{
+ gdk_gl_context_make_current(self->gl_context);
+
+ if (!self->gl_initialized) {
+ GLuint program = gl_make_program();
+ if (!program)
+ return false;
+
+ glDisable(GL_SCISSOR_TEST);
+ glDisable(GL_STENCIL_TEST);
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_CULL_FACE);
+ glDisable(GL_BLEND);
+ if (epoxy_has_gl_extension("GL_ARB_debug_output")) {
+ glEnable(GL_DEBUG_OUTPUT);
+ glDebugMessageCallback(gl_on_message, NULL);
+ }
+
+ self->gl_program = program;
+ self->gl_initialized = true;
+ }
+
+ // This limit is always less than that of Cairo/pixman,
+ // and we'd have to figure out tiling.
+ GLint max = 0;
+ glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
+ if (max < (GLint) self->frame->width ||
+ max < (GLint) self->frame->height) {
+ g_warning("OpenGL max. texture size is too small");
+ return false;
+ }
+
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
+ int dw = 0, dh = 0, dx = 0, dy = 0;
+ get_display_dimensions(self, &dw, &dh);
+
+ int clipw = dw, cliph = dh;
+ double x1 = 0., y1 = 0., x2 = 1., y2 = 1.;
+ if (self->hadjustment)
+ x1 = floor(gtk_adjustment_get_value(self->hadjustment)) / dw;
+ if (self->vadjustment)
+ y1 = floor(gtk_adjustment_get_value(self->vadjustment)) / dh;
+
+ if (dw <= allocation.width) {
+ dx = round((allocation.width - dw) / 2.);
+ } else {
+ x2 = x1 + (double) allocation.width / dw;
+ clipw = allocation.width;
+ }
+
+ if (dh <= allocation.height) {
+ dy = round((allocation.height - dh) / 2.);
+ } else {
+ y2 = y1 + (double) allocation.height / dh;
+ cliph = allocation.height;
+ }
+
+ enum { SRC, DEST };
+ GLuint textures[2] = {};
+ glGenTextures(2, textures);
+
+ // https://stackoverflow.com/questions/25157306 0..1
+ // GL_TEXTURE_RECTANGLE seems kind-of useful
+ glBindTexture(GL_TEXTURE_2D, textures[SRC]);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ if (self->filter) {
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ } else {
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ }
+
+ // GL_UNPACK_ALIGNMENT is initially 4, which is fine for these.
+ // Texture swizzling is OpenGL 3.3.
+ if (self->frame->format == CAIRO_FORMAT_ARGB32) {
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
+ self->frame->width, self->frame->height,
+ 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
+ } else if (self->frame->format == CAIRO_FORMAT_RGB24) {
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
+ self->frame->width, self->frame->height,
+ 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
+ } else if (self->frame->format == CAIRO_FORMAT_RGB30) {
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
+ self->frame->width, self->frame->height,
+ 0, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV, self->frame->data);
+ } else {
+ g_warning("GL: unsupported bitmap format");
+ }
+
+ // GtkGLArea creates textures like this.
+ glBindTexture(GL_TEXTURE_2D, textures[DEST]);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, clipw, cliph, 0, GL_BGRA,
+ GL_UNSIGNED_BYTE, NULL);
+
+ glViewport(0, 0, clipw, cliph);
+
+ GLuint vao = 0;
+ glGenVertexArrays(1, &vao);
+
+ GLuint frame_buffer = 0;
+ glGenFramebuffers(1, &frame_buffer);
+ glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer);
+ glFramebufferTexture2D(
+ GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[DEST], 0);
+
+ glClearColor(0., 0., 0., 1.);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+ if (status != GL_FRAMEBUFFER_COMPLETE)
+ g_warning("GL framebuffer status: %u", status);
+
+ glUseProgram(self->gl_program);
+ GLint position_location = glGetAttribLocation(
+ self->gl_program, "position");
+ GLint picture_location = glGetUniformLocation(
+ self->gl_program, "picture");
+ GLint checkerboard_location = glGetUniformLocation(
+ self->gl_program, "checkerboard");
+
+ glUniform1i(picture_location, 0);
+ glUniform1i(checkerboard_location, self->checkerboard);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, textures[SRC]);
+
+ // Note that the Y axis is flipped in the table.
+ double vertices[][4] = {
+ {-1., -1., x1, y2},
+ {+1., -1., x2, y2},
+ {+1., +1., x2, y1},
+ {-1., +1., x1, y1},
+ };
+
+ cairo_matrix_t matrix = fiv_io_orientation_matrix(self->orientation, 1, 1);
+ cairo_matrix_transform_point(&matrix, &vertices[0][2], &vertices[0][3]);
+ cairo_matrix_transform_point(&matrix, &vertices[1][2], &vertices[1][3]);
+ cairo_matrix_transform_point(&matrix, &vertices[2][2], &vertices[2][3]);
+ cairo_matrix_transform_point(&matrix, &vertices[3][2], &vertices[3][3]);
+
+ GLuint vertex_buffer = 0;
+ glGenBuffers(1, &vertex_buffer);
+ glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
+ glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
+ glBindVertexArray(vao);
+ glVertexAttribPointer(position_location,
+ G_N_ELEMENTS(vertices[0]), GL_DOUBLE, GL_FALSE, sizeof vertices[0], 0);
+ glEnableVertexAttribArray(position_location);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, G_N_ELEMENTS(vertices));
+ glDisableVertexAttribArray(position_location);
+ glBindVertexArray(0);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glUseProgram(0);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ // XXX: Native GdkWindows send this to the software fallback path.
+ // XXX: This only reliably alpha blends when using the software fallback,
+ // such as with a native window, because 7237f5d in GTK+ 3 is a regression.
+ // We had to resort to rendering the checkerboard pattern in the shader.
+ // Unfortunately, it is hard to retrieve the theme colours from CSS.
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
+ cairo_translate(cr, dx, dy);
+ gdk_cairo_draw_from_gl(
+ cr, window, textures[DEST], GL_TEXTURE, 1, 0, 0, clipw, cliph);
+ gdk_gl_context_make_current(self->gl_context);
+
+ glDeleteBuffers(1, &vertex_buffer);
+ glDeleteTextures(2, textures);
+ glDeleteVertexArrays(1, &vao);
+ glDeleteFramebuffers(1, &frame_buffer);
+
+ // TODO(p): Possibly use this clue as a hint to use Cairo rendering.
+ GLenum err = 0;
+ while ((err = glGetError()) != GL_NO_ERROR) {
+ const char *string = gl_error_string(err);
+ if (string)
+ g_warning("GL: error: %s", string);
+ else
+ g_warning("GL: error: %u", err);
+ }
+
+ gdk_gl_context_clear_current();
+ return true;
}
static gboolean
@@ -574,8 +998,10 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr)
if (!self->image ||
!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
return TRUE;
+ if (self->gl_context && gl_draw(self, cr))
+ return TRUE;
- int dw, dh;
+ int dw = 0, dh = 0;
get_display_dimensions(self, &dw, &dh);
double x = 0;
@@ -606,37 +1032,19 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr)
// Then all frames are pre-scaled.
if (self->page_scaled) {
- cairo_set_source_surface(cr, self->frame, 0, 0);
+ set_source_image(self, cr);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr);
return TRUE;
}
- // 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, dw, dh);
- cairo_t *tcr = cairo_create(image);
- cairo_scale(tcr, self->scale, self->scale);
- cairo_set_source_surface(tcr, self->frame, 0, 0);
- cairo_pattern_set_matrix(cairo_get_source(tcr), &matrix);
- cairo_paint(tcr);
- cairo_destroy(tcr);
-
- cairo_set_source_surface(cr, image, 0, 0);
- 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, 0, 0, dw, dh);
cairo_clip(cr);
cairo_scale(cr, self->scale, self->scale);
- cairo_set_source_surface(cr, self->frame, 0, 0);
+ set_source_image(self, cr);
cairo_pattern_t *pattern = cairo_get_source(cr);
cairo_pattern_set_matrix(pattern, &matrix);
@@ -810,15 +1218,13 @@ stop_animating(FivView *self)
self->frame_time = 0;
self->frame_update_connection = 0;
- self->remaining_loops = 0;
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
}
static gboolean
advance_frame(FivView *self)
{
- cairo_surface_t *next =
- cairo_surface_get_user_data(self->frame, &fiv_io_key_frame_next);
+ FivIoImage *next = self->frame->frame_next;
if (next) {
self->frame = next;
} else {
@@ -836,8 +1242,7 @@ 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);
+ int64_t duration = self->frame->frame_duration;
if (duration < 0)
return FALSE;
@@ -875,32 +1280,43 @@ 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))
+ if (!clock || !self->image || !self->page->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);
+
+ // Only restart looping the animation if it has stopped at the end.
+ if (!self->remaining_loops) {
+ self->remaining_loops = self->page->loops;
+ if (self->remaining_loops && !self->frame->frame_next) {
+ self->frame = self->page;
+ gtk_widget_queue_draw(GTK_WIDGET(self));
+ }
+ }
gdk_frame_clock_begin_updating(clock);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
}
static void
-switch_page(FivView *self, cairo_surface_t *page)
+switch_page(FivView *self, FivIoImage *page)
{
- g_clear_pointer(&self->page_scaled, cairo_surface_destroy);
+ g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
self->frame = self->page = page;
+
+ // XXX: When self->scale_to_fit is in effect,
+ // this uses an old value that may no longer be appropriate,
+ // resulting in wasted effort.
prescale_page(self);
if (!self->page ||
- (self->orientation = (uintptr_t) cairo_surface_get_user_data(
- self->page, &fiv_io_key_orientation)) == FivIoOrientationUnknown)
+ (self->orientation = self->page->orientation) ==
+ FivIoOrientationUnknown)
self->orientation = FivIoOrientation0;
+ self->remaining_loops = 0;
start_animating(self);
gtk_widget_queue_resize(GTK_WIDGET(self));
@@ -1027,7 +1443,7 @@ copy(FivView *self)
cairo_surface_t *transformed =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
cairo_t *cr = cairo_create(transformed);
- cairo_set_source_surface(cr, self->frame, 0, 0);
+ set_source_image(self, cr);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr);
cairo_destroy(cr);
@@ -1065,7 +1481,7 @@ on_draw_page(G_GNUC_UNUSED GtkPrintOperation *operation,
cairo_t *cr = gtk_print_context_get_cairo_context(context);
cairo_scale(cr, scale, scale);
- cairo_set_source_surface(cr, self->frame, 0, 0);
+ set_source_image(self, cr);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr);
}
@@ -1100,10 +1516,10 @@ print(FivView *self)
}
static gboolean
-save_as(FivView *self, cairo_surface_t *frame)
+save_as(FivView *self, FivIoImage *frame)
{
GtkWindow *window = get_toplevel(GTK_WIDGET(self));
- FivIoProfile target = NULL;
+ FivIoProfile *target = NULL;
if (self->enable_cms && (target = self->screen_cms_profile)) {
GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL,
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s",
@@ -1279,6 +1695,7 @@ fiv_view_class_init(FivViewClass *klass)
widget_class->map = fiv_view_map;
widget_class->unmap = fiv_view_unmap;
widget_class->realize = fiv_view_realize;
+ widget_class->unrealize = fiv_view_unrealize;
widget_class->draw = fiv_view_draw;
widget_class->button_press_event = fiv_view_button_press_event;
widget_class->scroll_event = fiv_view_scroll_event;
@@ -1362,11 +1779,12 @@ fiv_view_init(FivView *self)
// --- Public interface --------------------------------------------------------
-static cairo_surface_t *
+static FivIoImage *
open_without_swapping_in(FivView *self, const char *uri)
{
FivIoOpenContext ctx = {
.uri = uri,
+ .cmm = self->enable_cms ? fiv_io_cmm_get_default() : NULL,
.screen_profile = self->enable_cms ? self->screen_cms_profile : NULL,
.screen_dpi = 96, // TODO(p): Try to retrieve it from the screen.
.enhance = self->enhance,
@@ -1374,7 +1792,7 @@ open_without_swapping_in(FivView *self, const char *uri)
};
GError *error = NULL;
- cairo_surface_t *surface = fiv_io_open(&ctx, &error);
+ FivIoImage *image = fiv_io_open(&ctx, &error);
if (error) {
g_ptr_array_add(ctx.warnings, g_strdup(error->message));
g_error_free(error);
@@ -1387,7 +1805,7 @@ open_without_swapping_in(FivView *self, const char *uri)
}
g_ptr_array_free(ctx.warnings, TRUE);
- return surface;
+ return image;
}
// TODO(p): Progressive picture loading, or at least async/cancellable.
@@ -1395,18 +1813,18 @@ gboolean
fiv_view_set_uri(FivView *self, const char *uri)
{
// This is extremely expensive, and only works sometimes.
- g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
+ g_clear_pointer(&self->enhance_swap, fiv_io_image_unref);
if (self->enhance) {
self->enhance = FALSE;
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_ENHANCE]);
}
- cairo_surface_t *surface = open_without_swapping_in(self, uri);
- g_clear_pointer(&self->image, cairo_surface_destroy);
+ FivIoImage *image = open_without_swapping_in(self, uri);
+ g_clear_pointer(&self->image, fiv_io_image_unref);
self->frame = self->page = NULL;
- self->image = surface;
+ self->image = image;
switch_page(self, self->image);
// Otherwise, adjustment values and zoom are retained implicitly.
@@ -1418,15 +1836,15 @@ fiv_view_set_uri(FivView *self, const char *uri)
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_HAS_IMAGE]);
- return surface != NULL;
+ return image != NULL;
}
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);
+ FivIoImage *page = step < 0
+ ? self->page->page_previous
+ : self->page->page_next;
if (page)
switch_page(self, page);
}
@@ -1435,31 +1853,35 @@ 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)))
+
+ if (step > 0) {
+ // Decrease the loop counter as if running on a timer.
+ (void) advance_frame(self);
+ } else if (!step || !(self->frame = self->frame->frame_previous)) {
self->frame = self->page;
+ self->remaining_loops = 0;
+ }
gtk_widget_queue_draw(GTK_WIDGET(self));
}
static gboolean
reload(FivView *self)
{
- cairo_surface_t *surface = open_without_swapping_in(self, self->uri);
+ FivIoImage *image = open_without_swapping_in(self, self->uri);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]);
- if (!surface)
+ if (!image)
return FALSE;
- g_clear_pointer(&self->image, cairo_surface_destroy);
- g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
- switch_page(self, (self->image = surface));
+ g_clear_pointer(&self->image, fiv_io_image_unref);
+ g_clear_pointer(&self->enhance_swap, fiv_io_image_unref);
+ switch_page(self, (self->image = image));
return TRUE;
}
static void
swap_enhanced_image(FivView *self)
{
- cairo_surface_t *saved = self->image;
+ FivIoImage *saved = self->image;
self->image = self->page = self->frame = NULL;
if (self->enhance_swap) {
@@ -1546,9 +1968,8 @@ fiv_view_command(FivView *self, FivViewCommand command)
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;
+ for (FivIoImage *I = self->page; (I = I->page_next); )
+ self->page = I;
switch_page(self, self->page);
break; case FIV_VIEW_COMMAND_FRAME_FIRST:
diff --git a/fiv.c b/fiv.c
index d58a5a5..43041b0 100644
--- a/fiv.c
+++ b/fiv.c
@@ -1,7 +1,7 @@
//
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -27,6 +27,7 @@
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
+#include <string.h>
#ifdef G_OS_WIN32
#include <io.h>
@@ -38,6 +39,7 @@
#include "fiv-browser.h"
#include "fiv-collection.h"
#include "fiv-io.h"
+#include "fiv-io-model.h"
#include "fiv-sidebar.h"
#include "fiv-thumbnail.h"
#include "fiv-view.h"
@@ -94,16 +96,16 @@ struct key_section {
static struct key help_keys_general[] = {
{"F1", "Show help"},
{"F10", "Open menu"},
- {"<Control>comma", "Preferences"},
- {"<Control>question", "Keyboard shortcuts"},
- {"q <Control>q", "Quit"},
- {"<Control>w", "Quit"},
+ {"<Primary>comma", "Preferences"},
+ {"<Primary>question", "Keyboard shortcuts"},
+ {"q <Primary>q", "Quit"},
+ {"<Primary>w", "Quit"},
{}
};
static struct key help_keys_navigation[] = {
- {"<Control>l", "Open location..."},
- {"<Control>n", "Open a new window"},
+ {"<Primary>l", "Open location..."},
+ {"<Primary>n", "Open a new window"},
{"<Alt>Left", "Go back in history"},
{"<Alt>Right", "Go forward in history"},
{}
@@ -120,16 +122,20 @@ static struct key_group help_keys_browser[] = {
{"General: Navigation", help_keys_navigation},
{"General: View", help_keys_view},
{"Navigation", (struct key[]) {
- {"<Alt>Up", "Go to parent directory"},
{"<Alt>Home", "Go home"},
+ {"<Alt>Up", "Go to parent directory"},
+ {"bracketleft", "Go to previous directory in tree"},
+ {"bracketright", "Go to next directory in tree"},
{"Return", "Open selected item"},
{"<Alt>Return", "Show file information"},
{}
}},
{"View", (struct key[]) {
+ {"F7", "Toggle toolbar"},
{"F9", "Toggle navigation sidebar"},
{"F5 r <Control>r", "Reload"},
{"h <Control>h", "Toggle hiding unsupported files"},
+ {"t <Control>t", "Toggle showing filenames"},
{"<Control>plus", "Larger thumbnails"},
{"<Control>minus", "Smaller thumbnails"},
{}
@@ -148,7 +154,7 @@ static struct key_group help_keys_viewer[] = {
{}
}},
{"View", (struct key[]) {
- {"F9", "Toggle toolbar"},
+ {"F7", "Toggle toolbar"},
{"F5 r <Primary>r", "Reload"},
{}
}},
@@ -383,12 +389,6 @@ on_about_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
GtkStyleContext *style = gtk_widget_get_style_context(widget);
gtk_render_background(style, cr, 0, 0, allocation.width, allocation.height);
- // The transformation matrix turns out/is applied wrongly on Quartz.
- gboolean broken_backend = cairo_surface_get_type(cairo_get_target(cr)) ==
- CAIRO_SURFACE_TYPE_QUARTZ;
- if (broken_backend)
- cairo_push_group(cr);
-
cairo_translate(cr, (allocation.width - ABOUT_SIZE * ABOUT_SCALE) / 2,
ABOUT_SIZE * ABOUT_SCALE / 4);
cairo_scale(cr, ABOUT_SCALE, ABOUT_SCALE);
@@ -414,11 +414,6 @@ on_about_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
cairo_restore(cr);
draw_ligature(cr);
-
- if (broken_backend) {
- cairo_pop_group_to_source(cr);
- cairo_paint(cr);
- }
return TRUE;
}
@@ -459,7 +454,7 @@ show_about_dialog(GtkWidget *parent)
GtkWidget *website = gtk_label_new(NULL);
gtk_label_set_selectable(GTK_LABEL(website), TRUE);
- const char *url = "https://git.janouch.name/p/" PROJECT_NAME;
+ const char *url = PROJECT_URL;
gchar *link = g_strdup_printf("<a href='%s'>%s</a>", url, url);
gtk_label_set_markup(GTK_LABEL(website), link);
g_free(link);
@@ -515,12 +510,146 @@ show_about_dialog(GtkWidget *parent)
cairo_pattern_destroy(ctx.v_pattern);
}
+// --- Settings ----------------------------------------------------------------
+
+static void
+preferences_make_row(
+ GtkWidget *grid, int *row, GSettings *settings, GSettingsSchemaKey *key)
+{
+ const char *name = g_settings_schema_key_get_name(key);
+ const char *summary = g_settings_schema_key_get_summary(key);
+ const char *description = g_settings_schema_key_get_description(key);
+
+ GtkWidget *widget = NULL;
+ const GVariantType *type = g_settings_schema_key_get_value_type(key);
+ if (g_variant_type_equal(type, G_VARIANT_TYPE_BOOLEAN)) {
+ widget = gtk_switch_new();
+ g_settings_bind(
+ settings, name, widget, "active", G_SETTINGS_BIND_DEFAULT);
+ } else {
+ const gchar *type = NULL;
+ GVariant *value = NULL, *range = g_settings_schema_key_get_range(key);
+ g_variant_get(range, "(&sv)", &type, &value);
+ GVariantIter iter = {};
+ g_variant_iter_init(&iter, value);
+ if (g_str_equal(type, "enum")) {
+ widget = gtk_combo_box_text_new();
+
+ GVariant *child = NULL;
+ while ((child = g_variant_iter_next_value(&iter))) {
+ const char *id = g_variant_get_string(child, NULL);
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(widget), id, id);
+ g_variant_unref(child);
+ }
+
+ g_settings_bind(
+ settings, name, widget, "active-id", G_SETTINGS_BIND_DEFAULT);
+ }
+ g_variant_unref(value);
+ g_variant_unref(range);
+ }
+
+ // Ignore unimplemented value types.
+ if (!widget)
+ return;
+
+ GtkWidget *label = gtk_label_new(summary ? summary : name);
+ gtk_label_set_xalign(GTK_LABEL(label), 0);
+ gtk_widget_set_hexpand(label, TRUE);
+ gtk_grid_attach(GTK_GRID(grid), label, 0, (*row), 1, 1);
+ gtk_widget_set_halign(widget, GTK_ALIGN_END);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, (*row)++, 1, 1);
+
+ if (description) {
+ GtkWidget *label = gtk_label_new(description);
+ PangoAttrList *attr_list = pango_attr_list_new();
+ pango_attr_list_insert(
+ attr_list, pango_attr_scale_new(PANGO_SCALE_SMALL));
+ gtk_label_set_attributes(
+ GTK_LABEL(label), pango_attr_list_ref(attr_list));
+ pango_attr_list_unref(attr_list);
+
+ gtk_label_set_xalign(GTK_LABEL(label), 0);
+ gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
+ gtk_widget_set_sensitive(label, FALSE);
+ gtk_widget_set_size_request(label, 0, -1);
+ gtk_grid_attach(GTK_GRID(grid), label, 0, (*row)++, 1, 1);
+ }
+}
+
+static void
+show_preferences(GtkWidget *parent)
+{
+ GSettingsSchema *schema = NULL;
+ GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
+ g_object_get(settings, "settings-schema", &schema, NULL);
+
+ GtkWidget *dialog = gtk_widget_new(GTK_TYPE_DIALOG,
+ "use-header-bar", TRUE,
+ "title", "Preferences",
+ "transient-for", parent,
+ "destroy-with-parent", TRUE, NULL);
+
+ GtkWidget *grid = gtk_grid_new();
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 12);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 24);
+ g_object_set(grid, "margin", 12, NULL);
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
+ grid, TRUE, TRUE, 0);
+
+ int row = 0;
+ gchar **keys = g_settings_schema_list_keys(schema);
+ for (gchar **p = keys; *p; p++) {
+#ifndef GDK_WINDOWING_X11
+ if (g_str_equal(*p, "native-view-window"))
+ continue;
+#endif
+ GSettingsSchemaKey *key = g_settings_schema_get_key(schema, *p);
+ preferences_make_row(grid, &row, settings, key);
+ g_settings_schema_key_unref(key);
+ }
+ g_strfreev(keys);
+ g_object_unref(settings);
+
+ gtk_window_set_default_size(GTK_WINDOW(dialog), 600, -1);
+ gtk_widget_show_all(dialog);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+}
+
// --- Main --------------------------------------------------------------------
// TODO(p): See if it's possible to give separators room to shrink
// by some minor amount of pixels, margin-wise.
#define B make_toolbar_button
#define T make_toolbar_toggle
+#define R make_toolbar_radio
+#define BROWSEBAR(XX) \
+ XX(SIDEBAR, T("sidebar-show-symbolic", "Show sidebar")) \
+ XX(S1, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ XX(DIR_PREVIOUS, B("go-previous-symbolic", "Previous directory")) \
+ XX(DIR_NEXT, B("go-next-symbolic", "Next directory")) \
+ XX(S2, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ XX(PLUS, B("zoom-in-symbolic", "Larger thumbnails")) \
+ XX(MINUS, B("zoom-out-symbolic", "Smaller thumbnails")) \
+ XX(S3, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ XX(FILENAMES, T("text-symbolic", "Show filenames")) \
+ XX(FILTER, T("funnel-symbolic", "Hide unsupported files")) \
+ XX(S4, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ XX(SORT_DIR, B("view-sort-ascending-symbolic", "Sort ascending")) \
+ XX(SORT_NAME, R("Name", "Sort by filename")) \
+ XX(SORT_TIME, R("Time", "Sort by time of last modification")) \
+ XX(S5, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ /* We are YouTube. */ \
+ XX(FULLSCREEN, B("view-fullscreen-symbolic", "Fullscreen"))
+
+enum {
+#define XX(id, constructor) BROWSEBAR_ ## id,
+ BROWSEBAR(XX)
+#undef XX
+ BROWSEBAR_COUNT
+};
+
#define TOOLBAR(XX) \
XX(BROWSE, B("view-grid-symbolic", "Browse")) \
XX(FILE_PREVIOUS, B("go-previous-symbolic", "Previous file")) \
@@ -572,10 +701,9 @@ struct {
gchar *directory; ///< URI of the currently browsed directory
GList *directory_back; ///< History paths as URIs going backwards
GList *directory_forward; ///< History paths as URIs going forwards
- GPtrArray *files; ///< "directory" contents as URIs
gchar *uri; ///< Current image URI, if any
- gint files_index; ///< Where "uri" is within "files"
+ gint files_index; ///< Where "uri" is within the model's files
GtkWidget *window;
GtkWidget *menu;
@@ -583,11 +711,8 @@ struct {
GtkWidget *browser_paned;
GtkWidget *browser_sidebar;
- GtkWidget *plus;
- GtkWidget *minus;
- GtkWidget *funnel;
- GtkWidget *sort_field[FIV_IO_MODEL_SORT_COUNT];
- GtkWidget *sort_direction[2];
+ GtkWidget *browser_toolbar;
+ GtkWidget *browsebar[BROWSEBAR_COUNT];
GtkWidget *browser_scroller;
GtkWidget *browser;
@@ -663,56 +788,38 @@ parent_uri(GFile *child_file)
static void
update_files_index(void)
{
+ gsize files_len = 0;
+ FivIoModelEntry *const *files = fiv_io_model_get_files(g.model, &files_len);
+
g.files_index = -1;
- for (guint i = 0; i < g.files->len; i++)
- if (!g_strcmp0(g.uri, g_ptr_array_index(g.files, i)))
+ for (guint i = 0; i < files_len; i++)
+ if (!g_strcmp0(g.uri, files[i]->uri))
g.files_index = i;
}
static void
-load_directory_without_reload(const char *uri)
+change_directory_without_reload(const char *uri)
{
- gchar *uri_duplicated = g_strdup(uri);
- if (g.directory_back && !strcmp(uri, g.directory_back->data)) {
- // We're going back in history.
- if (g.directory) {
- g.directory_forward =
- g_list_prepend(g.directory_forward, g.directory);
- g.directory = NULL;
- }
+ if (g.directory) {
+ // Note that this function can be passed g.directory directly.
+ if (!strcmp(uri, g.directory))
+ return;
- GList *link = g.directory_back;
- g.directory_back = g_list_remove_link(g.directory_back, link);
- g_list_free_full(link, g_free);
- } else if (g.directory_forward && !strcmp(uri, g.directory_forward->data)) {
- // We're going forward in history.
- if (g.directory) {
- g.directory_back =
- g_list_prepend(g.directory_back, g.directory);
- g.directory = NULL;
- }
-
- GList *link = g.directory_forward;
- g.directory_forward = g_list_remove_link(g.directory_forward, link);
- g_list_free_full(link, g_free);
- } else if (g.directory && strcmp(uri, g.directory)) {
// We're on a new subpath.
g_list_free_full(g.directory_forward, g_free);
g.directory_forward = NULL;
g.directory_back = g_list_prepend(g.directory_back, g.directory);
- g.directory = NULL;
}
- g_free(g.directory);
- g.directory = uri_duplicated;
+ g.directory = g_strdup(uri);
}
static void
load_directory_without_switching(const char *uri)
{
if (uri) {
- load_directory_without_reload(uri);
+ change_directory_without_reload(uri);
GtkAdjustment *vadjustment = gtk_scrolled_window_get_vadjustment(
GTK_SCROLLED_WINDOW(g.browser_scroller));
@@ -723,7 +830,7 @@ load_directory_without_switching(const char *uri)
GError *error = NULL;
GFile *file = g_file_new_for_uri(g.directory);
if (fiv_io_model_open(g.model, file, &error)) {
- // This is handled by our ::files-changed callback.
+ // This is handled by our ::reloaded callback.
} else if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {
g_error_free(error);
} else {
@@ -738,9 +845,6 @@ load_directory(const char *uri)
{
load_directory_without_switching(uri);
- // XXX: When something outside the filtered entries is open, the index is
- // kept at -1, and browsing doesn't work. How to behave here?
- // Should we add it to the pointer array as an exception?
if (uri) {
switch_to_browser_noselect();
@@ -750,23 +854,71 @@ load_directory(const char *uri)
}
static void
-on_model_files_changed(FivIoModel *model, G_GNUC_UNUSED gpointer user_data)
+go_back(void)
+{
+ if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box) {
+ switch_to_browser_noselect();
+ } else if (g.directory_back) {
+ if (g.directory)
+ g.directory_forward =
+ g_list_prepend(g.directory_forward, g.directory);
+
+ const gchar *uri = g.directory = g.directory_back->data;
+
+ GList *link = g.directory_back;
+ g.directory_back = g_list_remove_link(g.directory_back, link);
+ g_list_free(link);
+
+ load_directory(uri);
+ }
+}
+
+static void
+go_forward(void)
+{
+ if (g.directory_forward) {
+ if (g.directory)
+ g.directory_back =
+ g_list_prepend(g.directory_back, g.directory);
+
+ const gchar *uri = g.directory = g.directory_forward->data;
+
+ GList *link = g.directory_forward;
+ g.directory_forward = g_list_remove_link(g.directory_forward, link);
+ g_list_free(link);
+
+ load_directory(uri);
+ } else if (g.uri) {
+ switch_to_view();
+ }
+}
+
+static void
+on_model_reloaded(FivIoModel *model, G_GNUC_UNUSED gpointer user_data)
{
g_return_if_fail(model == g.model);
- gsize len = 0;
- const FivIoModelEntry *files = fiv_io_model_get_files(g.model, &len);
- g_ptr_array_free(g.files, TRUE);
- g.files = g_ptr_array_new_full(len, g_free);
- for (gsize i = 0; i < len; i++)
- g_ptr_array_add(g.files, g_strdup(files[i].uri));
+ gsize files_len = 0;
+ (void) fiv_io_model_get_files(g.model, &files_len);
update_files_index();
- gtk_widget_set_sensitive(
- g.toolbar[TOOLBAR_FILE_PREVIOUS], g.files->len > 1);
- gtk_widget_set_sensitive(
- g.toolbar[TOOLBAR_FILE_NEXT], g.files->len > 1);
+ gtk_widget_set_sensitive(g.toolbar[TOOLBAR_FILE_PREVIOUS], files_len > 1);
+ gtk_widget_set_sensitive(g.toolbar[TOOLBAR_FILE_NEXT], files_len > 1);
+}
+
+static void
+on_model_files_changed(FivIoModel *model, G_GNUC_UNUSED FivIoModelEntry *old,
+ G_GNUC_UNUSED FivIoModelEntry *new, G_GNUC_UNUSED gpointer user_data)
+{
+ on_model_reloaded(model, NULL);
+}
+
+static void
+on_sidebar_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
+{
+ gboolean active = gtk_toggle_button_get_active(button);
+ gtk_widget_set_visible(g.browser_sidebar, active);
}
static void
@@ -777,21 +929,33 @@ on_filtering_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
}
static void
-on_sort_field(G_GNUC_UNUSED GtkMenuItem *item, gpointer data)
+on_filenames_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
+{
+ gboolean active = gtk_toggle_button_get_active(button);
+ g_object_set(g.browser, "show-labels", active, NULL);
+}
+
+static void
+on_sort_field(G_GNUC_UNUSED GtkToggleButton *button, gpointer data)
{
- int old = -1, new = (int) (intptr_t) data;
+ gboolean active = gtk_toggle_button_get_active(button);
+ if (!active)
+ return;
+
+ FivIoModelSort old = FIV_IO_MODEL_SORT_COUNT;
+ FivIoModelSort new = (FivIoModelSort) (intptr_t) data;
g_object_get(g.model, "sort-field", &old, NULL);
if (old != new)
g_object_set(g.model, "sort-field", new, NULL);
}
static void
-on_sort_direction(G_GNUC_UNUSED GtkMenuItem *item, gpointer data)
+on_sort_direction(G_GNUC_UNUSED GtkToggleButton *button,
+ G_GNUC_UNUSED gpointer data)
{
- gboolean old = FALSE, new = (gboolean) (intptr_t) data;
+ gboolean old = FALSE;
g_object_get(g.model, "sort-descending", &old, NULL);
- if (old != new)
- g_object_set(g.model, "sort-descending", new, NULL);
+ g_object_set(g.model, "sort-descending", !old, NULL);
}
static void
@@ -828,13 +992,17 @@ open_image(const char *uri)
// So that load_directory() itself can be used for reloading.
gchar *parent = parent_uri(file);
g_object_unref(file);
- if (!g.files->len /* hack to always load the directory after launch */ ||
- !g.directory || strcmp(parent, g.directory))
+ if (!fiv_io_model_get_location(g.model) || !g.directory ||
+ strcmp(parent, g.directory))
load_directory_without_switching(parent);
else
update_files_index();
g_free(parent);
+ // XXX: When something outside currently filtered entries is open,
+ // g.files_index is kept at -1, and browsing doesn't work.
+ // How to behave here?
+
switch_to_view();
}
@@ -902,18 +1070,22 @@ on_open(void)
static void
on_previous(void)
{
+ gsize files_len = 0;
+ FivIoModelEntry *const *files = fiv_io_model_get_files(g.model, &files_len);
if (g.files_index >= 0) {
- int previous = (g.files->len + g.files_index - 1) % g.files->len;
- open_image(g_ptr_array_index(g.files, previous));
+ int previous = (files_len + g.files_index - 1) % files_len;
+ open_image(files[previous]->uri);
}
}
static void
on_next(void)
{
+ gsize files_len = 0;
+ FivIoModelEntry *const *files = fiv_io_model_get_files(g.model, &files_len);
if (g.files_index >= 0) {
- int next = (g.files_index + 1) % g.files->len;
- open_image(g_ptr_array_index(g.files, next));
+ int next = (g.files_index + 1) % files_len;
+ open_image(files[next]->uri);
}
}
@@ -1089,6 +1261,40 @@ on_view_drag_data_received(G_GNUC_UNUSED GtkWidget *widget,
}
static void
+on_notify_sidebar_visible(
+ GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data)
+{
+ gboolean b = FALSE;
+ g_object_get(object, g_param_spec_get_name(param_spec), &b, NULL);
+ gtk_toggle_button_set_active(
+ GTK_TOGGLE_BUTTON(g.browsebar[BROWSEBAR_SIDEBAR]), b);
+}
+
+static void
+on_dir_previous(void)
+{
+ GFile *directory = fiv_io_model_get_previous_directory(g.model);
+ if (directory) {
+ gchar *uri = g_file_get_uri(directory);
+ g_object_unref(directory);
+ load_directory(uri);
+ g_free(uri);
+ }
+}
+
+static void
+on_dir_next(void)
+{
+ GFile *directory = fiv_io_model_get_next_directory(g.model);
+ if (directory) {
+ gchar *uri = g_file_get_uri(directory);
+ g_object_unref(directory);
+ load_directory(uri);
+ g_free(uri);
+ }
+}
+
+static void
on_toolbar_zoom(G_GNUC_UNUSED GtkButton *button, gpointer user_data)
{
FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
@@ -1105,10 +1311,22 @@ static void
on_notify_thumbnail_size(
GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data)
{
- FivThumbnailSize size = 0;
+ FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
g_object_get(object, g_param_spec_get_name(param_spec), &size, NULL);
- gtk_widget_set_sensitive(g.plus, size < FIV_THUMBNAIL_SIZE_MAX);
- gtk_widget_set_sensitive(g.minus, size > FIV_THUMBNAIL_SIZE_MIN);
+ gtk_widget_set_sensitive(
+ g.browsebar[BROWSEBAR_PLUS], size < FIV_THUMBNAIL_SIZE_MAX);
+ gtk_widget_set_sensitive(
+ g.browsebar[BROWSEBAR_MINUS], size > FIV_THUMBNAIL_SIZE_MIN);
+}
+
+static void
+on_notify_show_labels(
+ GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data)
+{
+ gboolean show_labels = 0;
+ g_object_get(object, g_param_spec_get_name(param_spec), &show_labels, NULL);
+ gtk_toggle_button_set_active(
+ GTK_TOGGLE_BUTTON(g.browsebar[BROWSEBAR_FILENAMES]), show_labels);
}
static void
@@ -1117,7 +1335,8 @@ on_notify_filtering(
{
gboolean b = FALSE;
g_object_get(object, g_param_spec_get_name(param_spec), &b, NULL);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g.funnel), b);
+ gtk_toggle_button_set_active(
+ GTK_TOGGLE_BUTTON(g.browsebar[BROWSEBAR_FILTER]), b);
}
static void
@@ -1126,8 +1345,8 @@ on_notify_sort_field(
{
gint field = -1;
g_object_get(object, g_param_spec_get_name(param_spec), &field, NULL);
- gtk_check_menu_item_set_active(
- GTK_CHECK_MENU_ITEM(g.sort_field[field]), TRUE);
+ gtk_toggle_button_set_active(
+ GTK_TOGGLE_BUTTON(g.browsebar[BROWSEBAR_SORT_NAME + field]), TRUE);
}
static void
@@ -1136,8 +1355,18 @@ on_notify_sort_descending(
{
gboolean b = FALSE;
g_object_get(object, g_param_spec_get_name(param_spec), &b, NULL);
- gtk_check_menu_item_set_active(
- GTK_CHECK_MENU_ITEM(g.sort_direction[b]), TRUE);
+
+ const char *title = b
+ ? "Sort ascending"
+ : "Sort descending";
+ const char *name = b
+ ? "view-sort-ascending-symbolic"
+ : "view-sort-descending-symbolic";
+
+ GtkButton *button = GTK_BUTTON(g.browsebar[BROWSEBAR_SORT_DIR]);
+ GtkImage *image = GTK_IMAGE(gtk_button_get_image(button));
+ gtk_widget_set_tooltip_text(GTK_WIDGET(button), title);
+ gtk_image_set_from_icon_name(image, name, GTK_ICON_SIZE_BUTTON);
}
static void
@@ -1161,9 +1390,14 @@ on_window_state_event(G_GNUC_UNUSED GtkWidget *widget,
? "view-restore-symbolic"
: "view-fullscreen-symbolic";
- GtkButton *button = GTK_BUTTON(g.toolbar[TOOLBAR_FULLSCREEN]);
- GtkImage *image = GTK_IMAGE(gtk_button_get_image(button));
- gtk_image_set_from_icon_name(image, name, GTK_ICON_SIZE_BUTTON);
+ gtk_image_set_from_icon_name(
+ GTK_IMAGE(gtk_button_get_image(
+ GTK_BUTTON(g.toolbar[TOOLBAR_FULLSCREEN]))),
+ name, GTK_ICON_SIZE_BUTTON);
+ gtk_image_set_from_icon_name(
+ GTK_IMAGE(gtk_button_get_image(
+ GTK_BUTTON(g.browsebar[BROWSEBAR_FULLSCREEN]))),
+ name, GTK_ICON_SIZE_BUTTON);
}
static void
@@ -1214,20 +1448,6 @@ show_help_shortcuts(void)
}
static void
-show_preferences(void)
-{
- char *argv[] = {"dconf-editor", PROJECT_NS PROJECT_NAME, NULL};
- GError *error = NULL;
- if (!g_spawn_async(
- NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error)) {
- if (g_error_matches(error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT))
- g_prefix_error_literal(&error,
- "Please install dconf-editor, or use the gsettings utility.\n");
- show_error_dialog(error);
- }
-}
-
-static void
toggle_sunlight(void)
{
GtkSettings *settings = gtk_settings_get_default();
@@ -1237,94 +1457,16 @@ toggle_sunlight(void)
g_object_set(settings, property, !set, NULL);
}
-// Cursor keys, e.g., simply cannot be bound through accelerators
-// (and GtkWidget::keynav-failed would arguably be an awful solution).
-//
-// GtkBindingSets can be added directly through GtkStyleContext,
-// but that would still require setting up action signals on the widget class,
-// which is extremely cumbersome. GtkWidget::move-focus has no return value,
-// so we can't override that and abort further handling.
-//
-// Therefore, bind directly to keypresses. Order can be fine-tuned with
-// g_signal_connect{,after}(), or overriding the handler and either tactically
-// chaining up or using gtk_window_propagate_key_event().
static gboolean
on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
G_GNUC_UNUSED gpointer data)
{
switch (event->state & gtk_accelerator_get_default_mod_mask()) {
- case GDK_MOD1_MASK | GDK_SHIFT_MASK:
- if (event->keyval == GDK_KEY_D)
- toggle_sunlight();
- break;
case GDK_CONTROL_MASK:
- case GDK_CONTROL_MASK | GDK_SHIFT_MASK:
switch (event->keyval) {
case GDK_KEY_h:
- gtk_button_clicked(GTK_BUTTON(g.funnel));
- return TRUE;
- case GDK_KEY_l:
- fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
- return TRUE;
- case GDK_KEY_n:
- if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
- spawn_uri(g.uri);
- else
- spawn_uri(g.directory);
- return TRUE;
- case GDK_KEY_o:
- on_open();
- return TRUE;
- case GDK_KEY_q:
- case GDK_KEY_w:
- gtk_widget_destroy(g.window);
- return TRUE;
-
- case GDK_KEY_question:
- show_help_shortcuts();
- return TRUE;
- case GDK_KEY_comma:
- show_preferences();
- return TRUE;
- }
- break;
- case GDK_MOD1_MASK:
- switch (event->keyval) {
- case GDK_KEY_Left:
- if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
- switch_to_browser_noselect();
- else if (g.directory_back)
- load_directory(g.directory_back->data);
- return TRUE;
- case GDK_KEY_Right:
- if (g.directory_forward)
- load_directory(g.directory_forward->data);
- else if (g.uri)
- switch_to_view();
- return TRUE;
- }
- break;
- case GDK_SHIFT_MASK:
- switch (event->keyval) {
- case GDK_KEY_F1:
- show_about_dialog(g.window);
- return TRUE;
- }
- break;
- case 0:
- switch (event->keyval) {
- case GDK_KEY_q:
- gtk_widget_destroy(g.window);
- return TRUE;
- case GDK_KEY_o:
- on_open();
- return TRUE;
- case GDK_KEY_F1:
- show_help_contents();
- return TRUE;
- case GDK_KEY_F11:
- case GDK_KEY_f:
- toggle_fullscreen();
+ // XXX: Command-H is already occupied on macOS.
+ gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
return TRUE;
}
}
@@ -1340,8 +1482,15 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
gtk_accelerator_parse(accelerator, &key, &mods);
g_free(accelerator);
+ // TODO(p): See how Unity 7 behaves,
+ // we might want to keep GtkApplicationWindow:show-menubar then.
+ gboolean shell_shows_menubar = FALSE;
+ (void) g_object_get(gtk_settings_get_default(),
+ "gtk-shell-shows-menubar", &shell_shows_menubar, NULL);
+
guint mask = gtk_accelerator_get_default_mod_mask();
- if (key && event->keyval == key && (event->state & mask) == mods) {
+ if (key && event->keyval == key && (event->state & mask) == mods &&
+ !shell_shows_menubar) {
gtk_widget_show(g.menu);
// _gtk_menu_shell_set_keyboard_mode() is private.
@@ -1351,6 +1500,17 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
return FALSE;
}
+// Cursor keys, e.g., simply cannot be bound through accelerators
+// (and GtkWidget::keynav-failed would arguably be an awful solution).
+//
+// GtkBindingSets can be added directly through GtkStyleContext,
+// but that would still require setting up action signals on the widget class,
+// which is extremely cumbersome. GtkWidget::move-focus has no return value,
+// so we can't override that and abort further handling.
+//
+// Therefore, bind directly to keypresses. Order can be fine-tuned with
+// g_signal_connect{,after}(), or overriding the handler and either tactically
+// chaining up or using gtk_window_propagate_key_event().
static gboolean
on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
G_GNUC_UNUSED gpointer data)
@@ -1358,7 +1518,7 @@ on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
switch (event->state & gtk_accelerator_get_default_mod_mask()) {
case 0:
switch (event->keyval) {
- case GDK_KEY_F9:
+ case GDK_KEY_F7:
gtk_widget_set_visible(g.view_toolbar,
!gtk_widget_is_visible(g.view_toolbar));
return TRUE;
@@ -1395,6 +1555,9 @@ on_key_press_browser_paned(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
case GDK_KEY_r:
load_directory(NULL);
return TRUE;
+ case GDK_KEY_t:
+ gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILENAMES]));
+ return TRUE;
}
break;
case GDK_MOD1_MASK:
@@ -1417,21 +1580,35 @@ on_key_press_browser_paned(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
break;
case 0:
switch (event->keyval) {
+ case GDK_KEY_F7:
+ gtk_widget_set_visible(g.browser_toolbar,
+ !gtk_widget_is_visible(g.browser_toolbar));
+ return TRUE;
case GDK_KEY_F9:
gtk_widget_set_visible(g.browser_sidebar,
!gtk_widget_is_visible(g.browser_sidebar));
return TRUE;
+ case GDK_KEY_bracketleft:
+ on_dir_previous();
+ return TRUE;
+ case GDK_KEY_bracketright:
+ on_dir_next();
+ return TRUE;
+
case GDK_KEY_Escape:
fiv_browser_select(FIV_BROWSER(g.browser), NULL);
return TRUE;
case GDK_KEY_h:
- gtk_button_clicked(GTK_BUTTON(g.funnel));
+ gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
return TRUE;
case GDK_KEY_F5:
case GDK_KEY_r:
load_directory(NULL);
return TRUE;
+ case GDK_KEY_t:
+ gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILENAMES]));
+ return TRUE;
}
}
return FALSE;
@@ -1445,7 +1622,7 @@ on_button_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event)
switch (event->button) {
case 4: // back (GdkWin32, GdkQuartz)
case 8: // back
- switch_to_browser_noselect();
+ go_back();
return TRUE;
case GDK_BUTTON_PRIMARY:
if (event->type == GDK_2BUTTON_PRESS) {
@@ -1467,15 +1644,11 @@ on_button_press_browser_paned(
switch (event->button) {
case 4: // back (GdkWin32, GdkQuartz)
case 8: // back
- if (g.directory_back)
- load_directory(g.directory_back->data);
+ go_back();
return TRUE;
case 5: // forward (GdkWin32, GdkQuartz)
case 9: // forward
- if (g.directory_forward)
- load_directory(g.directory_forward->data);
- else if (g.uri)
- switch_to_view();
+ go_forward();
return TRUE;
default:
return FALSE;
@@ -1510,6 +1683,81 @@ make_toolbar_toggle(const char *symbolic, const char *tooltip)
return button;
}
+static GtkWidget *
+make_toolbar_radio(const char *label, const char *tooltip)
+{
+ GtkWidget *button = gtk_radio_button_new_with_label(NULL, label);
+ gtk_widget_set_tooltip_text(button, tooltip);
+ gtk_widget_set_focus_on_click(button, FALSE);
+ gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(button), FALSE);
+ gtk_style_context_add_class(
+ gtk_widget_get_style_context(button), GTK_STYLE_CLASS_FLAT);
+ return button;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+browsebar_connect(int index, GCallback callback)
+{
+ g_signal_connect_swapped(g.browsebar[index], "clicked", callback, NULL);
+}
+
+static GtkWidget *
+make_browser_toolbar(void)
+{
+#define XX(id, constructor) g.browsebar[BROWSEBAR_ ## id] = constructor;
+ BROWSEBAR(XX)
+#undef XX
+
+ // GtkStatusBar solves a problem we do not have here.
+ GtkWidget *browser_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_style_context_add_class(
+ gtk_widget_get_style_context(browser_toolbar), "fiv-toolbar");
+ GtkBox *box = GTK_BOX(browser_toolbar);
+
+ // Exploring different versions of awkward layouts.
+ for (int i = 0; i <= BROWSEBAR_S2; i++)
+ gtk_box_pack_start(box, g.browsebar[i], FALSE, FALSE, 0);
+ for (int i = BROWSEBAR_COUNT; --i >= BROWSEBAR_S5; )
+ gtk_box_pack_end(box, g.browsebar[i], FALSE, FALSE, 0);
+
+ GtkWidget *center = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ for (int i = BROWSEBAR_S2; ++i < BROWSEBAR_S5; )
+ gtk_box_pack_start(GTK_BOX(center), g.browsebar[i], FALSE, FALSE, 0);
+ gtk_box_set_center_widget(box, center);
+
+ g_signal_connect(g.browsebar[BROWSEBAR_SIDEBAR], "toggled",
+ G_CALLBACK(on_sidebar_toggled), NULL);
+
+ browsebar_connect(BROWSEBAR_DIR_PREVIOUS, G_CALLBACK(on_dir_previous));
+ browsebar_connect(BROWSEBAR_DIR_NEXT, G_CALLBACK(on_dir_next));
+ browsebar_connect(BROWSEBAR_SORT_DIR, G_CALLBACK(on_sort_direction));
+ browsebar_connect(BROWSEBAR_FULLSCREEN, G_CALLBACK(toggle_fullscreen));
+
+ g_signal_connect(g.browsebar[BROWSEBAR_PLUS], "clicked",
+ G_CALLBACK(on_toolbar_zoom), (gpointer) +1);
+ g_signal_connect(g.browsebar[BROWSEBAR_MINUS], "clicked",
+ G_CALLBACK(on_toolbar_zoom), (gpointer) -1);
+
+ g_signal_connect(g.browsebar[BROWSEBAR_FILTER], "toggled",
+ G_CALLBACK(on_filtering_toggled), NULL);
+ g_signal_connect(g.browsebar[BROWSEBAR_FILENAMES], "toggled",
+ G_CALLBACK(on_filenames_toggled), NULL);
+
+ GtkRadioButton *last = GTK_RADIO_BUTTON(g.browsebar[BROWSEBAR_SORT_NAME]);
+ for (int i = BROWSEBAR_SORT_NAME; i <= BROWSEBAR_SORT_TIME; i++) {
+ GtkRadioButton *radio = GTK_RADIO_BUTTON(g.browsebar[i]);
+ g_signal_connect(radio, "toggled", G_CALLBACK(on_sort_field),
+ (gpointer) (gintptr) i - BROWSEBAR_SORT_NAME);
+ gtk_radio_button_join_group(radio, last);
+ last = radio;
+ }
+ return browser_toolbar;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
static void
on_view_actions_changed(void)
{
@@ -1655,7 +1903,8 @@ make_view_toolbar(void)
// GtkStatusBar solves a problem we do not have here.
GtkWidget *view_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_widget_set_name(view_toolbar, "toolbar");
+ gtk_style_context_add_class(
+ gtk_widget_get_style_context(view_toolbar), "fiv-toolbar");
GtkBox *box = GTK_BOX(view_toolbar);
// Exploring different versions of awkward layouts.
@@ -1746,124 +1995,202 @@ make_browser_sidebar(FivIoModel *model)
g_signal_connect(sidebar, "open-location",
G_CALLBACK(on_open_location), NULL);
- g.plus = gtk_button_new_from_icon_name("zoom-in-symbolic",
- GTK_ICON_SIZE_BUTTON);
- gtk_widget_set_tooltip_text(g.plus, "Larger thumbnails");
- g_signal_connect(g.plus, "clicked",
- G_CALLBACK(on_toolbar_zoom), (gpointer) +1);
-
- g.minus = gtk_button_new_from_icon_name("zoom-out-symbolic",
- GTK_ICON_SIZE_BUTTON);
- gtk_widget_set_tooltip_text(g.minus, "Smaller thumbnails");
- g_signal_connect(g.minus, "clicked",
- G_CALLBACK(on_toolbar_zoom), (gpointer) -1);
+ g_signal_connect(sidebar, "notify::visible",
+ G_CALLBACK(on_notify_sidebar_visible), NULL);
- GtkWidget *zoom_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_style_context_add_class(
- gtk_widget_get_style_context(zoom_group), GTK_STYLE_CLASS_LINKED);
- gtk_box_pack_start(GTK_BOX(zoom_group), g.plus, FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(zoom_group), g.minus, FALSE, FALSE, 0);
-
- g.funnel = gtk_toggle_button_new();
- gtk_container_add(GTK_CONTAINER(g.funnel),
- gtk_image_new_from_icon_name("funnel-symbolic", GTK_ICON_SIZE_BUTTON));
- gtk_widget_set_tooltip_text(g.funnel, "Hide unsupported files");
- g_signal_connect(g.funnel, "toggled",
- G_CALLBACK(on_filtering_toggled), NULL);
-
- GtkWidget *menu = gtk_menu_new();
- g.sort_field[0] = gtk_radio_menu_item_new_with_mnemonic(NULL, "By _Name");
- g.sort_field[1] = gtk_radio_menu_item_new_with_mnemonic(
- gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(g.sort_field[0])),
- "By _Modification Time");
- for (int i = FIV_IO_MODEL_SORT_MIN; i <= FIV_IO_MODEL_SORT_MAX; i++) {
- g_signal_connect(g.sort_field[i], "activate",
- G_CALLBACK(on_sort_field), (void *) (intptr_t) i);
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), g.sort_field[i]);
- }
-
- g.sort_direction[0] =
- gtk_radio_menu_item_new_with_mnemonic(NULL, "_Ascending");
- g.sort_direction[1] = gtk_radio_menu_item_new_with_mnemonic(
- gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(g.sort_direction[0])),
- "_Descending");
- g_signal_connect(g.sort_direction[0], "activate",
- G_CALLBACK(on_sort_direction), (void *) 0);
- g_signal_connect(g.sort_direction[1], "activate",
- G_CALLBACK(on_sort_direction), (void *) 1);
-
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), g.sort_direction[0]);
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), g.sort_direction[1]);
- gtk_widget_show_all(menu);
-
- GtkWidget *sort = gtk_menu_button_new();
- gtk_widget_set_tooltip_text(sort, "Sort order");
- gtk_button_set_image(GTK_BUTTON(sort),
- gtk_image_new_from_icon_name(
- "view-sort-ascending-symbolic", GTK_ICON_SIZE_BUTTON));
- gtk_menu_button_set_popup(GTK_MENU_BUTTON(sort), menu);
-
- GtkWidget *model_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_style_context_add_class(
- gtk_widget_get_style_context(model_group), GTK_STYLE_CLASS_LINKED);
- gtk_box_pack_start(GTK_BOX(model_group), g.funnel, FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(model_group), sort, FALSE, FALSE, 0);
-
- GtkBox *toolbar = fiv_sidebar_get_toolbar(FIV_SIDEBAR(sidebar));
- gtk_box_pack_start(toolbar, zoom_group, FALSE, FALSE, 0);
- gtk_box_pack_start(toolbar, model_group, FALSE, FALSE, 0);
- gtk_widget_set_halign(GTK_WIDGET(toolbar), GTK_ALIGN_CENTER);
+ g_object_notify(G_OBJECT(sidebar), "visible");
g_signal_connect(g.browser, "notify::thumbnail-size",
G_CALLBACK(on_notify_thumbnail_size), NULL);
+ g_signal_connect(g.browser, "notify::show-labels",
+ G_CALLBACK(on_notify_show_labels), NULL);
g_signal_connect(model, "notify::filtering",
G_CALLBACK(on_notify_filtering), NULL);
g_signal_connect(model, "notify::sort-field",
G_CALLBACK(on_notify_sort_field), NULL);
g_signal_connect(model, "notify::sort-descending",
G_CALLBACK(on_notify_sort_descending), NULL);
+
on_toolbar_zoom(NULL, (gpointer) 0);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g.funnel), TRUE);
- // TODO(p): Invoke sort configuration notifications explicitly.
+
+ g_object_notify(G_OBJECT(g.model), "filtering");
+ g_object_notify(G_OBJECT(g.model), "sort-field");
+ g_object_notify(G_OBJECT(g.model), "sort-descending");
return sidebar;
}
+// --- Actions -----------------------------------------------------------------
+
+#define ACTION(name) static void on_action_ ## name(void)
+
+ACTION(new_window) {
+ if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
+ spawn_uri(g.uri);
+ else
+ spawn_uri(g.directory);
+}
+
+ACTION(quit) {
+ gtk_widget_destroy(g.window);
+}
+
+ACTION(location) {
+ fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
+}
+
+ACTION(preferences) {
+ show_preferences(g.window);
+}
+
+ACTION(about) {
+ show_about_dialog(g.window);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+typedef struct {
+ const char *name; ///< Unprefixed action name
+ GCallback callback; ///< Simple callback
+ const char **accels; ///< NULL-terminated accelerator list
+} ActionEntry;
+
+static ActionEntry g_actions[] = {
+ {"preferences", on_action_preferences,
+ (const char *[]) {"<Primary>comma", NULL}},
+ {"new-window", on_action_new_window,
+ (const char *[]) {"<Primary>n", NULL}},
+ {"open", on_open,
+ (const char *[]) {"<Primary>o", "o", NULL}},
+ {"quit", on_action_quit,
+ (const char *[]) {"<Primary>q", "<Primary>w", "q", NULL}},
+ {"toggle-fullscreen", toggle_fullscreen,
+ (const char *[]) {"F11", "f", NULL}},
+ {"toggle-sunlight", toggle_sunlight,
+ (const char *[]) {"<Alt><Shift>d", NULL}},
+ {"go-back", go_back,
+ (const char *[]) {"<Alt>Left", "BackSpace", NULL}},
+ {"go-forward", go_forward,
+ (const char *[]) {"<Alt>Right", NULL}},
+ {"go-location", on_action_location,
+ (const char *[]) {"<Primary>l", NULL}},
+ {"help", show_help_contents,
+ (const char *[]) {"F1", NULL}},
+ {"shortcuts", show_help_shortcuts,
+ // Similar to win.show-help-overlay in gtkapplication.c.
+ (const char *[]) {"<Primary>question", "<Primary>F1", NULL}},
+ {"about", on_action_about,
+ (const char *[]) {"<Shift>F1", NULL}},
+ {}
+};
+
+static void
+dispatch_action(G_GNUC_UNUSED GSimpleAction *action,
+ G_GNUC_UNUSED GVariant *parameter, gpointer user_data)
+{
+ GCallback callback = user_data;
+ callback();
+}
+
+static void
+set_up_action(GtkApplication *app, const ActionEntry *a)
+{
+ GSimpleAction *action = g_simple_action_new(a->name, NULL);
+ g_signal_connect(action, "activate",
+ G_CALLBACK(dispatch_action), a->callback);
+ g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(action));
+ g_object_unref(action);
+
+ gchar *full_name = g_strdup_printf("app.%s", a->name);
+ gtk_application_set_accels_for_action(app, full_name, a->accels);
+ g_free(full_name);
+}
+
+// --- Menu --------------------------------------------------------------------
+
+typedef struct {
+ const char *label; ///< Label, with a mnemonic
+ const char *action; ///< Prefixed action name
+ gboolean macos; ///< Show in the macOS global menu?
+} MenuItem;
+
+typedef struct {
+ const char *label; ///< Label, with a mnemonic
+ const MenuItem *items; ///< ""-sectioned menu items
+} MenuRoot;
+
+// We're single-instance, skip the "win" namespace for simplicity.
+static MenuRoot g_menu[] = {
+ {"_File", (MenuItem[]) {
+ {"_New Window", "app.new-window", TRUE},
+ {"_Open...", "app.open", TRUE},
+ {"", NULL, TRUE},
+ {"_Quit", "app.quit", FALSE},
+ {}
+ }},
+ {"_Go", (MenuItem[]) {
+ {"_Back", "app.go-back", TRUE},
+ {"_Forward", "app.go-forward", TRUE},
+ {"", NULL, TRUE},
+ {"_Location...", "app.go-location", TRUE},
+ {}
+ }},
+ {"_Help", (MenuItem[]) {
+ {"_Contents", "app.help", TRUE},
+ {"_Keyboard Shortcuts", "app.shortcuts", TRUE},
+ {"_About", "app.about", FALSE},
+ {}
+ }},
+ {}
+};
+
+static GMenuModel *
+make_submenu(const MenuItem *items)
+{
+ GMenu *menu = g_menu_new();
+ while (items->label) {
+ GMenu *section = g_menu_new();
+ for (; items->label; items++) {
+ // Empty strings are interpreted as separators.
+ if (!*items->label) {
+ items++;
+ break;
+ }
+
+ GMenuItem *subitem = g_menu_item_new(items->label, items->action);
+ if (!items->macos) {
+ g_menu_item_set_attribute(
+ subitem, "hidden-when", "s", "macos-menubar");
+ }
+
+ g_menu_append_item(section, subitem);
+ g_object_unref(subitem);
+ }
+ g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
+ g_object_unref(section);
+ }
+ return G_MENU_MODEL(menu);
+}
+
+static GMenuModel *
+make_menu_model(void)
+{
+ GMenu *menu = g_menu_new();
+ for (const MenuRoot *root = g_menu; root->label; root++) {
+ GMenuModel *submenu = make_submenu(root->items);
+ g_menu_append_submenu(menu, root->label, submenu);
+ g_object_unref(submenu);
+ }
+ return G_MENU_MODEL(menu);
+}
+
static GtkWidget *
-make_menu_bar(void)
-{
- g.menu = gtk_menu_bar_new();
-
- GtkWidget *item_quit = gtk_menu_item_new_with_mnemonic("_Quit");
- g_signal_connect_swapped(item_quit, "activate",
- G_CALLBACK(gtk_widget_destroy), g.window);
-
- GtkWidget *menu_file = gtk_menu_new();
- gtk_menu_shell_append(GTK_MENU_SHELL(menu_file), item_quit);
- GtkWidget *item_file = gtk_menu_item_new_with_mnemonic("_File");
- gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_file), menu_file);
- gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_file);
-
- GtkWidget *item_contents = gtk_menu_item_new_with_mnemonic("_Contents");
- g_signal_connect_swapped(item_contents, "activate",
- G_CALLBACK(show_help_contents), NULL);
- GtkWidget *item_shortcuts =
- gtk_menu_item_new_with_mnemonic("_Keyboard Shortcuts");
- g_signal_connect_swapped(item_shortcuts, "activate",
- G_CALLBACK(show_help_shortcuts), NULL);
- GtkWidget *item_about = gtk_menu_item_new_with_mnemonic("_About");
- g_signal_connect_swapped(item_about, "activate",
- G_CALLBACK(show_about_dialog), g.window);
-
- GtkWidget *menu_help = gtk_menu_new();
- gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_contents);
- gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_shortcuts);
- gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_about);
- GtkWidget *item_help = gtk_menu_item_new_with_mnemonic("_Help");
- gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_help), menu_help);
- gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_help);
+make_menu_bar(GMenuModel *model)
+{
+ g.menu = gtk_menu_bar_new_from_model(model);
// Don't let it take up space by default. Firefox sets a precedent here.
+ // (gtk_application_window_set_show_menubar() doesn't seem viable for use
+ // for this purpose.)
gtk_widget_show_all(g.menu);
gtk_widget_set_no_show_all(g.menu, TRUE);
gtk_widget_hide(g.menu);
@@ -1871,6 +2198,8 @@ make_menu_bar(void)
return g.menu;
}
+// --- Application -------------------------------------------------------------
+
// This is incredibly broken https://stackoverflow.com/a/51054396/76313
// thus resolving the problem using overlaps.
// We're trying to be universal for light and dark themes both. It's hard.
@@ -1878,12 +2207,14 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
@define-color fiv-semiselected \
mix(@theme_selected_bg_color, @content_view_bg, 0.5); \
fiv-view, fiv-browser { background: @content_view_bg; } \
- placessidebar.fiv .toolbar { padding: 2px 6px; } \
placessidebar.fiv box > separator { margin: 4px 0; } \
- #toolbar button { padding-left: 0; padding-right: 0; } \
- #toolbar > button:first-child { padding-left: 4px; } \
- #toolbar > button:last-child { padding-right: 4px; } \
- #toolbar separator { \
+ placessidebar.fiv row { min-height: 2em; } \
+ .fiv-toolbar button { padding-left: 0; padding-right: 0; } \
+ .fiv-toolbar button.text-button { \
+ padding-left: 4px; padding-right: 4px; } \
+ .fiv-toolbar > button:first-child { padding-left: 4px; } \
+ .fiv-toolbar > button:last-child { padding-right: 4px; } \
+ .fiv-toolbar separator { \
background: mix(@insensitive_fg_color, \
@insensitive_bg_color, 0.4); margin: 6px 8px; \
} \
@@ -1902,6 +2233,12 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
background-size: 40px 40px; \
background-position: 0 0, 0 20px, 20px -20px, -20px 0px; \
} \
+ fiv-browser.item.label, fiv-browser.item.symbolic.label { \
+ color: @theme_fg_color; \
+ } \
+ fiv-browser.item.label:backdrop:not(:selected) { \
+ color: @theme_unfocused_fg_color; \
+ } \
fiv-browser.item:selected { \
color: @theme_selected_bg_color; \
border-color: @theme_selected_bg_color; \
@@ -1935,113 +2272,19 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
.fiv-information label { padding: 0 4px; }";
static void
-output_thumbnail(gchar **uris, gboolean extract, const char *size_arg)
-{
- if (!uris)
- exit_fatal("No path given");
- if (uris[1])
- exit_fatal("Only one thumbnail at a time may be produced");
-
- FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
- if (size_arg) {
- for (size = 0; size < FIV_THUMBNAIL_SIZE_COUNT; size++) {
- if (!strcmp(
- fiv_thumbnail_sizes[size].thumbnail_spec_name, size_arg))
- break;
- }
- if (size >= FIV_THUMBNAIL_SIZE_COUNT)
- exit_fatal("unknown thumbnail size: %s", size_arg);
- }
-
-#ifdef G_OS_WIN32
- _setmode(fileno(stdout), _O_BINARY);
-#endif
-
- GError *error = NULL;
- GFile *file = g_file_new_for_uri(uris[0]);
- cairo_surface_t *surface = NULL;
- if (extract && (surface = fiv_thumbnail_extract(file, size, &error)))
- fiv_io_serialize_to_stdout(surface, FIV_IO_SERIALIZE_LOW_QUALITY);
- else if (size_arg &&
- (g_clear_error(&error),
- (surface = fiv_thumbnail_produce(file, size, &error))))
- fiv_io_serialize_to_stdout(surface, 0);
- else
- g_assert(error != NULL);
-
- g_object_unref(file);
- if (error)
- exit_fatal("%s", error->message);
-
- cairo_surface_destroy(surface);
-}
-
-int
-main(int argc, char *argv[])
+on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
{
- gboolean show_version = FALSE, show_supported_media_types = FALSE,
- invalidate_cache = FALSE, browse = FALSE, extract_thumbnail = FALSE;
- gchar **args = NULL, *thumbnail_size = NULL;
- const GOptionEntry options[] = {
- {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args,
- NULL, "[PATH | URI]..."},
- {"list-supported-media-types", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_NONE, &show_supported_media_types,
- "Output supported media types and exit", NULL},
- {"browse", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_NONE, &browse,
- "Start in filesystem browsing mode", NULL},
- {"extract-thumbnail", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_NONE, &extract_thumbnail,
- "Output any embedded thumbnail (superseding --thumbnail)", NULL},
- {"thumbnail", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_STRING, &thumbnail_size,
- "Generate thumbnails, up to SIZE, and output that size", "SIZE"},
- {"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_NONE, &invalidate_cache,
- "Invalidate the wide thumbnail cache", NULL},
- {"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
- &show_version, "Output version information and exit", NULL},
- {},
- };
-
- GError *error = NULL;
- gboolean initialized = gtk_init_with_args(
- &argc, &argv, " - Image browser and viewer", options, NULL, &error);
- if (show_version) {
- const char *version = PROJECT_VERSION;
- printf("%s %s\n", PROJECT_NAME, &version[*version == 'v']);
- return 0;
- }
- if (show_supported_media_types) {
- for (char **types = fiv_io_all_supported_media_types(); *types; )
- g_print("%s\n", *types++);
- return 0;
- }
- if (invalidate_cache) {
- fiv_thumbnail_invalidate();
- return 0;
- }
- if (!initialized)
- exit_fatal("%s", error->message);
-
- // Normalize all arguments to URIs.
- for (gsize i = 0; args && args[i]; i++) {
- GFile *resolved = g_file_new_for_commandline_arg(args[i]);
- g_free(args[i]);
- args[i] = g_file_get_uri(resolved);
- g_object_unref(resolved);
- }
- if (extract_thumbnail || thumbnail_size) {
- output_thumbnail(args, extract_thumbnail, thumbnail_size);
- return 0;
- }
+ // We can't prevent GApplication from adding --gapplication-service.
+ if (g_application_get_flags(app) & G_APPLICATION_IS_SERVICE)
+ exit(EXIT_FAILURE);
// It doesn't make much sense to have command line arguments able to
// resolve to the VFS they may end up being contained within.
fiv_collection_register();
g.model = g_object_new(FIV_TYPE_IO_MODEL, NULL);
+ g_signal_connect(g.model, "reloaded",
+ G_CALLBACK(on_model_reloaded), NULL);
g_signal_connect(g.model, "files-changed",
G_CALLBACK(on_model_files_changed), NULL);
@@ -2068,7 +2311,7 @@ main(int argc, char *argv[])
G_CALLBACK(on_view_drag_data_received), NULL);
gtk_container_add(GTK_CONTAINER(view_scroller), g.view);
- // We need to hide it together with the separator.
+ // We need to hide it together with its separator.
g.view_toolbar = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_pack_start(GTK_BOX(g.view_toolbar),
make_view_toolbar(), FALSE, FALSE, 0);
@@ -2108,10 +2351,23 @@ main(int argc, char *argv[])
G_CALLBACK(on_item_activated), NULL);
gtk_container_add(GTK_CONTAINER(g.browser_scroller), g.browser);
+ // We need to hide it together with its separator.
+ g.browser_toolbar = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_box_pack_start(GTK_BOX(g.browser_toolbar),
+ make_browser_toolbar(), FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(g.browser_toolbar),
+ gtk_separator_new(GTK_ORIENTATION_VERTICAL), FALSE, FALSE, 0);
+
+ GtkWidget *browser_right = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_box_pack_start(GTK_BOX(browser_right),
+ g.browser_toolbar, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(browser_right),
+ g.browser_scroller, TRUE, TRUE, 0);
+
g.browser_sidebar = make_browser_sidebar(g.model);
g.browser_paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
gtk_paned_add1(GTK_PANED(g.browser_paned), g.browser_sidebar);
- gtk_paned_add2(GTK_PANED(g.browser_paned), g.browser_scroller);
+ gtk_paned_add2(GTK_PANED(g.browser_paned), browser_right);
g_signal_connect(g.browser_paned, "key-press-event",
G_CALLBACK(on_key_press_browser_paned), NULL);
g_signal_connect(g.browser_paned, "button-press-event",
@@ -2123,18 +2379,35 @@ main(int argc, char *argv[])
gtk_container_add(GTK_CONTAINER(g.stack), g.view_box);
gtk_container_add(GTK_CONTAINER(g.stack), g.browser_paned);
- g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
- g_signal_connect(g.window, "destroy",
- G_CALLBACK(gtk_main_quit), NULL);
+ g.window = gtk_application_window_new(GTK_APPLICATION(app));
+ g_signal_connect_swapped(g.window, "destroy",
+ G_CALLBACK(g_application_quit), app);
g_signal_connect(g.window, "key-press-event",
G_CALLBACK(on_key_press), NULL);
g_signal_connect(g.window, "window-state-event",
G_CALLBACK(on_window_state_event), NULL);
+ for (const ActionEntry *a = g_actions; a->name; a++)
+ set_up_action(GTK_APPLICATION(app), a);
+
+ // GtkApplicationWindow overrides GtkContainer/GtkWidget virtual methods
+ // so that it has the menu bar as an extra child (if it so decides).
+ // However, we currently want this menu bar to only show on a key press,
+ // and to hide as soon as it's no longer being used.
+ // Messing with the window's internal state seems at best quirky,
+ // so we'll manage the menu entirely by ourselves.
+ gtk_application_window_set_show_menubar(
+ GTK_APPLICATION_WINDOW(g.window), FALSE);
+
+ GMenuModel *menu = make_menu_model();
+ gtk_application_set_menubar(GTK_APPLICATION(app), menu);
+ // The default "app menu" is good, in particular for macOS, so keep it.
+
GtkWidget *menu_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar());
+ gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar(menu));
gtk_container_add(GTK_CONTAINER(menu_box), g.stack);
gtk_container_add(GTK_CONTAINER(g.window), menu_box);
+ g_object_unref(menu);
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
if (g_settings_get_boolean(settings, "dark-theme"))
@@ -2145,6 +2418,8 @@ main(int argc, char *argv[])
gtk_widget_show_all(menu_box);
gtk_widget_set_visible(g.browser_sidebar,
g_settings_get_boolean(settings, "show-browser-sidebar"));
+ gtk_widget_set_visible(g.browser_toolbar,
+ g_settings_get_boolean(settings, "show-browser-toolbar"));
gtk_widget_set_visible(g.view_toolbar,
g_settings_get_boolean(settings, "show-view-toolbar"));
@@ -2174,24 +2449,34 @@ main(int argc, char *argv[])
// XXX: The widget wants to read the display's profile. The realize is ugly.
gtk_widget_realize(g.view);
+}
+
+static struct {
+ gboolean browse, collection, extract_thumbnail;
+ gchar **args, *thumbnail_size, *thumbnail_size_search;
+} o;
+static void
+on_app_activate(
+ G_GNUC_UNUSED GApplication *app, G_GNUC_UNUSED gpointer user_data)
+{
// XXX: We follow the behaviour of Firefox and Eye of GNOME, which both
// interpret multiple command line arguments differently, as a collection.
- // However, single-element collections are unrepresentable this way.
- // Should we allow multiple targets only in a special new mode?
- g.files = g_ptr_array_new_full(0, g_free);
- if (args) {
- const gchar *target = *args;
- if (args[1]) {
- fiv_collection_reload(args);
+ // However, single-element collections are unrepresentable this way,
+ // so we have a switch to enforce it.
+ g.files_index = -1;
+ if (o.args) {
+ const gchar *target = *o.args;
+ if (o.args[1] || o.collection) {
+ fiv_collection_reload(o.args);
target = FIV_COLLECTION_SCHEME ":/";
}
GFile *file = g_file_new_for_uri(target);
- open_any_file(file, browse);
+ open_any_file(file, o.browse);
g_object_unref(file);
- g_strfreev(args);
}
+
if (!g.directory) {
GFile *file = g_file_new_for_path(".");
open_any_file(file, FALSE);
@@ -2199,6 +2484,182 @@ main(int argc, char *argv[])
}
gtk_widget_show(g.window);
- gtk_main();
- return 0;
+}
+
+// --- Plumbing ----------------------------------------------------------------
+
+static FivThumbnailSize
+output_thumbnail_prologue(gchar **uris, const char *size_arg)
+{
+ if (!uris)
+ exit_fatal("No path given");
+ if (uris[1])
+ exit_fatal("Only one thumbnail at a time may be produced");
+
+ FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
+ if (size_arg) {
+ for (size = 0; size < FIV_THUMBNAIL_SIZE_COUNT; size++) {
+ if (!strcmp(
+ fiv_thumbnail_sizes[size].thumbnail_spec_name, size_arg))
+ break;
+ }
+ if (size >= FIV_THUMBNAIL_SIZE_COUNT)
+ exit_fatal("unknown thumbnail size: %s", size_arg);
+ }
+
+#ifdef G_OS_WIN32
+ _setmode(fileno(stdout), _O_BINARY);
+#endif
+ return size;
+}
+
+static void
+output_thumbnail_for_search(gchar **uris, const char *size_arg)
+{
+ FivThumbnailSize size = output_thumbnail_prologue(uris, size_arg);
+
+ GError *error = NULL;
+ GFile *file = g_file_new_for_uri(uris[0]);
+ cairo_surface_t *surface = NULL;
+ GBytes *bytes = NULL;
+ if ((surface = fiv_thumbnail_produce(file, size, &error)) &&
+ (bytes = fiv_io_serialize_for_search(surface, &error))) {
+ fwrite(
+ g_bytes_get_data(bytes, NULL), 1, g_bytes_get_size(bytes), stdout);
+ g_bytes_unref(bytes);
+ } else {
+ g_assert(error != NULL);
+ }
+
+ g_object_unref(file);
+ if (error)
+ exit_fatal("%s", error->message);
+
+ cairo_surface_destroy(surface);
+}
+
+static void
+output_thumbnail(gchar **uris, gboolean extract, const char *size_arg)
+{
+ FivThumbnailSize size = output_thumbnail_prologue(uris, size_arg);
+
+ GError *error = NULL;
+ GFile *file = g_file_new_for_uri(uris[0]);
+ cairo_surface_t *surface = NULL;
+ if (extract && (surface = fiv_thumbnail_extract(file, size, &error)))
+ fiv_io_serialize_to_stdout(surface, FIV_IO_SERIALIZE_LOW_QUALITY);
+ else if (size_arg &&
+ (g_clear_error(&error),
+ (surface = fiv_thumbnail_produce(file, size, &error))))
+ fiv_io_serialize_to_stdout(surface, 0);
+ else
+ g_assert(error != NULL);
+
+ g_object_unref(file);
+ if (error)
+ exit_fatal("%s", error->message);
+
+ cairo_surface_destroy(surface);
+}
+
+static gint
+on_app_handle_local_options(G_GNUC_UNUSED GApplication *app,
+ GVariantDict *options, G_GNUC_UNUSED gpointer user_data)
+{
+ if (g_variant_dict_contains(options, "version")) {
+ const char *version = PROJECT_VERSION;
+ printf("%s %s\n", PROJECT_NAME, &version[*version == 'v']);
+ return 0;
+ }
+ if (g_variant_dict_contains(options, "list-supported-media-types")) {
+ char **types = fiv_io_all_supported_media_types();
+ for (char **p = types; *p; p++)
+ g_print("%s\n", *p);
+ g_strfreev(types);
+ return 0;
+ }
+ if (g_variant_dict_contains(options, "invalidate-cache")) {
+ fiv_thumbnail_invalidate();
+ return 0;
+ }
+
+ // Normalize all arguments to URIs, and run thumbnailing modes first.
+ for (gsize i = 0; o.args && o.args[i]; i++) {
+ GFile *resolved = g_file_new_for_commandline_arg(o.args[i]);
+ g_free(o.args[i]);
+ o.args[i] = g_file_get_uri(resolved);
+ g_object_unref(resolved);
+ }
+
+ // These come from an option group that doesn't get copied to "options".
+ if (o.thumbnail_size_search) {
+ output_thumbnail_for_search(o.args, o.thumbnail_size_search);
+ return 0;
+ }
+ if (o.extract_thumbnail || o.thumbnail_size) {
+ output_thumbnail(o.args, o.extract_thumbnail, o.thumbnail_size);
+ return 0;
+ }
+ return -1;
+}
+
+int
+main(int argc, char *argv[])
+{
+ const GOptionEntry options[] = {
+ {G_OPTION_REMAINING, 0, 0,
+ G_OPTION_ARG_FILENAME_ARRAY, &o.args,
+ NULL, "[PATH | URI]..."},
+ {"browse", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, &o.browse,
+ "Start in filesystem browsing mode", NULL},
+ {"collection", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, &o.collection,
+ "Always put arguments in a collection (implies --browse)", NULL},
+ {"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, NULL,
+ "Invalidate the wide thumbnail cache", NULL},
+ {"list-supported-media-types", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, NULL,
+ "Output supported media types and exit", NULL},
+ {"version", 'V', G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, NULL,
+ "Output version information and exit", NULL},
+ {},
+ };
+ const GOptionEntry options_internal[] = {
+ {"extract-thumbnail", 0, 0,
+ G_OPTION_ARG_NONE, &o.extract_thumbnail,
+ "Output any embedded thumbnail (superseding --thumbnail)", NULL},
+ {"thumbnail", 0, 0,
+ G_OPTION_ARG_STRING, &o.thumbnail_size,
+ "Generate thumbnails, up to SIZE, and output that size", "SIZE"},
+ {"thumbnail-for-search", 0, 0,
+ G_OPTION_ARG_STRING, &o.thumbnail_size_search,
+ "Output an image file suitable for searching by content", "SIZE"},
+ {},
+ };
+
+ // We never get the ::open signal, thanks to G_OPTION_ARG_FILENAME_ARRAY.
+ GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
+ g_application_set_option_context_parameter_string(
+ G_APPLICATION(app), " - Image browser and viewer");
+ g_application_add_main_option_entries(G_APPLICATION(app), options);
+
+ GOptionGroup *internals = g_option_group_new(
+ "internal", "Internal Options:", "Show internal options", NULL, NULL);
+ g_option_group_add_entries(internals, options_internal);
+ g_application_add_option_group(G_APPLICATION(app), internals);
+
+ g_signal_connect(app, "handle-local-options",
+ G_CALLBACK(on_app_handle_local_options), NULL);
+ g_signal_connect(app, "startup",
+ G_CALLBACK(on_app_startup), NULL);
+ g_signal_connect(app, "activate",
+ G_CALLBACK(on_app_activate), NULL);
+
+ int status = g_application_run(G_APPLICATION(app), argc, argv);
+ g_object_unref(app);
+ g_strfreev(o.args);
+ return status;
}
diff --git a/fiv.gschema.xml b/fiv.gschema.xml
index a0b9aab..f869d2f 100644
--- a/fiv.gschema.xml
+++ b/fiv.gschema.xml
@@ -17,6 +17,13 @@
double buffering.
</description>
</key>
+ <key name='opengl' type='b'>
+ <default>false</default>
+ <summary>Use experimental OpenGL rendering</summary>
+ <description>
+ OpenGL within GTK+ is highly problematic--you don't want this.
+ </description>
+ </key>
<key name='dark-theme' type='b'>
<default>false</default>
<summary>Use a dark theme variant on start-up</summary>
@@ -25,6 +32,10 @@
<default>true</default>
<summary>Show the browser's sidebar</summary>
</key>
+ <key name='show-browser-toolbar' type='b'>
+ <default>true</default>
+ <summary>Show a toolbar in the browser view</summary>
+ </key>
<key name='show-view-toolbar' type='b'>
<default>true</default>
<summary>Show a toolbar in the image view</summary>
diff --git a/fiv.wxs.in b/fiv.wxs.in
new file mode 100644
index 0000000..f44611b
--- /dev/null
+++ b/fiv.wxs.in
@@ -0,0 +1,71 @@
+<?xml version='1.0' encoding='utf-8'?>
+<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
+ <?define FullName = "@ProjectName@ @ProjectVersion@" ?>
+ <?if $(sys.BUILDARCH) = x64 ?>
+ <?define ProgramFilesFolder = "ProgramFiles64Folder" ?>
+ <?else?>
+ <?define ProgramFilesFolder = "ProgramFilesFolder" ?>
+ <?endif?>
+
+ <Product Id='*'
+ Name='$(var.FullName)'
+ UpgradeCode='a3e64e2d-4310-4c5f-8562-bb0e0b3e0a53'
+ Language='1033'
+ Codepage='1252'
+ Version='@ProjectVersion@'
+ Manufacturer='Premysl Eric Janouch'>
+
+ <Package Id='*'
+ Keywords='Installer,Image,Viewer'
+ Description='$(var.FullName) Installer'
+ Manufacturer='Premysl Eric Janouch'
+ InstallerVersion='200'
+ Compressed='yes'
+ Languages='1033'
+ SummaryCodepage='1252' />
+
+ <Media Id='1' Cabinet='data.cab' EmbedCab='yes' />
+ <Icon Id='fiv.ico' SourceFile='fiv.ico' />
+ <Property Id='ARPPRODUCTICON' Value='fiv.ico' />
+ <Property Id='ARPURLINFOABOUT' Value='@ProjectURL@' />
+
+ <UIRef Id='WixUI_Minimal' />
+ <!-- This isn't supported by msitools, but is necessary for WiX.
+ <WixVariable Id='WixUILicenseRtf' Value='License.rtf' />
+ -->
+
+ <Directory Id='TARGETDIR' Name='SourceDir'>
+ <Directory Id='$(var.ProgramFilesFolder)'>
+ <Directory Id='INSTALLDIR' Name='$(var.FullName)' />
+ </Directory>
+
+ <Directory Id='ProgramMenuFolder'>
+ <Directory Id='ProgramMenuDir' Name='$(var.FullName)' />
+ </Directory>
+
+ <Directory Id='DesktopFolder' />
+ </Directory>
+
+ <DirectoryRef Id='ProgramMenuDir'>
+ <Component Id='ProgramMenuDir' Guid='*'>
+ <Shortcut Id='ProgramsMenuShortcut'
+ Name='@ProjectName@'
+ Target='[INSTALLDIR]\fiv.exe'
+ WorkingDirectory='INSTALLDIR'
+ Arguments='"%USERPROFILE%"'
+ Icon='fiv.ico' />
+ <RemoveFolder Id='ProgramMenuDir' On='uninstall' />
+ <RegistryValue Root='HKCU'
+ Key='Software\[Manufacturer]\[ProductName]'
+ Type='string'
+ Value=''
+ KeyPath='yes' />
+ </Component>
+ </DirectoryRef>
+
+ <Feature Id='Complete' Level='1'>
+ <ComponentGroupRef Id='CG.fiv' />
+ <ComponentRef Id='ProgramMenuDir' />
+ </Feature>
+ </Product>
+</Wix>
diff --git a/meson.build b/meson.build
index 6f6d156..82e7978 100644
--- a/meson.build
+++ b/meson.build
@@ -1,7 +1,8 @@
# vim: noet ts=4 sts=4 sw=4:
project('fiv', 'c',
default_options : ['c_std=gnu99', 'warning_level=2'],
- version : '0.1.0')
+ version : '0.1.0',
+ meson_version : '>=0.57')
cc = meson.get_compiler('c')
add_project_arguments(
@@ -25,22 +26,25 @@ libjpegqs = dependency('libjpegqs', required : get_option('libjpegqs'),
lcms2 = dependency('lcms2', required : get_option('lcms2'))
# Note that only libraw_r is thread-safe, but we'll just run it out-of process.
libraw = dependency('libraw', required : get_option('libraw'))
+# This is a direct runtime dependency, but its usage may be disabled for images.
librsvg = dependency('librsvg-2.0', required : get_option('librsvg'))
xcursor = dependency('xcursor', required : get_option('xcursor'))
libheif = dependency('libheif', required : get_option('libheif'))
libtiff = dependency('libtiff-4', required : get_option('libtiff'))
-# This is a direct dependency of GTK+, but its usage may be disabled.
+# This is a direct dependency of GTK+, but its usage may be disabled for images.
gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
dependencies = [
dependency('gtk+-3.0'),
dependency('pixman-1'),
+ dependency('epoxy'),
- # Wuffs is included as a submodule.
+ dependency('libjpeg'),
dependency('libturbojpeg'),
dependency('libwebp'),
dependency('libwebpdemux'),
dependency('libwebpdecoder', required : false),
dependency('libwebpmux'),
+ # Wuffs is included as a submodule.
lcms2,
libjpegqs,
@@ -53,13 +57,31 @@ dependencies = [
cc.find_library('m', required : false),
]
+# As of writing, no pkg-config file is produced, and the plugin is not installed
+# by default. The library can be built statically, but it's a bit of a hassle.
+have_lcms2_fast_float = false
+if not get_option('lcms2fastfloat').disabled()
+ lcms2ff = dependency('lcms2_fast_float', required : false)
+ if not lcms2ff.found()
+ lcms2ff = cc.find_library(
+ 'lcms2_fast_float', required : get_option('lcms2fastfloat'))
+ if lcms2ff.found() and not cc.has_header('lcms2_fast_float.h')
+ error('lcms2_fast_float.h not found')
+ endif
+ endif
+ if lcms2ff.found()
+ dependencies += lcms2ff
+ have_lcms2_fast_float = true
+ endif
+endif
+
# As of writing, the API is unstable, and no pkg-config file is produced.
# Trying to wrap Cargo in Meson is a recipe for pain, so no version pinning.
have_resvg = false
if not get_option('resvg').disabled()
resvg = dependency('resvg', required : false)
if not resvg.found()
- resvg = cc.find_library('libresvg', required : get_option('resvg'))
+ resvg = cc.find_library('resvg', required : get_option('resvg'))
if resvg.found() and not cc.has_header('resvg.h')
error('resvg.h not found')
endif
@@ -73,11 +95,13 @@ endif
# XXX: https://github.com/mesonbuild/meson/issues/825
docdir = get_option('datadir') / 'doc' / meson.project_name()
application_ns = 'name.janouch.'
+application_url = 'https://janouch.name/p/' + meson.project_name()
conf = configuration_data()
conf.set_quoted('PROJECT_NAME', meson.project_name())
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
conf.set_quoted('PROJECT_NS', application_ns)
+conf.set_quoted('PROJECT_URL', application_url)
conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
if win32
conf.set_quoted('PROJECT_DOCDIR', docdir)
@@ -85,6 +109,7 @@ endif
conf.set('HAVE_JPEG_QS', libjpegqs.found())
conf.set('HAVE_LCMS2', lcms2.found())
+conf.set('HAVE_LCMS2_FAST_FLOAT', have_lcms2_fast_float)
conf.set('HAVE_LIBRAW', libraw.found())
conf.set('HAVE_RESVG', have_resvg)
conf.set('HAVE_LIBRSVG', librsvg.found())
@@ -118,7 +143,8 @@ if win32
'--width', size, '--height', size, '@INPUT@'])
endforeach
- icon_ico = custom_target(input : icon_png_list, output : 'fiv.ico',
+ icon_ico = custom_target('fiv.ico',
+ output : 'fiv.ico', input : icon_png_list,
command : [icotool, '-c', '-o', '@OUTPUT@', '@INPUT@'])
rc += windows.compile_resources('fiv.rc', depends : icon_ico)
endif
@@ -133,23 +159,23 @@ gresources = gnome.compile_resources('resources',
tiff_tables = custom_target('tiff-tables.h',
output : 'tiff-tables.h',
input : 'tiff-tables.db',
- command : ['tiff-tables.awk', '@INPUT@'],
+ # Meson 0.56 chokes on files() as well as on a relative path.
+ command : [meson.current_source_dir() / 'tiff-tables.awk', '@INPUT@'],
capture : true,
)
desktops = ['fiv.desktop', 'fiv-browse.desktop']
-exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-io.c', 'fiv-context-menu.c',
+iolib = static_library('fiv-io', 'fiv-io.c', 'fiv-io-cmm.c', 'xdg.c',
+ tiff_tables, config,
+ dependencies : dependencies).extract_all_objects(recursive : true)
+exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-context-menu.c',
'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',
- 'xdg.c', gresources, rc, config,
- install : true,
+ 'fiv-io-model.c', gresources, rc, config,
+ objects : iolib,
dependencies : dependencies,
+ install : true,
win_subsystem : 'windows',
)
-if gdkpixbuf.found()
- executable('io-benchmark', 'fiv-io-benchmark.c', 'fiv-io.c', 'xdg.c',
- build_by_default : false,
- dependencies : [dependencies, gdkpixbuf])
-endif
desktops += 'fiv-jpegcrop.desktop'
jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
@@ -162,38 +188,55 @@ jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
)
if get_option('tools').enabled()
- # libjq 1.6 lacks a pkg-config file, and there is no release in sight.
- # libjq 1.6 is required.
- tools_dependencies = [cc.find_library('libjq'), dependency('libpng')]
+ # libjq has only received a pkg-config file in version 1.7.
+ # libjq >= 1.6 is required.
+ tools_dependencies = [
+ cc.find_library('jq'), dependency('libpng'), dependency('libraw')]
tools_c_args = cc.get_supported_arguments(
'-Wno-unused-function', '-Wno-unused-parameter')
- foreach tool : ['pnginfo', 'jpeginfo', 'tiffinfo', 'webpinfo', 'bmffinfo']
+ foreach tool : ['info', 'pnginfo', 'rawinfo', 'hotpixels']
executable(tool, 'tools/' + tool + '.c', tiff_tables,
dependencies : tools_dependencies,
c_args: tools_c_args)
endforeach
+
+ if gdkpixbuf.found()
+ executable('benchmark-io', 'tools/benchmark-io.c',
+ objects : iolib,
+ dependencies : [dependencies, gdkpixbuf])
+ endif
endif
+# Copying the files to the build directory makes GSettings find them in devenv.
gsettings_schemas = ['fiv.gschema.xml']
foreach schema : gsettings_schemas
- install_data(schema,
- rename : [application_ns + schema],
+ configure_file(
+ input : schema,
+ output : application_ns + schema,
+ copy : true,
+ install: true,
install_dir : get_option('datadir') / 'glib-2.0' / 'schemas')
endforeach
# For the purposes of development: make the program find its GSettings schemas.
gnome.compile_schemas(depend_files : files(gsettings_schemas))
+gnome.post_install(glib_compile_schemas : true, gtk_update_icon_cache : true)
-# Validate various files, if there are tools around to do it.
-xmls = ['fiv.svg', 'fiv.manifest', 'resources/resources.gresource.xml'] + \
- gsettings_schemas
-xmls += run_command(find_program('sed', required : false, disabler : true),
- '-n', 's@.*>\([^<>]*[.]svg\)<.*@resources/\\1@p',
+# Meson is broken on Windows and removes the backslashes, so this ends up empty.
+symbolics = run_command(find_program('sed', required : false, disabler : true),
+ '-n', 's@.*>\\([^<>]*[.]svg\\)<.*@resources/\\1@p',
configure_file(
input : 'resources/resources.gresource.xml',
output : 'resources.gresource.xml.stamp',
copy : true,
- ), capture : true, check : true).stdout().strip().split('\n')
+ ), capture : true, check : true).stdout().strip()
+
+# Validate various files, if there are tools around to do it.
+xmls = ['fiv.svg', 'fiv.manifest', 'resources/resources.gresource.xml'] + \
+ gsettings_schemas
+if symbolics != ''
+ xmls += symbolics.split('\n')
+endif
xmlwf = find_program('xmlwf', required : false, disabler : true)
xmllint = find_program('xmllint', required : false, disabler : true)
@@ -217,9 +260,11 @@ if not win32
asciidoctor = find_program('asciidoctor', required : false)
a2x = find_program('a2x', required : false)
if not asciidoctor.found() and not a2x.found()
- error('Neither asciidoctor nor a2x were found')
+ warning('Neither asciidoctor nor a2x were found, ' +
+ 'falling back to a substandard manual page generator')
endif
foreach page : [meson.project_name()]
+ man_capture = false
if asciidoctor.found()
command = [asciidoctor, '-b', 'manpage',
'-a', 'release-version=' + meson.project_version(),
@@ -228,10 +273,17 @@ if not win32
command = [a2x, '--doctype', 'manpage', '--format', 'manpage',
'-a', 'release-version=' + meson.project_version(),
'-D', '@OUTDIR@', '@INPUT@']
+ else
+ command = ['env', 'LC_ALL=C',
+ 'asciidoc-release-version=' + meson.project_version(),
+ 'awk', '-f', files('submodules/liberty/tools/asciiman.awk'),
+ '@INPUT@']
+ man_capture = true
endif
custom_target('manpage for ' + page,
input : 'docs' / page + '.adoc',
output : page + '.1',
+ capture : man_capture,
command : command,
install : true,
install_dir : get_option('mandir') / 'man1')
@@ -243,6 +295,32 @@ if not win32
install_dir : get_option('datadir') / 'applications')
endforeach
+ # TODO(p): Consider moving this to /usr/share or /usr/lib.
+ install_data('fiv-reverse-search',
+ install_dir : get_option('bindir'))
+
+ # As usual, handling generated files in Meson is a fucking pain.
+ updatable_desktops = [application_ns + 'fiv.desktop']
+ foreach name, uri : {
+ 'Google' : 'https://lens.google.com/uploadbyurl?url=',
+ 'Bing' : 'https://www.bing.com/images/searchbyimage?cbir=sbi&imgurl=',
+ 'Yandex' : 'https://yandex.com/images/search?rpt=imageview&url=',
+ 'TinEye' : 'https://tineye.com/search?url=',
+ 'SauceNAO' : 'https://saucenao.com/search.php?url=',
+ 'IQDB' : 'https://iqdb.org/?url=',
+ }
+ desktop = 'fiv-reverse-search-' + name.to_lower() + '.desktop'
+ updatable_desktops += application_ns + desktop
+
+ test(desktop, dfv, args : configure_file(
+ input : 'fiv-reverse-search.desktop.in',
+ output : application_ns + desktop,
+ configuration : {'NAME' : name, 'URL' : uri},
+ install : true,
+ install_dir : get_option('datadir') / 'applications',
+ ))
+ endforeach
+
# With gdk-pixbuf, fiv.desktop depends on currently installed modules;
# the package manager needs to be told to run this script as necessary.
dynamic_desktops = gdkpixbuf.found()
@@ -251,20 +329,52 @@ if not win32
input : 'fiv-update-desktop-files.in',
output : 'fiv-update-desktop-files',
configuration : {
- 'EXE' : get_option('prefix') / get_option('bindir') / exe.name(),
- 'DESKTOP' : get_option('prefix') / get_option('datadir') \
- / 'applications' / application_ns + 'fiv.desktop',
+ 'FIV' : get_option('prefix') / get_option('bindir') / exe.name(),
+ 'DESKTOPDIR' : get_option('prefix') /
+ get_option('datadir') / 'applications',
+ 'DESKTOPS' : ' \\\n\t'.join(updatable_desktops),
},
install : dynamic_desktops,
install_dir : get_option('bindir'))
if not meson.is_cross_build()
meson.add_install_script(updater, skip_if_destdir : dynamic_desktops)
endif
+
+ # Quick and dirty package generation, lacking dependencies.
+ packaging = configuration_data({
+ 'name' : meson.project_name(),
+ 'version' : meson.project_version(),
+ 'summary' : 'Image viewer',
+ 'author' : 'Přemysl Eric Janouch',
+ })
+
+ subdir('submodules/liberty/meson/packaging')
elif meson.is_cross_build()
+ # Note that even compiling /from within MSYS2/ can still be a cross-build.
msys2_root = meson.get_external_property('msys2_root')
- meson.add_install_script('msys2-cross-install.sh', msys2_root)
+ meson.add_install_script('msys2-install.sh', msys2_root)
+
+ wxs = configure_file(
+ input : 'fiv.wxs.in',
+ output : 'fiv.wxs',
+ configuration : configuration_data({
+ 'ProjectName' : meson.project_name(),
+ 'ProjectVersion' : meson.project_version(),
+ 'ProjectURL' : application_url,
+ }),
+ )
+ custom_target('package',
+ output : 'fiv.msi',
+ command : [meson.current_source_dir() / 'msys2-package.sh',
+ host_machine.cpu(), 'fiv.msi', wxs],
+ env : ['MESON_BUILD_ROOT=' + meson.current_build_dir(),
+ 'MESON_SOURCE_ROOT=' + meson.current_source_dir()],
+ console : true,
+ build_always_stale : true,
+ build_by_default : false,
+ )
- # This is the minimum to run targets from msys2-cross-configure.sh builds.
+ # This is the minimum to run targets from msys2-configure.sh builds.
meson.add_devenv({
'WINEPATH' : msys2_root / 'bin',
'XDG_DATA_DIRS' : msys2_root / 'share',
diff --git a/meson_options.txt b/meson_options.txt
index dad40f8..2aa0f9c 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -3,6 +3,8 @@ option('tools', type : 'feature', value : 'disabled',
option('lcms2', type : 'feature', value : 'auto',
description : 'Build with Little CMS colour management')
+option('lcms2fastfloat', type : 'feature', value : 'auto',
+ description : 'Build with Little CMS fast float plugin support')
option('libjpegqs', type : 'feature', value : 'auto',
description : 'Build with JPEG Quant Smooth integration')
option('libraw', type : 'feature', value : 'auto',
diff --git a/msys2-cross-configure.sh b/msys2-configure.sh
index 8887928..2f43d0d 100755
--- a/msys2-cross-configure.sh
+++ b/msys2-configure.sh
@@ -1,8 +1,26 @@
#!/bin/sh -e
-# msys2-cross-configure.sh: set up an MSYS2-based cross-compiled Meson build.
-# Dependencies: AWK, sed, sha256sum, cURL, bsdtar,
+# msys2-configure.sh: set up an MSYS2-based Meson build (x86-64 by default)
+#
+# Dependencies: AWK, sed, coreutils, cURL, bsdtar (libarchive),
# wine64, Meson, mingw-w64-binutils, mingw-w64-gcc, pkg-config
-repository=https://repo.msys2.org/mingw/mingw64/
+#
+# We support running directly from within MSYS2 on Windows,
+# albeit while still downloading a complete copy of runtime depencies.
+pkg=${MINGW_PACKAGE_PREFIX:-mingw-w64-x86_64}
+prefix=${MSYSTEM_PREFIX:-/mingw64}
+repo=https://repo.msys2.org/mingw$prefix
+
+chost=${MSYSTEM_CHOST:-x86_64-w64-mingw32}
+carch=${MSYSTEM_CARCH:-x86_64}
+[ "$carch" = "i686" ] && carch=x86
+
+if [ -n "$MSYSTEM" ]
+then
+ wine64() { "$@"; }
+ awk() { command awk -v RS='\r?\n' "$@"; }
+ pacman -S --needed libarchive $pkg-ca-certificates $pkg-gcc $pkg-icoutils \
+ $pkg-librsvg $pkg-meson $pkg-msitools $pkg-pkgconf
+fi
status() {
echo "$(tput bold)-- $*$(tput sgr0)"
@@ -10,7 +28,7 @@ status() {
dbsync() {
status Fetching repository DB
- [ -f db.tsv ] || curl -# "$repository/mingw64.db" | bsdtar -xOf- | awk '
+ [ -f db.tsv ] || curl -# "$repo$prefix.db" | bsdtar -xOf- | awk '
function flush() { print f["%NAME%"] f["%FILENAME%"] f["%DEPENDS%"] }
NR > 1 && $0 == "%FILENAME%" { flush(); for (i in f) delete f[i] }
!/^[^%]/ { field = $0; next } { f[field] = f[field] $0 "\t" }
@@ -28,10 +46,11 @@ fetch() {
} BEGIN { while ((getline < "db.tsv") > 0) {
filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
- } for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | while IFS= read -r name
+ } for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | tee db.want | \
+ while IFS= read -r name
do
status Fetching "$name"
- [ -f "packages/$name" ] || curl -#o "packages/$name" "$repository/$name"
+ [ -f "packages/$name" ] || curl -#o "packages/$name" "$repo/$name"
done
version=$(curl -# https://exiftool.org/ver.txt)
@@ -51,9 +70,10 @@ extract() {
for subdir in *
do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
done
- for i in packages/*
- do bsdtar -xf "$i" --strip-components 1 mingw64
- done
+ while IFS= read -r name
+ do bsdtar -xf "packages/$name" --strip-components 1 \
+ --exclude '*/share/man' --exclude '*/share/doc'
+ done < db.want
bsdtar -xf exiftool.tar.gz
mv Image-ExifTool-*/exiftool bin
@@ -74,49 +94,50 @@ configure() {
setup() {
status Setting up Meson
+ wrap=true pclibdir=$msys2_root/share/pkgconfig:$msys2_root/lib/pkgconfig
+ [ -n "$MSYSTEM" ] && \
+ wrap=false pclibdir="$(pwd -W)/share/pkgconfig;$(pwd -W)/lib/pkgconfig"
+
cat >"$toolchain" <<-EOF
[binaries]
- c = 'x86_64-w64-mingw32-gcc'
- cpp = 'x86_64-w64-mingw32-g++'
- ar = 'x86_64-w64-mingw32-gcc-ar'
- ranlib = 'x86_64-w64-mingw32-gcc-ranlib'
- strip = 'x86_64-w64-mingw32-strip'
- windres = 'x86_64-w64-mingw32-windres'
+ c = '$chost-gcc'
+ cpp = '$chost-g++'
+ ar = '$chost-gcc-ar'
+ ranlib = '$chost-gcc-ranlib'
+ strip = '$chost-strip'
+ windres = '$chost-windres'
pkgconfig = 'pkg-config'
[properties]
sys_root = '$builddir'
msys2_root = '$msys2_root'
- pkg_config_libdir = '$msys2_root/share/pkgconfig:$msys2_root/lib/pkgconfig'
- needs_exe_wrapper = true
+ pkg_config_libdir = '$pclibdir'
+ needs_exe_wrapper = $wrap
[host_machine]
system = 'windows'
- cpu_family = 'x86_64'
- cpu = 'x86_64'
+ cpu_family = '$carch'
+ cpu = '$carch'
endian = 'little'
EOF
- meson --buildtype=debugoptimized --prefix="$packagedir" \
+ meson setup --buildtype=debugoptimized --prefix=/ \
--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
}
sourcedir=$(realpath "${2:-$(dirname "$0")}")
builddir=$(realpath "${1:-builddir}")
-packagedir=$builddir/package
toolchain=$builddir/msys2-cross-toolchain.meson
# This directory name matches the prefix in .pc files, so we don't need to
# modify them (pkgconf has --prefix-variable, but Meson can't pass that option).
-msys2_root=$builddir/mingw64
+msys2_root=$builddir$prefix
mkdir -p "$msys2_root"
cd "$msys2_root"
dbsync
-fetch mingw-w64-x86_64-gtk3 mingw-w64-x86_64-lcms2 \
- mingw-w64-x86_64-libraw mingw-w64-x86_64-libheif \
- mingw-w64-x86_64-perl mingw-w64-x86_64-perl-win32-api \
- mingw-w64-x86_64-libwinpthread-git # Because we don't do "provides"?
+fetch $pkg-gtk3 $pkg-lcms2 $pkg-libraw $pkg-libheif $pkg-libjxl $pkg-perl \
+ $pkg-perl-win32-api $pkg-libwinpthread-git # Because we don't do "provides"?
verify
extract
configure
diff --git a/msys2-cross-install.sh b/msys2-install.sh
index 7cbc2cc..da2d2f1 100755
--- a/msys2-cross-install.sh
+++ b/msys2-install.sh
@@ -3,6 +3,13 @@ export LC_ALL=C
cd "$MESON_INSTALL_DESTDIR_PREFIX"
msys2_root=$1
+# Support running directly from within MSYS2 on Windows.
+if [ -n "$MSYSTEM" ]
+then
+ wine64() { "$@"; }
+ awk() { command awk -v RS='\r?\n' "$@"; }
+fi
+
# Copy binaries we directly or indirectly depend on.
cp -p "$msys2_root"/bin/*.dll .
cp -p "$msys2_root"/bin/wperl.exe .
@@ -16,12 +23,13 @@ cp -pR "$msys2_root"/lib/gdk-pixbuf-2.0/ lib
cp -pR "$msys2_root"/lib/perl5/ lib
mkdir -p share/glib-2.0/schemas
cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas
-mkdir -p share
-cp -pR "$msys2_root"/share/mime/ share
mkdir -p share/icons
cp -pR "$msys2_root"/share/icons/Adwaita/ share/icons
mkdir -p share/icons/hicolor
cp -p "$msys2_root"/share/icons/hicolor/index.theme share/icons/hicolor
+mkdir -p share/mime
+# GIO doesn't use the database on Windows, this subset is for us.
+find "$msys2_root"/share/mime/ -maxdepth 1 -type f -exec cp -p {} share/mime \;
# Remove unreferenced libraries.
find lib -name '*.a' -exec rm -- {} +
diff --git a/msys2-package.sh b/msys2-package.sh
new file mode 100755
index 0000000..363c36a
--- /dev/null
+++ b/msys2-package.sh
@@ -0,0 +1,34 @@
+#!/bin/sh -e
+export LC_ALL=C
+cd "$MESON_BUILD_ROOT"
+arch=$1 msi=$2 files=package-files.wxs destdir=$(pwd)/package
+shift 2
+
+# We're being passed host_machine.cpu(), which will be either x86 or x86_64.
+[ "$arch" = "x86" ] || arch=x64
+
+rm -rf "$destdir"
+meson install --destdir "$destdir"
+
+txt2rtf() {
+ LC_ALL=C.UTF-8 iconv -f utf-8 -t ascii//translit "$@" | awk 'BEGIN {
+ print "{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0 Tahoma;}}"
+ print "\\f0\\fs24{\\pard\\sa240"
+ } {
+ gsub(/\\/, "\\\\"); gsub(/{/, "\\{"); gsub(/}/, "\\}")
+ if (!$0) { print "\\par}{\\pard\\sa240"; prefix = "" }
+ else { print prefix $0; prefix = " " }
+ } END {
+ print "\\par}}"
+ }'
+}
+
+# msitools have this filename hardcoded in UI files, and it's required.
+txt2rtf "$MESON_SOURCE_ROOT/LICENSE" > License.rtf
+
+find "$destdir" -type f \
+ | wixl-heat --prefix "$destdir/" --directory-ref INSTALLDIR \
+ --component-group CG.fiv --var var.SourceDir > "$files"
+
+wixl --verbose --arch "$arch" -D SourceDir="$destdir" --ext ui \
+ --output "$msi" "$@" "$files"
diff --git a/resources/cross-large-symbolic.svg b/resources/cross-large-symbolic.svg
new file mode 100644
index 0000000..b9b8f9a
--- /dev/null
+++ b/resources/cross-large-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 1.980469 2 h 1 h 0.03125 c 0.253906 0.011719 0.511719 0.128906 0.6875 0.3125 l 4.28125 4.28125 l 4.3125 -4.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035157 0.550781 -0.25 0.75 l -4.28125 4.28125 l 4.25 4.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -4.28125 -4.28125 l -4.28125 4.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -1 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 4.28125 -4.25 l -4.28125 -4.28125 c -0.210938 -0.195312 -0.304688 -0.46875 -0.28125 -0.75 z m 0 0" fill="#222222"/>
+</svg>
diff --git a/resources/resources.gresource.xml b/resources/resources.gresource.xml
index b3d6b1c..3e4aef3 100644
--- a/resources/resources.gresource.xml
+++ b/resources/resources.gresource.xml
@@ -4,7 +4,9 @@
<file alias="LICENSE">../LICENSE</file>
</gresource>
<gresource prefix="/org/gnome/design/IconLibrary/scalable/actions/">
+ <file preprocess="xml-stripblanks">text-symbolic.svg</file>
<file preprocess="xml-stripblanks">circle-filled-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">cross-large-symbolic.svg</file>
<file preprocess="xml-stripblanks">funnel-symbolic.svg</file>
<file preprocess="xml-stripblanks">blend-tool-symbolic.svg</file>
<file preprocess="xml-stripblanks">checkerboard-symbolic.svg</file>
diff --git a/resources/text-symbolic.svg b/resources/text-symbolic.svg
new file mode 100644
index 0000000..6528635
--- /dev/null
+++ b/resources/text-symbolic.svg
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <filter id="a" height="100%" width="100%" x="0%" y="0%">
+ <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
+ </filter>
+ <mask id="b">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
+ </g>
+ </mask>
+ <clipPath id="c">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="d">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="e">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="f">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="g">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="h">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="i">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="j">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="k">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="l">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="m">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="n">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
+ </g>
+ </mask>
+ <clipPath id="o">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="p">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
+ </g>
+ </mask>
+ <clipPath id="q">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="r">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
+ </g>
+ </mask>
+ <clipPath id="s">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="t">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
+ </g>
+ </mask>
+ <clipPath id="u">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="v">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
+ </g>
+ </mask>
+ <clipPath id="w">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="x">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
+ </g>
+ </mask>
+ <clipPath id="y">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <mask id="z">
+ <g filter="url(#a)">
+ <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
+ </g>
+ </mask>
+ <clipPath id="A">
+ <path d="m 0 0 h 1024 v 800 h -1024 z"/>
+ </clipPath>
+ <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
+ </g>
+ <path d="m 6 1 l -5 14 h 3 c 1.484375 -4 0.023438 0 1.507812 -4 h 4.984376 l 1.507812 4 h 3 l -5 -14 z m 2 3 l 2.023438 5 h -4 z m 0 0" fill="#2e3436"/>
+ <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
+ </g>
+ <g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -56 -640)">
+ <path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
+ </g>
+</svg>
diff --git a/submodules/liberty b/submodules/liberty
new file mode 160000
+Subproject 0e86ffe7c30a4d52eea35856b792567ca1040f5
diff --git a/submodules/wuffs-mirror-release-c b/submodules/wuffs-mirror-release-c
new file mode 160000
+Subproject c63c4a9348fb1b52a9b60a6eb62328a97d979d9
diff --git a/subprojects/libjpegqs.wrap b/subprojects/libjpegqs.wrap
index c6a8f6f..6a8085b 100644
--- a/subprojects/libjpegqs.wrap
+++ b/subprojects/libjpegqs.wrap
@@ -1,8 +1,8 @@
[wrap-file]
-directory = jpeg-quantsmooth-1.20210408
-source_url = https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20210408.tar.gz
-source_filename = jpeg-quantsmooth-1.20210408.tar.gz
-source_hash = 5937ca26db33888cab8638c1a8dc7a367a953bd0857ceb1290d5abc6febf3116
+directory = jpeg-quantsmooth-1.20230818
+source_url = https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20230818.tar.gz
+source_filename = jpeg-quantsmooth-1.20230818.tar.gz
+source_hash = ff9a62e8560851648c60d84b3d97ebd9769f01ce6b995779e071d19a759eca06
patch_directory = libjpegqs
[provide]
diff --git a/subprojects/packagefiles/libjpegqs/meson.build b/subprojects/packagefiles/libjpegqs/meson.build
index 03c219b..82377c8 100644
--- a/subprojects/packagefiles/libjpegqs/meson.build
+++ b/subprojects/packagefiles/libjpegqs/meson.build
@@ -1,8 +1,6 @@
# vim: noet ts=4 sts=4 sw=4:
project('jpeg-qs', 'c')
-add_project_arguments(meson.get_compiler('c')
- .get_supported_arguments('-Wno-misleading-indentation'),
- '-DWITH_LOG', language : 'c')
+add_project_arguments('-DWITH_LOG', language : 'c')
deps = [
dependency('libjpeg'),
diff --git a/tiff-tables.awk b/tiff-tables.awk
index 2d93c36..29b462b 100755
--- a/tiff-tables.awk
+++ b/tiff-tables.awk
@@ -2,6 +2,22 @@
BEGIN {
FS = ", *"
print "// Generated by tiff-tables.awk. DO NOT MODIFY."
+ print ""
+ print "#ifndef TIFF_TABLES_CONSTANTS_ONLY"
+ print "#include <stddef.h>"
+ print "#include <stdint.h>"
+ print ""
+ print "struct tiff_value {"
+ print "\tconst char *name;"
+ print "\tuint16_t value;"
+ print "};"
+ print ""
+ print "struct tiff_entry {"
+ print "\tconst char *name;"
+ print "\tuint16_t tag;"
+ print "\tstruct tiff_value *values;"
+ print "};"
+ print "#endif"
}
{
@@ -55,8 +71,10 @@ function flushvalues() {
function flushsection() {
if (section) {
flushvalues()
- print "};\n\n" allvalues "static struct tiff_entry " \
+ print "};\n\n" allvalues "#ifndef TIFF_TABLES_CONSTANTS_ONLY"
+ print "static struct tiff_entry " \
sectionsnakecase "_entries[] = {" fields "\n\t{}\n};"
+ print "#endif"
}
}
diff --git a/tiff-tables.db b/tiff-tables.db
index 8b0e206..341c36e 100644
--- a/tiff-tables.db
+++ b/tiff-tables.db
@@ -30,8 +30,14 @@
# Exif Version 2.32 (2019)
# https://www.cipa.jp/e/std/std-sec.html
#
+# ISO/DIS 12234-2 (TIFF/EP) (2000-06-21)
+# http://www.barrypearson.co.uk/top2009/downloads/TAG2000-22_DIS12234-2.pdf
+#
# Digital Negative (DNG) Specification 1.5.0.0 (2019)
# https://www.adobe.com/content/dam/acom/en/products/photoshop/pdfs/dng_spec_1.5.0.0.pdf
+#
+# CIPA DC-007-2021 (Multi-Picture Format)
+# https://www.cipa.jp/e/std/std-sec.html
# TIFF 6.0
= TIFF
@@ -64,6 +70,8 @@
6, YCbCr
8, CIELab
9, ICCLab # Adobe PageMaker 6.0 TIFF Technical Notes
+ 32803, Color filter array # DIS/ISO 12234-2 + DNG 1.5.0.0
+ 34892, LinearRaw # DNG 1.5.0.0
263, Threshholding
1, No dithering or halftoning
2, Ordered dither or halftoning
@@ -185,6 +193,9 @@
532, ReferenceBlackWhite
700, XMP # Adobe XMP Specification Part 3 Table 12/13/39
32781, ImageID # Adobe PageMaker 6.0 TIFF Technical Notes
+33421, CFARepeatPatternDim # DIS/ISO 12234-2
+33422, CFAPattern # DIS/ISO 12234-2
+33423, BatteryLevel # DIS/ISO 12234-2
33432, Copyright
# TODO(p): Extract IPTC DataSets, like we do directly with PSIRs.
33723, IPTC # Adobe XMP Specification Part 3 Table 12/39
@@ -192,7 +203,18 @@
34377, Photoshop # Adobe XMP Specification Part 3 Table 12/39
34665, Exif IFD Pointer # Exif 2.3
34853, GPS Info IFD Pointer # Exif 2.3
-37398, TIFF/EP StandardID # ISO 12234 TIFF/EP image data format
+37398, TIFF/EP StandardID # DIS/ISO 12234-2
+37399, SensingMethod # DIS/ISO 12234-2, similar to Exif 41495
+ 0, Undefined
+ 1, Monochrome area sensor
+ 2, One-chip color area sensor
+ 3, Two-chip color area sensor
+ 4, Three-chip color area sensor
+ 5, Color sequential area sensor
+ 6, Monochrome linear sensor
+ 7, Trilinear sensor
+ 8, Color sequential linear sensor
+# TODO(p): Add more TIFF/EP tags that can be only in IFD0.
37724, ImageSourceData # Adobe Photoshop TIFF Technical Notes
50706, DNGVersion # DNG 1.5.0.0
50707, DNGBackwardVersion # DNG 1.5.0.0
@@ -425,3 +447,25 @@
# Exif 2.3 4.6.7 (Notice it starts at 1, and collides with GPS.)
= Exif Interoperability
1, InteroperabilityIndex
+
+# CIPA DC-007-2021 5.2.3., 5.2.4. (But derive "field names" from "tag names".)
+= MPF
+45056, MP Format Version Number # MPFVersion
+45057, Number of Images # NumberOfImages
+45058, MP Entry # MPEntry
+45059, Individual Image Unique ID List # ImageUIDList
+45060, Total Number of Captured Frames # TotalFrames
+45313, MP Individual Image Number # MPIndividualNum
+45569, Panorama Scanning Orientation # PanOrientation
+45570, Panorama Horizontal Overlap # PanOverlap_H
+45571, Panorama Vertical Overlap # PanOverlap_V
+45572, Base Viewpoint Number # BaseViewpointNum
+45573, Convergence Angle # ConvergenceAngle
+45574, Baseline Length # BaselineLength
+45575, Divergence Angle # VerticalDivergence
+45576, Horizontal Axis Distance # AxisDistance_X
+45577, Vertical Axis Distance # AxisDistance_Y
+45578, Collimation Axis Distance # AxisDistance_Z
+45579, Yaw Angle # YawAngle
+45580, Pitch Angle # PitchAngle
+45581, Roll Angle # RollAngle
diff --git a/tiffer.h b/tiffer.h
new file mode 100644
index 0000000..870ad26
--- /dev/null
+++ b/tiffer.h
@@ -0,0 +1,356 @@
+//
+// tiffer.h: TIFF reading utilities
+//
+// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
+//
+// 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 <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+// --- Utilities ---------------------------------------------------------------
+
+static uint64_t
+tiffer_u64be(const uint8_t *p)
+{
+ return (uint64_t) p[0] << 56 | (uint64_t) p[1] << 48 |
+ (uint64_t) p[2] << 40 | (uint64_t) p[3] << 32 |
+ (uint64_t) p[4] << 24 | p[5] << 16 | p[6] << 8 | p[7];
+}
+
+static uint32_t
+tiffer_u32be(const uint8_t *p)
+{
+ return (uint32_t) p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
+}
+
+static uint16_t
+tiffer_u16be(const uint8_t *p)
+{
+ return (uint16_t) p[0] << 8 | p[1];
+}
+
+static uint64_t
+tiffer_u64le(const uint8_t *p)
+{
+ return (uint64_t) p[7] << 56 | (uint64_t) p[6] << 48 |
+ (uint64_t) p[5] << 40 | (uint64_t) p[4] << 32 |
+ (uint64_t) p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];
+}
+
+static uint32_t
+tiffer_u32le(const uint8_t *p)
+{
+ return (uint32_t) p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];
+}
+
+static uint16_t
+tiffer_u16le(const uint8_t *p)
+{
+ return (uint16_t) p[1] << 8 | p[0];
+}
+
+// --- TIFF --------------------------------------------------------------------
+// libtiff is a mess, and the format is not particularly complicated.
+// Exiv2 is senselessly copylefted, and cannot do much.
+// libexif is only marginally better.
+// ExifTool is too user-oriented.
+
+struct un {
+ uint64_t (*u64) (const uint8_t *);
+ uint32_t (*u32) (const uint8_t *);
+ uint16_t (*u16) (const uint8_t *);
+};
+
+static struct un tiffer_unbe = {tiffer_u64be, tiffer_u32be, tiffer_u16be};
+static struct un tiffer_unle = {tiffer_u64le, tiffer_u32le, tiffer_u16le};
+
+struct tiffer {
+ struct un *un;
+ const uint8_t *begin, *p, *end;
+ uint16_t remaining_fields;
+};
+
+static bool
+tiffer_u32(struct tiffer *self, uint32_t *u)
+{
+ if (self->end - self->p < 4)
+ return false;
+
+ *u = self->un->u32(self->p);
+ self->p += 4;
+ return true;
+}
+
+static bool
+tiffer_u16(struct tiffer *self, uint16_t *u)
+{
+ if (self->end - self->p < 2)
+ return false;
+
+ *u = self->un->u16(self->p);
+ self->p += 2;
+ return true;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+tiffer_init(struct tiffer *self, const uint8_t *tiff, size_t len)
+{
+ self->un = NULL;
+ self->begin = self->p = tiff;
+ self->end = tiff + len;
+ self->remaining_fields = 0;
+
+ const uint8_t
+ le[4] = {'I', 'I', 42, 0},
+ be[4] = {'M', 'M', 0, 42};
+
+ if (tiff + 8 > self->end)
+ return false;
+ else if (!memcmp(tiff, le, sizeof le))
+ self->un = &tiffer_unle;
+ else if (!memcmp(tiff, be, sizeof be))
+ self->un = &tiffer_unbe;
+ else
+ return false;
+
+ self->p = tiff + 4;
+ // The first IFD needs to be read by caller explicitly,
+ // even though it's required to be present by TIFF 6.0.
+ return true;
+}
+
+/// Read the next IFD in a sequence.
+static bool
+tiffer_next_ifd(struct tiffer *self)
+{
+ // All fields from any previous IFD need to be read first.
+ if (self->remaining_fields)
+ return false;
+
+ uint32_t ifd_offset = 0;
+ if (!tiffer_u32(self, &ifd_offset))
+ return false;
+
+ // There is nothing more to read, this chain has terminated.
+ if (!ifd_offset)
+ return false;
+
+ // Note that TIFF 6.0 requires there to be at least one entry,
+ // but there is no need for us to check it.
+ self->p = self->begin + ifd_offset;
+ return tiffer_u16(self, &self->remaining_fields);
+}
+
+static size_t
+tiffer_length(const struct tiffer *self)
+{
+ return self->begin > self->end ? 0 : self->end - self->begin;
+}
+
+/// Initialize a derived TIFF reader for a subIFD at the given location.
+static bool
+tiffer_subifd(
+ const struct tiffer *self, uint32_t offset, struct tiffer *subreader)
+{
+ if (tiffer_length(self) < offset)
+ return false;
+
+ *subreader = *self;
+ subreader->p = subreader->begin + offset;
+ return tiffer_u16(subreader, &subreader->remaining_fields);
+}
+
+enum tiffer_type {
+ TIFFER_BYTE = 1, TIFFER_ASCII, TIFFER_SHORT, TIFFER_LONG,
+ TIFFER_RATIONAL,
+ TIFFER_SBYTE, TIFFER_UNDEFINED, TIFFER_SSHORT, TIFFER_SLONG,
+ TIFFER_SRATIONAL,
+ TIFFER_FLOAT,
+ TIFFER_DOUBLE,
+ // This last type from TIFF Technical Note 1 isn't really used much.
+ TIFFER_IFD,
+};
+
+static size_t
+tiffer_value_size(enum tiffer_type type)
+{
+ switch (type) {
+ case TIFFER_BYTE:
+ case TIFFER_SBYTE:
+ case TIFFER_ASCII:
+ case TIFFER_UNDEFINED:
+ return 1;
+ case TIFFER_SHORT:
+ case TIFFER_SSHORT:
+ return 2;
+ case TIFFER_LONG:
+ case TIFFER_SLONG:
+ case TIFFER_FLOAT:
+ case TIFFER_IFD:
+ return 4;
+ case TIFFER_RATIONAL:
+ case TIFFER_SRATIONAL:
+ case TIFFER_DOUBLE:
+ return 8;
+ default:
+ return 0;
+ }
+}
+
+/// A lean iterator for values within entries.
+struct tiffer_entry {
+ uint16_t tag;
+ enum tiffer_type type;
+ // For {S,}BYTE, ASCII, UNDEFINED, use these fields directly.
+ const uint8_t *p;
+ uint32_t remaining_count;
+};
+
+static bool
+tiffer_next_value(struct tiffer_entry *entry)
+{
+ if (!entry->remaining_count)
+ return false;
+
+ entry->p += tiffer_value_size(entry->type);
+ entry->remaining_count--;
+ return true;
+}
+
+static bool
+tiffer_integer(
+ const struct tiffer *self, const struct tiffer_entry *entry, int64_t *out)
+{
+ if (!entry->remaining_count)
+ return false;
+
+ // Somewhat excessively lenient, intended for display.
+ // TIFF 6.0 only directly suggests that a reader is should accept
+ // any of BYTE/SHORT/LONG for unsigned integers.
+ switch (entry->type) {
+ case TIFFER_BYTE:
+ case TIFFER_ASCII:
+ case TIFFER_UNDEFINED:
+ *out = *entry->p;
+ return true;
+ case TIFFER_SBYTE:
+ *out = (int8_t) *entry->p;
+ return true;
+ case TIFFER_SHORT:
+ *out = self->un->u16(entry->p);
+ return true;
+ case TIFFER_SSHORT:
+ *out = (int16_t) self->un->u16(entry->p);
+ return true;
+ case TIFFER_LONG:
+ case TIFFER_IFD:
+ *out = self->un->u32(entry->p);
+ return true;
+ case TIFFER_SLONG:
+ *out = (int32_t) self->un->u32(entry->p);
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool
+tiffer_rational(const struct tiffer *self, const struct tiffer_entry *entry,
+ int64_t *numerator, int64_t *denominator)
+{
+ if (!entry->remaining_count)
+ return false;
+
+ // Somewhat excessively lenient, intended for display.
+ switch (entry->type) {
+ case TIFFER_RATIONAL:
+ *numerator = self->un->u32(entry->p);
+ *denominator = self->un->u32(entry->p + 4);
+ return true;
+ case TIFFER_SRATIONAL:
+ *numerator = (int32_t) self->un->u32(entry->p);
+ *denominator = (int32_t) self->un->u32(entry->p + 4);
+ return true;
+ default:
+ if (tiffer_integer(self, entry, numerator)) {
+ *denominator = 1;
+ return true;
+ }
+ return false;
+ }
+}
+
+static bool
+tiffer_real(
+ const struct tiffer *self, const struct tiffer_entry *entry, double *out)
+{
+ if (!entry->remaining_count)
+ return false;
+
+ // Somewhat excessively lenient, intended for display.
+ // Assuming the host architecture uses IEEE 754.
+ switch (entry->type) {
+ int64_t numerator, denominator;
+ case TIFFER_FLOAT:
+ *out = *(float *) entry->p;
+ return true;
+ case TIFFER_DOUBLE:
+ *out = *(double *) entry->p;
+ return true;
+ default:
+ if (tiffer_rational(self, entry, &numerator, &denominator)) {
+ *out = (double) numerator / denominator;
+ return true;
+ }
+ return false;
+ }
+}
+
+static bool
+tiffer_next_entry(struct tiffer *self, struct tiffer_entry *entry)
+{
+ if (!self->remaining_fields)
+ return false;
+
+ uint16_t type = entry->type = 0xFFFF;
+ if (!tiffer_u16(self, &entry->tag) || !tiffer_u16(self, &type) ||
+ !tiffer_u32(self, &entry->remaining_count))
+ return false;
+
+ // Short values may and will be inlined, rather than pointed to.
+ size_t values_size = tiffer_value_size(type) * entry->remaining_count;
+ uint32_t offset = 0;
+ if (values_size <= sizeof offset) {
+ entry->p = self->p;
+ self->p += sizeof offset;
+ } else if (tiffer_u32(self, &offset) && tiffer_length(self) >= offset) {
+ entry->p = self->begin + offset;
+ } else {
+ return false;
+ }
+
+ // All entries are pre-checked not to overflow.
+ if (values_size > PTRDIFF_MAX ||
+ self->end - entry->p < (ptrdiff_t) values_size)
+ return false;
+
+ // Setting it at the end may provide an indication while debugging.
+ entry->type = type;
+ self->remaining_fields--;
+ return true;
+}
diff --git a/fiv-io-benchmark.c b/tools/benchmark-io.c
index 00406cd..3dadaae 100644
--- a/fiv-io-benchmark.c
+++ b/tools/benchmark-io.c
@@ -1,7 +1,7 @@
//
-// fiv-io-benchmark.c: see if we suck
+// benchmark-io.c: measure and compare image loading times
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -32,35 +32,36 @@ timestamp(void)
static void
one_file(const char *filename)
{
- double since_us = timestamp();
+ GFile *file = g_file_new_for_commandline_arg(filename);
+ double since_us = timestamp(), us = 0;
FivIoOpenContext ctx = {
- .uri = g_filename_to_uri(filename, NULL, NULL),
+ .uri = g_file_get_uri(file),
.screen_dpi = 96,
// Only using this array as a redirect.
.warnings = g_ptr_array_new_with_free_func(g_free),
};
- cairo_surface_t *loaded_by_us = fiv_io_open(&ctx, NULL);
+ FivIoImage *loaded_by_us = fiv_io_open(&ctx, NULL);
+ g_clear_object(&file);
g_free((char *) ctx.uri);
g_ptr_array_free(ctx.warnings, TRUE);
if (!loaded_by_us)
return;
- cairo_surface_destroy(loaded_by_us);
- double us = timestamp() - since_us;
+ fiv_io_image_unref(loaded_by_us);
+ us = timestamp() - since_us;
- double since_pixbuf = timestamp();
+ double since_pixbuf = timestamp(), pixbuf = 0;
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;
+ if (gdk_pixbuf) {
+ 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);
+ pixbuf = timestamp() - since_pixbuf;
+ }
- printf("%f\t%f\t%.0f%%\t%s\n", us, pixbuf, us / pixbuf * 100, filename);
+ printf("%.3f\t%.3f\t%.0f%%\t%s\n", us, pixbuf, us / pixbuf * 100, filename);
}
int
diff --git a/tools/bmffinfo.c b/tools/bmffinfo.c
deleted file mode 100644
index f0c6ff1..0000000
--- a/tools/bmffinfo.c
+++ /dev/null
@@ -1,142 +0,0 @@
-//
-// bmffinfo.c: acquire information about BMFF files in JSON format
-//
-// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
-//
-// 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 "info.h"
-
-#include <jv.h>
-
-#include <errno.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-// --- ISO/IEC base media file format ------------------------------------------
-// ISO/IEC 14496-12:2015(E), used to be publicly available, now there's only:
-// https://mpeg.chiariglione.org/standards/mpeg-4/iso-base-media-file-format/text-isoiec-14496-12-5th-edition
-// but people have managed to archive the final version as well:
-// https://b.goeswhere.com/ISO_IEC_14496-12_2015.pdf
-//
-// ISO/IEC 23008-12:2017 Information technology -
-// High efficiency coding and media delivery in heterogeneous environments -
-// Part 12: Image File Format + Cor 1:2020 Technical Corrigendum 1
-// https://standards.iso.org/ittf/PubliclyAvailableStandards/
-
-static jv
-parse_bmff_box(jv o, const char *type, const uint8_t *data, size_t len)
-{
- // TODO(p): Parse out "uuid"'s uint8_t[16] initial field, present as hex.
- // TODO(p): Parse out "ftyp" contents: 14496-12:2015 4.3
- // TODO(p): Parse out other important boxes: 14496-12:2015 8+
- return add_to_subarray(o, "boxes", jv_string(type));
-}
-
-static jv
-parse_bmff(jv o, const uint8_t *p, size_t len)
-{
- // 4.2 Object Structure--this box need not be present, nor at the beginning
- // TODO(p): What does `aligned(8)` mean? It's probably in bits.
- if (len < 8 || memcmp(p + 4, "ftyp", 4))
- return add_error(o, "not BMFF at all or unsupported");
-
- const uint8_t *end = p + len;
- while (p < end) {
- if (end - p < 8) {
- o = add_warning(o, "box framing mismatch");
- break;
- }
-
- char type[5] = "";
- memcpy(type, p + 4, 4);
-
- uint64_t box_size = u32be(p);
- const uint8_t *data = p + 8;
- if (box_size == 1) {
- if (end - p < 16) {
- o = add_warning(o, "unexpected EOF");
- break;
- }
- box_size = u64be(data);
- data += 8;
- } else if (!box_size)
- box_size = end - p;
-
- if (box_size > (uint64_t) (end - p)) {
- o = add_warning(o, "unexpected EOF");
- break;
- }
-
- size_t data_len = box_size - (data - p);
- o = parse_bmff_box(o, type, data, data_len);
- p += box_size;
- }
- return o;
-}
-
-// --- I/O ---------------------------------------------------------------------
-
-static jv
-do_file(const char *filename, jv o)
-{
- const char *err = NULL;
- FILE *fp = fopen(filename, "rb");
- if (!fp) {
- err = strerror(errno);
- goto error;
- }
-
- uint8_t *data = NULL, buf[256 << 10];
- size_t n, len = 0;
- while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
- data = realloc(data, len + n);
- memcpy(data + len, buf, n);
- len += n;
- }
- if (ferror(fp)) {
- err = strerror(errno);
- goto error_read;
- }
-
- o = parse_bmff(o, data, len);
-error_read:
- fclose(fp);
- free(data);
-error:
- if (err)
- o = add_error(o, err);
- return o;
-}
-
-int
-main(int argc, char *argv[])
-{
- (void) parse_icc;
- (void) parse_exif;
- (void) parse_psir;
-
- // XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
- // Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo
- for (int i = 1; i < argc; i++) {
- const char *filename = argv[i];
-
- jv o = jv_object();
- o = jv_object_set(o, jv_string("filename"), jv_string(filename));
- o = do_file(filename, o);
- jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */);
- fputc('\n', stdout);
- }
- return 0;
-}
diff --git a/tools/hotpixels.c b/tools/hotpixels.c
new file mode 100644
index 0000000..ee1028c
--- /dev/null
+++ b/tools/hotpixels.c
@@ -0,0 +1,210 @@
+//
+// hotpixels.c: look for hot pixels in raw image files
+//
+// Usage: pass a bunch of raw photo images taken with the lens cap on at,
+// e.g., ISO 8000-12800 @ 1/20-1/60, and store the resulting file as,
+// e.g., Nikon D7500.badpixels, which can then be directly used by Rawtherapee.
+//
+// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
+//
+// 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 <libraw.h>
+
+#if LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
+#error LibRaw 0.21.0 or newer is required.
+#endif
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void *
+xreallocarray(void *o, size_t n, size_t m)
+{
+ if (m && n > SIZE_MAX / m) {
+ fprintf(stderr, "xreallocarray: %s\n", strerror(ENOMEM));
+ exit(EXIT_FAILURE);
+ }
+ void *p = realloc(o, n * m);
+ if (!p && n && m) {
+ fprintf(stderr, "xreallocarray: %s\n", strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+ return p;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct coord { ushort x, y; };
+
+static bool
+coord_equals(struct coord a, struct coord b)
+{
+ return a.x == b.x && a.y == b.y;
+}
+
+static int
+coord_cmp(const void *a, const void *b)
+{
+ const struct coord *ca = (const struct coord *) a;
+ const struct coord *cb = (const struct coord *) b;
+ return ca->y != cb->y
+ ? (int) ca->y - (int) cb->y
+ : (int) ca->x - (int) cb->x;
+}
+
+struct candidates {
+ struct coord *xy;
+ size_t len;
+ size_t alloc;
+};
+
+static void
+candidates_add(struct candidates *c, ushort x, ushort y)
+{
+ if (c->len == c->alloc) {
+ c->alloc += 64;
+ c->xy = xreallocarray(c->xy, sizeof *c->xy, c->alloc);
+ }
+
+ c->xy[c->len++] = (struct coord) {x, y};
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// A stretch of zeroes that is assumed to mean start of outliers.
+#define SPAN 10
+
+static const char *
+process_raw(struct candidates *c, const uint8_t *p, size_t len)
+{
+ libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
+ if (!iprc)
+ return "failed to obtain a LibRaw handle";
+
+ int err = 0;
+ if ((err = libraw_open_buffer(iprc, p, len)) ||
+ (err = libraw_unpack(iprc))) {
+ libraw_close(iprc);
+ return libraw_strerror(err);
+ }
+ if (!iprc->rawdata.raw_image) {
+ libraw_close(iprc);
+ return "only Bayer raws are supported, not Foveon";
+ }
+
+ // Make a histogram.
+ uint64_t bins[USHRT_MAX] = {};
+ for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
+ for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
+ ushort y = iprc->sizes.top_margin + yy;
+ ushort x = iprc->sizes.left_margin + xx;
+ bins[iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x]]++;
+ }
+ }
+
+ // Detecting outliers is not completely straight-forward,
+ // it may help to see the histogram.
+ if (getenv("HOTPIXELS_HISTOGRAM")) {
+ for (ushort i = 0; i < USHRT_MAX; i++)
+ fprintf(stderr, "%u ", (unsigned) bins[i]);
+ fputc('\n', stderr);
+ }
+
+ // Go to the first non-zero pixel value.
+ size_t last = 0;
+ for (; last < USHRT_MAX; last++)
+ if (bins[last])
+ break;
+
+ // Find the last pixel value we assume to not be hot.
+ for (; last < USHRT_MAX - SPAN - 1; last++) {
+ uint64_t nonzero = 0;
+ for (int i = 1; i <= SPAN; i++)
+ nonzero += bins[last + i];
+ if (!nonzero)
+ break;
+ }
+
+ // Store coordinates for all pixels above that value.
+ for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
+ for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
+ ushort y = iprc->sizes.top_margin + yy;
+ ushort x = iprc->sizes.left_margin + xx;
+ if (iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x] > last)
+ candidates_add(c, xx, yy);
+ }
+ }
+
+ libraw_close(iprc);
+ return NULL;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static const char *
+do_file(struct candidates *c, const char *filename)
+{
+ FILE *fp = fopen(filename, "rb");
+ if (!fp)
+ return strerror(errno);
+
+ uint8_t *data = NULL, buf[256 << 10];
+ size_t n, len = 0;
+ while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
+ data = xreallocarray(data, len + n, 1);
+ memcpy(data + len, buf, n);
+ len += n;
+ }
+
+ const char *err = ferror(fp)
+ ? strerror(errno)
+ : process_raw(c, data, len);
+
+ fclose(fp);
+ free(data);
+ return err;
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct candidates c = {};
+ for (int i = 1; i < argc; i++) {
+ const char *filename = argv[i], *err = do_file(&c, filename);
+ if (err) {
+ fprintf(stderr, "%s: %s\n", filename, err);
+ return EXIT_FAILURE;
+ }
+ }
+
+ qsort(c.xy, c.len, sizeof *c.xy, coord_cmp);
+
+ // If it is detected in all passed photos, it is probably indeed bad.
+ int count = 1;
+ for (size_t i = 1; i <= c.len; i++) {
+ if (i != c.len && coord_equals(c.xy[i - 1], c.xy[i])) {
+ count++;
+ continue;
+ }
+
+ if (count == argc - 1)
+ printf("%u %u\n", c.xy[i - 1].x, c.xy[i - 1].y);
+
+ count = 1;
+ }
+ return 0;
+}
diff --git a/tools/info.c b/tools/info.c
new file mode 100644
index 0000000..440939f
--- /dev/null
+++ b/tools/info.c
@@ -0,0 +1,286 @@
+//
+// info.c: acquire information about JPEG/TIFF/BMFF/WebP files in JSON format
+//
+// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
+//
+// 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 "info.h"
+
+#include <jv.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+// --- ISO/IEC base media file format ------------------------------------------
+// ISO/IEC 14496-12:2015(E), used to be publicly available, now there's only:
+// https://mpeg.chiariglione.org/standards/mpeg-4/iso-base-media-file-format/text-isoiec-14496-12-5th-edition
+// but people have managed to archive the final version as well:
+// https://b.goeswhere.com/ISO_IEC_14496-12_2015.pdf
+//
+// ISO/IEC 23008-12:2017 Information technology -
+// High efficiency coding and media delivery in heterogeneous environments -
+// Part 12: Image File Format + Cor 1:2020 Technical Corrigendum 1
+// https://standards.iso.org/ittf/PubliclyAvailableStandards/
+
+static jv
+parse_bmff_box(jv o, const char *type, const uint8_t *data, size_t len)
+{
+ // TODO(p): Parse out "uuid"'s uint8_t[16] initial field, present as hex.
+ // TODO(p): Parse out "ftyp" contents: 14496-12:2015 4.3
+ // TODO(p): Parse out other important boxes: 14496-12:2015 8+
+ return add_to_subarray(o, "boxes", jv_string(type));
+}
+
+static bool
+detect_bmff(const uint8_t *p, size_t len)
+{
+ // 4.2 Object Structure--this box need not be present, nor at the beginning
+ // TODO(p): What does `aligned(8)` mean? It's probably in bits.
+ return len >= 8 && !memcmp(p + 4, "ftyp", 4);
+}
+
+static jv
+parse_bmff(jv o, const uint8_t *p, size_t len)
+{
+ if (!detect_bmff(p, len))
+ return add_error(o, "not BMFF at all or unsupported");
+
+ const uint8_t *end = p + len;
+ while (p < end) {
+ if (end - p < 8) {
+ o = add_warning(o, "box framing mismatch");
+ break;
+ }
+
+ char type[5] = "";
+ memcpy(type, p + 4, 4);
+
+ uint64_t box_size = u32be(p);
+ const uint8_t *data = p + 8;
+ if (box_size == 1) {
+ if (end - p < 16) {
+ o = add_warning(o, "unexpected EOF");
+ break;
+ }
+ box_size = u64be(data);
+ data += 8;
+ } else if (!box_size)
+ box_size = end - p;
+
+ if (box_size > (uint64_t) (end - p)) {
+ o = add_warning(o, "unexpected EOF");
+ break;
+ }
+
+ size_t data_len = box_size - (data - p);
+ o = parse_bmff_box(o, type, data, data_len);
+ p += box_size;
+ }
+ return o;
+}
+
+// --- WebP --------------------------------------------------------------------
+// libwebp won't let us simply iterate over all chunks, so handroll it.
+//
+// https://github.com/webmproject/libwebp/blob/master/doc/webp-container-spec.txt
+// https://github.com/webmproject/libwebp/blob/master/doc/webp-lossless-bitstream-spec.txt
+// https://datatracker.ietf.org/doc/html/rfc6386
+//
+// Pretty versions, hopefully not outdated:
+// https://developers.google.com/speed/webp/docs/riff_container
+// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
+
+static bool
+detect_webp(const uint8_t *p, size_t len)
+{
+ return len >= 12 && !memcmp(p, "RIFF", 4) && !memcmp(p + 8, "WEBP", 4);
+}
+
+static jv
+parse_webp_vp8(jv o, const uint8_t *p, size_t len)
+{
+ if (len < 10 || (p[0] & 1) != 0 /* key frame */ ||
+ p[3] != 0x9d || p[4] != 0x01 || p[5] != 0x2a) {
+ return add_warning(o, "invalid VP8 chunk");
+ }
+
+ o = jv_set(o, jv_string("width"), jv_number(u16le(p + 6) & 0x3fff));
+ o = jv_set(o, jv_string("height"), jv_number(u16le(p + 8) & 0x3fff));
+ return o;
+}
+
+static jv
+parse_webp_vp8l(jv o, const uint8_t *p, size_t len)
+{
+ if (len < 5 || p[0] != 0x2f)
+ return add_warning(o, "invalid VP8L chunk");
+
+ // Reading LSB-first from a little endian value means reading in order.
+ uint32_t header = u32le(p + 1);
+ o = jv_set(o, jv_string("width"), jv_number((header & 0x3fff) + 1));
+ header >>= 14;
+ o = jv_set(o, jv_string("height"), jv_number((header & 0x3fff) + 1));
+ header >>= 14;
+ o = jv_set(o, jv_string("alpha_is_used"), jv_bool(header & 1));
+ return o;
+}
+
+static jv
+parse_webp_vp8x(jv o, const uint8_t *p, size_t len)
+{
+ if (len < 10)
+ return add_warning(o, "invalid VP8X chunk");
+
+ // Most of the fields in this chunk are duplicate or inferrable.
+ // Probably not worth decoding or verifying.
+ // TODO(p): For animations, we need to use the width and height from here.
+ uint8_t flags = p[0];
+ o = jv_set(o, jv_string("animation"), jv_bool((flags >> 1) & 1));
+ return o;
+}
+
+static jv
+parse_webp(jv o, const uint8_t *p, size_t len)
+{
+ if (!detect_webp(p, len))
+ return add_error(o, "not a WEBP file");
+
+ // TODO(p): This can still be parseable.
+ // TODO(p): Warn on trailing data.
+ uint32_t size = u32le(p + 4);
+ if (8 + size < len)
+ return add_error(o, "truncated file");
+
+ const uint8_t *end = p + 8 + size;
+ p += 12;
+
+ jv chunks = jv_array();
+ while (p < end) {
+ if (end - p < 8) {
+ o = add_warning(o, "framing mismatch");
+ printf("%ld", end - p);
+ break;
+ }
+
+ uint32_t chunk_size = u32le(p + 4);
+ uint32_t chunk_advance = (chunk_size + 1) & ~1;
+ if (p + 8 + chunk_advance > end) {
+ o = add_warning(o, "runaway chunk payload");
+ break;
+ }
+
+ char fourcc[5] = "";
+ memcpy(fourcc, p, 4);
+ chunks = jv_array_append(chunks, jv_string(fourcc));
+ p += 8;
+
+ // TODO(p): Decode more chunks.
+ if (!strcmp(fourcc, "VP8 "))
+ o = parse_webp_vp8(o, p, chunk_size);
+ if (!strcmp(fourcc, "VP8L"))
+ o = parse_webp_vp8l(o, p, chunk_size);
+ if (!strcmp(fourcc, "VP8X"))
+ o = parse_webp_vp8x(o, p, chunk_size);
+ if (!strcmp(fourcc, "EXIF"))
+ o = parse_exif(o, p, chunk_size);
+ if (!strcmp(fourcc, "ICCP"))
+ o = parse_icc(o, p, chunk_size);
+ p += chunk_advance;
+ }
+ return jv_set(o, jv_string("chunks"), chunks);
+}
+
+// --- I/O ---------------------------------------------------------------------
+
+static struct {
+ const char *name;
+ bool (*detect) (const uint8_t *, size_t);
+ jv (*parse) (jv, const uint8_t *, size_t);
+} formats[] = {
+ {"JPEG", detect_jpeg, parse_jpeg},
+ {"TIFF", detect_tiff, parse_tiff},
+ {"BMFF", detect_bmff, parse_bmff},
+ {"WebP", detect_webp, parse_webp},
+};
+
+static jv
+parse_any(jv o, const uint8_t *p, size_t len)
+{
+ // TODO(p): Also see if the file extension is appropriate.
+ for (size_t i = 0; i < sizeof formats / sizeof *formats; i++) {
+ if (!formats[i].detect(p, len))
+ continue;
+ if (getenv("INFO_IDENTIFY"))
+ o = jv_set(o, jv_string("format"), jv_string(formats[i].name));
+ return formats[i].parse(o, p, len);
+ }
+ return add_error(o, "unsupported file format");
+}
+
+static jv
+do_file(const char *filename, jv o)
+{
+ const char *err = NULL;
+ FILE *fp = fopen(filename, "rb");
+ if (!fp) {
+ err = strerror(errno);
+ goto error;
+ }
+
+ uint8_t *data = NULL, buf[256 << 10];
+ size_t n, len = 0;
+ while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
+ data = realloc(data, len + n);
+ memcpy(data + len, buf, n);
+ len += n;
+ }
+ if (ferror(fp)) {
+ err = strerror(errno);
+ goto error_read;
+ }
+
+#if 0
+ // Not sure if I want to ensure their existence...
+ o = jv_object_set(o, jv_string("info"), jv_array());
+ o = jv_object_set(o, jv_string("warnings"), jv_array());
+#endif
+
+ o = parse_any(o, data, len);
+error_read:
+ fclose(fp);
+ free(data);
+error:
+ if (err)
+ o = add_error(o, err);
+ return o;
+}
+
+int
+main(int argc, char *argv[])
+{
+ // XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
+ // Usage: find . -print0 | xargs -0 ./info
+ for (int i = 1; i < argc; i++) {
+ const char *filename = argv[i];
+
+ jv o = jv_object();
+ o = jv_object_set(o, jv_string("filename"), jv_string(filename));
+ o = do_file(filename, o);
+ jv_dumpf(o, stdout, 0 /* JV_PRINT_SORTED would discard information. */);
+ fputc('\n', stdout);
+ }
+ return 0;
+}
diff --git a/tools/info.h b/tools/info.h
index 816c9cf..b6c6391 100644
--- a/tools/info.h
+++ b/tools/info.h
@@ -1,7 +1,7 @@
//
// info.h: metadata extraction utilities
//
-// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -17,350 +17,14 @@
#include <jv.h>
+#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
-#include <stdbool.h>
-
-// --- Utilities ---------------------------------------------------------------
-
-static char *
-binhex(const uint8_t *data, size_t len)
-{
- static const char *alphabet = "0123456789abcdef";
- char *buf = calloc(1, len * 2 + 1), *p = buf;
- for (size_t i = 0; i < len; i++) {
- *p++ = alphabet[data[i] >> 4];
- *p++ = alphabet[data[i] & 0xF];
- }
- return buf;
-}
-
-static uint64_t
-u64be(const uint8_t *p)
-{
- return (uint64_t) p[0] << 56 | (uint64_t) p[1] << 48 |
- (uint64_t) p[2] << 40 | (uint64_t) p[3] << 32 |
- (uint64_t) p[4] << 24 | p[5] << 16 | p[6] << 8 | p[7];
-}
-
-static uint32_t
-u32be(const uint8_t *p)
-{
- return (uint32_t) p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
-}
-
-static uint16_t
-u16be(const uint8_t *p)
-{
- return (uint16_t) p[0] << 8 | p[1];
-}
-
-static uint64_t
-u64le(const uint8_t *p)
-{
- return (uint64_t) p[7] << 56 | (uint64_t) p[6] << 48 |
- (uint64_t) p[5] << 40 | (uint64_t) p[4] << 32 |
- (uint64_t) p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];
-}
-
-static uint32_t
-u32le(const uint8_t *p)
-{
- return (uint32_t) p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];
-}
-
-static uint16_t
-u16le(const uint8_t *p)
-{
- return (uint16_t) p[1] << 8 | p[0];
-}
-
-// --- TIFF --------------------------------------------------------------------
-// libtiff is a mess, and the format is not particularly complicated.
-// Exiv2 is senselessly copylefted, and cannot do much.
-// libexif is only marginally better.
-// ExifTool is too user-oriented.
-
-static struct un {
- uint64_t (*u64) (const uint8_t *);
- uint32_t (*u32) (const uint8_t *);
- uint16_t (*u16) (const uint8_t *);
-} unbe = {u64be, u32be, u16be}, unle = {u64le, u32le, u16le};
-
-struct tiffer {
- struct un *un;
- const uint8_t *begin, *p, *end;
- uint16_t remaining_fields;
-};
-static bool
-tiffer_u32(struct tiffer *self, uint32_t *u)
-{
- if (self->p + 4 > self->end)
- return false;
- *u = self->un->u32(self->p);
- self->p += 4;
- return true;
-}
-
-static bool
-tiffer_u16(struct tiffer *self, uint16_t *u)
-{
- if (self->p + 2 > self->end)
- return false;
- *u = self->un->u16(self->p);
- self->p += 2;
- return true;
-}
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-static bool
-tiffer_init(struct tiffer *self, const uint8_t *tiff, size_t len)
-{
- self->un = NULL;
- self->begin = self->p = tiff;
- self->end = tiff + len;
- self->remaining_fields = 0;
-
- const uint8_t
- le[4] = {'I', 'I', 42, 0},
- be[4] = {'M', 'M', 0, 42};
-
- if (tiff + 8 > self->end)
- return false;
- else if (!memcmp(tiff, le, sizeof le))
- self->un = &unle;
- else if (!memcmp(tiff, be, sizeof be))
- self->un = &unbe;
- else
- return false;
-
- self->p = tiff + 4;
- // The first IFD needs to be read by caller explicitly,
- // even though it's required to be present by TIFF 6.0.
- return true;
-}
-
-/// Read the next IFD in a sequence.
-static bool
-tiffer_next_ifd(struct tiffer *self)
-{
- // All fields from any previous IFD need to be read first.
- if (self->remaining_fields)
- return false;
-
- uint32_t ifd_offset = 0;
- if (!tiffer_u32(self, &ifd_offset))
- return false;
-
- // There is nothing more to read, this chain has terminated.
- if (!ifd_offset)
- return false;
-
- // Note that TIFF 6.0 requires there to be at least one entry,
- // but there is no need for us to check it.
- self->p = self->begin + ifd_offset;
- return tiffer_u16(self, &self->remaining_fields);
-}
-
-/// Initialize a derived TIFF reader for a subIFD at the given location.
-static bool
-tiffer_subifd(struct tiffer *self, uint32_t offset, struct tiffer *subreader)
-{
- *subreader = *self;
- subreader->p = subreader->begin + offset;
- return tiffer_u16(subreader, &subreader->remaining_fields);
-}
-
-enum tiffer_type {
- BYTE = 1, ASCII, SHORT, LONG, RATIONAL,
- SBYTE, UNDEFINED, SSHORT, SLONG, SRATIONAL, FLOAT, DOUBLE,
- IFD // This last type from TIFF Technical Note 1 isn't really used much.
-};
-
-static size_t
-tiffer_value_size(enum tiffer_type type)
-{
- switch (type) {
- case BYTE:
- case SBYTE:
- case ASCII:
- case UNDEFINED:
- return 1;
- case SHORT:
- case SSHORT:
- return 2;
- case LONG:
- case SLONG:
- case FLOAT:
- case IFD:
- return 4;
- case RATIONAL:
- case SRATIONAL:
- case DOUBLE:
- return 8;
- default:
- return 0;
- }
-}
-
-/// A lean iterator for values within entries.
-struct tiffer_entry {
- uint16_t tag;
- enum tiffer_type type;
- // For {S,}BYTE, ASCII, UNDEFINED, use these fields directly.
- const uint8_t *p;
- uint32_t remaining_count;
-};
-
-static bool
-tiffer_next_value(struct tiffer_entry *entry)
-{
- if (!entry->remaining_count)
- return false;
-
- entry->p += tiffer_value_size(entry->type);
- entry->remaining_count--;
- return true;
-}
-
-static bool
-tiffer_integer(
- const struct tiffer *self, const struct tiffer_entry *entry, int64_t *out)
-{
- if (!entry->remaining_count)
- return false;
-
- // Somewhat excessively lenient, intended for display.
- // TIFF 6.0 only directly suggests that a reader is should accept
- // any of BYTE/SHORT/LONG for unsigned integers.
- switch (entry->type) {
- case BYTE:
- case ASCII:
- case UNDEFINED:
- *out = *entry->p;
- return true;
- case SBYTE:
- *out = (int8_t) *entry->p;
- return true;
- case SHORT:
- *out = self->un->u16(entry->p);
- return true;
- case SSHORT:
- *out = (int16_t) self->un->u16(entry->p);
- return true;
- case LONG:
- case IFD:
- *out = self->un->u32(entry->p);
- return true;
- case SLONG:
- *out = (int32_t) self->un->u32(entry->p);
- return true;
- default:
- return false;
- }
-}
-
-static bool
-tiffer_rational(const struct tiffer *self, const struct tiffer_entry *entry,
- int64_t *numerator, int64_t *denominator)
-{
- if (!entry->remaining_count)
- return false;
-
- // Somewhat excessively lenient, intended for display.
- switch (entry->type) {
- case RATIONAL:
- *numerator = self->un->u32(entry->p);
- *denominator = self->un->u32(entry->p + 4);
- return true;
- case SRATIONAL:
- *numerator = (int32_t) self->un->u32(entry->p);
- *denominator = (int32_t) self->un->u32(entry->p + 4);
- return true;
- default:
- if (tiffer_integer(self, entry, numerator)) {
- *denominator = 1;
- return true;
- }
- return false;
- }
-}
-
-static bool
-tiffer_real(
- const struct tiffer *self, const struct tiffer_entry *entry, double *out)
-{
- if (!entry->remaining_count)
- return false;
-
- // Somewhat excessively lenient, intended for display.
- // Assuming the host architecture uses IEEE 754.
- switch (entry->type) {
- int64_t numerator, denominator;
- case FLOAT:
- *out = *(float *) entry->p;
- return true;
- case DOUBLE:
- *out = *(double *) entry->p;
- return true;
- default:
- if (tiffer_rational(self, entry, &numerator, &denominator)) {
- *out = (double) numerator / denominator;
- return true;
- }
- return false;
- }
-}
-
-static bool
-tiffer_next_entry(struct tiffer *self, struct tiffer_entry *entry)
-{
- if (!self->remaining_fields)
- return false;
-
- uint16_t type = entry->type = 0xFFFF;
- if (!tiffer_u16(self, &entry->tag) || !tiffer_u16(self, &type) ||
- !tiffer_u32(self, &entry->remaining_count))
- return false;
-
- // Short values may and will be inlined, rather than pointed to.
- size_t values_size = tiffer_value_size(type) * entry->remaining_count;
- uint32_t offset = 0;
- if (values_size <= sizeof offset) {
- entry->p = self->p;
- self->p += sizeof offset;
- } else if (tiffer_u32(self, &offset)) {
- entry->p = self->begin + offset;
- } else {
- return false;
- }
-
- // All entries are pre-checked not to overflow.
- if (entry->p + values_size > self->end)
- return false;
-
- // Setting it at the end may provide an indication while debugging.
- entry->type = type;
- self->remaining_fields--;
- return true;
-}
-
-// --- TIFF/Exif tags ----------------------------------------------------------
-
-struct tiff_value {
- const char *name;
- uint16_t value;
-};
-
-struct tiff_entry {
- const char *name;
- uint16_t tag;
- struct tiff_value *values;
-};
+// --- TIFF/Exif ---------------------------------------------------------------
#include "tiff-tables.h"
+#include "tiffer.h"
// TODO(p): Consider if these can't be inlined into `tiff_entries`.
static struct {
@@ -374,6 +38,27 @@ static struct {
{}
};
+// --- Utilities ---------------------------------------------------------------
+
+#define u64be tiffer_u64be
+#define u32be tiffer_u32be
+#define u16be tiffer_u16be
+#define u64le tiffer_u64le
+#define u32le tiffer_u32le
+#define u16le tiffer_u16le
+
+static char *
+binhex(const uint8_t *data, size_t len)
+{
+ static const char *alphabet = "0123456789abcdef";
+ char *buf = calloc(1, len * 2 + 1), *p = buf;
+ for (size_t i = 0; i < len; i++) {
+ *p++ = alphabet[data[i] >> 4];
+ *p++ = alphabet[data[i] & 0xF];
+ }
+ return buf;
+}
+
// --- Analysis ----------------------------------------------------------------
static jv
@@ -399,29 +84,78 @@ add_error(jv o, const char *message)
return jv_object_set(o, jv_string("error"), jv_string(message));
}
+// Forward declaration.
+static jv parse_jpeg(jv o, const uint8_t *p, size_t len);
+
// --- Exif --------------------------------------------------------------------
static jv parse_exif_ifd(struct tiffer *T, const struct tiff_entry *info);
+static bool
+parse_exif_subifds_entry(const struct tiffer *T,
+ const struct tiffer_entry *entry, struct tiffer *subT)
+{
+ int64_t offset = 0;
+ return tiffer_integer(T, entry, &offset) &&
+ offset >= 0 && offset <= UINT32_MAX && tiffer_subifd(T, offset, subT);
+}
+
static jv
-parse_exif_subifds(struct tiffer *T, const struct tiffer_entry *entry,
+parse_exif_subifds(const struct tiffer *T, struct tiffer_entry *entry,
struct tiff_entry *info)
{
- int64_t offset = 0;
struct tiffer subT = {};
- if (!tiffer_integer(T, entry, &offset) ||
- offset < 0 || offset > UINT32_MAX || !tiffer_subifd(T, offset, &subT))
+ if (!parse_exif_subifds_entry(T, entry, &subT))
return jv_null();
- // The chain should correspond to the values in the entry
- // (TIFF Technical Note 1), we are not going to verify it.
- // Note that Nikon NEFs do not follow this rule.
jv a = jv_array();
do a = jv_array_append(a, parse_exif_ifd(&subT, info));
while (tiffer_next_ifd(&subT));
+
+ // The chain should correspond to the values in the entry (see TIFF
+ // Technical Note 1: "the NextIFD value of Child #1 must point to Child #2,
+ // and so on"), but at least some Nikon NEFs do not follow this rule.
+ if (jv_array_length(jv_copy(a)) == 1) {
+ while (tiffer_next_value(entry) &&
+ parse_exif_subifds_entry(T, entry, &subT))
+ a = jv_array_append(a, parse_exif_ifd(&subT, info));
+ }
return a;
}
+// Implemented partially, out of curiosity--it is not particularly useful,
+// because there is a ton more parsing to do here.
+static bool
+parse_exif_makernote(jv *v, const struct tiffer_entry *entry)
+{
+ if (!getenv("INFO_MAKERNOTE") ||
+ entry->tag != Exif_MakerNote || entry->type != TIFFER_UNDEFINED)
+ return false;
+
+ struct tiffer T = {};
+ if (entry->remaining_count >= 16 &&
+ !memcmp(entry->p, "Nikon\x00\x02", 7) &&
+ tiffer_init(&T, entry->p + 10, entry->remaining_count - 10) &&
+ tiffer_next_ifd(&T)) {
+ *v = parse_exif_ifd(&T, NULL);
+ return true;
+ }
+ if (entry->remaining_count >= 16 &&
+ !memcmp(entry->p, "Apple iOS\x00\x00\x01MM", 14)) {
+ T.un = &tiffer_unbe;
+ T.begin = T.p = entry->p + 14;
+ T.end = entry->p + entry->remaining_count - 14;
+ T.remaining_fields = 0;
+
+ struct tiffer subT = {};
+ if (tiffer_subifd(&T, 0, &subT)) {
+ *v = parse_exif_ifd(&subT, NULL);
+ return true;
+ }
+ }
+ return false;
+}
+
static jv
parse_exif_ascii(struct tiffer_entry *entry)
{
@@ -472,11 +206,13 @@ parse_exif_extract_sole_array_element(jv a)
}
static jv
-parse_exif_entry(jv o, struct tiffer *T, struct tiffer_entry *entry,
+parse_exif_entry(jv o, const struct tiffer *T, struct tiffer_entry *entry,
const struct tiff_entry *info)
{
+ const struct tiff_entry *info_begin = info;
+ static struct tiff_entry empty[] = {{}};
if (!info)
- info = (struct tiff_entry[]) {{}};
+ info = empty;
for (; info->name; info++)
if (info->tag == entry->tag)
@@ -491,13 +227,18 @@ parse_exif_entry(jv o, struct tiffer *T, struct tiffer_entry *entry,
double real = 0;
if (!entry->remaining_count) {
v = jv_null();
- } else if (entry->type == IFD || subentries) {
+ } else if (entry->type == TIFFER_IFD || subentries) {
v = parse_exif_subifds(T, entry, subentries);
- } else if (entry->type == ASCII) {
+ } else if (entry->type == TIFFER_ASCII) {
v = parse_exif_extract_sole_array_element(parse_exif_ascii(entry));
- } else if (entry->type == UNDEFINED && !info->values) {
+ } else if (info_begin == exif_entries && parse_exif_makernote(&v, entry)) {
+ // Already processed.
+ } else if (entry->type == TIFFER_UNDEFINED && !info->values) {
// Several Exif entries of UNDEFINED type contain single-byte numbers.
v = parse_exif_undefined(entry);
+ } else if (info_begin == tiff_entries && entry->tag == TIFF_XMP &&
+ (entry->type == TIFFER_UNDEFINED || entry->type == TIFFER_BYTE)) {
+ v = jv_string_sized((const char *) entry->p, entry->remaining_count);
} else if (tiffer_real(T, entry, &real)) {
v = jv_array();
do v = jv_array_append(v, parse_exif_value(info->values, real));
@@ -513,10 +254,55 @@ parse_exif_entry(jv o, struct tiffer *T, struct tiffer_entry *entry,
static jv
parse_exif_ifd(struct tiffer *T, const struct tiff_entry *info)
{
+ int64_t compression = 0,
+ jpeg = 0, jpeg_length = 0, strip_offsets = 0, strip_byte_counts = 0;
+
jv ifd = jv_object();
struct tiffer_entry entry = {};
- while (tiffer_next_entry(T, &entry))
+ while (tiffer_next_entry(T, &entry)) {
+ switch (entry.tag) {
+ case TIFF_Compression:
+ tiffer_integer(T, &entry, &compression);
+ break;
+ case TIFF_JPEGInterchangeFormat:
+ tiffer_integer(T, &entry, &jpeg);
+ break;
+ case TIFF_JPEGInterchangeFormatLength:
+ tiffer_integer(T, &entry, &jpeg_length);
+ break;
+ case TIFF_StripOffsets:
+ tiffer_integer(T, &entry, &strip_offsets);
+ break;
+ case TIFF_StripByteCounts:
+ tiffer_integer(T, &entry, &strip_byte_counts);
+ break;
+ }
+
ifd = parse_exif_entry(ifd, T, &entry, info);
+ }
+
+ // This is how Exif specifies it, which doesn't follow TIFF 6.0.
+ // Also support CR2 IFD1, which isn't tagged with compression at all.
+ if (info == tiff_entries && /* compression == TIFF_Compression_JPEG && */
+ jpeg > 0 && jpeg_length > 0 &&
+ jpeg + jpeg_length <= (T->end - T->begin)) {
+ ifd = jv_set(ifd, jv_string("JPEG image data"),
+ parse_jpeg(
+ jv_object(), T->begin + jpeg, jpeg_length));
+ }
+
+ // As specified by DRAFT TIFF Technical Note 2 + TIFFphotoshop.pdf.
+ // Theoretically, there may be more strips, but this is not expected.
+ // Also support CR2 IFD0, which is tagged with the "wrong" compression.
+ if (info == tiff_entries &&
+ (compression == TIFF_Compression_JPEGDatastream ||
+ compression == TIFF_Compression_JPEG) &&
+ strip_offsets > 0 && strip_byte_counts > 0 &&
+ strip_offsets + strip_byte_counts <= (T->end - T->begin)) {
+ ifd = jv_set(ifd, jv_string("JPEG image data"),
+ parse_jpeg(
+ jv_object(), T->begin + strip_offsets, strip_byte_counts));
+ }
return ifd;
}
@@ -531,6 +317,25 @@ parse_exif(jv o, const uint8_t *p, size_t len)
return o;
}
+static bool
+detect_tiff(const uint8_t *p, size_t len)
+{
+ return tiffer_init(&(struct tiffer) {}, p, len);
+}
+
+// TODO(p): Photoshop data and ICC profiles also have their tag in TIFF,
+// they're not currently processed.
+static jv
+parse_tiff(jv o, const uint8_t *p, size_t len)
+{
+ struct tiffer T = {};
+ if (!tiffer_init(&T, p, len))
+ return add_warning(o, "invalid TIFF");
+ while (tiffer_next_ifd(&T))
+ o = add_to_subarray(o, "TIFF", parse_exif_ifd(&T, tiff_entries));
+ return o;
+}
+
// --- Photoshop Image Resources -----------------------------------------------
// Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 + 3.1.3
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
@@ -885,3 +690,513 @@ parse_icc(jv o, const uint8_t *profile, size_t profile_len)
// The description is required, so this should be unreachable.
return jv_set(o, jv_string("ICC"), jv_bool(true));
}
+
+// --- Multi-Picture Format ----------------------------------------------------
+
+static uint32_t
+parse_mpf_mpentry(jv *a, const uint8_t *p, const struct tiffer *T)
+{
+ uint32_t attrs = T->un->u32(p);
+ uint32_t offset = T->un->u32(p + 8);
+
+ uint32_t type_number = attrs & 0xFFFFFF;
+ jv type = jv_number(type_number);
+ switch (type_number) {
+ break; case 0x030000: type = jv_string("Baseline MP Primary Image");
+ break; case 0x010001: type = jv_string("Large Thumbnail - VGA");
+ break; case 0x010002: type = jv_string("Large Thumbnail - Full HD");
+ break; case 0x020001: type = jv_string("Multi-Frame Image Panorama");
+ break; case 0x020002: type = jv_string("Multi-Frame Image Disparity");
+ break; case 0x020003: type = jv_string("Multi-Frame Image Multi-Angle");
+ break; case 0x000000: type = jv_string("Undefined");
+ }
+
+ uint32_t format_number = (attrs >> 24) & 0x7;
+ jv format = jv_number(format_number);
+ if (format_number == 0)
+ format = jv_string("JPEG");
+
+ *a = jv_array_append(*a, JV_OBJECT(
+ jv_string("Individual Image Attribute"), JV_OBJECT(
+ jv_string("Dependent Parent Image"), jv_bool((attrs >> 31) & 1),
+ jv_string("Dependent Child Image"), jv_bool((attrs >> 30) & 1),
+ jv_string("Representative Image"), jv_bool((attrs >> 29) & 1),
+ jv_string("Reserved"), jv_number((attrs >> 27) & 0x3),
+ jv_string("Image Data Format"), format,
+ jv_string("MP Type Code"), type
+ ),
+ jv_string("Individual Image Size"),
+ jv_number(T->un->u32(p + 4)),
+ jv_string("Individual Image Data Offset"),
+ jv_number(offset),
+ jv_string("Dependent Image 1 Entry Number"),
+ jv_number(T->un->u16(p + 12)),
+ jv_string("Dependent Image 2 Entry Number"),
+ jv_number(T->un->u16(p + 14))
+ ));
+
+ // Don't report non-JPEGs, even though they're unlikely.
+ return format_number == 0 ? offset : 0;
+}
+
+static jv
+parse_mpf_index_entry(jv o, uint32_t **offsets, const struct tiffer *T,
+ struct tiffer_entry *entry)
+{
+ // 5.2.3.3. MP Entry
+ if (entry->tag != MPF_MPEntry || entry->type != TIFFER_UNDEFINED ||
+ entry->remaining_count % 16) {
+ return parse_exif_entry(o, T, entry, mpf_entries);
+ }
+
+ uint32_t count = entry->remaining_count / 16;
+ jv a = jv_array_sized(count);
+ uint32_t *out = *offsets = calloc(sizeof *out, count + 1);
+ for (uint32_t i = 0; i < count; i++) {
+ // 5.2.3.3.3. Individual Image Data Offset
+ uint32_t offset = parse_mpf_mpentry(&a, entry->p + i * 16, T);
+ if (offset)
+ *out++ = offset;
+ }
+ return jv_set(o, jv_string("MP Entry"), a);
+}
+
+static jv
+parse_mpf_index_ifd(uint32_t **offsets, struct tiffer *T)
+{
+ jv ifd = jv_object();
+ struct tiffer_entry entry = {};
+ while (tiffer_next_entry(T, &entry))
+ ifd = parse_mpf_index_entry(ifd, offsets, T, &entry);
+ return ifd;
+}
+
+static jv
+parse_mpf(jv o, const uint8_t ***individuals, const uint8_t *p, size_t len,
+ const uint8_t *end)
+{
+ struct tiffer T;
+ if (!tiffer_init(&T, p, len) || !tiffer_next_ifd(&T))
+ return add_warning(o, "invalid MPF segment");
+
+ // First image: IFD0 is Index IFD, any IFD1 is Attribute IFD.
+ // Other images: IFD0 is Attribute IFD, there is no Index IFD.
+ uint32_t *offsets = NULL;
+ if (!*individuals) {
+ o = add_to_subarray(o, "MPF", parse_mpf_index_ifd(&offsets, &T));
+ if (!tiffer_next_ifd(&T))
+ goto out;
+ }
+
+ // This isn't optimal, but it will do.
+ o = add_to_subarray(o, "MPF", parse_exif_ifd(&T, mpf_entries));
+
+out:
+ if (offsets) {
+ size_t count = 0;
+ for (const uint32_t *i = offsets; *i; i++)
+ count++;
+
+ free(*individuals);
+ const uint8_t **out = *individuals = calloc(sizeof *out, count + 1);
+ for (const uint32_t *i = offsets; *i; i++) {
+ if (*i > end - p)
+ o = add_warning(o, "MPF offset points past available data");
+ else
+ *out++ = p + *i;
+ }
+
+ free(offsets);
+ }
+ return o;
+}
+
+// --- JPEG --------------------------------------------------------------------
+// Because the JPEG file format is simple, just do it manually.
+// See: https://www.w3.org/Graphics/JPEG/itu-t81.pdf
+
+enum {
+ TEM = 0x01,
+ SOF0 = 0xC0, SOF1, SOF2, SOF3,
+ DHT = 0xC4,
+ SOF5, SOF6, SOF7,
+ JPG = 0xC8,
+ SOF9, SOF10, SOF11,
+ DAC = 0xCC,
+ SOF13, SOF14, SOF15,
+
+ RST0 = 0xD0, RST1, RST2, RST3, RST4, RST5, RST6, RST7,
+
+ SOI = 0xD8,
+ EOI = 0xD9,
+ SOS = 0xDA,
+ DQT = 0xDB,
+ DNL = 0xDC,
+ DRI = 0xDD,
+ DHP = 0xDE,
+ EXP = 0xDF,
+
+ APP0 = 0xE0, APP1, APP2, APP3, APP4, APP5, APP6, APP7,
+ APP8, APP9, APP10, APP11, APP12, APP13, APP14, APP15,
+
+ JPG0 = 0xF0, JPG1, JPG2, JPG3, JPG4, JPG5, JPG6, JPG7,
+ JPG8, JPG9, JPG10, JPG11, JPG12, JPG13,
+
+ COM = 0xFE
+};
+
+// The rest is "RES (Reserved)", except for 0xFF (filler) and 0x00 (invalid).
+static const char *marker_ids[0xFF] = {
+ [TEM] = "TEM",
+ [SOF0] = "SOF0", [SOF1] = "SOF1", [SOF2] = "SOF2", [SOF3] = "SOF3",
+ [DHT] = "DHT", [SOF5] = "SOF5", [SOF6] = "SOF6", [SOF7] = "SOF7",
+ [JPG] = "JPG", [SOF9] = "SOF9", [SOF10] = "SOF10", [SOF11] = "SOF11",
+ [DAC] = "DAC", [SOF13] = "SOF13", [SOF14] = "SOF14", [SOF15] = "SOF15",
+ [RST0] = "RST0", [RST1] = "RST1", [RST2] = "RST2", [RST3] = "RST3",
+ [RST4] = "RST4", [RST5] = "RST5", [RST6] = "RST6", [RST7] = "RST7",
+ [SOI] = "SOI", [EOI] = "EOI", [SOS] = "SOS", [DQT] = "DQT",
+ [DNL] = "DNL", [DRI] = "DRI", [DHP] = "DHP", [EXP] = "EXP",
+ [APP0] = "APP0", [APP1] = "APP1", [APP2] = "APP2", [APP3] = "APP3",
+ [APP4] = "APP4", [APP5] = "APP5", [APP6] = "APP6", [APP7] = "APP7",
+ [APP8] = "APP8", [APP9] = "APP9", [APP10] = "APP10", [APP11] = "APP11",
+ [APP12] = "APP12", [APP13] = "APP13", [APP14] = "APP14", [APP15] = "APP15",
+ [JPG0] = "JPG0", [JPG1] = "JPG1", [JPG2] = "JPG2", [JPG3] = "JPG3",
+ [JPG4] = "JPG4", [JPG5] = "JPG5", [JPG6] = "JPG6", [JPG7] = "JPG7",
+ [JPG8] = "JPG8", [JPG9] = "JPG9", [JPG10] = "JPG10", [JPG11] = "JPG11",
+ [JPG12] = "JPG12", [JPG13] = "JPG13", [COM] = "COM"
+};
+
+// The rest is "RES (Reserved)", except for 0xFF (filler) and 0x00 (invalid).
+static const char *marker_descriptions[0xFF] = {
+ [TEM] = "For temporary private use in arithmetic coding",
+ [SOF0] = "Baseline DCT",
+ [SOF1] = "Extended sequential DCT",
+ [SOF2] = "Progressive DCT",
+ [SOF3] = "Lossless (sequential)",
+ [DHT] = "Define Huffman table(s)",
+ [SOF5] = "Differential sequential DCT",
+ [SOF6] = "Differential progressive DCT",
+ [SOF7] = "Differential lossless (sequential)",
+ [JPG] = "Reserved for JPEG extensions",
+ [SOF9] = "Extended sequential DCT",
+ [SOF10] = "Progressive DCT",
+ [SOF11] = "Lossless (sequential)",
+ [DAC] = "Define arithmetic coding conditioning(s)",
+ [SOF13] = "Differential sequential DCT",
+ [SOF14] = "Differential progressive DCT",
+ [SOF15] = "Differential lossless (sequential)",
+ [RST0] = "Restart with module 8 count 0",
+ [RST1] = "Restart with module 8 count 1",
+ [RST2] = "Restart with module 8 count 2",
+ [RST3] = "Restart with module 8 count 3",
+ [RST4] = "Restart with module 8 count 4",
+ [RST5] = "Restart with module 8 count 5",
+ [RST6] = "Restart with module 8 count 6",
+ [RST7] = "Restart with module 8 count 7",
+ [SOI] = "Start of image",
+ [EOI] = "End of image",
+ [SOS] = "Start of scan",
+ [DQT] = "Define quantization table(s)",
+ [DNL] = "Define number of lines",
+ [DRI] = "Define restart interval",
+ [DHP] = "Define hierarchical progression",
+ [EXP] = "Expand reference component(s)",
+ [APP0] = "Reserved for application segments, 0",
+ [APP1] = "Reserved for application segments, 1",
+ [APP2] = "Reserved for application segments, 2",
+ [APP3] = "Reserved for application segments, 3",
+ [APP4] = "Reserved for application segments, 4",
+ [APP5] = "Reserved for application segments, 5",
+ [APP6] = "Reserved for application segments, 6",
+ [APP7] = "Reserved for application segments, 7",
+ [APP8] = "Reserved for application segments, 8",
+ [APP9] = "Reserved for application segments, 9",
+ [APP10] = "Reserved for application segments, 10",
+ [APP11] = "Reserved for application segments, 11",
+ [APP12] = "Reserved for application segments, 12",
+ [APP13] = "Reserved for application segments, 13",
+ [APP14] = "Reserved for application segments, 14",
+ [APP15] = "Reserved for application segments, 15",
+ [JPG0] = "Reserved for JPEG extensions, 0",
+ [JPG1] = "Reserved for JPEG extensions, 1",
+ [JPG2] = "Reserved for JPEG extensions, 2",
+ [JPG3] = "Reserved for JPEG extensions, 3",
+ [JPG4] = "Reserved for JPEG extensions, 4",
+ [JPG5] = "Reserved for JPEG extensions, 5",
+ [JPG6] = "Reserved for JPEG extensions, 6",
+ [JPG7] = "Reserved for JPEG extensions, 7",
+ [JPG8] = "Reserved for JPEG extensions, 8",
+ [JPG9] = "Reserved for JPEG extensions, 9",
+ [JPG10] = "Reserved for JPEG extensions, 10",
+ [JPG11] = "Reserved for JPEG extensions, 11",
+ [JPG12] = "Reserved for JPEG extensions, 12",
+ [JPG13] = "Reserved for JPEG extensions, 13",
+ [COM] = "Comment",
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct data {
+ bool ended;
+ uint8_t *exif, *icc, *psir;
+ size_t exif_len, icc_len, psir_len;
+ int icc_sequence, icc_done;
+ const uint8_t **mpf_individuals, **mpf_next;
+};
+
+static void
+parse_append(uint8_t **buffer, size_t *buffer_len, const uint8_t *p, size_t len)
+{
+ size_t buffer_longer = *buffer_len + len;
+ *buffer = realloc(*buffer, buffer_longer);
+ memcpy(*buffer + *buffer_len, p, len);
+ *buffer_len = buffer_longer;
+}
+
+static const uint8_t *
+parse_marker(uint8_t marker, const uint8_t *p, const uint8_t *end,
+ struct data *data, jv *o)
+{
+ // Suspected: MJPEG? Undetected format recursion, e.g., thumbnails?
+ // Found: Random metadata! Multi-Picture Format!
+ if ((data->ended = marker == EOI)) {
+ // TODO(p): Handle Exifs independently--flush the last one.
+ if ((data->mpf_next || (data->mpf_next = data->mpf_individuals)) &&
+ *data->mpf_next)
+ return *data->mpf_next++;
+ if (p != end)
+ *o = add_warning(*o, "trailing data");
+ }
+
+ // These markers stand alone, not starting a marker segment.
+ switch (marker) {
+ case RST0:
+ case RST1:
+ case RST2:
+ case RST3:
+ case RST4:
+ case RST5:
+ case RST6:
+ case RST7:
+ *o = add_warning(*o, "unexpected restart marker");
+ // Fall-through
+ case SOI:
+ case EOI:
+ case TEM:
+ return p;
+ }
+
+ uint16_t length = p[0] << 8 | p[1];
+ const uint8_t *payload = p + 2;
+ if ((p += length) > end) {
+ *o = add_error(*o, "runaway marker segment");
+ return NULL;
+ }
+
+ switch (marker) {
+ case SOF0:
+ case SOF1:
+ case SOF2:
+ case SOF3:
+ case SOF5:
+ case SOF6:
+ case SOF7:
+ case SOF9:
+ case SOF10:
+ case SOF11:
+ case SOF13:
+ case SOF14:
+ case SOF15:
+ case DHP: // B.2.2 and B.3.2.
+ // As per B.2.5, Y can be zero, then there needs to be a DNL segment.
+ *o = add_to_subarray(*o, "info", JV_OBJECT(
+ jv_string("type"), jv_string(marker_descriptions[marker]),
+ jv_string("bits"), jv_number(payload[0]),
+ jv_string("height"), jv_number(payload[1] << 8 | payload[2]),
+ jv_string("width"), jv_number(payload[3] << 8 | payload[4]),
+ jv_string("components"), jv_number(payload[5])
+ ));
+ return p;
+ }
+
+ // See B.1.1.5, we can brute-force our way through the entropy-coded data.
+ if (marker == SOS) {
+ while (p + 2 <= end && (p[0] != 0xFF || p[1] < 0xC0 || p[1] > 0xFE ||
+ (p[1] >= RST0 && p[1] <= RST7)))
+ p++;
+ return p;
+ }
+
+ // "The interpretation is left to the application."
+ if (marker == COM) {
+ int superascii = 0;
+ char *buf = calloc(3, p - payload), *bufp = buf;
+ for (const uint8_t *q = payload; q < p; q++) {
+ if (*q < 128) {
+ *bufp++ = *q;
+ } else {
+ superascii++;
+ *bufp++ = 0xC0 | (*q >> 6);
+ *bufp++ = 0x80 | (*q & 0x3F);
+ }
+ }
+ *bufp++ = 0;
+ *o = add_to_subarray(*o, "comments", jv_string(buf));
+ free(buf);
+
+ if (superascii)
+ *o = add_warning(*o, "super-ASCII comments");
+ }
+
+ // These mostly contain an ASCII string header, following JPEG FIF:
+ //
+ // "Application-specific APP0 marker segments are identified
+ // by a zero terminated string which identifies the application
+ // (not 'JFIF' or 'JFXX')."
+ if (marker >= APP0 && marker <= APP15) {
+ const uint8_t *nul = memchr(payload, 0, p - payload);
+ int unprintable = !nul;
+ if (nul) {
+ for (const uint8_t *q = payload; q < nul; q++)
+ unprintable += *q < 32 || *q >= 127;
+ }
+ *o = add_to_subarray(*o, "apps",
+ unprintable ? jv_null() : jv_string((const char *) payload));
+ }
+
+ // CIPA DC-007-2021 (Multi-Picture Format) 5.2
+ // https://www.cipa.jp/e/std/std-sec.html
+ if (marker == APP2 && p - payload >= 8 && !memcmp(payload, "MPF\0", 4)) {
+ payload += 4;
+ *o = parse_mpf(*o, &data->mpf_individuals, payload, p - payload, end);
+ }
+
+ // CIPA DC-006 (Stereo Still Image Format for Digital Cameras)
+ // TODO(p): Handle by properly skipping trailing data (use Stim offsets).
+
+ // https://www.w3.org/Graphics/JPEG/jfif3.pdf
+ if (marker == APP0 && p - payload >= 14 && !memcmp(payload, "JFIF\0", 5)) {
+ payload += 5;
+
+ jv units = jv_number(payload[2]);
+ switch (payload[2]) {
+ break; case 0: units = jv_null();
+ break; case 1: units = jv_string("DPI");
+ break; case 2: units = jv_string("dots per cm");
+ }
+
+ // The rest is picture data.
+ *o = add_to_subarray(*o, "JFIF", JV_OBJECT(
+ jv_string("version"), jv_number(payload[0] * 100 + payload[1]),
+ jv_string("units"), units,
+ jv_string("density-x"), jv_number(payload[3] << 8 | payload[4]),
+ jv_string("density-y"), jv_number(payload[5] << 8 | payload[6]),
+ jv_string("thumbnail-w"), jv_number(payload[7]),
+ jv_string("thumbnail-h"), jv_number(payload[8])
+ ));
+ }
+ if (marker == APP0 && p - payload >= 6 && !memcmp(payload, "JFXX\0", 5)) {
+ payload += 5;
+
+ jv extension = jv_number(payload[0]);
+ switch (payload[0]) {
+ break; case 0x10: extension = jv_string("JPEG thumbnail");
+ break; case 0x11: extension = jv_string("Paletted thumbnail");
+ break; case 0x13: extension = jv_string("RGB thumbnail");
+ }
+
+ // The rest is picture data.
+ *o = add_to_subarray(*o, "JFXX",
+ JV_OBJECT(jv_string("extension"), extension));
+ }
+
+ // 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
+ if (marker == APP1 && p - payload >= 6 && !memcmp(payload, "Exif\0", 5)) {
+ payload += 6;
+ if (payload[-1] != 0)
+ *o = add_warning(*o, "weirdly padded Exif header");
+ if (data->exif)
+ *o = add_warning(*o, "multiple Exif segments");
+ parse_append(&data->exif, &data->exif_len, 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) && !data->icc_done &&
+ payload[12] == ++data->icc_sequence && payload[13] >= payload[12]) {
+ payload += 14;
+ parse_append(&data->icc, &data->icc_len, payload, p - payload);
+ data->icc_done = payload[-1] == data->icc_sequence;
+ }
+
+ // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 + 3.1.3
+ // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
+ if (marker == APP13 && p - payload >= 14 &&
+ !memcmp(payload, "Photoshop 3.0\0", 14)) {
+ payload += 14;
+ parse_append(&data->psir, &data->psir_len, payload, p - payload);
+ }
+
+ // TODO(p): Extract all XMP segments.
+ return p;
+}
+
+static bool
+detect_jpeg(const uint8_t *p, size_t len)
+{
+ return len >= 2 && p[0] == 0xff && p[1] == SOI;
+}
+
+static jv
+parse_jpeg(jv o, const uint8_t *p, size_t len)
+{
+ struct data data = {};
+ const uint8_t *end = p + len;
+ jv markers = jv_array();
+ while (p) {
+ // This is an expectable condition, use a simple warning.
+ if (p + 2 > end) {
+ if (!data.ended)
+ o = add_warning(o, "unexpected EOF");
+ break;
+ }
+ if (*p++ != 0xFF || *p == 0) {
+ if (!data.ended)
+ o = add_error(o, "no marker found where one was expected");
+ break;
+ }
+
+ // Markers may be preceded by fill bytes.
+ if (*p == 0xFF) {
+ o = jv_object_set(o, jv_string("fillers"), jv_bool(true));
+ continue;
+ }
+
+ uint8_t marker = *p++;
+ markers = jv_array_append(markers,
+ jv_string(marker_ids[marker] ? marker_ids[marker] : "RES"));
+ p = parse_marker(marker, p, end, &data, &o);
+ }
+
+ if (data.exif) {
+ // TODO(p): Probably extend it until the end of the JPEG,
+ // seeing as, e.g., thumbnail data can overflow into follow-up segments.
+ o = parse_exif(o, data.exif, data.exif_len);
+ free(data.exif);
+ }
+ if (data.icc) {
+ if (data.icc_done)
+ o = parse_icc(o, data.icc, data.icc_len);
+ else
+ o = add_warning(o, "bad ICC profile sequence");
+ free(data.icc);
+ }
+ if (data.psir) {
+ o = parse_psir(o, data.psir, data.psir_len);
+ free(data.psir);
+ }
+
+ free(data.mpf_individuals);
+ return jv_set(o, jv_string("markers"), markers);
+}
diff --git a/tools/jpeginfo.c b/tools/jpeginfo.c
deleted file mode 100644
index 6a0994b..0000000
--- a/tools/jpeginfo.c
+++ /dev/null
@@ -1,610 +0,0 @@
-//
-// jpeginfo.c: acquire information about JPEG files in JSON format
-//
-// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
-//
-// 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 "info.h"
-
-#include <jv.h>
-
-#include <errno.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-// --- Multi-Picture Format ----------------------------------------------------
-
-enum {
- MPF_MPFVersion = 45056,
- MPF_NumberOfImages = 45057,
- MPF_MPEntry = 45058,
- MPF_ImageUIDList = 45059,
- MPF_TotalFrames = 45060,
-
- MPF_MPIndividualNum = 45313,
- MPF_PanOrientation = 45569,
- MPF_PanOverlap_H = 45570,
- MPF_PanOverlap_V = 45571,
- MPF_BaseViewpointNum = 45572,
- MPF_ConvergenceAngle = 45573,
- MPF_BaselineLength = 45574,
- MPF_VerticalDivergence = 45575,
- MPF_AxisDistance_X = 45576,
- MPF_AxisDistance_Y = 45577,
- MPF_AxisDistance_Z = 45578,
- MPF_YawAngle = 45579,
- MPF_PitchAngle = 45580,
- MPF_RollAngle = 45581
-};
-
-static struct tiff_entry mpf_entries[] = {
- {"MP Format Version Number", MPF_MPFVersion, NULL},
- {"Number of Images", MPF_NumberOfImages, NULL},
- {"MP Entry", MPF_MPEntry, NULL},
- {"Individual Image Unique ID List", MPF_ImageUIDList, NULL},
- {"Total Number of Captured Frames", MPF_TotalFrames, NULL},
-
- {"MP Individual Image Number", MPF_MPIndividualNum, NULL},
- {"Panorama Scanning Orientation", MPF_PanOrientation, NULL},
- {"Panorama Horizontal Overlap", MPF_PanOverlap_H, NULL},
- {"Panorama Vertical Overlap", MPF_PanOverlap_V, NULL},
- {"Base Viewpoint Number", MPF_BaseViewpointNum, NULL},
- {"Convergence Angle", MPF_ConvergenceAngle, NULL},
- {"Baseline Length", MPF_BaselineLength, NULL},
- {"Divergence Angle", MPF_VerticalDivergence, NULL},
- {"Horizontal Axis Distance", MPF_AxisDistance_X, NULL},
- {"Vertical Axis Distance", MPF_AxisDistance_Y, NULL},
- {"Collimation Axis Distance", MPF_AxisDistance_Z, NULL},
- {"Yaw Angle", MPF_YawAngle, NULL},
- {"Pitch Angle", MPF_PitchAngle, NULL},
- {"Roll Angle", MPF_RollAngle, NULL},
- {}
-};
-
-static uint32_t
-parse_mpf_mpentry(jv *a, const uint8_t *p, struct tiffer *T)
-{
- uint32_t attrs = T->un->u32(p);
- uint32_t offset = T->un->u32(p + 8);
-
- uint32_t type_number = attrs & 0xFFFFFF;
- jv type = jv_number(type_number);
- switch (type_number) {
- break; case 0x030000: type = jv_string("Baseline MP Primary Image");
- break; case 0x010001: type = jv_string("Large Thumbnail - VGA");
- break; case 0x010002: type = jv_string("Large Thumbnail - Full HD");
- break; case 0x020001: type = jv_string("Multi-Frame Image Panorama");
- break; case 0x020002: type = jv_string("Multi-Frame Image Disparity");
- break; case 0x020003: type = jv_string("Multi-Frame Image Multi-Angle");
- break; case 0x000000: type = jv_string("Undefined");
- }
-
- uint32_t format_number = (attrs >> 24) & 0x7;
- jv format = jv_number(format_number);
- if (format_number == 0)
- format = jv_string("JPEG");
-
- *a = jv_array_append(*a, JV_OBJECT(
- jv_string("Individual Image Attribute"), JV_OBJECT(
- jv_string("Dependent Parent Image"), jv_bool((attrs >> 31) & 1),
- jv_string("Dependent Child Image"), jv_bool((attrs >> 30) & 1),
- jv_string("Representative Image"), jv_bool((attrs >> 29) & 1),
- jv_string("Reserved"), jv_number((attrs >> 27) & 0x3),
- jv_string("Image Data Format"), format,
- jv_string("MP Type Code"), type
- ),
- jv_string("Individual Image Size"),
- jv_number(T->un->u32(p + 4)),
- jv_string("Individual Image Data Offset"),
- jv_number(offset),
- jv_string("Dependent Image 1 Entry Number"),
- jv_number(T->un->u16(p + 12)),
- jv_string("Dependent Image 2 Entry Number"),
- jv_number(T->un->u16(p + 14))
- ));
-
- // Don't report non-JPEGs, even though they're unlikely.
- return format_number == 0 ? offset : 0;
-}
-
-static jv
-parse_mpf_index_entry(jv o, const uint8_t ***offsets, struct tiffer *T,
- struct tiffer_entry *entry)
-{
- // 5.2.3.3. MP Entry
- if (entry->tag != MPF_MPEntry || entry->type != UNDEFINED ||
- entry->remaining_count % 16) {
- return parse_exif_entry(o, T, entry, mpf_entries);
- }
-
- uint32_t count = entry->remaining_count / 16;
- jv a = jv_array_sized(count);
- const uint8_t **out = *offsets = calloc(sizeof *out, count + 1);
- for (uint32_t i = 0; i < count; i++) {
- uint32_t offset = parse_mpf_mpentry(&a, entry->p + i * 16, T);
- if (offset)
- *out++ = T->begin + offset;
- }
- return jv_set(o, jv_string("MP Entry"), a);
-}
-
-static jv
-parse_mpf_index_ifd(const uint8_t ***offsets, struct tiffer *T)
-{
- jv ifd = jv_object();
- struct tiffer_entry entry = {};
- while (tiffer_next_entry(T, &entry))
- ifd = parse_mpf_index_entry(ifd, offsets, T, &entry);
- return ifd;
-}
-
-static jv
-parse_mpf(jv o, const uint8_t ***offsets, const uint8_t *p, size_t len)
-{
- struct tiffer T;
- if (!tiffer_init(&T, p, len) || !tiffer_next_ifd(&T))
- return add_warning(o, "invalid MPF segment");
-
- // First image: IFD0 is Index IFD, any IFD1 is Attribute IFD.
- // Other images: IFD0 is Attribute IFD, there is no Index IFD.
- if (!*offsets) {
- o = add_to_subarray(o, "MPF", parse_mpf_index_ifd(offsets, &T));
- if (!tiffer_next_ifd(&T))
- return o;
- }
-
- // This isn't optimal, but it will do.
- return add_to_subarray(o, "MPF", parse_exif_ifd(&T, mpf_entries));
-}
-
-// --- JPEG --------------------------------------------------------------------
-// Because the JPEG file format is simple, just do it manually.
-// See: https://www.w3.org/Graphics/JPEG/itu-t81.pdf
-
-enum {
- TEM = 0x01,
- SOF0 = 0xC0, SOF1, SOF2, SOF3,
- DHT = 0xC4,
- SOF5, SOF6, SOF7,
- JPG = 0xC8,
- SOF9, SOF10, SOF11,
- DAC = 0xCC,
- SOF13, SOF14, SOF15,
-
- RST0 = 0xD0, RST1, RST2, RST3, RST4, RST5, RST6, RST7,
-
- SOI = 0xD8,
- EOI = 0xD9,
- SOS = 0xDA,
- DQT = 0xDB,
- DNL = 0xDC,
- DRI = 0xDD,
- DHP = 0xDE,
- EXP = 0xDF,
-
- APP0 = 0xE0, APP1, APP2, APP3, APP4, APP5, APP6, APP7,
- APP8, APP9, APP10, APP11, APP12, APP13, APP14, APP15,
-
- JPG0 = 0xF0, JPG1, JPG2, JPG3, JPG4, JPG5, JPG6, JPG7,
- JPG8, JPG9, JPG10, JPG11, JPG12, JPG13,
-
- COM = 0xFE
-};
-
-// The rest is "RES (Reserved)", except for 0xFF (filler) and 0x00 (invalid).
-static const char *marker_ids[0xFF] = {
- [TEM] = "TEM",
- [SOF0] = "SOF0", [SOF1] = "SOF1", [SOF2] = "SOF2", [SOF3] = "SOF3",
- [DHT] = "DHT", [SOF5] = "SOF5", [SOF6] = "SOF6", [SOF7] = "SOF7",
- [JPG] = "JPG", [SOF9] = "SOF9", [SOF10] = "SOF10", [SOF11] = "SOF11",
- [DAC] = "DAC", [SOF13] = "SOF13", [SOF14] = "SOF14", [SOF15] = "SOF15",
- [RST0] = "RST0", [RST1] = "RST1", [RST2] = "RST2", [RST3] = "RST3",
- [RST4] = "RST4", [RST5] = "RST5", [RST6] = "RST6", [RST7] = "RST7",
- [SOI] = "SOI", [EOI] = "EOI", [SOS] = "SOS", [DQT] = "DQT",
- [DNL] = "DNL", [DRI] = "DRI", [DHP] = "DHP", [EXP] = "EXP",
- [APP0] = "APP0", [APP1] = "APP1", [APP2] = "APP2", [APP3] = "APP3",
- [APP4] = "APP4", [APP5] = "APP5", [APP6] = "APP6", [APP7] = "APP7",
- [APP8] = "APP8", [APP9] = "APP9", [APP10] = "APP10", [APP11] = "APP11",
- [APP12] = "APP12", [APP13] = "APP13", [APP14] = "APP14", [APP15] = "APP15",
- [JPG0] = "JPG0", [JPG1] = "JPG1", [JPG2] = "JPG2", [JPG3] = "JPG3",
- [JPG4] = "JPG4", [JPG5] = "JPG5", [JPG6] = "JPG6", [JPG7] = "JPG7",
- [JPG8] = "JPG8", [JPG9] = "JPG9", [JPG10] = "JPG10", [JPG11] = "JPG11",
- [JPG12] = "JPG12", [JPG13] = "JPG13", [COM] = "COM"
-};
-
-// The rest is "RES (Reserved)", except for 0xFF (filler) and 0x00 (invalid).
-static const char *marker_descriptions[0xFF] = {
- [TEM] = "For temporary private use in arithmetic coding",
- [SOF0] = "Baseline DCT",
- [SOF1] = "Extended sequential DCT",
- [SOF2] = "Progressive DCT",
- [SOF3] = "Lossless (sequential)",
- [DHT] = "Define Huffman table(s)",
- [SOF5] = "Differential sequential DCT",
- [SOF6] = "Differential progressive DCT",
- [SOF7] = "Differential lossless (sequential)",
- [JPG] = "Reserved for JPEG extensions",
- [SOF9] = "Extended sequential DCT",
- [SOF10] = "Progressive DCT",
- [SOF11] = "Lossless (sequential)",
- [DAC] = "Define arithmetic coding conditioning(s)",
- [SOF13] = "Differential sequential DCT",
- [SOF14] = "Differential progressive DCT",
- [SOF15] = "Differential lossless (sequential)",
- [RST0] = "Restart with module 8 count 0",
- [RST1] = "Restart with module 8 count 1",
- [RST2] = "Restart with module 8 count 2",
- [RST3] = "Restart with module 8 count 3",
- [RST4] = "Restart with module 8 count 4",
- [RST5] = "Restart with module 8 count 5",
- [RST6] = "Restart with module 8 count 6",
- [RST7] = "Restart with module 8 count 7",
- [SOI] = "Start of image",
- [EOI] = "End of image",
- [SOS] = "Start of scan",
- [DQT] = "Define quantization table(s)",
- [DNL] = "Define number of lines",
- [DRI] = "Define restart interval",
- [DHP] = "Define hierarchical progression",
- [EXP] = "Expand reference component(s)",
- [APP0] = "Reserved for application segments, 0",
- [APP1] = "Reserved for application segments, 1",
- [APP2] = "Reserved for application segments, 2",
- [APP3] = "Reserved for application segments, 3",
- [APP4] = "Reserved for application segments, 4",
- [APP5] = "Reserved for application segments, 5",
- [APP6] = "Reserved for application segments, 6",
- [APP7] = "Reserved for application segments, 7",
- [APP8] = "Reserved for application segments, 8",
- [APP9] = "Reserved for application segments, 9",
- [APP10] = "Reserved for application segments, 10",
- [APP11] = "Reserved for application segments, 11",
- [APP12] = "Reserved for application segments, 12",
- [APP13] = "Reserved for application segments, 13",
- [APP14] = "Reserved for application segments, 14",
- [APP15] = "Reserved for application segments, 15",
- [JPG0] = "Reserved for JPEG extensions, 0",
- [JPG1] = "Reserved for JPEG extensions, 1",
- [JPG2] = "Reserved for JPEG extensions, 2",
- [JPG3] = "Reserved for JPEG extensions, 3",
- [JPG4] = "Reserved for JPEG extensions, 4",
- [JPG5] = "Reserved for JPEG extensions, 5",
- [JPG6] = "Reserved for JPEG extensions, 6",
- [JPG7] = "Reserved for JPEG extensions, 7",
- [JPG8] = "Reserved for JPEG extensions, 8",
- [JPG9] = "Reserved for JPEG extensions, 9",
- [JPG10] = "Reserved for JPEG extensions, 10",
- [JPG11] = "Reserved for JPEG extensions, 11",
- [JPG12] = "Reserved for JPEG extensions, 12",
- [JPG13] = "Reserved for JPEG extensions, 13",
- [COM] = "Comment",
-};
-
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-struct data {
- bool ended;
- uint8_t *exif, *icc, *psir;
- size_t exif_len, icc_len, psir_len;
- int icc_sequence, icc_done;
- const uint8_t **mpf_offsets, **mpf_next;
-};
-
-static void
-parse_append(uint8_t **buffer, size_t *buffer_len, const uint8_t *p, size_t len)
-{
- size_t buffer_longer = *buffer_len + len;
- *buffer = realloc(*buffer, buffer_longer);
- memcpy(*buffer + *buffer_len, p, len);
- *buffer_len = buffer_longer;
-}
-
-static const uint8_t *
-parse_marker(uint8_t marker, const uint8_t *p, const uint8_t *end,
- struct data *data, jv *o)
-{
- // Suspected: MJPEG? Undetected format recursion, e.g., thumbnails?
- // Found: Random metadata! Multi-Picture Format!
- if ((data->ended = marker == EOI)) {
- // TODO(p): Handle Exifs independently--flush the last one.
- if ((data->mpf_next || (data->mpf_next = data->mpf_offsets)) &&
- *data->mpf_next)
- return *data->mpf_next++;
- if (p != end)
- *o = add_warning(*o, "trailing data");
- }
-
- // These markers stand alone, not starting a marker segment.
- switch (marker) {
- case RST0:
- case RST1:
- case RST2:
- case RST3:
- case RST4:
- case RST5:
- case RST6:
- case RST7:
- *o = add_warning(*o, "unexpected restart marker");
- // Fall-through
- case SOI:
- case EOI:
- case TEM:
- return p;
- }
-
- uint16_t length = p[0] << 8 | p[1];
- const uint8_t *payload = p + 2;
- if ((p += length) > end) {
- *o = add_error(*o, "runaway marker segment");
- return NULL;
- }
-
- switch (marker) {
- case SOF0:
- case SOF1:
- case SOF2:
- case SOF3:
- case SOF5:
- case SOF6:
- case SOF7:
- case SOF9:
- case SOF10:
- case SOF11:
- case SOF13:
- case SOF14:
- case SOF15:
- case DHP: // B.2.2 and B.3.2.
- // As per B.2.5, Y can be zero, then there needs to be a DNL segment.
- *o = add_to_subarray(*o, "info", JV_OBJECT(
- jv_string("type"), jv_string(marker_descriptions[marker]),
- jv_string("bits"), jv_number(payload[0]),
- jv_string("height"), jv_number(payload[1] << 8 | payload[2]),
- jv_string("width"), jv_number(payload[3] << 8 | payload[4]),
- jv_string("components"), jv_number(payload[5])
- ));
- return p;
- }
-
- // See B.1.1.5, we can brute-force our way through the entropy-coded data.
- if (marker == SOS) {
- while (p + 2 <= end && (p[0] != 0xFF || p[1] < 0xC0 || p[1] > 0xFE ||
- (p[1] >= RST0 && p[1] <= RST7)))
- p++;
- return p;
- }
-
- // "The interpretation is left to the application."
- if (marker == COM) {
- int superascii = 0;
- char *buf = calloc(3, p - payload), *bufp = buf;
- for (const uint8_t *q = payload; q < p; q++) {
- if (*q < 128) {
- *bufp++ = *q;
- } else {
- superascii++;
- *bufp++ = 0xC0 | (*q >> 6);
- *bufp++ = 0x80 | (*q & 0x3F);
- }
- }
- *bufp++ = 0;
- *o = add_to_subarray(*o, "comments", jv_string(buf));
- free(buf);
-
- if (superascii)
- *o = add_warning(*o, "super-ASCII comments");
- }
-
- // These mostly contain an ASCII string header, following JPEG FIF:
- //
- // "Application-specific APP0 marker segments are identified
- // by a zero terminated string which identifies the application
- // (not 'JFIF' or 'JFXX')."
- if (marker >= APP0 && marker <= APP15) {
- const uint8_t *nul = memchr(payload, 0, p - payload);
- int unprintable = !nul;
- if (nul) {
- for (const uint8_t *q = payload; q < nul; q++)
- unprintable += *q < 32 || *q >= 127;
- }
- *o = add_to_subarray(*o, "apps",
- unprintable ? jv_null() : jv_string((const char *) payload));
- }
-
- // CIPA DC-007 (Multi-Picture Format) 5.2
- // http://fileformats.archiveteam.org/wiki/Multi-Picture_Format
- if (marker == APP2 && p - payload >= 8 && !memcmp(payload, "MPF\0", 4)) {
- payload += 4;
- *o = parse_mpf(*o, &data->mpf_offsets, payload, p - payload);
- }
-
- // CIPA DC-006 (Stereo Still Image Format for Digital Cameras)
- // TODO(p): Handle by properly skipping trailing data (use Stim offsets).
-
- // https://www.w3.org/Graphics/JPEG/jfif3.pdf
- if (marker == APP0 && p - payload >= 14 && !memcmp(payload, "JFIF\0", 5)) {
- payload += 5;
-
- jv units = jv_number(payload[2]);
- switch (payload[2]) {
- break; case 0: units = jv_null();
- break; case 1: units = jv_string("DPI");
- break; case 2: units = jv_string("dots per cm");
- }
-
- // The rest is picture data.
- *o = add_to_subarray(*o, "JFIF", JV_OBJECT(
- jv_string("version"), jv_number(payload[0] * 100 + payload[1]),
- jv_string("units"), units,
- jv_string("density-x"), jv_number(payload[3] << 8 | payload[4]),
- jv_string("density-y"), jv_number(payload[5] << 8 | payload[6]),
- jv_string("thumbnail-w"), jv_number(payload[7]),
- jv_string("thumbnail-h"), jv_number(payload[8])
- ));
- }
- if (marker == APP0 && p - payload >= 6 && !memcmp(payload, "JFXX\0", 5)) {
- payload += 5;
-
- jv extension = jv_number(payload[0]);
- switch (payload[0]) {
- break; case 0x10: extension = jv_string("JPEG thumbnail");
- break; case 0x11: extension = jv_string("Paletted thumbnail");
- break; case 0x13: extension = jv_string("RGB thumbnail");
- }
-
- // The rest is picture data.
- *o = add_to_subarray(*o, "JFXX",
- JV_OBJECT(jv_string("extension"), extension));
- }
-
- // 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
- if (marker == APP1 && p - payload >= 6 && !memcmp(payload, "Exif\0", 5)) {
- payload += 6;
- if (payload[-1] != 0)
- *o = add_warning(*o, "weirdly padded Exif header");
- if (data->exif)
- *o = add_warning(*o, "multiple Exif segments");
- parse_append(&data->exif, &data->exif_len, 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) && !data->icc_done &&
- payload[12] == ++data->icc_sequence && payload[13] >= payload[12]) {
- payload += 14;
- parse_append(&data->icc, &data->icc_len, payload, p - payload);
- data->icc_done = payload[-1] == data->icc_sequence;
- }
-
- // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 + 3.1.3
- // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
- if (marker == APP13 && p - payload >= 14 &&
- !memcmp(payload, "Photoshop 3.0\0", 14)) {
- payload += 14;
- parse_append(&data->psir, &data->psir_len, payload, p - payload);
- }
-
- // TODO(p): Extract all XMP segments.
- return p;
-}
-
-static jv
-parse_jpeg(jv o, const uint8_t *p, size_t len)
-{
- struct data data = {};
- const uint8_t *end = p + len;
- jv markers = jv_array();
- while (p) {
- // This is an expectable condition, use a simple warning.
- if (p + 2 > end) {
- if (!data.ended)
- o = add_warning(o, "unexpected EOF");
- break;
- }
- if (*p++ != 0xFF || *p == 0) {
- if (!data.ended)
- o = add_error(o, "no marker found where one was expected");
- break;
- }
-
- // Markers may be preceded by fill bytes.
- if (*p == 0xFF) {
- o = jv_object_set(o, jv_string("fillers"), jv_bool(true));
- continue;
- }
-
- uint8_t marker = *p++;
- markers = jv_array_append(markers,
- jv_string(marker_ids[marker] ? marker_ids[marker] : "RES"));
- p = parse_marker(marker, p, end, &data, &o);
- }
-
- if (data.exif) {
- o = parse_exif(o, data.exif, data.exif_len);
- free(data.exif);
- }
- if (data.icc) {
- if (data.icc_done)
- o = parse_icc(o, data.icc, data.icc_len);
- else
- o = add_warning(o, "bad ICC profile sequence");
- free(data.icc);
- }
- if (data.psir) {
- o = parse_psir(o, data.psir, data.psir_len);
- free(data.psir);
- }
-
- free(data.mpf_offsets);
- return jv_set(o, jv_string("markers"), markers);
-}
-
-// --- I/O ---------------------------------------------------------------------
-
-static jv
-do_file(const char *filename, jv o)
-{
- const char *err = NULL;
- FILE *fp = fopen(filename, "rb");
- if (!fp) {
- err = strerror(errno);
- goto error;
- }
-
- uint8_t *data = NULL, buf[256 << 10];
- size_t n, len = 0;
- while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
- data = realloc(data, len + n);
- memcpy(data + len, buf, n);
- len += n;
- }
- if (ferror(fp)) {
- err = strerror(errno);
- goto error_read;
- }
-
-#if 0
- // Not sure if I want to ensure their existence...
- o = jv_object_set(o, jv_string("info"), jv_array());
- o = jv_object_set(o, jv_string("warnings"), jv_array());
-#endif
-
- o = parse_jpeg(o, data, len);
-error_read:
- fclose(fp);
- free(data);
-error:
- if (err)
- o = add_error(o, err);
- return o;
-}
-
-int
-main(int argc, char *argv[])
-{
- // XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
- // Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo
- for (int i = 1; i < argc; i++) {
- const char *filename = argv[i];
-
- jv o = jv_object();
- o = jv_object_set(o, jv_string("filename"), jv_string(filename));
- o = do_file(filename, o);
- jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */);
- fputc('\n', stdout);
- }
- return 0;
-}
diff --git a/tools/rawinfo.c b/tools/rawinfo.c
new file mode 100644
index 0000000..6409d33
--- /dev/null
+++ b/tools/rawinfo.c
@@ -0,0 +1,175 @@
+//
+// rawinfo.c: acquire information about raw image files in JSON format
+//
+// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
+//
+// 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 "info.h"
+
+#include <jv.h>
+#include <libraw.h>
+
+#if LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
+#error LibRaw 0.21.0 or newer is required.
+#endif
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+// --- Raw image files ---------------------------------------------------------
+// This is in principle similar to LibRaw's `raw-identify -v`,
+// but the output is machine-processable.
+
+static jv
+parse_raw(jv o, const uint8_t *p, size_t len)
+{
+ libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
+ if (!iprc)
+ return add_error(o, "failed to obtain a LibRaw handle");
+
+ int err = 0;
+ if ((err = libraw_open_buffer(iprc, p, len))) {
+ libraw_close(iprc);
+ return add_error(o, libraw_strerror(err));
+ }
+
+ // -> iprc->rawparams.shot_select
+ o = jv_set(o, jv_string("count"), jv_number(iprc->idata.raw_count));
+
+ o = jv_set(o, jv_string("width"), jv_number(iprc->sizes.width));
+ o = jv_set(o, jv_string("height"), jv_number(iprc->sizes.height));
+ o = jv_set(o, jv_string("flip"), jv_number(iprc->sizes.flip));
+ o = jv_set(o, jv_string("pixel_aspect_ratio"),
+ jv_number(iprc->sizes.pixel_aspect));
+
+ if ((err = libraw_adjust_sizes_info_only(iprc))) {
+ o = add_warning(o, libraw_strerror(err));
+ } else {
+ o = jv_set(
+ o, jv_string("output_width"), jv_number(iprc->sizes.iwidth));
+ o = jv_set(
+ o, jv_string("output_height"), jv_number(iprc->sizes.iheight));
+ }
+
+ jv thumbnails = jv_array();
+ for (int i = 0; i < iprc->thumbs_list.thumbcount; i++) {
+ libraw_thumbnail_item_t *item = iprc->thumbs_list.thumblist + i;
+
+ const char *format = "?";
+ switch (item->tformat) {
+ case LIBRAW_INTERNAL_THUMBNAIL_UNKNOWN:
+ format = "unknown";
+ break;
+ case LIBRAW_INTERNAL_THUMBNAIL_KODAK_THUMB:
+ format = "Kodak thumbnail";
+ break;
+ case LIBRAW_INTERNAL_THUMBNAIL_KODAK_YCBCR:
+ format = "Kodak YCbCr";
+ break;
+ case LIBRAW_INTERNAL_THUMBNAIL_KODAK_RGB:
+ format = "Kodak RGB";
+ break;
+ case LIBRAW_INTERNAL_THUMBNAIL_JPEG:
+ format = "JPEG";
+ break;
+ case LIBRAW_INTERNAL_THUMBNAIL_LAYER:
+ format = "layer";
+ break;
+ case LIBRAW_INTERNAL_THUMBNAIL_ROLLEI:
+ format = "Rollei";
+ break;
+ case LIBRAW_INTERNAL_THUMBNAIL_PPM:
+ format = "PPM";
+ break;
+ case LIBRAW_INTERNAL_THUMBNAIL_PPM16:
+ format = "PPM16";
+ break;
+ case LIBRAW_INTERNAL_THUMBNAIL_X3F:
+ format = "X3F";
+ break;
+ }
+
+ jv to = JV_OBJECT(
+ jv_string("width"), jv_number(item->twidth),
+ jv_string("height"), jv_number(item->theight),
+ jv_string("flip"), jv_number(item->tflip),
+ jv_string("format"), jv_string(format));
+
+ if (item->tformat == LIBRAW_INTERNAL_THUMBNAIL_JPEG &&
+ item->toffset > 0 &&
+ (size_t) item->toffset + item->tlength <= len) {
+ to = jv_set(to, jv_string("JPEG"),
+ parse_jpeg(jv_object(), p + item->toffset, item->tlength));
+ }
+
+ thumbnails = jv_array_append(thumbnails, to);
+ }
+
+ libraw_close(iprc);
+ return jv_set(o, jv_string("thumbnails"), thumbnails);
+}
+
+// --- I/O ---------------------------------------------------------------------
+
+static jv
+do_file(const char *filename, jv o)
+{
+ const char *err = NULL;
+ FILE *fp = fopen(filename, "rb");
+ if (!fp) {
+ err = strerror(errno);
+ goto error;
+ }
+
+ uint8_t *data = NULL, buf[256 << 10];
+ size_t n, len = 0;
+ while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
+ data = realloc(data, len + n);
+ memcpy(data + len, buf, n);
+ len += n;
+ }
+ if (ferror(fp)) {
+ err = strerror(errno);
+ goto error_read;
+ }
+
+ o = parse_raw(o, data, len);
+
+error_read:
+ fclose(fp);
+ free(data);
+error:
+ if (err)
+ o = add_error(o, err);
+ return o;
+}
+
+int
+main(int argc, char *argv[])
+{
+ // XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
+ // Usage: find . -print0 | xargs -0 ./rawinfo
+ for (int i = 1; i < argc; i++) {
+ const char *filename = argv[i];
+
+ jv o = jv_object();
+ o = jv_object_set(o, jv_string("filename"), jv_string(filename));
+ o = do_file(filename, o);
+ jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */);
+ fputc('\n', stdout);
+ }
+ return 0;
+}
diff --git a/tools/tiffinfo.c b/tools/tiffinfo.c
deleted file mode 100644
index da629c6..0000000
--- a/tools/tiffinfo.c
+++ /dev/null
@@ -1,79 +0,0 @@
-//
-// tiffinfo.c: acquire information about TIFF files in JSON format
-//
-// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
-//
-// 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 "info.h"
-
-#include <jv.h>
-
-#include <errno.h>
-#include <stdlib.h>
-#include <string.h>
-#include <stdbool.h>
-
-// This is essentially the same as jpeginfo.c, but we only have an Exif segment.
-// TODO(p): Photoshop data and ICC profiles also have their tag,
-// they're not currently processed.
-
-static jv
-do_file(const char *filename, jv o)
-{
- const char *err = NULL;
- FILE *fp = fopen(filename, "rb");
- if (!fp) {
- err = strerror(errno);
- goto error;
- }
-
- uint8_t *data = NULL, buf[256 << 10];
- size_t n, len = 0;
- while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
- data = realloc(data, len + n);
- memcpy(data + len, buf, n);
- len += n;
- }
- if (ferror(fp)) {
- err = strerror(errno);
- goto error_read;
- }
-
- o = parse_exif(o, data, len);
-
-error_read:
- fclose(fp);
- free(data);
-error:
- if (err)
- o = add_error(o, err);
- return o;
-}
-
-int
-main(int argc, char *argv[])
-{
- // XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
- // Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo
- for (int i = 1; i < argc; i++) {
- const char *filename = argv[i];
-
- jv o = jv_object();
- o = jv_object_set(o, jv_string("filename"), jv_string(filename));
- o = do_file(filename, o);
- jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */);
- fputc('\n', stdout);
- }
- return 0;
-}
diff --git a/tools/webpinfo.c b/tools/webpinfo.c
deleted file mode 100644
index f3417f9..0000000
--- a/tools/webpinfo.c
+++ /dev/null
@@ -1,133 +0,0 @@
-//
-// webpinfo.c: acquire information about WebP files in JSON format
-//
-// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
-//
-// 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 "info.h"
-
-#include <jv.h>
-
-#include <errno.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-// --- WebP --------------------------------------------------------------------
-// https://github.com/webmproject/libwebp/blob/master/doc/webp-container-spec.txt
-// https://github.com/webmproject/libwebp/blob/master/doc/webp-lossless-bitstream-spec.txt
-// https://datatracker.ietf.org/doc/html/rfc6386
-//
-// Pretty versions, hopefully not outdated:
-// https://developers.google.com/speed/webp/docs/riff_container
-// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
-
-static jv
-parse_webp(jv o, const uint8_t *p, size_t len)
-{
- // libwebp won't let us simply iterate over all chunks, so handroll it.
- if (len < 12 || memcmp(p, "RIFF", 4) || memcmp(p + 8, "WEBP", 4))
- return add_error(o, "not a WEBP file");
-
- // TODO(p): This can still be parseable.
- // TODO(p): Warn on trailing data.
- uint32_t size = u32le(p + 4);
- if (8 + size < len)
- return add_error(o, "truncated file");
-
- const uint8_t *end = p + 8 + size;
- p += 12;
-
- jv chunks = jv_array();
- while (p < end) {
- if (end - p < 8) {
- o = add_warning(o, "framing mismatch");
- printf("%ld", end - p);
- break;
- }
-
- uint32_t chunk_size = u32le(p + 4);
- uint32_t chunk_advance = (chunk_size + 1) & ~1;
- if (p + 8 + chunk_advance > end) {
- o = add_warning(o, "runaway chunk payload");
- break;
- }
-
- char fourcc[5] = "";
- memcpy(fourcc, p, 4);
- chunks = jv_array_append(chunks, jv_string(fourcc));
- p += 8;
-
- // TODO(p): Decode VP8 and VP8L chunk metadata.
- if (!strcmp(fourcc, "EXIF"))
- o = parse_exif(o, p, chunk_size);
- if (!strcmp(fourcc, "ICCP"))
- o = parse_icc(o, p, chunk_size);
- p += chunk_advance;
- }
- return jv_set(o, jv_string("chunks"), chunks);
-}
-
-// --- I/O ---------------------------------------------------------------------
-
-static jv
-do_file(const char *filename, jv o)
-{
- const char *err = NULL;
- FILE *fp = fopen(filename, "rb");
- if (!fp) {
- err = strerror(errno);
- goto error;
- }
-
- uint8_t *data = NULL, buf[256 << 10];
- size_t n, len = 0;
- while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
- data = realloc(data, len + n);
- memcpy(data + len, buf, n);
- len += n;
- }
- if (ferror(fp)) {
- err = strerror(errno);
- goto error_read;
- }
-
- o = parse_webp(o, data, len);
-error_read:
- fclose(fp);
- free(data);
-error:
- if (err)
- o = add_error(o, err);
- return o;
-}
-
-int
-main(int argc, char *argv[])
-{
- (void) parse_psir;
-
- // XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
- // Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo
- for (int i = 1; i < argc; i++) {
- const char *filename = argv[i];
-
- jv o = jv_object();
- o = jv_object_set(o, jv_string("filename"), jv_string(filename));
- o = do_file(filename, o);
- jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */);
- fputc('\n', stdout);
- }
- return 0;
-}
diff --git a/wuffs-mirror-release-c b/wuffs-mirror-release-c
deleted file mode 160000
-Subproject 123a5c6ede3c052aaf9bbef59afb9410baa2b40
diff --git a/xdg.c b/xdg.c
index aeb8073..65ba7ab 100644
--- a/xdg.c
+++ b/xdg.c
@@ -17,6 +17,9 @@
#include <glib.h>
+#include <stdlib.h>
+#include <string.h>
+
/// Add `element` to the `output` set. `relation` is a map of sets of strings
/// defining is-a relations, and is traversed recursively.
static void