From f24885db545d260c816329ae69510eca2e9ec41d Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch
Date: Sun, 26 Sep 2021 07:37:13 +0200
Subject: Add a thumbnail loader
So far not used, we'll need another widget to make use of it.
---
README.adoc | 2 +-
fastiv-io.c | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
fastiv.c | 2 +-
meson.build | 1 +
4 files changed, 191 insertions(+), 2 deletions(-)
diff --git a/README.adoc b/README.adoc
index 851df6d..1ac5e02 100644
--- a/README.adoc
+++ b/README.adoc
@@ -46,7 +46,7 @@ a package with the latest development version from Archlinux's AUR.
Building and Running
--------------------
Build dependencies: Meson, pkg-config +
-Runtime dependencies: gtk+-3.0, libturbojpeg, LibRaw (optional),
+Runtime dependencies: gtk+-3.0, libpng > 1.5.4, libturbojpeg, LibRaw (optional),
shared-mime-info
$ git clone --recursive https://git.janouch.name/p/fastiv.git
diff --git a/fastiv-io.c b/fastiv-io.c
index d4d3e9a..28e2c4e 100644
--- a/fastiv-io.c
+++ b/fastiv-io.c
@@ -425,3 +425,191 @@ fastiv_io_open(const gchar *path, GError **error)
free(data);
return surface;
}
+
+// --- Thumbnails --------------------------------------------------------------
+
+// NOTE: "It is important to note that when an image with an alpha channel is
+// scaled, linear encoded, pre-multiplied component values must be used!"
+//
+// We can use the pixman library to scale, PIXMAN_a8r8g8b8_sRGB.
+#include
+#include
+
+// TODO(p): Reorganize the sources.
+gchar *get_xdg_home_dir(const char *var, const char *default_);
+
+static void
+redirect_png_error(png_structp pngp, const char *error)
+{
+ set_error(png_get_error_ptr(pngp), error);
+ png_longjmp(pngp, 1);
+}
+
+static void
+discard_png_warning(png_structp pngp, const char *warning)
+{
+ (void) pngp;
+ (void) warning;
+}
+
+static int
+check_png_thumbnail(png_structp pngp, png_infop infop, const gchar *target,
+ time_t mtime)
+{
+ // May contain Thumb::Image::Width Thumb::Image::Height,
+ // but those aren't interesting currently (would be for fast previews).
+ int texts_len = 0;
+ png_textp texts = NULL;
+ png_get_text(pngp, infop, &texts, &texts_len);
+
+ gboolean need_uri = TRUE, need_mtime = TRUE;
+ for (int i = 0; i < texts_len; i++) {
+ png_textp text = texts + i;
+ if (!strcmp(text->key, "Thumb::URI")) {
+ need_uri = FALSE;
+ if (strcmp(target, text->text))
+ return FALSE;
+ }
+ if (!strcmp(text->key, "Thumb::MTime")) {
+ need_mtime = FALSE;
+ if (atol(text->text) != mtime)
+ return FALSE;
+ }
+ }
+ return need_uri || need_mtime ? -1 : TRUE;
+}
+
+// TODO(p): Support spng as well (it can't premultiply alpha by itself,
+// but at least it won't gamma-adjust it for us).
+static cairo_surface_t *
+read_png_thumbnail(const gchar *filename, const gchar *target, time_t mtime,
+ GError **error)
+{
+ FILE *fp;
+ if (!(fp = fopen(filename, "rb"))) {
+ set_error(error, g_strerror(errno));
+ return NULL;
+ }
+
+ cairo_surface_t *volatile surface = NULL;
+ png_structp pngp = png_create_read_struct(PNG_LIBPNG_VER_STRING,
+ error, redirect_png_error, discard_png_warning);
+ png_infop infop = png_create_info_struct(pngp);
+ if (!infop) {
+ set_error(error, g_strerror(errno));
+ goto fail_preread;
+ }
+
+ volatile png_bytepp row_pointers = NULL;
+ if (setjmp(png_jmpbuf(pngp))) {
+ if (surface) {
+ cairo_surface_destroy(surface);
+ surface = NULL;
+ }
+ goto fail;
+ }
+
+ png_init_io(pngp, fp);
+
+ // XXX: libpng will premultiply with the alpha, but it also gamma-adjust it.
+ png_set_alpha_mode(pngp, PNG_ALPHA_BROKEN, PNG_DEFAULT_sRGB);
+ png_read_info(pngp, infop);
+ if (check_png_thumbnail(pngp, infop, target, mtime) == FALSE)
+ png_error(pngp, "mismatch");
+
+ // Asking for at least 8-bit channels. This call is a superset of:
+ // - png_set_palette_to_rgb(),
+ // - png_set_tRNS_to_alpha(),
+ // - png_set_expand_gray_1_2_4_to_8().
+ png_set_expand(pngp);
+
+ // Reduce the possibilities further to RGB or RGBA...
+ png_set_gray_to_rgb(pngp);
+
+ // ...and /exactly/ 8-bit channels.
+ // Alternatively, use png_set_expand_16() above to obtain 16-bit channels.
+ png_set_scale_16(pngp);
+
+ // PNG uses RGBA order, we want either ARGB (BE) or BGRA (LE).
+ if (G_BYTE_ORDER == G_LITTLE_ENDIAN) {
+ png_set_bgr(pngp);
+ png_set_add_alpha(pngp, 0xFFFF, PNG_FILLER_AFTER);
+ png_set_swap(pngp);
+ } else {
+ // This doesn't change a row's `color_type` in png_do_read_filler(),
+ // and the following transformation thus ignores it.
+ png_set_add_alpha(pngp, 0xFFFF, PNG_FILLER_BEFORE);
+ png_set_swap_alpha(pngp);
+ }
+
+ (void) png_set_interlace_handling(pngp);
+ png_read_update_info(pngp, infop);
+
+ png_uint_32 w = png_get_image_width(pngp, infop);
+ png_uint_32 h = png_get_image_height(pngp, infop);
+ if (w > INT16_MAX || h > INT16_MAX)
+ png_error(pngp, "the image is too large");
+
+ 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)
+ png_error(pngp, cairo_status_to_string(surface_status));
+
+ size_t row_bytes = png_get_rowbytes(pngp, infop);
+ g_assert((size_t) cairo_image_surface_get_stride(surface) == row_bytes);
+
+ unsigned char *buffer = cairo_image_surface_get_data(surface);
+ png_uint_32 height = png_get_image_height(pngp, infop);
+ if (!(row_pointers = calloc(height, sizeof *row_pointers)))
+ png_error(pngp, g_strerror(errno));
+ for (size_t y = 0; y < height; y++)
+ row_pointers[y] = buffer + y * row_bytes;
+
+ cairo_surface_flush(surface);
+ png_read_image(pngp, row_pointers);
+ cairo_surface_mark_dirty(surface);
+
+ // The specification does not say where the required metadata should be,
+ // it could very well be broken up into two parts.
+ png_read_end(pngp, infop);
+ if (check_png_thumbnail(pngp, infop, target, mtime) != TRUE)
+ png_error(pngp, "mismatch or not a thumbnail");
+
+fail:
+ free(row_pointers);
+fail_preread:
+ png_destroy_read_struct(&pngp, &infop, NULL);
+ fclose(fp);
+ return surface;
+}
+
+cairo_surface_t *
+fastiv_io_lookup_thumbnail(const gchar *target)
+{
+ GStatBuf st;
+ if (g_stat(target, &st))
+ return NULL;
+
+ // TODO(p): Consider making the `target` an absolute path, if it isn't.
+ // Or maybe let it fail, and document the requirement.
+ gchar *uri = g_filename_to_uri(target, NULL, NULL);
+ if (!uri)
+ return NULL;
+
+ gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1);
+ gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
+
+ cairo_surface_t *result = NULL;
+ const gchar *sizes[] = {"large", "x-large", "xx-large", "normal"};
+ for (gsize i = 0; !result && i < G_N_ELEMENTS(sizes); i++) {
+ gchar *path = g_strdup_printf("%s/thumbnails/%s/%s.png",
+ cache_dir, "large", sum);
+ result = read_png_thumbnail(path, target, st.st_mtim.tv_sec, NULL);
+ g_free(path);
+ }
+
+ g_free(cache_dir);
+ g_free(sum);
+ g_free(uri);
+ return result;
+}
diff --git a/fastiv.c b/fastiv.c
index df02084..7aa5b4b 100644
--- a/fastiv.c
+++ b/fastiv.c
@@ -74,7 +74,7 @@ add_applying_transitive_closure(const gchar *element, GHashTable *relation,
// --- XDG ---------------------------------------------------------------------
-static gchar *
+gchar *
get_xdg_home_dir(const char *var, const char *default_)
{
const char *env = getenv(var);
diff --git a/meson.build b/meson.build
index c38458d..ee20657 100644
--- a/meson.build
+++ b/meson.build
@@ -5,6 +5,7 @@ libraw = dependency('libraw', required : get_option('libraw'))
dependencies = [
dependency('gtk+-3.0'),
dependency('libturbojpeg'),
+ dependency('libpng', version : '>=1.5.4'),
libraw,
meson.get_compiler('c').find_library('m', required : false),
]
--
cgit v1.2.3-70-g09d2