diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-21 18:39:47 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-21 18:39:47 +0300 |
| commit | cfda8297cc4432a77e3f7e8679753a908e9f5759 (patch) | |
| tree | bc8c1469989bb55656c93a65c277ab86e17adf9a /tests | |
| parent | dc127b5089656944dab7ab72799d0755ca122803 (diff) | |
enhance: wire up GEGL image filters (a cycle / s save) + fix export bugs
The enhancer module existed (GEGL presets: Auto-fix, Brightness, Contrast,
Saturation, Warm, Cool, Sharpen, Denoise) but was never connected to the
app and GEGL was never initialized. Wire it up:
- app.c: gegl_init() once in GApplication::startup (GEGL-gated).
- enhancer.c: add enhancer_load(GFile) -> GeglBuffer (gegl:load) and
enhancer_buffer_to_texture(GeglBuffer) -> GdkTexture (RGBA8 ->
GdkMemoryTexture) for the live preview bridge.
- enhancer_export: pick the saver from the output extension (jpg/png/webp)
instead of always gegl:jpg-save (ju0 - was writing JPEG bytes into .png);
verify the save actually produced a non-empty newer file instead of
trusting g_file_test(EXISTS) on a pre-existing path (ku0 - false success).
- window.c: win.enhance (a) cycles the active preset and previews it on the
current image (switches to large view, loads via GEGL, applies, shows the
texture); win.enhance-save (s) exports <stem>-enhanced.<ext> with the
active preset, never overwriting the original. Preview resets on
navigation; the title shows the active preset.
- shortcuts.c: a -> win.enhance, s -> win.enhance-save; ? overlay Enhance
group.
- tests: test_enhancer gains load_and_to_texture, export_format (PNG/JPEG
signature checks), export_real_success (ku0 - parent-missing and
pre-existing-directory both return FALSE). LSAN suppressions cover
GEGL's jpg-load plugin leak. test_shortcut full-table updated (25 rows).
Synchronous apply (may briefly block on large images) and a navigate-away
dirty prompt are deliberately out of scope for v1.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/lsan_suppressions.txt | 5 | ||||
| -rw-r--r-- | tests/meson.build | 2 | ||||
| -rw-r--r-- | tests/test_enhancer.c | 185 | ||||
| -rw-r--r-- | tests/test_shortcut.c | 15 |
4 files changed, 199 insertions, 8 deletions
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); |
