diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/loader/backends/jpeg.c | 178 | ||||
| -rw-r--r-- | src/loader/loader.c | 58 | ||||
| -rw-r--r-- | src/loader/loader.h | 9 | ||||
| -rw-r--r-- | src/window.c | 34 |
4 files changed, 271 insertions, 8 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. */ diff --git a/src/window.c b/src/window.c index 4c26ea7..688f879 100644 --- a/src/window.c +++ b/src/window.c @@ -464,6 +464,34 @@ _prefetch_finish_cb(GObject *p_src, GAsyncResult *p_res, gpointer p_data) { g_object_unref(p_win); } +/* --- M6: progressive low-res preview ------------------------------------ */ + +typedef struct { + GgazeWindow *p_win; + GdkTexture *p_tex; +} ProgressInvoke; + +static gboolean +_on_progress_main(gpointer p_data) { + ProgressInvoke *p_pi = (ProgressInvoke *)p_data; + /* Show the partial; the full result replaces it in _load_finish_cb. */ + ggaze_viewer_set_texture(GGAZE_VIEWER(p_pi->p_win->p_viewer), p_pi->p_tex); + g_object_unref(p_pi->p_tex); + g_object_unref(p_pi->p_win); + g_free(p_pi); + return (G_SOURCE_REMOVE); +} + +static void +_load_progress_cb(GdkTexture *p_partial, gpointer p_data) { + GgazeWindow *p_win = GGAZE_WINDOW(p_data); + ProgressInvoke *p_pi = g_new(ProgressInvoke, 1); + p_pi->p_win = (GgazeWindow *)g_object_ref(p_win); + p_pi->p_tex = (GdkTexture *)g_object_ref(p_partial); + g_main_context_invoke_full(NULL, G_PRIORITY_DEFAULT, _on_progress_main, p_pi, + NULL); +} + /* Visible-load callback: show only if this is still the current file * (last-write-wins), then cache it and prefetch neighbours. */ static void @@ -518,7 +546,7 @@ _prefetch(GgazeWindow *p_win) { } GFile *p_file = navigator_get_file(p_win->p_nav, (guint)i_j); if (p_file != NULL && texturecache_get(p_win->p_cache, p_file) == NULL) { - loader_load_async(p_file, p_win->p_prefetch_cancel, + loader_load_async(p_file, p_win->p_prefetch_cancel, NULL, NULL, _prefetch_finish_cb, g_object_ref(p_win)); } } @@ -554,8 +582,8 @@ _load_current(GgazeWindow *p_win) { g_cancellable_cancel(p_win->p_cancel); g_clear_object(&p_win->p_cancel); p_win->p_cancel = g_cancellable_new(); - loader_load_async(p_cur, p_win->p_cancel, _load_finish_cb, - g_object_ref(p_win)); + loader_load_async(p_cur, p_win->p_cancel, _load_progress_cb, + g_object_ref(p_win), _load_finish_cb, g_object_ref(p_win)); _update_header(p_win); } |
