From 6b0f2de39c2d8b9e8d346abd8548024da4ae8185 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 12 Jul 2026 17:51:22 +0300 Subject: showimage: loader, detect, viewer widget, EXIF orientation, tests (gt0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/loader/detect.c | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/loader/detect.c (limited to 'src/loader/detect.c') diff --git a/src/loader/detect.c b/src/loader/detect.c new file mode 100644 index 0000000..1806874 --- /dev/null +++ b/src/loader/detect.c @@ -0,0 +1,82 @@ +/*:* + * ggaze — image format detection + * + * Magic-byte sniffing. Pure function, no I/O, no GTK -> unit-testable. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "detect.h" + +#include + +GgazeFormat +detect_format(const guint8 *p_head, gsize u_len) { + if (p_head == NULL || u_len == 0) { + return (GGAZE_FMT_UNKNOWN); + } + + /* JPEG: FF D8 FF */ + if (u_len >= 3 && p_head[0] == 0xFF && p_head[1] == 0xD8 && + p_head[2] == 0xFF) { + return (GGAZE_FMT_JPEG); + } + + /* PNG: 89 50 4E 47 0D 0A 1A 0A */ + if (u_len >= 8 && p_head[0] == 0x89 && p_head[1] == 'P' && + p_head[2] == 'N' && p_head[3] == 'G' && p_head[4] == 0x0D && + p_head[5] == 0x0A && p_head[6] == 0x1A && p_head[7] == 0x0A) { + return (GGAZE_FMT_PNG); + } + + /* GIF: "GIF8" */ + if (u_len >= 4 && p_head[0] == 'G' && p_head[1] == 'I' && p_head[2] == 'F' && + p_head[3] == '8') { + return (GGAZE_FMT_GIF); + } + + /* WebP: RIFF .... WEBP */ + if (u_len >= 12 && memcmp(p_head, "RIFF", 4) == 0 && + memcmp(p_head + 8, "WEBP", 4) == 0) { + return (GGAZE_FMT_WEBP); + } + + /* TIFF: II 2A 00 (little) | MM 00 2A (big) */ + if (u_len >= 4 && ((p_head[0] == 'I' && p_head[1] == 'I' && + p_head[2] == 0x2A && p_head[3] == 0x00) || + (p_head[0] == 'M' && p_head[1] == 'M' && + p_head[2] == 0x00 && p_head[3] == 0x2A))) { + return (GGAZE_FMT_TIFF); + } + + /* ICO: 00 00 01 00 */ + if (u_len >= 4 && p_head[0] == 0x00 && p_head[1] == 0x00 && + p_head[2] == 0x01 && p_head[3] == 0x00) { + return (GGAZE_FMT_ICO); + } + + /* JPEG XL: codestream FF 0A, or container 00 00 00 0C "JXL " */ + if (u_len >= 2 && p_head[0] == 0xFF && p_head[1] == 0x0A) { + return (GGAZE_FMT_JXL); + } + if (u_len >= 12 && p_head[0] == 0x00 && p_head[1] == 0x00 && + p_head[2] == 0x00 && p_head[3] == 0x0C && + memcmp(p_head + 4, "JXL ", 4) == 0) { + return (GGAZE_FMT_JXL); + } + + /* AVIF / HEIF: ISO BMFF ftyp box at offset 4; brand at offset 8. */ + if (u_len >= 12 && memcmp(p_head + 4, "ftyp", 4) == 0) { + const guint8 *p_brand = p_head + 8; + if (memcmp(p_brand, "avif", 4) == 0 || memcmp(p_brand, "avis", 4) == 0) { + return (GGAZE_FMT_AVIF); + } + if (memcmp(p_brand, "heic", 4) == 0 || memcmp(p_brand, "heix", 4) == 0 || + memcmp(p_brand, "mif1", 4) == 0) { + return (GGAZE_FMT_HEIF); + } + } + + return (GGAZE_FMT_UNKNOWN); +} \ No newline at end of file -- cgit v1.2.3