diff --git a/README.adoc b/README.adoc index 69649e9..33691d8 100644 --- a/README.adoc +++ b/README.adoc @@ -2,7 +2,7 @@ fastiv ====== 'fastiv' is a fast image viewer, supporting BMP, PNG, GIF, JPEG, and optionally -raw photos, SVG, X11 cursors and TIFF, or whatever gdk-pixbuf loads. +raw photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever gdk-pixbuf loads. It still has some road to go, but it's already become quite usable, and it has received basic polishing. @@ -24,8 +24,9 @@ Building and Running -------------------- Build dependencies: Meson, pkg-config + Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info, -spng>=0.7.0, libturbojpeg, LibRaw (optional), librsvg-2.0 (optional), -xcursor (optional), libtiff (optional), gdk-pixbuf-2.0 (optional) +spng>=0.7.0, libturbojpeg + +Optional dependencies: LibRaw, librsvg-2.0, xcursor, libheif, libtiff, +gdk-pixbuf-2.0 $ git clone --recursive https://git.janouch.name/p/fastiv.git $ meson builddir diff --git a/fastiv-io.c b/fastiv-io.c index e0c3dea..639038b 100644 --- a/fastiv-io.c +++ b/fastiv-io.c @@ -33,6 +33,9 @@ #ifdef HAVE_XCURSOR #include #endif // HAVE_XCURSOR +#ifdef HAVE_LIBHEIF +#include +#endif // HAVE_LIBHEIF #ifdef HAVE_LIBTIFF #include #include @@ -79,6 +82,11 @@ const char *fastiv_io_supported_media_types[] = { #ifdef HAVE_XCURSOR "image/x-xcursor", #endif // HAVE_XCURSOR +#ifdef HAVE_LIBHEIF + "image/heic", + "image/heif", + "image/avif", +#endif // HAVE_LIBHEIF #ifdef HAVE_LIBTIFF "image/tiff", #endif // HAVE_LIBTIFF @@ -1078,6 +1086,146 @@ open_xcursor(const gchar *data, gsize len, GError **error) } #endif // HAVE_XCURSOR -------------------------------------------------------- +#ifdef HAVE_LIBHEIF //--------------------------------------------------------- + +static cairo_surface_t * +load_libheif_image(struct heif_context *ctx, heif_item_id id, GError **error) +{ + struct heif_image_handle *handle = NULL; + struct heif_error err = heif_context_get_image_handle(ctx, id, &handle); + if (err.code != heif_error_Ok) { + set_error(error, err.message); + return NULL; + } + + cairo_surface_t *surface = NULL; + int has_alpha = heif_image_handle_has_alpha_channel(handle); + int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle); + if (bit_depth < 0) { + set_error(error, "undefined bit depth"); + goto fail; + } + + // Setting `convert_hdr_to_8bit` seems to be a no-op for RGBA32/64. + struct heif_decoding_options *opts = heif_decoding_options_alloc(); + + // TODO(p): We can get 16-bit depth, in reality most likely 10-bit. + struct heif_image *image = NULL; + err = heif_decode_image(handle, &image, heif_colorspace_RGB, + heif_chroma_interleaved_RGBA, opts); + if (err.code != heif_error_Ok) { + set_error(error, err.message); + goto fail_decode; + } + + int w = heif_image_get_width(image, heif_channel_interleaved); + int h = heif_image_get_height(image, heif_channel_interleaved); + + // TODO(p): Add more pages with depth, thumbnails, and auxiliary images. + surface = cairo_image_surface_create( + has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, w, h); + cairo_status_t surface_status = cairo_surface_status(surface); + if (surface_status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(surface_status)); + cairo_surface_destroy(surface); + surface = NULL; + goto fail_process; + } + + // As of writing, the library is using 16-byte alignment, unlike Cairo. + int src_stride = 0; + const uint8_t *src = heif_image_get_plane_readonly( + image, heif_channel_interleaved, &src_stride); + int dst_stride = cairo_image_surface_get_stride(surface); + const uint8_t *dst = cairo_image_surface_get_data(surface); + + for (int y = 0; y < h; y++) { + uint32_t *dstp = (uint32_t *) (dst + dst_stride * y); + const uint32_t *srcp = (const uint32_t *) (src + src_stride * y); + for (int x = 0; x < w; x++) { + uint32_t rgba = g_ntohl(srcp[x]); + *dstp++ = rgba << 24 | rgba >> 8; + } + } + + // TODO(p): Test real behaviour on real transparent images. + if (has_alpha && !heif_image_handle_is_premultiplied_alpha(handle)) { + for (int y = 0; y < h; y++) { + uint32_t *dstp = (uint32_t *) (dst + dst_stride * y); + for (int x = 0; x < w; x++) { + uint32_t pixel = dstp[x], a = pixel >> 24; + uint8_t r = pixel >> 16; + uint8_t g = pixel >> 8; + uint8_t b = pixel; + dstp[x] = a << 24 | + (r * a / 255) << 16 | (g * a / 255) << 8 | (b * a / 255); + } + } + } + + // TODO(p): Attach any ICC color profile and Exif data. + cairo_surface_mark_dirty(surface); + +fail_process: + heif_image_release(image); +fail_decode: + heif_decoding_options_free(opts); +fail: + heif_image_handle_release(handle); + return surface; +} + +static cairo_surface_t * +open_libheif(const gchar *data, gsize len, GError **error) +{ + // libheif will throw C++ exceptions on allocation failures. + // The library is generally awful through and through. + struct heif_context *ctx = heif_context_alloc(); + cairo_surface_t *result = NULL, *result_tail = NULL; + + struct heif_error err; + err = heif_context_read_from_memory_without_copy(ctx, data, len, NULL); + if (err.code != heif_error_Ok) { + set_error(error, err.message); + goto fail_read; + } + + // TODO(p): Only fail if there is absolutely nothing to extract, + // see open_libtiff() below. + int n = heif_context_get_number_of_top_level_images(ctx); + heif_item_id *ids = g_malloc0_n(n, sizeof *ids); + n = heif_context_get_list_of_top_level_image_IDs(ctx, ids, n); + + for (int i = 0; i < n; i++) { + cairo_surface_t *surface = load_libheif_image(ctx, ids[i], error); + if (!surface) { + if (result) { + cairo_surface_destroy(result); + result = NULL; + } + goto fail_decode; + } + + if (result) { + cairo_surface_set_user_data(result_tail, + &fastiv_io_key_page_next, surface, + (cairo_destroy_func_t) cairo_surface_destroy); + cairo_surface_set_user_data(surface, + &fastiv_io_key_page_previous, result_tail, NULL); + result_tail = surface; + } else { + result = result_tail = surface; + } + } + +fail_decode: + g_free(ids); +fail_read: + heif_context_free(ctx); + return result; +} + +#endif // HAVE_LIBHEIF -------------------------------------------------------- #ifdef HAVE_LIBTIFF //--------------------------------------------------------- struct fastiv_io_tiff { @@ -1452,6 +1600,14 @@ fastiv_io_open_from_data(const char *data, size_t len, const gchar *path, g_clear_error(error); } #endif // HAVE_XCURSOR -------------------------------------------------------- +#ifdef HAVE_LIBHEIF //--------------------------------------------------------- + if ((surface = open_libheif(data, len, error))) + break; + if (error) { + g_debug("%s", (*error)->message); + g_clear_error(error); + } +#endif // HAVE_LIBHEIF -------------------------------------------------------- #ifdef HAVE_LIBTIFF //--------------------------------------------------------- // This needs to be positioned after LibRaw. if ((surface = open_libtiff(data, len, path, error))) @@ -1463,8 +1619,9 @@ fastiv_io_open_from_data(const char *data, size_t len, const gchar *path, #endif // HAVE_LIBTIFF -------------------------------------------------------- #ifdef HAVE_GDKPIXBUF // ------------------------------------------------------ // This is only used as a last resort, the rest above is special-cased. - if ((surface = open_gdkpixbuf(data, len, error)) || - (error && (*error)->code != GDK_PIXBUF_ERROR_UNKNOWN_TYPE)) + if ((surface = open_gdkpixbuf(data, len, error))) + break; + if (error && (*error)->code != GDK_PIXBUF_ERROR_UNKNOWN_TYPE) break; if (error) { diff --git a/meson.build b/meson.build index 34508c6..bbd0cc5 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,7 @@ endif libraw = dependency('libraw', required : get_option('libraw')) 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')) gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf')) dependencies = [ @@ -28,6 +29,7 @@ dependencies = [ libraw, librsvg, xcursor, + libheif, libtiff, gdkpixbuf, meson.get_compiler('c').find_library('m', required : false), @@ -39,6 +41,7 @@ conf.set_quoted('PROJECT_VERSION', meson.project_version()) conf.set('HAVE_LIBRAW', libraw.found()) conf.set('HAVE_LIBRSVG', librsvg.found()) conf.set('HAVE_XCURSOR', xcursor.found()) +conf.set('HAVE_LIBHEIF', libheif.found()) conf.set('HAVE_LIBTIFF', libtiff.found()) conf.set('HAVE_GDKPIXBUF', gdkpixbuf.found()) configure_file( diff --git a/meson_options.txt b/meson_options.txt index b360330..f428622 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,6 +4,8 @@ option('librsvg', type : 'feature', value : 'auto', description : 'Build with SVG support, requires librsvg') option('xcursor', type : 'feature', value : 'auto', description : 'Build with Xcursor support, requires libXcursor') +option('libheif', type : 'feature', value : 'auto', + description : 'Build with HEIF/AVIF support, requires libheif') option('libtiff', type : 'feature', value : 'auto', description : 'Build with TIFF support, requires libtiff') option('gdk-pixbuf', type : 'feature', value : 'auto',