From 484fbca06312dbe0f381109763f701671f22e602 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 13 Jul 2026 21:13:35 +0300 Subject: modernfmt: JXL/HEIF/AVIF loader backends, conditional (qt0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M5 (task qt0): modern format backends. - src/loader/backends/jxl.c: libjxl decoder → RGBA → GdkMemoryTexture. - src/loader/backends/heif.c: libheif decoder → interleaved RGBA → GdkMemoryTexture (also handles AVIF when libavif is absent). - src/loader/backends/avif.c: libavif decoder (written; auto-disabled when libavif is not installed). - src/loader/loader.c: BACKENDS array conditionally includes the backends via -DHAVE_JXL/HAVE_AVIF/HAVE_HEIF; pixbuf remains last (fallback). - meson.build: conditional backend sources + c_args + extra deps. - tests: assert_unsupports updated to assert NULL only (any error, works with or without the specific backend compiled in). - Minimal build (all disabled) + full build (jxl+heif) both 14/14 green, ASan clean. --- meson.build | 30 +++++++++++++-- src/loader/backends/avif.c | 82 +++++++++++++++++++++++++++++++++++++++++ src/loader/backends/heif.c | 88 ++++++++++++++++++++++++++++++++++++++++++++ src/loader/backends/jxl.c | 91 ++++++++++++++++++++++++++++++++++++++++++++++ src/loader/loader.c | 21 ++++++++++- tests/test_loader_pixbuf.c | 4 +- 6 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 src/loader/backends/avif.c create mode 100644 src/loader/backends/heif.c create mode 100644 src/loader/backends/jxl.c diff --git a/meson.build b/meson.build index 7f9c3f2..4611532 100644 --- a/meson.build +++ b/meson.build @@ -53,8 +53,7 @@ src_inc = include_directories('src') # tests so the GApplication/GtkApplicationWindow code is exercised without # duplicating sources). Plain-C modules will join this library as they # land (navigator, trash, mover, ...). -ggaze_lib = static_library('ggaze', - files( +ggaze_src = files( 'src/app.c', 'src/window.c', 'src/viewer.c', @@ -68,9 +67,32 @@ ggaze_lib = static_library('ggaze', 'src/loader/loader.c', 'src/loader/detect.c', 'src/loader/backends/pixbuf.c', - ), + ) +ggaze_c_args = [] +ggaze_extra_deps = [] + +if jxl_dep.found() + ggaze_src += files('src/loader/backends/jxl.c') + ggaze_c_args += '-DHAVE_JXL' + ggaze_extra_deps += jxl_dep +endif +if avif_dep.found() + ggaze_src += files('src/loader/backends/avif.c') + ggaze_c_args += '-DHAVE_AVIF' + ggaze_extra_deps += avif_dep +endif +if heif_dep.found() + ggaze_src += files('src/loader/backends/heif.c') + ggaze_c_args += '-DHAVE_HEIF' + ggaze_extra_deps += heif_dep +endif + +ggaze_lib = static_library('ggaze', + ggaze_src, include_directories : [inc, src_inc], - dependencies : [gtk4_dep, glib_dep, gio_dep, adwaita_dep, gdkpixbuf_dep, libexif_dep], + dependencies : [gtk4_dep, glib_dep, gio_dep, adwaita_dep, gdkpixbuf_dep, + libexif_dep] + ggaze_extra_deps, + c_args : ggaze_c_args, install : false, ) diff --git a/src/loader/backends/avif.c b/src/loader/backends/avif.c new file mode 100644 index 0000000..112d52d --- /dev/null +++ b/src/loader/backends/avif.c @@ -0,0 +1,82 @@ +/*:* + * ggaze — AVIF loader backend (libavif) + * + * Decodes AVIF via libavif. Compiled only when meson feature `avif` is enabled. + * If libavif is absent, the heif backend (libheif) also handles AVIF (its + * can_load accepts GGAZE_FMT_AVIF). This backend is preferred when present. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "../loader.h" +#include "../detect.h" + +#include +#include +#include + +static gboolean +_avif_can_load(const guint8 *p_head, gsize u_len) { + return (detect_format(p_head, u_len) == GGAZE_FMT_AVIF); +} + +static GdkTexture * +_avif_load(GFile *p_file, GCancellable *p_cancel, GError **p_err) { + (void)p_cancel; + char *c_path = g_file_get_path(p_file); + if (c_path == NULL) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "avif: non-local file"); + return (NULL); + } + + avifDecoder *p_dec = avifDecoderCreate(); + avifResult e_r = avifDecoderSetIOFile(p_dec, c_path); + g_free(c_path); + if (e_r != AVIF_RESULT_OK) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "avif: %s", + avifResultToString(e_r)); + avifDecoderDestroy(p_dec); + return (NULL); + } + e_r = avifDecoderParse(p_dec); + if (e_r != AVIF_RESULT_OK) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "avif: %s", + avifResultToString(e_r)); + avifDecoderDestroy(p_dec); + return (NULL); + } + e_r = avifDecoderNextImage(p_dec); + if (e_r != AVIF_RESULT_OK) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "avif: %s", + avifResultToString(e_r)); + avifDecoderDestroy(p_dec); + return (NULL); + } + + avifRGBImage st_rgb; + avifRGBImageSetDefaults(&st_rgb, &p_dec->image); + st_rgb.format = AVIF_RGB_FORMAT_RGBA; + st_rgb.depth = 8; + avifRGBImageAllocatePixels(&st_rgb); + avifImageYCToRGB(&p_dec->image, &st_rgb); + + GdkTexture *p_tex = NULL; + if (st_rgb.pixels != NULL) { + GBytes *p_bytes = g_bytes_new_static( + st_rgb.pixels, (gsize)st_rgb.rowBytes * (gsize)st_rgb.height); + p_tex = gdk_memory_texture_new((int)st_rgb.width, (int)st_rgb.height, + GDK_MEMORY_R8G8B8A8, p_bytes, + (gsize)st_rgb.rowBytes); + g_bytes_unref(p_bytes); + } + + avifRGBImageFreePixels(&st_rgb); + avifDecoderDestroy(p_dec); + return (p_tex); +} + +const GgazeLoaderBackend avif_backend = { + .can_load = _avif_can_load, + .load = _avif_load, +}; \ No newline at end of file diff --git a/src/loader/backends/heif.c b/src/loader/backends/heif.c new file mode 100644 index 0000000..fda7573 --- /dev/null +++ b/src/loader/backends/heif.c @@ -0,0 +1,88 @@ +/*:* + * ggaze — HEIF/HEIC loader backend (libheif) + * + * Decodes HEIF/HEIC (and AVIF if libavif is absent) via libheif into RGBA + * pixels, then wraps them in a GdkMemoryTexture. Compiled only when meson + * feature `heif` is enabled. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "../loader.h" +#include "../detect.h" + +#include +#include +#include + +static gboolean +_heif_can_load(const guint8 *p_head, gsize u_len) { + return (detect_format(p_head, u_len) == GGAZE_FMT_HEIF || + detect_format(p_head, u_len) == GGAZE_FMT_AVIF); +} + +static GdkTexture * +_heif_load(GFile *p_file, GCancellable *p_cancel, GError **p_err) { + (void)p_cancel; + char *c_path = g_file_get_path(p_file); + if (c_path == NULL) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "heif: non-local file"); + return (NULL); + } + + struct heif_context *p_ctx = heif_context_alloc(); + struct heif_error st_err = heif_context_read_from_file(p_ctx, c_path, NULL); + g_free(c_path); + if (st_err.code != heif_error_Ok) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "heif: %s", + st_err.message); + heif_context_free(p_ctx); + return (NULL); + } + + struct heif_image_handle *p_handle = NULL; + st_err = heif_context_get_primary_image_handle(p_ctx, &p_handle); + if (st_err.code != heif_error_Ok) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "heif: %s", + st_err.message); + heif_context_free(p_ctx); + return (NULL); + } + + struct heif_image *p_img = NULL; + st_err = heif_decode_image(p_handle, &p_img, heif_colorspace_RGB, + heif_chroma_interleaved_RGBA, NULL); + if (st_err.code != heif_error_Ok) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "heif: %s", + st_err.message); + heif_image_handle_release(p_handle); + heif_context_free(p_ctx); + return (NULL); + } + + int i_stride = 0; + const uint8_t *p_data = + heif_image_get_plane_readonly(p_img, heif_channel_interleaved, &i_stride); + int i_w = heif_image_get_width(p_img, heif_channel_interleaved); + int i_h = heif_image_get_height(p_img, heif_channel_interleaved); + + GdkTexture *p_tex = NULL; + if (p_data != NULL && i_w > 0 && i_h > 0) { + gsize u_len = (gsize)(i_h - 1) * (gsize)i_stride + (gsize)i_w * 4u; + GBytes *p_bytes = g_bytes_new_static(p_data, u_len); + p_tex = gdk_memory_texture_new(i_w, i_h, GDK_MEMORY_R8G8B8A8, p_bytes, + (gsize)i_stride); + g_bytes_unref(p_bytes); + } + + heif_image_release(p_img); + heif_image_handle_release(p_handle); + heif_context_free(p_ctx); + return (p_tex); +} + +const GgazeLoaderBackend heif_backend = { + .can_load = _heif_can_load, + .load = _heif_load, +}; \ No newline at end of file diff --git a/src/loader/backends/jxl.c b/src/loader/backends/jxl.c new file mode 100644 index 0000000..7e98be8 --- /dev/null +++ b/src/loader/backends/jxl.c @@ -0,0 +1,91 @@ +/*:* + * ggaze — JPEG XL loader backend (libjxl) + * + * Decodes JPEG XL (codestream or container) via libjxl's JxlDecoder into RGBA + * pixels, then wraps them in a GdkMemoryTexture. Honors EXIF orientation is + * not needed for JXL (orientation is handled in the container). Compiled only + * when meson feature `jxl` is enabled. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "../loader.h" +#include "../detect.h" + +#include +#include +#include +#include + +static gboolean +_jxl_can_load(const guint8 *p_head, gsize u_len) { + return (detect_format(p_head, u_len) == GGAZE_FMT_JXL); +} + +static GdkTexture * +_jxl_load(GFile *p_file, GCancellable *p_cancel, GError **p_err) { + (void)p_cancel; + gchar *c_buf = NULL; + gsize u_len = 0; + if (!g_file_load_contents(p_file, NULL, &c_buf, &u_len, NULL, p_err)) { + return (NULL); + } + + JxlDecoder *p_dec = JxlDecoderCreate(NULL); + if (p_dec == NULL) { + g_free(c_buf); + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, + "jxl: cannot create decoder"); + return (NULL); + } + JxlDecoderSubscribeEvents(p_dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE); + JxlDecoderSetInput(p_dec, (const uint8_t *)c_buf, u_len); + JxlDecoderCloseInput(p_dec); + + JxlBasicInfo st_info; + memset(&st_info, 0, sizeof(st_info)); + uint8_t *p_pixels = NULL; + size_t u_w = 0, u_h = 0; + GdkTexture *p_tex = NULL; + + for (;;) { + JxlDecoderStatus e_st = JxlDecoderProcessInput(p_dec); + if (e_st == JXL_DEC_ERROR) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "jxl: decode error"); + break; + } + if (e_st == JXL_DEC_SUCCESS) { + /* Done — build the texture. */ + if (p_pixels != NULL) { + GBytes *p_bytes = g_bytes_new_take(p_pixels, u_w * u_h * 4u); + p_pixels = NULL; + p_tex = + gdk_memory_texture_new((int)u_w, (int)u_h, GDK_MEMORY_R8G8B8A8, + p_bytes, (gsize)u_w * 4u); + g_bytes_unref(p_bytes); + } + break; + } + if (e_st == JXL_DEC_BASIC_INFO) { + JxlDecoderGetBasicInfo(p_dec, &st_info); + u_w = st_info.xsize; + u_h = st_info.ysize; + p_pixels = g_malloc(u_w * u_h * 4u); + JxlPixelFormat st_fmt = {4, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0}; + JxlDecoderSetImageOutBuffer(p_dec, &st_fmt, p_pixels, u_w * u_h * 4u); + } else if (e_st == JXL_DEC_FULL_IMAGE) { + /* pixels are now filled; continue to SUCCESS */ + } + } + + JxlDecoderDestroy(p_dec); + g_free(p_pixels); + g_free(c_buf); + return (p_tex); +} + +const GgazeLoaderBackend jxl_backend = { + .can_load = _jxl_can_load, + .load = _jxl_load, +}; \ No newline at end of file diff --git a/src/loader/loader.c b/src/loader/loader.c index 2121534..9721c2a 100644 --- a/src/loader/loader.c +++ b/src/loader/loader.c @@ -19,8 +19,27 @@ /* Registered backends, priority order (specific first, fallback LAST). * pixbuf_backend must remain last: it accepts GGAZE_FMT_UNKNOWN. */ +#ifdef HAVE_JXL +extern const GgazeLoaderBackend jxl_backend; +#endif +#ifdef HAVE_AVIF +extern const GgazeLoaderBackend avif_backend; +#endif +#ifdef HAVE_HEIF +extern const GgazeLoaderBackend heif_backend; +#endif + static const GgazeLoaderBackend *BACKENDS[] = { - /* jxl_backend, avif_backend, heif_backend land here in M5. */ +#ifdef HAVE_JXL + &jxl_backend, +#endif +#ifdef HAVE_AVIF + &avif_backend, +#endif +#ifdef HAVE_HEIF + &heif_backend, +#endif + /* pixbuf fallback (PNG/JPEG/GIF/WebP/TIFF/ICO) — must be last. */ &pixbuf_backend, }; diff --git a/tests/test_loader_pixbuf.c b/tests/test_loader_pixbuf.c index ba37522..32e3a2e 100644 --- a/tests/test_loader_pixbuf.c +++ b/tests/test_loader_pixbuf.c @@ -104,8 +104,8 @@ assert_unsupported(const guint8 *p_buf, gsize u_len) { GdkTexture *p_tex = load_bytes(p_buf, u_len, &p_err); g_assert_null(p_tex); g_assert_nonnull(p_err); - g_assert_cmpint(p_err->code, ==, G_IO_ERROR_NOT_SUPPORTED); - g_error_free(p_err); + g_error_free(p_err); /* NOT_SUPPORTED if no backend, or a decode error if + * the specific backend is compiled in. */ } static void -- cgit v1.2.3