diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-12 17:51:22 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-12 17:51:22 +0300 |
| commit | 6b0f2de39c2d8b9e8d346abd8548024da4ae8185 (patch) | |
| tree | 29e67e6a75488b4fdd34756be3944c4262e0b85a /src/loader/backends | |
| parent | 51b47a0130005bd19039422d027da23ff46dd6b8 (diff) | |
showimage: loader, detect, viewer widget, EXIF orientation, tests (gt0)
M1 (task gt0): show one image with zoom/pan.
- src/loader/detect.{c,h}: magic-byte format sniffing (JPEG/PNG/GIF/WebP/
TIFF/ICO/JXL/AVIF/HEIF) -> GgazeFormat; pure, no I/O, unit-testable.
- src/loader/loader.{c,h}: loader_load() sniffs the header and dispatches to
the first registered backend; GgazeLoaderBackend struct; pixbuf is the
fallback (last, accepts UNKNOWN). M1 ships only the pixbuf backend.
- src/loader/backends/pixbuf.c: GdkPixbufLoader decode +
gdk_pixbuf_apply_embedded_orientation (decision #26) -> GdkTexture via
gdk_memory_texture_new (avoids the deprecated gdk_texture_new_for_pixbuf).
- src/viewer.{c,h}: GgazeViewer : GtkWidget custom widget (decision #31) —
fit/100%/in/out zoom, cursor-centered zoom, drag-to-pan with clamping,
dark background, GtkSnapshot render nodes.
- src/window.c: open -> loader_load -> viewer_set_texture -> stack 'large'.
- tests: unit test_detect (13 cases) + test_loader_pixbuf (plain/rotated-EXIF
8x4 orient6->4x8/png/rgba/missing/unsupported jxl-avif-heif/corrupt),
integration test_open_and_show (fixture + rotated + ./sample-images
skip-if-absent). 6/6 green; detect 97% / loader 91% / pixbuf 87% coverage.
- fixtures: gen.py produces plain.jpg, rot6.jpg, small.png, rgba.png.
- AGENTS.md: documents the ./sample-images optional test corpus convention.
Sub-agent review fixes: use-after-free of c_name in ggaze_window_open
(BLOCKER), gtk_stack_get_pages leak in test_window (BLOCKER), coverage gap,
dead branch, viewer measure, _prefix/_cb naming, include order, extern in
header, pan clamp, stale comments — all addressed.
Diffstat (limited to 'src/loader/backends')
| -rw-r--r-- | src/loader/backends/pixbuf.c | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/src/loader/backends/pixbuf.c b/src/loader/backends/pixbuf.c new file mode 100644 index 0000000..e66f461 --- /dev/null +++ b/src/loader/backends/pixbuf.c @@ -0,0 +1,121 @@ +/*:* + * ggaze — GdkPixbuf loader backend (fallback) + * + * Decodes any GdkPixbuf-supported format (PNG/JPEG/GIF/WebP/TIFF/ICO) via a + * GdkPixbufLoader, applies the embedded EXIF Orientation (decision #26) so the + * returned GdkTexture is upright, and hands the result to the caller. Acts as + * the fallback backend: can_load() returns TRUE for unknown formats too (let + * GdkPixbuf try) and FALSE only for formats owned by the JXL/AVIF/HEIF + * backends in M5. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <gdk/gdk.h> +#include <gio/gio.h> + +#include "../detect.h" +#include "../loader.h" + +static GdkTexture *_texture_from_pixbuf(GdkPixbuf *p_pix); + +static gboolean +_pixbuf_can_load(const guint8 *p_head, gsize u_len) { + switch (detect_format(p_head, u_len)) { + case GGAZE_FMT_JXL: + case GGAZE_FMT_AVIF: + case GGAZE_FMT_HEIF: + return (FALSE); /* owned by specific backends (M5) */ + case GGAZE_FMT_UNKNOWN: + case GGAZE_FMT_JPEG: + case GGAZE_FMT_PNG: + case GGAZE_FMT_GIF: + case GGAZE_FMT_WEBP: + case GGAZE_FMT_TIFF: + case GGAZE_FMT_ICO: + return (TRUE); + } + return (FALSE); /* unreachable; keeps -Wreturn-type calm */ +} + +static GdkTexture * +_pixbuf_load(GFile *p_file, GCancellable *p_cancel, GError **p_err) { + gchar *c_buf = NULL; + gsize u_len = 0; + if (!g_file_load_contents(p_file, p_cancel, &c_buf, &u_len, NULL, p_err)) { + return (NULL); + } + + GdkPixbufLoader *p_loader = gdk_pixbuf_loader_new(); + GError *p_sub = NULL; + if (!gdk_pixbuf_loader_write(p_loader, (const guchar *)c_buf, u_len, + &p_sub)) { + g_propagate_error(p_err, p_sub); + g_object_unref(p_loader); + g_free(c_buf); + return (NULL); + } + + /* Close may fail on truncated data but a pixbuf may still be available. */ + if (!gdk_pixbuf_loader_close(p_loader, &p_sub)) { + if (p_sub != NULL) { + g_error_free(p_sub); + } + } + + GdkPixbuf *p_pix = gdk_pixbuf_loader_get_pixbuf(p_loader); + if (p_pix == NULL) { + g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, + "could not decode image (GdkPixbuf produced no pixbuf)"); + g_object_unref(p_loader); + g_free(c_buf); + return (NULL); + } + + /* Honor EXIF Orientation so the texture is upright (decision #26). */ + GdkPixbuf *p_oriented = gdk_pixbuf_apply_embedded_orientation(p_pix); + GdkPixbuf *p_use = + (p_oriented != NULL) ? p_oriented : GDK_PIXBUF(g_object_ref(p_pix)); + + GdkTexture *p_tex = _texture_from_pixbuf(p_use); + + g_object_unref(p_use); + g_object_unref(p_loader); + g_free(c_buf); + return (p_tex); +} + +const GgazeLoaderBackend pixbuf_backend = { + .can_load = _pixbuf_can_load, + .load = _pixbuf_load, +}; + +/* Build a GdkTexture from a GdkPixbuf without the deprecated + * gdk_texture_new_for_pixbuf(). GdkPixbuf stores non-premultiplied R8G8B8A8 + * when it has alpha; otherwise we add an alpha channel first. */ +static GdkTexture * +_texture_from_pixbuf(GdkPixbuf *p_pix) { + g_return_val_if_fail(GDK_IS_PIXBUF(p_pix), NULL); + int i_w = gdk_pixbuf_get_width(p_pix); + int i_h = gdk_pixbuf_get_height(p_pix); + g_return_val_if_fail(i_w > 0 && i_h > 0, NULL); + + GdkPixbuf *p_rgba = gdk_pixbuf_get_has_alpha(p_pix) + ? GDK_PIXBUF(g_object_ref(p_pix)) + : gdk_pixbuf_add_alpha(p_pix, FALSE, 0, 0, 0); + if (p_rgba == NULL) { + return (NULL); + } + + int i_rowstride = gdk_pixbuf_get_rowstride(p_rgba); + guchar *p_pixels = gdk_pixbuf_get_pixels(p_rgba); + gsize u_len = (gsize)(i_h - 1) * (gsize)i_rowstride + (gsize)i_w * 4u; + GBytes *p_bytes = g_bytes_new_with_free_func( + p_pixels, u_len, (GDestroyNotify)g_object_unref, p_rgba); + GdkTexture *p_tex = gdk_memory_texture_new(i_w, i_h, GDK_MEMORY_R8G8B8A8, + p_bytes, (gsize)i_rowstride); + g_bytes_unref(p_bytes); + return (p_tex); +}
\ No newline at end of file |
