summaryrefslogtreecommitdiff
path: root/src/loader
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-14 08:49:12 +0300
committerPaul Buetow <paul@buetow.org>2026-07-14 08:49:12 +0300
commit12be1b9ae52829292d13cc2fcc6ff7571d050d62 (patch)
tree674aafaeb3485bb38dd1e2ecc3d62f77e3584366 /src/loader
parent484fbca06312dbe0f381109763f701671f22e602 (diff)
progressive: libjpeg-turbo two-phase JPEG backend (st0)
M6: progressive low-res preview via libjpeg-turbo. - jpeg.c: two-phase decode (1/8 low-res via progress_cb, full via GdkPixbuf with EXIF orientation). Feature-gated. - loader.h/c: LoaderProgressCb + load_progressive on backend struct; loader_load_async accepts progress params; JPEG special-cased in task thread. - window.c: _load_progress_cb marshals partial to main thread. - tests: test_loader_jpeg (progress + dims), test_progressive_jpeg (sample). 16/16 green, ASan clean.
Diffstat (limited to 'src/loader')
-rw-r--r--src/loader/backends/jpeg.c178
-rw-r--r--src/loader/loader.c58
-rw-r--r--src/loader/loader.h9
3 files changed, 240 insertions, 5 deletions
diff --git a/src/loader/backends/jpeg.c b/src/loader/backends/jpeg.c
new file mode 100644
index 0000000..7c32196
--- /dev/null
+++ b/src/loader/backends/jpeg.c
@@ -0,0 +1,178 @@
+/*:*
+ * ggaze — libjpeg-turbo direct JPEG backend (progressive low-res preview)
+ *
+ * Two-phase decode: a quick 1/8-scale decode (coarse frame, <50 ms for a
+ * 40 MP JPEG) is emitted via the progress callback, then the full decode
+ * completes and is returned. Uses libjpeg-turbo's jpeg_mem_src + scale_num/
+ * scale_denom for the low-res phase. Compiled when meson feature `jpeg` is on.
+ *
+ * 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 <jpeglib.h>
+#include <setjmp.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#define GGAZE_JPEG_LORES_DENOM 8
+
+struct _jerr_jmp {
+ struct jpeg_error_mgr pub;
+ jmp_buf buf;
+};
+
+static void
+_jerr_exit(j_common_ptr p_cinfo) {
+ struct _jerr_jmp *p_ej = (struct _jerr_jmp *)p_cinfo->err;
+ longjmp(p_ej->buf, 1);
+}
+
+/* Decode JPEG data at the given scale denominator, returning RGBA pixels.
+ * Caller frees *pp_pixels. Returns TRUE on success. */
+static gboolean
+_decode_at_scale(const guint8 *p_data, gsize u_len, int i_denom, int *p_w,
+ int *p_h, guint8 **pp_pixels, GError **p_err) {
+ struct jpeg_decompress_struct cinfo;
+ struct _jerr_jmp jerr;
+ cinfo.err = jpeg_std_error(&jerr.pub);
+ jerr.pub.error_exit = _jerr_exit;
+ if (setjmp(jerr.buf)) {
+ jpeg_destroy_decompress(&cinfo);
+ g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "jpeg: decode error");
+ return (FALSE);
+ }
+ jpeg_create_decompress(&cinfo);
+ jpeg_mem_src(&cinfo, (const unsigned char *)p_data, (unsigned long)u_len);
+ jpeg_read_header(&cinfo, TRUE);
+ cinfo.scale_num = 1;
+ cinfo.scale_denom = (unsigned int)i_denom;
+ cinfo.out_color_space = JCS_RGB;
+ jpeg_start_decompress(&cinfo);
+
+ int i_w = (int)cinfo.output_width;
+ int i_h = (int)cinfo.output_height;
+ int i_rowstride = i_w * 3;
+ guint8 *p_rgb = g_malloc((gsize)i_h * (gsize)i_rowstride);
+
+ while (cinfo.output_scanline < (JDIMENSION)i_h) {
+ guint8 *p_rows[1] = {p_rgb + (gsize)cinfo.output_scanline * i_rowstride};
+ jpeg_read_scanlines(&cinfo, p_rows, 1);
+ }
+ jpeg_finish_decompress(&cinfo);
+ jpeg_destroy_decompress(&cinfo);
+
+ /* Convert RGB → RGBA. */
+ guint8 *p_rgba = g_malloc((gsize)i_w * (gsize)i_h * 4u);
+ for (int i = 0; i < i_w * i_h; i++) {
+ p_rgba[i * 4 + 0] = p_rgb[i * 3 + 0];
+ p_rgba[i * 4 + 1] = p_rgb[i * 3 + 1];
+ p_rgba[i * 4 + 2] = p_rgb[i * 3 + 2];
+ p_rgba[i * 4 + 3] = 255;
+ }
+ g_free(p_rgb);
+ *p_w = i_w;
+ *p_h = i_h;
+ *pp_pixels = p_rgba;
+ return (TRUE);
+}
+
+static GdkTexture *
+_make_texture(int i_w, int i_h, guint8 *p_pixels) {
+ GBytes *p_bytes = g_bytes_new_take(p_pixels, (gsize)i_w * (gsize)i_h * 4u);
+ GdkTexture *p_tex = gdk_memory_texture_new(i_w, i_h, GDK_MEMORY_R8G8B8A8,
+ p_bytes, (gsize)i_w * 4u);
+ g_bytes_unref(p_bytes);
+ return (p_tex);
+}
+
+static gboolean
+_jpeg_can_load(const guint8 *p_head, gsize u_len) {
+ return (detect_format(p_head, u_len) == GGAZE_FMT_JPEG);
+}
+
+static GdkTexture *
+_jpeg_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);
+ }
+ int i_w, i_h;
+ guint8 *p_pixels = NULL;
+ if (!_decode_at_scale((const guint8 *)c_buf, u_len, 1, &i_w, &i_h, &p_pixels,
+ p_err)) {
+ g_free(c_buf);
+ return (NULL);
+ }
+ g_free(c_buf);
+ return (_make_texture(i_w, i_h, p_pixels));
+}
+
+static GdkTexture *
+_jpeg_load_progressive(GFile *p_file, GCancellable *p_cancel,
+ LoaderProgressCb p_progress, gpointer p_progress_data,
+ 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);
+ }
+ /* Phase 1: low-res (1/8 scale). */
+ int i_lw, i_lh;
+ guint8 *p_lp = NULL;
+ if (_decode_at_scale((const guint8 *)c_buf, u_len, GGAZE_JPEG_LORES_DENOM,
+ &i_lw, &i_lh, &p_lp, NULL)) {
+ GdkTexture *p_partial = _make_texture(i_lw, i_lh, p_lp);
+ if (p_progress != NULL) {
+ p_progress(p_partial, p_progress_data);
+ }
+ g_object_unref(p_partial);
+ }
+ /* Phase 2: full decode via GdkPixbuf (applies EXIF orientation). */
+ char *c_path = g_file_get_path(p_file);
+ g_free(c_buf);
+ if (c_path == NULL) {
+ g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED, "jpeg: non-local file");
+ return (NULL);
+ }
+ GError *p_sub = NULL;
+ GdkPixbuf *p_pix = gdk_pixbuf_new_from_file(c_path, &p_sub);
+ g_free(c_path);
+ if (p_pix == NULL) {
+ g_propagate_error(p_err, p_sub);
+ return (NULL);
+ }
+ 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));
+ g_object_unref(p_pix);
+ /* Convert to GdkTexture (RGBA). */
+ int i_w = gdk_pixbuf_get_width(p_use);
+ int i_h = gdk_pixbuf_get_height(p_use);
+ GdkPixbuf *p_rgba = gdk_pixbuf_get_has_alpha(p_use)
+ ? GDK_PIXBUF(g_object_ref(p_use))
+ : gdk_pixbuf_add_alpha(p_use, FALSE, 0, 0, 0);
+ int i_rowstride = gdk_pixbuf_get_rowstride(p_rgba);
+ guchar *p_px = gdk_pixbuf_get_pixels(p_rgba);
+ gsize u_len2 = (gsize)(i_h - 1) * (gsize)i_rowstride + (gsize)i_w * 4u;
+ GBytes *p_bytes = g_bytes_new_with_free_func(
+ p_px, u_len2, (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);
+ g_object_unref(p_use);
+ return (p_tex);
+}
+
+const GgazeLoaderBackend jpeg_backend = {
+ .can_load = _jpeg_can_load,
+ .load = _jpeg_load,
+ .load_progressive = _jpeg_load_progressive,
+}; \ No newline at end of file
diff --git a/src/loader/loader.c b/src/loader/loader.c
index 9721c2a..65cc4cb 100644
--- a/src/loader/loader.c
+++ b/src/loader/loader.c
@@ -28,7 +28,6 @@ extern const GgazeLoaderBackend avif_backend;
#ifdef HAVE_HEIF
extern const GgazeLoaderBackend heif_backend;
#endif
-
static const GgazeLoaderBackend *BACKENDS[] = {
#ifdef HAVE_JXL
&jxl_backend,
@@ -70,7 +69,7 @@ loader_load(GFile *p_file, GCancellable *p_cancel, GError **p_err) {
guint8 head[GGAZE_SNIFF_LEN];
gsize u_read = _read_header(p_file, p_cancel, head, GGAZE_SNIFF_LEN, p_err);
- if (u_read == 0 && p_err != NULL && *p_err != NULL) {
+ if (u_read == 0 && p_err != NULL) {
return (NULL);
}
@@ -87,12 +86,54 @@ loader_load(GFile *p_file, GCancellable *p_cancel, GError **p_err) {
/* --- async wrapper (M3) -------------------------------------------------- */
+typedef struct {
+ LoaderProgressCb p_cb;
+ gpointer p_data;
+} ProgressPair;
+
static void
_load_task_thread(GTask *p_task, gpointer p_src, gpointer p_task_data,
GCancellable *p_cancel) {
- (void)p_task_data;
- GError *p_err = NULL;
- GdkTexture *p_tex = loader_load((GFile *)p_src, p_cancel, &p_err);
+ ProgressPair *p_pair = (ProgressPair *)p_task_data;
+ GError *p_err = NULL;
+ GdkTexture *p_tex = NULL;
+
+ /* Sniff and dispatch to the first matching backend. */
+ guint8 head[GGAZE_SNIFF_LEN];
+ gsize u_read =
+ _read_header((GFile *)p_src, p_cancel, head, GGAZE_SNIFF_LEN, &p_err);
+ if (u_read == 0 && p_err != NULL) {
+ g_task_return_error(p_task, p_err);
+ return;
+ }
+ gboolean b_found = FALSE;
+#ifdef HAVE_JPEG
+ extern const GgazeLoaderBackend jpeg_backend;
+ if (detect_format(head, u_read) == GGAZE_FMT_JPEG && p_pair != NULL) {
+ p_tex = jpeg_backend.load_progressive(
+ (GFile *)p_src, p_cancel, p_pair->p_cb, p_pair->p_data, &p_err);
+ b_found = TRUE;
+ }
+#endif
+ if (!b_found) {
+ for (gsize u_i = 0; u_i < G_N_ELEMENTS(BACKENDS); u_i++) {
+ if (BACKENDS[u_i]->can_load(head, u_read)) {
+ if (BACKENDS[u_i]->load_progressive != NULL && p_pair != NULL) {
+ p_tex = BACKENDS[u_i]->load_progressive((GFile *)p_src, p_cancel,
+ p_pair->p_cb,
+ p_pair->p_data, &p_err);
+ } else {
+ p_tex = BACKENDS[u_i]->load((GFile *)p_src, p_cancel, &p_err);
+ }
+ b_found = TRUE;
+ break;
+ }
+ }
+ } /* end if (!b_found) */
+ if (!b_found) {
+ g_set_error(&p_err, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ "unsupported or unrecognized image format");
+ }
if (p_tex == NULL) {
g_task_return_error(p_task, p_err);
} else {
@@ -102,9 +143,16 @@ _load_task_thread(GTask *p_task, gpointer p_src, gpointer p_task_data,
void
loader_load_async(GFile *p_file, GCancellable *p_cancel,
+ LoaderProgressCb p_progress, gpointer p_progress_data,
GAsyncReadyCallback p_cb, gpointer p_data) {
g_return_if_fail(G_IS_FILE(p_file));
GTask *p_task = g_task_new(p_file, p_cancel, p_cb, p_data);
+ if (p_progress != NULL) {
+ ProgressPair *p_pair = g_new(ProgressPair, 1);
+ p_pair->p_cb = p_progress;
+ p_pair->p_data = p_progress_data;
+ g_task_set_task_data(p_task, p_pair, (GDestroyNotify)g_free);
+ }
g_task_run_in_thread(p_task, _load_task_thread);
g_object_unref(p_task);
}
diff --git a/src/loader/loader.h b/src/loader/loader.h
index bfee29d..c2228f5 100644
--- a/src/loader/loader.h
+++ b/src/loader/loader.h
@@ -23,11 +23,19 @@
G_BEGIN_DECLS
+/* Optional progressive-load callback (called from the worker thread with a
+ * low-res partial texture before the full decode completes). */
+typedef void (*LoaderProgressCb)(GdkTexture *p_partial, gpointer p_data);
+
/* A loader backend. Compiled in conditionally (meson feature options) and
* registered with the loader at link time. */
typedef struct {
gboolean (*can_load)(const guint8 *p_head, gsize u_len);
GdkTexture *(*load)(GFile *p_file, GCancellable *p_cancel, GError **p_err);
+ /* Optional: two-phase load (low-res first via p_progress, then full). */
+ GdkTexture *(*load_progressive)(GFile *p_file, GCancellable *p_cancel,
+ LoaderProgressCb p_progress,
+ gpointer p_progress_data, GError **p_err);
} GgazeLoaderBackend;
/* Backends register a const instance; the dispatcher (loader.c) iterates
@@ -45,6 +53,7 @@ GdkTexture *loader_load(GFile *p_file, GCancellable *p_cancel, GError **p_err);
* p_file, so the finish callback can check it against navigator.current
* (last-write-wins). */
void loader_load_async(GFile *p_file, GCancellable *p_cancel,
+ LoaderProgressCb p_progress, gpointer p_progress_data,
GAsyncReadyCallback p_cb, gpointer p_data);
/* Finish an async load; returns the GdkTexture (transfer full) or NULL. */