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), ]