summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--meson.build30
-rw-r--r--src/loader/backends/avif.c82
-rw-r--r--src/loader/backends/heif.c88
-rw-r--r--src/loader/backends/jxl.c91
-rw-r--r--src/loader/loader.c21
-rw-r--r--tests/test_loader_pixbuf.c4
6 files changed, 309 insertions, 7 deletions
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 <avif/avif.h>
+#include <gdk/gdk.h>
+#include <gio/gio.h>
+
+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 <gdk/gdk.h>
+#include <gio/gio.h>
+#include <libheif/heif.h>
+
+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 <gdk/gdk.h>
+#include <gio/gio.h>
+#include <jxl/decode.h>
+#include <jxl/types.h>
+
+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