Add preliminary HEIF/AVIF support

The gdk-pixbuf plugin does not work here, for whatever reason.

Moreover, close integration exposes higher bit depths, metadata,
and auxiliary images.

The library is awful and copylefted, but it's the only reasonable
thing that works.
This commit is contained in:
Přemysl Eric Janouch 2021-12-11 14:40:10 +01:00
parent 5f4090aaee
commit ac70c7724b
Signed by: p
GPG Key ID: A0420B94F92B9493
4 changed files with 168 additions and 5 deletions

View File

@ -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

View File

@ -33,6 +33,9 @@
#ifdef HAVE_XCURSOR
#include <X11/Xcursor/Xcursor.h>
#endif // HAVE_XCURSOR
#ifdef HAVE_LIBHEIF
#include <libheif/heif.h>
#endif // HAVE_LIBHEIF
#ifdef HAVE_LIBTIFF
#include <tiff.h>
#include <tiffio.h>
@ -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) {

View File

@ -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(

View File

@ -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',