summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/app.c15
-rw-r--r--src/enhancer.c144
-rw-r--r--src/enhancer.h20
-rw-r--r--src/shortcuts.c2
-rw-r--r--src/window.c213
-rw-r--r--tests/lsan_suppressions.txt5
-rw-r--r--tests/meson.build2
-rw-r--r--tests/test_enhancer.c185
-rw-r--r--tests/test_shortcut.c15
9 files changed, 577 insertions, 24 deletions
diff --git a/src/app.c b/src/app.c
index 587feb7..4bd351b 100644
--- a/src/app.c
+++ b/src/app.c
@@ -18,6 +18,10 @@
#include <glib.h>
+#if GGAZE_HAVE_GEGL
+#include <gegl.h>
+#endif
+
struct _GgazeApp {
AdwApplication parent_instance;
};
@@ -41,6 +45,16 @@ ggaze_app_activate(GApplication *p_app) {
}
static void
+ggaze_app_startup(GApplication *p_app) {
+ G_APPLICATION_CLASS(ggaze_app_parent_class)->startup(p_app);
+#if GGAZE_HAVE_GEGL
+ /* Init GEGL once before any window is created (before the first enhance
+ * call). Idempotent if already initialised. */
+ gegl_init(NULL, NULL);
+#endif
+}
+
+static void
ggaze_app_open(GApplication *p_app, GFile **p_files, gint n_files,
const gchar *c_hint) {
(void)c_hint;
@@ -62,6 +76,7 @@ ggaze_app_init(GgazeApp *p_app) {
static void
ggaze_app_class_init(GgazeAppClass *p_klass) {
GApplicationClass *p_app_class = G_APPLICATION_CLASS(p_klass);
+ p_app_class->startup = ggaze_app_startup;
p_app_class->activate = ggaze_app_activate;
p_app_class->open = ggaze_app_open;
}
diff --git a/src/enhancer.c b/src/enhancer.c
index b195035..aea40f9 100644
--- a/src/enhancer.c
+++ b/src/enhancer.c
@@ -1,5 +1,10 @@
/* enhancer.c — GEGL quick-enhance presets (optional, feature-gated). */
#include "enhancer.h"
+#include "ggaze-config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <string.h>
struct Enhancer {
GPtrArray *p_presets;
@@ -140,38 +145,151 @@ enhancer_apply(Enhancer *e, GeglBuffer *p_in, const EnhancerPreset *p_preset,
return NULL;
}
+/* Pick the GEGL saver op and (for jpeg) quality from the output extension.
+ * Returns the op name, or NULL if the extension is unsupported / the op is
+ * not installed. ju0: never write JPEG bytes into a .png. */
+static const char *
+_saver_for_ext(GFile *p_out) {
+ char *c_base = g_file_get_basename(p_out);
+ const char *c_dot = strrchr(c_base, '.');
+ const char *c_op = NULL;
+ if (c_dot != NULL) {
+ if (g_ascii_strcasecmp(c_dot, ".jpg") == 0 ||
+ g_ascii_strcasecmp(c_dot, ".jpeg") == 0) {
+ c_op = "gegl:jpg-save";
+ } else if (g_ascii_strcasecmp(c_dot, ".png") == 0) {
+ c_op = "gegl:png-save";
+ } else if (g_ascii_strcasecmp(c_dot, ".webp") == 0) {
+ c_op = "gegl:webp-save";
+ }
+ }
+ g_free(c_base);
+ /* webp-save ships as a plugin; only promise it if installed. */
+ if (c_op != NULL && !gegl_has_operation(c_op)) {
+ return NULL;
+ }
+ return c_op;
+}
+
gboolean
enhancer_export(Enhancer *e, GeglBuffer *p_in, const EnhancerPreset *p_preset,
GFile *p_out, GError **p_err) {
(void)e;
- (void)p_preset;
g_return_val_if_fail(p_in != NULL, FALSE);
g_return_val_if_fail(p_out != NULL, FALSE);
- /* Apply the preset first. */
- GeglBuffer *p_buf = enhancer_apply(NULL, p_in, p_preset, p_err);
- if (p_buf == NULL)
- return FALSE;
-
char *c_path = g_file_get_path(p_out);
if (c_path == NULL) {
- g_object_unref(p_buf);
g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED,
"enhancer: non-local export path");
return FALSE;
}
+ const char *c_op = _saver_for_ext(p_out);
+ if (c_op == NULL) {
+ g_free(c_path);
+ g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ "enhancer: unsupported export extension");
+ return FALSE;
+ }
+
+ /* Apply the preset first. */
+ GeglBuffer *p_buf = enhancer_apply(NULL, p_in, p_preset, p_err);
+ if (p_buf == NULL) {
+ g_free(c_path);
+ return FALSE;
+ }
+
+ /* ku0: capture the save's real success. Record the output's pre-state so
+ * a pre-existing file can't masquerade as a successful save. */
+ GStatBuf st_before;
+ gboolean b_existed = (g_stat(c_path, &st_before) == 0);
+
GeglNode *p_graph = gegl_node_new();
GeglNode *p_src = gegl_node_new_child(
p_graph, "operation", "gegl:buffer-source", "buffer", p_buf, NULL);
- GeglNode *p_save = gegl_node_new_child(p_graph, "operation", "gegl:jpg-save",
- "path", c_path, NULL);
+ GeglNode *p_save;
+ if (g_str_equal(c_op, "gegl:jpg-save")) {
+ p_save = gegl_node_new_child(p_graph, "operation", c_op, "path", c_path,
+ "quality", 95, NULL);
+ } else {
+ p_save =
+ gegl_node_new_child(p_graph, "operation", c_op, "path", c_path, NULL);
+ }
gegl_node_link(p_src, p_save);
gegl_node_process(p_save);
- gboolean b_ok = g_file_test(c_path, G_FILE_TEST_EXISTS);
-
g_object_unref(p_graph);
g_object_unref(p_buf);
+
+ /* Verify the save actually produced a non-empty file newer than before. */
+ GStatBuf st_after;
+ gboolean b_ok = FALSE;
+ if (g_stat(c_path, &st_after) == 0 && st_after.st_size > 0) {
+ if (!b_existed || st_after.st_mtime != st_before.st_mtime ||
+ st_after.st_size != st_before.st_size) {
+ b_ok = TRUE;
+ }
+ }
g_free(c_path);
- return b_ok;
-} \ No newline at end of file
+ if (!b_ok) {
+ g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "enhancer: export produced no valid file");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+#if GGAZE_HAVE_GEGL
+
+GeglBuffer *
+enhancer_load(GFile *p_file, GError **p_err) {
+ g_return_val_if_fail(p_file != NULL, NULL);
+ 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,
+ "enhancer: non-local load path");
+ return NULL;
+ }
+ GeglBuffer *p_buf = NULL;
+ GeglNode *p_graph = gegl_node_new();
+ GeglNode *p_load = gegl_node_new_child(p_graph, "operation", "gegl:load",
+ "path", c_path, NULL);
+ GeglNode *p_sink = gegl_node_new_child(
+ p_graph, "operation", "gegl:buffer-sink", "buffer", &p_buf, NULL);
+ gegl_node_link(p_load, p_sink);
+ gegl_node_process(p_sink);
+ g_object_unref(p_graph);
+ if (p_buf == NULL) {
+ g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "enhancer: failed to load %s", c_path);
+ g_free(c_path);
+ return NULL;
+ }
+ g_free(c_path);
+ return p_buf;
+}
+
+GdkTexture *
+enhancer_buffer_to_texture(GeglBuffer *p_buf, GError **p_err) {
+ g_return_val_if_fail(p_buf != NULL, NULL);
+ gint i_w = gegl_buffer_get_width(p_buf);
+ gint i_h = gegl_buffer_get_height(p_buf);
+ if (i_w <= 0 || i_h <= 0) {
+ g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_FAILED,
+ "enhancer: empty buffer");
+ return NULL;
+ }
+ const Babl *p_fmt = babl_format("R'G'B'A u8");
+ gint i_stride = i_w * 4;
+ gsize u_size = (gsize)i_stride * (gsize)i_h;
+ gpointer p_data = g_malloc(u_size);
+ GeglRectangle rect = {0, 0, i_w, i_h};
+ gegl_buffer_get(p_buf, &rect, 1.0, p_fmt, p_data, i_stride, GEGL_ABYSS_NONE);
+ GBytes *p_bytes = g_bytes_new_take(p_data, u_size);
+ GdkTexture *p_tex = gdk_memory_texture_new(
+ i_w, i_h, GDK_MEMORY_R8G8B8A8_PREMULTIPLIED, p_bytes, i_stride);
+ g_bytes_unref(p_bytes);
+ return p_tex;
+}
+
+#endif /* GGAZE_HAVE_GEGL */ \ No newline at end of file
diff --git a/src/enhancer.h b/src/enhancer.h
index fce3fe7..f466d21 100644
--- a/src/enhancer.h
+++ b/src/enhancer.h
@@ -1,6 +1,9 @@
#ifndef GGAZE_ENHANCER_H
#define GGAZE_ENHANCER_H
+#include "ggaze-config.h"
+
+#include <gdk/gdk.h>
#include <gio/gio.h>
#include <glib.h>
#include <gegl.h>
@@ -25,12 +28,25 @@ const GPtrArray *enhancer_get_presets(Enhancer *p_e);
GeglBuffer *enhancer_apply(Enhancer *p_e, GeglBuffer *p_in,
const EnhancerPreset *p_preset, GError **p_err);
-/* Export the enhanced buffer to a file (JPEG quality 95, EXIF Orientation=1).
- * Returns TRUE on success. */
+/* Export the enhanced buffer to a file. The saver is chosen from p_out's
+ * extension: .jpg/.jpeg -> gegl:jpg-save (quality 95), .png -> gegl:png-save,
+ * .webp -> gegl:webp-save (if available). Other extensions fail with
+ * G_IO_ERROR_NOT_SUPPORTED. Success is verified by a real stat of the output
+ * (not pre-existence). Returns TRUE on success. */
gboolean enhancer_export(Enhancer *p_e, GeglBuffer *p_in,
const EnhancerPreset *p_preset, GFile *p_out,
GError **p_err);
+#if GGAZE_HAVE_GEGL
+/* Load a file into a GeglBuffer via the gegl:load op. Returns a new buffer
+ * (caller unrefs) or NULL with p_err set. */
+GeglBuffer *enhancer_load(GFile *p_file, GError **p_err);
+
+/* Convert a GeglBuffer to a GdkTexture for preview (RGBA8 bytes). Returns a
+ * new GdkTexture (caller unrefs) or NULL with p_err set. Needs no display. */
+GdkTexture *enhancer_buffer_to_texture(GeglBuffer *p_buf, GError **p_err);
+#endif /* GGAZE_HAVE_GEGL */
+
G_END_DECLS
#endif \ No newline at end of file
diff --git a/src/shortcuts.c b/src/shortcuts.c
index 8ea978d..60c0c22 100644
--- a/src/shortcuts.c
+++ b/src/shortcuts.c
@@ -35,6 +35,8 @@ static const ShortcutEntry SHORTCUTS[] = {
{GDK_KEY_t, 0, "win.toggle-view"},
{GDK_KEY_v, 0, "win.mark"},
{GDK_KEY_a, GDK_CONTROL_MASK, "win.mark-all"},
+ {GDK_KEY_a, 0, "win.enhance"},
+ {GDK_KEY_s, 0, "win.enhance-save"},
{GDK_KEY_question, 0, "win.shortcuts"},
{GDK_KEY_plus, 0, "win.zoom-in"},
{GDK_KEY_equal, 0, "win.zoom-in"},
diff --git a/src/window.c b/src/window.c
index 2810d0e..4e4e253 100644
--- a/src/window.c
+++ b/src/window.c
@@ -15,8 +15,11 @@
#include <adwaita.h>
#include <glib.h>
+#include <glib/gstdio.h>
+#include <string.h>
#include <gtk/gtk.h>
+#include "ggaze-config.h"
#include "gridview.h"
#include "info.h"
#include "loader/loader.h"
@@ -26,6 +29,9 @@
#include "thumbnail.h"
#include "trash.h"
#include "viewer.h"
+#if GGAZE_HAVE_GEGL
+#include "enhancer.h"
+#endif
struct _GgazeWindow {
GtkApplicationWindow parent_instance;
@@ -45,6 +51,10 @@ struct _GgazeWindow {
guint u_slideshow; /* slideshow timeout id (0=off) */
gboolean b_fullscreen;
guint u_hdr_hide; /* fullscreen header auto-hide timeout */
+#if GGAZE_HAVE_GEGL
+ int i_enhance_idx; /* active preset index (-1 = original) */
+ Enhancer *p_enhancer; /* GEGL preset engine (NULL w/o GEGL) */
+#endif
};
G_DEFINE_TYPE(GgazeWindow, ggaze_window, GTK_TYPE_APPLICATION_WINDOW)
@@ -52,6 +62,7 @@ G_DEFINE_TYPE(GgazeWindow, ggaze_window, GTK_TYPE_APPLICATION_WINDOW)
/* --- forward decls ------------------------------------------------------- */
static void _load_current(GgazeWindow *p_win);
static void _prefetch(GgazeWindow *p_win);
+static void _show_texture(GgazeWindow *p_win, GdkTexture *p_tex);
static void _update_header(GgazeWindow *p_win);
static void _on_grid_activate(GgazeGrid *p_grid, gpointer p_data);
static void _show_info(GgazeWindow *p_win);
@@ -440,6 +451,24 @@ static const char *SHORTCUTS_UI =
" </child>\n"
" <child>\n"
" <object class=\"GtkShortcutsGroup\">\n"
+ " <property name=\"title\">Enhance</property>\n"
+ " <child>\n"
+ " <object class=\"GtkShortcutsShortcut\">\n"
+ " <property name=\"accelerator\">a</property>\n"
+ " <property name=\"title\">Cycle enhance preset "
+ "(preview)</property>\n"
+ " </object>\n"
+ " </child>\n"
+ " <child>\n"
+ " <object class=\"GtkShortcutsShortcut\">\n"
+ " <property name=\"accelerator\">s</property>\n"
+ " <property name=\"title\">Save enhanced copy</property>\n"
+ " </object>\n"
+ " </child>\n"
+ " </object>\n"
+ " </child>\n"
+ " <child>\n"
+ " <object class=\"GtkShortcutsGroup\">\n"
" <property name=\"title\">Zoom</property>\n"
" <child>\n"
" <object class=\"GtkShortcutsShortcut\">\n"
@@ -580,6 +609,156 @@ _action_back(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
}
}
+#if GGAZE_HAVE_GEGL
+/* win.enhance (key 'a'): cycle the active enhance preset and preview it on
+ * the current image. Synchronous - may briefly block the UI on large images
+ * (acceptable for v1; async apply is a later milestone). On any failure reset
+ * to the original. */
+static void
+_action_enhance(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
+ (void)p_a;
+ (void)p_v;
+ GgazeWindow *p_win = GGAZE_WINDOW(p_data);
+ if (p_win->p_nav == NULL || p_win->p_enhancer == NULL) {
+ return;
+ }
+ /* Only meaningful in large view; switch there first so the result shows. */
+ const char *c_cur =
+ gtk_stack_get_visible_child_name(GTK_STACK(p_win->p_stack));
+ if (g_strcmp0(c_cur, "large") != 0) {
+ gtk_stack_set_visible_child_name(GTK_STACK(p_win->p_stack), "large");
+ }
+
+ const GPtrArray *p_presets = enhancer_get_presets(p_win->p_enhancer);
+ gint i_n = (gint)(p_presets != NULL ? p_presets->len : 0);
+ gint i_next = p_win->i_enhance_idx + 1;
+ if (i_next >= i_n) {
+ i_next = -1; /* wrap back to original */
+ }
+ p_win->i_enhance_idx = i_next;
+
+ if (i_next < 0) {
+ _load_current(p_win); /* restore original (texturecache makes it fast) */
+ _update_header(p_win);
+ return;
+ }
+
+ GFile *p_file = navigator_get_current(p_win->p_nav);
+ if (p_file == NULL) {
+ p_win->i_enhance_idx = -1;
+ _update_header(p_win);
+ return;
+ }
+
+ GError *p_err = NULL;
+ GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
+ GdkTexture *p_tex = NULL;
+ if (p_buf != NULL) {
+ const EnhancerPreset *p_preset =
+ g_ptr_array_index((GPtrArray *)p_presets, (guint)i_next);
+ GeglBuffer *p_enh =
+ enhancer_apply(p_win->p_enhancer, p_buf, p_preset, &p_err);
+ if (p_enh != NULL) {
+ p_tex = enhancer_buffer_to_texture(p_enh, &p_err);
+ g_object_unref(p_enh);
+ }
+ g_object_unref(p_buf);
+ }
+ if (p_tex == NULL) {
+ g_warning("ggaze: enhance failed: %s",
+ p_err != NULL ? p_err->message : "(no detail)");
+ g_clear_error(&p_err);
+ p_win->i_enhance_idx = -1;
+ _load_current(p_win);
+ } else {
+ _show_texture(p_win, p_tex);
+ g_object_unref(p_tex);
+ }
+ _update_header(p_win);
+}
+
+/* win.enhance-save (key 's'): export the current image with the active preset
+ * to <stem>-enhanced.<ext>. Never overwrites the original. No-op (with a
+ * warning) when no preview is active. */
+static void
+_action_enhance_save(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
+ (void)p_a;
+ (void)p_v;
+ GgazeWindow *p_win = GGAZE_WINDOW(p_data);
+ if (p_win->p_nav == NULL || p_win->p_enhancer == NULL) {
+ return;
+ }
+ if (p_win->i_enhance_idx < 0) {
+ g_warning("ggaze: nothing to save (no enhance preview active)");
+ return;
+ }
+ GFile *p_file = navigator_get_current(p_win->p_nav);
+ if (p_file == NULL) {
+ return;
+ }
+
+ /* Build <stem>-enhanced.<ext>; default .jpg for unknown extensions. */
+ char *c_base = g_file_get_basename(p_file);
+ char *c_dot = strrchr(c_base, '.');
+ const char *c_ext = ".jpg";
+ if (c_dot != NULL && (g_ascii_strcasecmp(c_dot, ".jpg") == 0 ||
+ g_ascii_strcasecmp(c_dot, ".jpeg") == 0 ||
+ g_ascii_strcasecmp(c_dot, ".png") == 0 ||
+ g_ascii_strcasecmp(c_dot, ".webp") == 0)) {
+ c_ext = c_dot;
+ }
+ char *c_stem;
+ if (c_dot != NULL && c_ext == c_dot) {
+ c_stem = g_strndup(c_base, (gsize)(c_dot - c_base));
+ } else {
+ c_stem = g_strdup(c_base);
+ }
+ GFile *p_dir = g_file_get_parent(p_file);
+ char *c_outname = g_strdup_printf("%s-enhanced%s", c_stem, c_ext);
+ GFile *p_out = g_file_get_child(p_dir, c_outname);
+ g_free(c_outname);
+ g_free(c_stem);
+ g_free(c_base);
+ g_object_unref(p_dir);
+
+ GError *p_err = NULL;
+ GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
+ gboolean b_ok = FALSE;
+ if (p_buf != NULL) {
+ const GPtrArray *p_presets = enhancer_get_presets(p_win->p_enhancer);
+ const EnhancerPreset *p_preset =
+ g_ptr_array_index((GPtrArray *)p_presets, (guint)p_win->i_enhance_idx);
+ b_ok = enhancer_export(p_win->p_enhancer, p_buf, p_preset, p_out, &p_err);
+ g_object_unref(p_buf);
+ }
+ char *c_saved = b_ok ? g_file_get_basename(p_out) : NULL;
+ g_object_unref(p_out);
+ if (b_ok) {
+ g_printerr("ggaze: saved %s\n", c_saved);
+ } else {
+ g_warning("ggaze: enhance-save failed: %s",
+ p_err != NULL ? p_err->message : "(no detail)");
+ }
+ g_free(c_saved);
+ g_clear_error(&p_err);
+}
+#else /* !GGAZE_HAVE_GEGL */
+static void
+_action_enhance(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
+ (void)p_a;
+ (void)p_v;
+ (void)p_data;
+ g_warning("ggaze: GEGL not built in");
+}
+static void
+_action_enhance_save(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) {
+ (void)p_a;
+ (void)p_v;
+ (void)p_data;
+ g_warning("ggaze: GEGL not built in");
+}
+#endif /* GGAZE_HAVE_GEGL */
+
static gboolean
_slideshow_tick(gpointer p_data) {
GgazeWindow *p_win = GGAZE_WINDOW(p_data);
@@ -646,6 +825,8 @@ static const GActionEntry ACTIONS[] = {
{.name = "slideshow", .activate = _action_slideshow},
{.name = "info", .activate = _action_info},
{.name = "back", .activate = _action_back},
+ {.name = "enhance", .activate = _action_enhance},
+ {.name = "enhance-save", .activate = _action_enhance_save},
};
/* --- drop target --------------------------------------------------------- */
@@ -675,7 +856,13 @@ drop_cb(GtkDropTarget *p_t, const GValue *p_val, gdouble d_x, gdouble d_y,
static void
nav_changed_cb(Navigator *p_nav, gpointer p_data) {
(void)p_nav;
- _load_current(GGAZE_WINDOW(p_data));
+ GgazeWindow *p_win = GGAZE_WINDOW(p_data);
+#if GGAZE_HAVE_GEGL
+ /* New image starts fresh: drop any enhanced preview so we don't show a
+ * stale enhanced texture for a different file. */
+ p_win->i_enhance_idx = -1;
+#endif
+ _load_current(p_win);
}
static void
@@ -866,6 +1053,23 @@ _update_header(GgazeWindow *p_win) {
c_title = c_tmp;
}
}
+#if GGAZE_HAVE_GEGL
+ /* Append the active enhance preset name when a preview is showing. */
+ if (p_win->i_enhance_idx >= 0 && p_win->p_enhancer != NULL &&
+ c_title != NULL) {
+ const GPtrArray *p_presets = enhancer_get_presets(p_win->p_enhancer);
+ if (p_presets != NULL && (guint)p_win->i_enhance_idx < p_presets->len) {
+ const EnhancerPreset *p_preset = g_ptr_array_index(
+ (GPtrArray *)p_presets, (guint)p_win->i_enhance_idx);
+ if (p_preset->c_name != NULL) {
+ char *c_tmp =
+ g_strdup_printf("%s \u00b7 %s", c_title, p_preset->c_name);
+ g_free(c_title);
+ c_title = c_tmp;
+ }
+ }
+ }
+#endif
if (c_title == NULL) {
c_title = g_strdup("ggaze");
}
@@ -904,6 +1108,9 @@ ggaze_window_dispose(GObject *p_obj) {
g_clear_pointer(&p_win->p_cache, texturecache_delete);
g_clear_pointer(&p_win->p_trash, trash_delete);
g_clear_pointer(&p_win->p_thumb, thumbnail_delete);
+#if GGAZE_HAVE_GEGL
+ g_clear_pointer(&p_win->p_enhancer, enhancer_delete);
+#endif
/* p_stack/p_viewer/p_grid are GtkWidgets parented to the window; GTK
* releases them. */
G_OBJECT_CLASS(ggaze_window_parent_class)->dispose(p_obj);
@@ -951,6 +1158,10 @@ ggaze_window_init(GgazeWindow *p_win) {
p_win->p_trash = NULL; /* created on open */
p_win->p_grid = NULL; /* created on open */
p_win->i_grid_size = 128;
+#if GGAZE_HAVE_GEGL
+ p_win->i_enhance_idx = -1; /* start on the original */
+ p_win->p_enhancer = enhancer_new();
+#endif
/* Header bar (libadwaita, decision #29). */
GtkWidget *p_header = adw_header_bar_new();
diff --git a/tests/lsan_suppressions.txt b/tests/lsan_suppressions.txt
index ac2c4db..27d2450 100644
--- a/tests/lsan_suppressions.txt
+++ b/tests/lsan_suppressions.txt
@@ -7,3 +7,8 @@ leak:babl
leak:gegl_node_new_child
leak:gegl_node_new
leak:gegl_operation
+# GEGL's jpg-load plugin leaks a gio source buffer during jpeg_read_header
+# (third-party, in jpg-load.so).
+leak:gio_source_init
+leak:gegl_jpg_load
+leak:jpg-load
diff --git a/tests/meson.build b/tests/meson.build
index 6a93277..e895210 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -147,7 +147,7 @@ if gegl_dep.found()
test_enhancer = executable(
'test_enhancer', ['test_enhancer.c', ggaze_conf_h],
include_directories : [inc, src_inc],
- dependencies : [glib_dep, gegl_dep],
+ dependencies : [glib_dep, gegl_dep, gtk4_dep],
link_with : ggaze_lib, install : false,
)
enhancer_env = environment()
diff --git a/tests/test_enhancer.c b/tests/test_enhancer.c
index 83768a2..1445c41 100644
--- a/tests/test_enhancer.c
+++ b/tests/test_enhancer.c
@@ -79,11 +79,196 @@ test_export(void) {
g_free(tmp);
}
+/* /enhancer/load_and_to_texture: load a fixture via the gegl:load bridge and
+ * convert it to a GdkTexture (no display needed). */
+static void
+test_load_and_to_texture(void) {
+ const gchar *c_fx = g_getenv("GGAZE_FIXTURES_DIR");
+ g_assert_nonnull(c_fx);
+ char *c_path = g_build_filename(c_fx, "plain.jpg", NULL);
+ GFile *p_file = g_file_new_for_path(c_path);
+ g_free(c_path);
+
+ GError *p_err = NULL;
+ GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
+ g_assert_no_error(p_err);
+ g_assert_nonnull(p_buf);
+ g_assert_cmpint(gegl_buffer_get_width(p_buf), >, 0);
+ g_assert_cmpint(gegl_buffer_get_height(p_buf), >, 0);
+
+ GdkTexture *p_tex = enhancer_buffer_to_texture(p_buf, &p_err);
+ g_assert_no_error(p_err);
+ g_assert_nonnull(p_tex);
+ g_assert_true(GDK_IS_TEXTURE(p_tex));
+ g_assert_cmpint(gdk_texture_get_width(p_tex), >, 0);
+ g_assert_cmpint(gdk_texture_get_height(p_tex), >, 0);
+
+ g_object_unref(p_tex);
+ g_object_unref(p_buf);
+ g_object_unref(p_file);
+}
+
+/* /enhancer/export_format: apply Auto-fix and export to .png and .jpg,
+ * asserting the file signatures (catches ju0 — never write JPEG into a .png).
+ * Skips gracefully if the saver op is unavailable. */
+static void
+test_export_format(void) {
+ const gchar *c_fx = g_getenv("GGAZE_FIXTURES_DIR");
+ g_assert_nonnull(c_fx);
+ char *c_path = g_build_filename(c_fx, "plain.jpg", NULL);
+ GFile *p_file = g_file_new_for_path(c_path);
+ g_free(c_path);
+
+ GError *p_err = NULL;
+ GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
+ g_assert_nonnull(p_buf);
+
+ Enhancer *e = enhancer_new();
+ const EnhancerPreset *preset =
+ g_ptr_array_index((GPtrArray *)enhancer_get_presets(e), 0); /* Auto-fix */
+ char *tmp = g_dir_make_tmp("ggaze-fmt-XXXXXX", NULL);
+
+ /* PNG */
+ {
+ char *c_p = g_build_filename(tmp, "out.png", NULL);
+ GFile *p_out = g_file_new_for_path(c_p);
+ g_clear_error(&p_err);
+ gboolean ok = enhancer_export(e, p_buf, preset, p_out, &p_err);
+ if (ok) {
+ gchar *data = NULL;
+ gsize len = 0;
+ g_assert_true(g_file_get_contents(c_p, &data, &len, NULL));
+ g_assert_cmpint(len, >=, 8);
+ g_assert_cmpmem(data, 8, "\x89PNG\r\n\x1a\n", 8);
+ g_free(data);
+ } else {
+ g_clear_error(&p_err);
+ }
+ g_object_unref(p_out);
+ g_free(c_p);
+ }
+
+ /* JPEG */
+ {
+ char *c_p = g_build_filename(tmp, "out.jpg", NULL);
+ GFile *p_out = g_file_new_for_path(c_p);
+ g_clear_error(&p_err);
+ gboolean ok = enhancer_export(e, p_buf, preset, p_out, &p_err);
+ if (ok) {
+ gchar *data = NULL;
+ gsize len = 0;
+ g_assert_true(g_file_get_contents(c_p, &data, &len, NULL));
+ g_assert_cmpint(len, >=, 2);
+ g_assert_cmpmem(data, 2, "\xff\xd8", 2);
+ g_free(data);
+ } else {
+ g_clear_error(&p_err);
+ }
+ g_object_unref(p_out);
+ g_free(c_p);
+ }
+
+ g_object_unref(p_buf);
+ g_object_unref(p_file);
+ enhancer_delete(e);
+
+ /* Cleanup tmp. */
+ GFile *td = g_file_new_for_path(tmp);
+ GFileEnumerator *en = g_file_enumerate_children(
+ td, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL);
+ if (en) {
+ GFileInfo *i;
+ while ((i = g_file_enumerator_next_file(en, NULL, NULL))) {
+ GFile *c = g_file_get_child(td, g_file_info_get_name(i));
+ g_file_delete(c, NULL, NULL);
+ g_object_unref(c);
+ g_object_unref(i);
+ }
+ g_object_unref(en);
+ }
+ g_file_delete(td, NULL, NULL);
+ g_object_unref(td);
+ g_free(tmp);
+}
+
+/* /enhancer/export_real_success (ku0): a save that produces no real file must
+ * return FALSE with a GError, not TRUE-on-pre-existence. Two cases: (a) an
+ * output path whose parent directory does not exist, and (b) an output path
+ * that is a pre-existing directory (named like a supported extension so a
+ * saver op is actually selected). */
+static void
+test_export_real_success(void) {
+ const gchar *c_fx = g_getenv("GGAZE_FIXTURES_DIR");
+ g_assert_nonnull(c_fx);
+ char *c_path = g_build_filename(c_fx, "plain.jpg", NULL);
+ GFile *p_file = g_file_new_for_path(c_path);
+ g_free(c_path);
+
+ GError *p_err = NULL;
+ GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
+ g_assert_nonnull(p_buf);
+
+ Enhancer *e = enhancer_new();
+ const EnhancerPreset *preset =
+ g_ptr_array_index((GPtrArray *)enhancer_get_presets(e), 0);
+ char *tmp = g_dir_make_tmp("ggaze-real-XXXXXX", NULL);
+
+ /* (a) parent dir does not exist: the saver cannot write, no file appears.
+ * GEGL emits a g_warning on the failed save; relax the fatal mask so
+ * enhancer_export can return FALSE and be asserted instead of aborting. */
+ {
+ char *c_bad = g_build_filename(tmp, "no-such-dir", "out.png", NULL);
+ GFile *p_out = g_file_new_for_path(c_bad);
+ g_clear_error(&p_err);
+ GLogLevelFlags old_mask = g_log_set_always_fatal(G_LOG_LEVEL_ERROR);
+ gboolean ok = enhancer_export(e, p_buf, preset, p_out, &p_err);
+ g_log_set_always_fatal(old_mask);
+ g_assert_false(ok);
+ g_assert_nonnull(p_err);
+ g_assert_cmpint(p_err->code, ==, G_IO_ERROR_FAILED);
+ g_clear_error(&p_err);
+ g_object_unref(p_out);
+ g_free(c_bad);
+ }
+
+ /* (b) output path is a pre-existing directory named out.png: the saver op
+ * is selected (extension matches), the write fails (EISDIR), and the
+ * pre-existing directory must NOT count as a successful save. */
+ {
+ char *c_dir = g_build_filename(tmp, "out.png", NULL);
+ g_assert_true(g_mkdir_with_parents(c_dir, 0700) == 0);
+ GFile *p_out = g_file_new_for_path(c_dir);
+ g_clear_error(&p_err);
+ GLogLevelFlags old_mask = g_log_set_always_fatal(G_LOG_LEVEL_ERROR);
+ gboolean ok = enhancer_export(e, p_buf, preset, p_out, &p_err);
+ g_log_set_always_fatal(old_mask);
+ g_assert_false(ok);
+ g_assert_nonnull(p_err);
+ g_clear_error(&p_err);
+ g_object_unref(p_out);
+ GFile *p_dirf = g_file_new_for_path(c_dir);
+ g_assert_true(g_file_delete(p_dirf, NULL, NULL));
+ g_object_unref(p_dirf);
+ g_free(c_dir);
+ }
+
+ g_object_unref(p_buf);
+ g_object_unref(p_file);
+ enhancer_delete(e);
+ GFile *p_tmpf = g_file_new_for_path(tmp);
+ g_file_delete(p_tmpf, NULL, NULL);
+ g_object_unref(p_tmpf);
+ g_free(tmp);
+}
+
int
main(int argc, char **argv) {
gegl_init(&argc, &argv);
g_test_init(&argc, &argv, NULL);
g_test_add_func("/enhancer/builtin_presets", test_builtin_presets);
g_test_add_func("/enhancer/export", test_export);
+ g_test_add_func("/enhancer/load_and_to_texture", test_load_and_to_texture);
+ g_test_add_func("/enhancer/export_format", test_export_format);
+ g_test_add_func("/enhancer/export_real_success", test_export_real_success);
return g_test_run();
} \ No newline at end of file
diff --git a/tests/test_shortcut.c b/tests/test_shortcut.c
index e57d361..c9a66a9 100644
--- a/tests/test_shortcut.c
+++ b/tests/test_shortcut.c
@@ -328,11 +328,12 @@ test_shortcut_keypath_toggle_and_back(void) {
static void
test_shortcut_full_table_registered(void) {
static const char *ACTIONS[] = {
- "win.prev", "win.next", "win.first", "win.last",
- "win.open", "win.quit", "win.trash", "win.delete",
- "win.undo", "win.toggle-view", "win.mark", "win.mark-all",
- "win.shortcuts", "win.zoom-in", "win.zoom-out", "win.fullscreen",
- "win.slideshow", "win.info", "win.back",
+ "win.prev", "win.next", "win.first", "win.last",
+ "win.open", "win.quit", "win.trash", "win.delete",
+ "win.undo", "win.toggle-view", "win.mark", "win.mark-all",
+ "win.shortcuts", "win.zoom-in", "win.zoom-out", "win.fullscreen",
+ "win.slideshow", "win.info", "win.back", "win.enhance",
+ "win.enhance-save",
};
GgazeWindow *p_win = new_window();
GtkShortcutController *p_sc = find_shortcut_controller(GTK_WIDGET(p_win));
@@ -342,9 +343,9 @@ test_shortcut_full_table_registered(void) {
g_assert_nonnull(p_s);
g_object_unref(p_s);
}
- /* The SHORTCUTS[] table has 23 rows now (some actions appear twice, e.g.
+ /* The SHORTCUTS[] table has 25 rows now (some actions appear twice, e.g.
* win.prev for h and Left; win.zoom-in for plus and equal). */
- g_assert_cmpint(g_list_model_get_n_items(G_LIST_MODEL(p_sc)), ==, 23);
+ g_assert_cmpint(g_list_model_get_n_items(G_LIST_MODEL(p_sc)), ==, 25);
g_object_unref(p_sc);
g_object_unref(p_win);
drain_main(200);