diff options
| -rw-r--r-- | src/app.c | 15 | ||||
| -rw-r--r-- | src/enhancer.c | 144 | ||||
| -rw-r--r-- | src/enhancer.h | 20 | ||||
| -rw-r--r-- | src/shortcuts.c | 2 | ||||
| -rw-r--r-- | src/window.c | 213 | ||||
| -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 |
9 files changed, 577 insertions, 24 deletions
@@ -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); |
