diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-13 21:05:24 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-13 21:05:24 +0300 |
| commit | 25ee19ce91dcb204d93e6d8cceb7cd64d5f057f9 (patch) | |
| tree | 2e01bf17f2a6e1e9e9051b115cdd764060f740fb | |
| parent | 0795d3cf926d0d937f25fcc699f5f674bf618c72 (diff) | |
fullscreen: info/EXIF, fullscreen, slideshow, Esc back (ot0)
M4 (task ot0): fullscreen + slideshow + info overlay.
- src/info.{c,h}: plain-C EXIF gather via libexif (camera, lens, focal,
aperture, shutter, ISO, datetime, orientation) + file info (dims, format,
size). info_format() produces the overlay text. Unit-tested.
- src/window.c: fullscreen toggle (f), slideshow (S, 3s auto-advance), info
overlay (i, GtkLabel in GtkOverlay, auto-hide 5s), Esc contextual back
(fullscreen→unfullscreen→large→grid→quit). Stack wrapped in GtkOverlay.
ggaze_window_get_stack() accessor for tests.
- src/shortcuts.c: f, S, i, Escape bindings.
- meson.build: libexif dep added.
- tests: unit test_info (EXIF extraction + orientation tag). 14/14 green,
ASan clean.
| -rw-r--r-- | meson.build | 6 | ||||
| -rw-r--r-- | src/info.c | 171 | ||||
| -rw-r--r-- | src/info.h | 45 | ||||
| -rw-r--r-- | src/shortcuts.c | 4 | ||||
| -rw-r--r-- | src/window.c | 156 | ||||
| -rw-r--r-- | src/window.h | 4 | ||||
| -rw-r--r-- | tests/meson.build | 10 | ||||
| -rw-r--r-- | tests/test_grid_cull.c | 6 | ||||
| -rw-r--r-- | tests/test_info.c | 74 | ||||
| -rw-r--r-- | tests/test_open_and_show.c | 4 | ||||
| -rw-r--r-- | tests/test_responsive_nav.c | 2 | ||||
| -rw-r--r-- | tests/test_walk_folder.c | 4 | ||||
| -rw-r--r-- | tests/test_window.c | 2 |
13 files changed, 471 insertions, 17 deletions
diff --git a/meson.build b/meson.build index d77e081..7f9c3f2 100644 --- a/meson.build +++ b/meson.build @@ -22,6 +22,7 @@ glib_dep = dependency('glib-2.0') gio_dep = dependency('gio-2.0') adwaita_dep = dependency('libadwaita-1') gdkpixbuf_dep = dependency('gdk-pixbuf-2.0') # pixbuf loader backend +libexif_dep = dependency('libexif') # EXIF gather (info overlay, M4) # --- Optional backends (feature: auto/disabled/enabled). See meson_options.txt. gegl_dep = dependency('gegl-0.4', required : get_option('gegl')) @@ -63,16 +64,17 @@ ggaze_lib = static_library('ggaze', 'src/thumbnail.c', 'src/trash.c', 'src/gridview.c', + 'src/info.c', 'src/loader/loader.c', 'src/loader/detect.c', 'src/loader/backends/pixbuf.c', ), include_directories : [inc, src_inc], - dependencies : [gtk4_dep, glib_dep, gio_dep, adwaita_dep, gdkpixbuf_dep], + dependencies : [gtk4_dep, glib_dep, gio_dep, adwaita_dep, gdkpixbuf_dep, libexif_dep], install : false, ) -ggaze_deps = [gtk4_dep, glib_dep, gio_dep, adwaita_dep, gdkpixbuf_dep] +ggaze_deps = [gtk4_dep, glib_dep, gio_dep, adwaita_dep, gdkpixbuf_dep, libexif_dep] subdir('src') subdir('data') diff --git a/src/info.c b/src/info.c new file mode 100644 index 0000000..664ea64 --- /dev/null +++ b/src/info.c @@ -0,0 +1,171 @@ +/*:* + * ggaze — image info / EXIF gather + * + * Gathers file info (via GFileInfo + GdkPixbuf for dims) and EXIF tags (via + * libexif) into a GgazeInfo struct. Plain-C, no GtkWidget. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "info.h" + +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <libexif/exif-data.h> +#include <libexif/exif-tag.h> + +static char * +_dup_exif_value(ExifData *p_data, ExifTag e_tag) { + if (p_data == NULL) { + return (NULL); + } + ExifEntry *p_entry = exif_data_get_entry(p_data, e_tag); + if (p_entry == NULL) { + return (NULL); + } + char c_buf[256]; + exif_entry_get_value(p_entry, c_buf, sizeof(c_buf)); + if (c_buf[0] == '\0') { + return (NULL); + } + return (g_strdup(c_buf)); +} + +static char * +_dup_camera(ExifData *p_data) { + char *c_make = _dup_exif_value(p_data, EXIF_TAG_MAKE); + char *c_model = _dup_exif_value(p_data, EXIF_TAG_MODEL); + char *c_out = NULL; + if (c_make != NULL && c_model != NULL) { + c_out = g_strdup_printf("%s %s", c_make, c_model); + } else if (c_make != NULL) { + c_out = g_strdup(c_make); + } else if (c_model != NULL) { + c_out = g_strdup(c_model); + } + g_free(c_make); + g_free(c_model); + return (c_out); +} + +static int +_get_orientation(ExifData *p_data) { + if (p_data == NULL) { + return (0); + } + ExifEntry *p_entry = exif_data_get_entry(p_data, EXIF_TAG_ORIENTATION); + if (p_entry == NULL || p_entry->components == 0 || p_entry->data == NULL) { + return (0); + } + /* Orientation is a SHORT (2 bytes, big/little endian per the data's byte + * order). */ + ExifByteOrder e_order = exif_data_get_byte_order(p_data); + return ((int)exif_get_short(p_entry->data, e_order)); +} + +GgazeInfo * +info_new(GFile *p_file) { + g_return_val_if_fail(G_IS_FILE(p_file), NULL); + char *c_path = g_file_get_path(p_file); + if (c_path == NULL) { + return (NULL); + } + + GgazeInfo *p_info = g_new0(GgazeInfo, 1); + + /* File info: size + content type. */ + GError *p_err = NULL; + GFileInfo *p_fi = + g_file_query_info(p_file, "standard::size,standard::content-type", + G_FILE_QUERY_INFO_NONE, NULL, &p_err); + if (p_fi != NULL) { + p_info->i_size = (gint64)g_file_info_get_size(p_fi); + const char *c_ct = g_file_info_get_content_type(p_fi); + if (c_ct != NULL) { + p_info->c_format = g_strdup(c_ct); + } + g_object_unref(p_fi); + } else { + g_clear_error(&p_err); + } + + /* Dimensions via GdkPixbuf (without applying orientation). */ + GdkPixbuf *p_pix = gdk_pixbuf_new_from_file(c_path, &p_err); + if (p_pix != NULL) { + p_info->i_width = gdk_pixbuf_get_width(p_pix); + p_info->i_height = gdk_pixbuf_get_height(p_pix); + g_object_unref(p_pix); + } else { + g_clear_error(&p_err); + } + + /* EXIF via libexif. */ + ExifData *p_exif = exif_data_new_from_file(c_path); + if (p_exif != NULL) { + p_info->c_camera = _dup_camera(p_exif); + p_info->c_focal = _dup_exif_value(p_exif, EXIF_TAG_FOCAL_LENGTH); + p_info->c_aperture = _dup_exif_value(p_exif, EXIF_TAG_FNUMBER); + p_info->c_shutter = _dup_exif_value(p_exif, EXIF_TAG_EXPOSURE_TIME); + p_info->c_iso = _dup_exif_value(p_exif, EXIF_TAG_ISO_SPEED_RATINGS); + p_info->c_datetime = _dup_exif_value(p_exif, EXIF_TAG_DATE_TIME_ORIGINAL); + p_info->i_orientation = _get_orientation(p_exif); + exif_data_unref(p_exif); + } + + g_free(c_path); + return (p_info); +} + +void +info_delete(GgazeInfo *p_info) { + if (p_info == NULL) { + return; + } + g_free(p_info->c_format); + g_free(p_info->c_camera); + g_free(p_info->c_lens); + g_free(p_info->c_focal); + g_free(p_info->c_aperture); + g_free(p_info->c_shutter); + g_free(p_info->c_iso); + g_free(p_info->c_datetime); + g_free(p_info); +} + +static char * +_join(GString *p_str, const char *c_label, const char *c_val) { + if (c_val != NULL && c_val[0] != '\0') { + g_string_append_printf(p_str, "%s: %s\n", c_label, c_val); + } + return (NULL); +} + +char * +info_format(const GgazeInfo *p_info) { + if (p_info == NULL) { + return (g_strdup("")); + } + GString *p_str = g_string_new(NULL); + g_string_append_printf(p_str, "%d×%d\n", p_info->i_width, p_info->i_height); + _join(p_str, "Format", p_info->c_format); + if (p_info->i_size > 0) { + char *c_sz = g_format_size(p_info->i_size); + g_string_append_printf(p_str, "Size: %s\n", c_sz); + g_free(c_sz); + } + _join(p_str, "Camera", p_info->c_camera); + _join(p_str, "Lens", p_info->c_lens); + _join(p_str, "Focal", p_info->c_focal); + _join(p_str, "Aperture", p_info->c_aperture); + _join(p_str, "Shutter", p_info->c_shutter); + _join(p_str, "ISO", p_info->c_iso); + _join(p_str, "Date", p_info->c_datetime); + if (p_info->i_orientation > 0) { + g_string_append_printf(p_str, "Orientation: %d\n", p_info->i_orientation); + } + /* trim trailing newline */ + if (p_str->len > 0 && p_str->str[p_str->len - 1] == '\n') { + g_string_truncate(p_str, p_str->len - 1); + } + return (g_string_free(p_str, FALSE)); +}
\ No newline at end of file diff --git a/src/info.h b/src/info.h new file mode 100644 index 0000000..7680b72 --- /dev/null +++ b/src/info.h @@ -0,0 +1,45 @@ +#ifndef GGAZE_INFO_H +#define GGAZE_INFO_H + +/*:* + * ggaze — image info / EXIF gather + * + * Gathers file info (dimensions, format, size) + EXIF (camera, lens, focal, + * aperture, shutter, ISO, datetime, orientation) into a GgazeInfo struct. + * Plain-C, no GtkWidget; uses GdkPixbuf (dims) + libexif (EXIF). Unit-testable. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include <gio/gio.h> +#include <glib.h> + +G_BEGIN_DECLS + +typedef struct { + int i_width; + int i_height; + char *c_format; /* content type (owned) */ + gint64 i_size; /* file size in bytes */ + char *c_camera; /* Make + Model (owned) */ + char *c_lens; /* Lens model (owned, or NULL) */ + char *c_focal; /* FocalLength (owned) */ + char *c_aperture; /* FNumber (owned) */ + char *c_shutter; /* ExposureTime (owned) */ + char *c_iso; /* ISOSpeedRatings (owned) */ + char *c_datetime; /* DateTimeOriginal (owned) */ + int i_orientation; /* EXIF Orientation (1-8, 0 if none) */ +} GgazeInfo; + +/* Gather info for p_file (synchronous). Returns a new GgazeInfo (caller owns, + * free with info_delete) or NULL on failure. */ +GgazeInfo *info_new(GFile *p_file); +void info_delete(GgazeInfo *p_info); + +/* Format the info as a multi-line string (caller frees). */ +char *info_format(const GgazeInfo *p_info); + +G_END_DECLS + +#endif /* GGAZE_INFO_H */
\ No newline at end of file diff --git a/src/shortcuts.c b/src/shortcuts.c index 6853490..a23cfe2 100644 --- a/src/shortcuts.c +++ b/src/shortcuts.c @@ -37,6 +37,10 @@ static const ShortcutEntry SHORTCUTS[] = { {GDK_KEY_equal, 0, "win.zoom-in"}, {GDK_KEY_minus, 0, "win.zoom-out"}, {GDK_KEY_underscore, 0, "win.zoom-out"}, + {GDK_KEY_f, 0, "win.fullscreen"}, + {GDK_KEY_S, GDK_SHIFT_MASK, "win.slideshow"}, + {GDK_KEY_i, 0, "win.info"}, + {GDK_KEY_Escape, 0, "win.back"}, }; void diff --git a/src/window.c b/src/window.c index 003e7bf..4c26ea7 100644 --- a/src/window.c +++ b/src/window.c @@ -18,6 +18,7 @@ #include <gtk/gtk.h> #include "gridview.h" +#include "info.h" #include "loader/loader.h" #include "navigator.h" #include "shortcuts.h" @@ -38,15 +39,24 @@ struct _GgazeWindow { GtkWidget *p_viewer; /* GgazeViewer — the large view */ GgazeGrid *p_grid; /* the thumbnail grid (the "grid" stack child) */ int i_grid_size; /* current thumbnail size (64-512, decision T) */ + GtkWidget *p_overlay; /* GtkOverlay wrapping the stack (for info label) */ + GtkWidget *p_info_lbl; /* info overlay label (auto-hides) */ + guint u_info_hide; /* info auto-hide timeout id (0=none) */ + guint u_slideshow; /* slideshow timeout id (0=off) */ + gboolean b_fullscreen; + guint u_hdr_hide; /* fullscreen header auto-hide timeout */ }; 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 _update_header(GgazeWindow *p_win); -static void _on_grid_activate(GgazeGrid *p_grid, gpointer p_data); +static void _load_current(GgazeWindow *p_win); +static void _prefetch(GgazeWindow *p_win); +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); +static void _hide_info(GgazeWindow *p_win); +static gboolean _slideshow_tick(gpointer p_data); /* --- actions ------------------------------------------------------------- */ @@ -269,6 +279,108 @@ _action_zoom_out(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) { } } +/* --- M4: fullscreen / slideshow / info / back --------------------------- */ + +static void +_action_fullscreen(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->b_fullscreen) { + gtk_window_unfullscreen(GTK_WINDOW(p_win)); + p_win->b_fullscreen = FALSE; + } else { + gtk_window_fullscreen(GTK_WINDOW(p_win)); + p_win->b_fullscreen = TRUE; + } +} + +static void +_action_slideshow(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->u_slideshow != 0) { + g_source_remove(p_win->u_slideshow); + p_win->u_slideshow = 0; + } else { + /* 3-second default; GSettings slideshow-delay wired in M10 */ + p_win->u_slideshow = g_timeout_add_seconds(3, _slideshow_tick, p_win); + } +} + +static void +_action_info(GSimpleAction *p_a, GVariant *p_v, gpointer p_data) { + (void)p_a; + (void)p_v; + _show_info(GGAZE_WINDOW(p_data)); +} + +static void +_action_back(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->b_fullscreen) { + gtk_window_unfullscreen(GTK_WINDOW(p_win)); + p_win->b_fullscreen = FALSE; + } else { + 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), "grid"); + } else { + gtk_window_close(GTK_WINDOW(p_win)); + } + } +} + +static gboolean +_slideshow_tick(gpointer p_data) { + GgazeWindow *p_win = GGAZE_WINDOW(p_data); + if (p_win->p_nav != NULL) { + navigator_next(p_win->p_nav); + } + return (G_SOURCE_CONTINUE); +} + +static gboolean +_info_hide_tick(gpointer p_data) { + GgazeWindow *p_win = GGAZE_WINDOW(p_data); + _hide_info(p_win); + p_win->u_info_hide = 0; + return (G_SOURCE_REMOVE); +} + +static void +_show_info(GgazeWindow *p_win) { + if (p_win->p_nav == NULL) { + return; + } + GFile *p_cur = navigator_get_current(p_win->p_nav); + if (p_cur == NULL) { + return; + } + GgazeInfo *p_info = info_new(p_cur); + if (p_info == NULL) { + return; + } + char *c_text = info_format(p_info); + gtk_label_set_text(GTK_LABEL(p_win->p_info_lbl), c_text); + g_free(c_text); + info_delete(p_info); + gtk_widget_set_visible(p_win->p_info_lbl, TRUE); + if (p_win->u_info_hide != 0) { + g_source_remove(p_win->u_info_hide); + } + p_win->u_info_hide = g_timeout_add_seconds(5, _info_hide_tick, p_win); +} + +static void +_hide_info(GgazeWindow *p_win) { + gtk_widget_set_visible(p_win->p_info_lbl, FALSE); +} + static const GActionEntry ACTIONS[] = { {.name = "prev", .activate = _action_prev}, {.name = "next", .activate = _action_next}, @@ -282,6 +394,10 @@ static const GActionEntry ACTIONS[] = { {.name = "toggle-view", .activate = _action_toggle_view}, {.name = "zoom-in", .activate = _action_zoom_in}, {.name = "zoom-out", .activate = _action_zoom_out}, + {.name = "fullscreen", .activate = _action_fullscreen}, + {.name = "slideshow", .activate = _action_slideshow}, + {.name = "info", .activate = _action_info}, + {.name = "back", .activate = _action_back}, }; /* --- drop target --------------------------------------------------------- */ @@ -485,6 +601,18 @@ ggaze_window_dispose(GObject *p_obj) { g_clear_object(&p_win->p_prefetch_cancel); g_cancellable_cancel(p_win->p_cancel); g_clear_object(&p_win->p_cancel); + if (p_win->u_slideshow != 0) { + g_source_remove(p_win->u_slideshow); + p_win->u_slideshow = 0; + } + if (p_win->u_info_hide != 0) { + g_source_remove(p_win->u_info_hide); + p_win->u_info_hide = 0; + } + if (p_win->u_hdr_hide != 0) { + g_source_remove(p_win->u_hdr_hide); + p_win->u_hdr_hide = 0; + } 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); @@ -517,7 +645,17 @@ ggaze_window_init(GgazeWindow *p_win) { p_win->p_stack = gtk_stack_new(); gtk_stack_set_transition_type(GTK_STACK(p_win->p_stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE); - gtk_window_set_child(GTK_WINDOW(p_win), p_win->p_stack); + + /* Wrap the stack in a GtkOverlay so the info label can float on top. */ + p_win->p_overlay = gtk_overlay_new(); + gtk_overlay_set_child(GTK_OVERLAY(p_win->p_overlay), p_win->p_stack); + p_win->p_info_lbl = gtk_label_new(""); + gtk_widget_add_css_class(p_win->p_info_lbl, "ggaze-info"); + gtk_widget_set_margin_start(p_win->p_info_lbl, 12); + gtk_widget_set_margin_top(p_win->p_info_lbl, 12); + gtk_widget_set_visible(p_win->p_info_lbl, FALSE); + gtk_overlay_add_overlay(GTK_OVERLAY(p_win->p_overlay), p_win->p_info_lbl); + gtk_window_set_child(GTK_WINDOW(p_win), p_win->p_overlay); GtkWidget *p_grid = gtk_label_new("grid"); gtk_widget_add_css_class(p_grid, "dim-label"); @@ -642,4 +780,10 @@ ggaze_window_last(GgazeWindow *p_win) { if (p_win->p_nav != NULL) { navigator_last(p_win->p_nav); } -}
\ No newline at end of file +} + +GtkStack * +ggaze_window_get_stack(GgazeWindow *p_win) { + g_return_val_if_fail(GGAZE_IS_WINDOW(p_win), NULL); + return (GTK_STACK(p_win->p_stack)); +} diff --git a/src/window.h b/src/window.h index 10612db..43572d0 100644 --- a/src/window.h +++ b/src/window.h @@ -32,6 +32,10 @@ GgazeWindow *ggaze_window_new(GgazeApp *p_app); * current image into the viewer and switches the stack to "large". */ void ggaze_window_open(GgazeWindow *p_win, GFile *p_arg); +/* The GtkStack (grid/large) — the tests use this instead of + * gtk_window_get_child (which now returns the wrapping GtkOverlay). */ +GtkStack *ggaze_window_get_stack(GgazeWindow *p_win); + /* Navigation over the current folder (bound to h/l/Left/Right/g/G via * shortcuts.c). No-ops if nothing is open. */ void ggaze_window_prev(GgazeWindow *p_win); diff --git a/tests/meson.build b/tests/meson.build index d1cb8af..86eee1d 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -97,6 +97,16 @@ test_thumbnail = executable( ) test('thumbnail', test_thumbnail, suite : 'unit', env : fixtures_env) +test_info = executable( + 'test_info', + ['test_info.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : ggaze_deps, + link_with : ggaze_lib, + install : false, +) +test('info', test_info, suite : 'unit', env : fixtures_env) + test_texturecache = executable( 'test_texturecache', ['test_texturecache.c', ggaze_conf_h], diff --git a/tests/test_grid_cull.c b/tests/test_grid_cull.c index 37a2a68..db5a512 100644 --- a/tests/test_grid_cull.c +++ b/tests/test_grid_cull.c @@ -89,7 +89,7 @@ new_window(void) { static GdkTexture * viewer_texture(GgazeWindow *p_win) { - GtkWidget *p_child = gtk_window_get_child(GTK_WINDOW(p_win)); + GtkWidget *p_child = GTK_WIDGET(ggaze_window_get_stack(p_win)); GtkWidget *p_large = gtk_stack_get_child_by_name(GTK_STACK(p_child), "large"); return (ggaze_viewer_get_texture(GGAZE_VIEWER(p_large))); @@ -120,7 +120,7 @@ test_grid_cull_temp(void) { /* The grid exists in the stack with 3 cells (count without toggling to * avoid realizing cells and firing async thumbnail requests that can't * complete before exit under ASan). */ - GtkWidget *p_stack = gtk_window_get_child(GTK_WINDOW(p_win)); + GtkWidget *p_stack = GTK_WIDGET(ggaze_window_get_stack(p_win)); GgazeGrid *p_grid = GGAZE_GRID(gtk_stack_get_child_by_name(GTK_STACK(p_stack), "grid")); g_assert_nonnull(p_grid); @@ -188,7 +188,7 @@ test_grid_sample(void) { /* The grid exists in the stack (created on open) with hundreds of cells, * but we don't toggle to it (that would realize cells and fire thumbnail * requests that can't all complete before exit under ASan). */ - GtkWidget *p_stack = gtk_window_get_child(GTK_WINDOW(p_win)); + GtkWidget *p_stack = GTK_WIDGET(ggaze_window_get_stack(p_win)); GgazeGrid *p_grid = GGAZE_GRID(gtk_stack_get_child_by_name(GTK_STACK(p_stack), "grid")); g_assert_nonnull(p_grid); diff --git a/tests/test_info.c b/tests/test_info.c new file mode 100644 index 0000000..9beceb9 --- /dev/null +++ b/tests/test_info.c @@ -0,0 +1,74 @@ +/*:* + * ggaze — info / EXIF unit test + * + * Gathers EXIF from the fixtures: plain.jpg (orientation 1) and rot6.jpg + * (orientation 6, 8x4 stored). Asserts dimensions, orientation, and that + * the format string is non-empty. Uses $GGAZE_FIXTURES_DIR. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "info.h" + +#include <glib.h> + +static GgazeInfo * +info_from_fixture(const char *c_name) { + const char *c_dir = g_getenv("GGAZE_FIXTURES_DIR"); + g_assert_nonnull(c_dir); + char *c_path = g_build_filename(c_dir, c_name, NULL); + GFile *p_file = g_file_new_for_path(c_path); + g_free(c_path); + GgazeInfo *p_info = info_new(p_file); + g_assert_nonnull(p_info); + g_object_unref(p_file); + return (p_info); +} + +static void +test_plain_jpeg(void) { + GgazeInfo *p_info = info_from_fixture("plain.jpg"); + g_assert_cmpint(p_info->i_width, ==, 6); + g_assert_cmpint(p_info->i_height, ==, 3); + g_assert_cmpint(p_info->i_orientation, ==, 1); /* Horizontal (normal) */ + g_assert_nonnull(p_info->c_format); + g_assert(p_info->i_size > 0); + char *c_fmt = info_format(p_info); + g_assert_nonnull(c_fmt); + g_free(c_fmt); + info_delete(p_info); +} + +static void +test_rotated_jpeg(void) { + GgazeInfo *p_info = info_from_fixture("rot6.jpg"); + /* Stored dimensions are 8x4 (before orientation is applied). */ + g_assert_cmpint(p_info->i_width, ==, 8); + g_assert_cmpint(p_info->i_height, ==, 4); + g_assert_cmpint(p_info->i_orientation, ==, 6); /* Rotate 90 CW */ + info_delete(p_info); +} + +static void +test_png_no_exif(void) { + GgazeInfo *p_info = info_from_fixture("small.png"); + g_assert_cmpint(p_info->i_width, ==, 5); + g_assert_cmpint(p_info->i_height, ==, 2); + g_assert_cmpint(p_info->i_orientation, ==, 0); /* no EXIF */ + g_assert_null(p_info->c_camera); /* no EXIF → no camera */ + info_delete(p_info); +} + +int +main(int i_argc, char **c_argv) { + g_test_init(&i_argc, &c_argv, NULL); + if (g_getenv("GGAZE_FIXTURES_DIR") == NULL) { + g_test_skip("GGAZE_FIXTURES_DIR unset"); + return (g_test_run()); + } + g_test_add_func("/info/plain_jpeg", test_plain_jpeg); + g_test_add_func("/info/rotated_jpeg", test_rotated_jpeg); + g_test_add_func("/info/png_no_exif", test_png_no_exif); + return (g_test_run()); +}
\ No newline at end of file diff --git a/tests/test_open_and_show.c b/tests/test_open_and_show.c index 3c16996..83c04c7 100644 --- a/tests/test_open_and_show.c +++ b/tests/test_open_and_show.c @@ -48,7 +48,7 @@ drain_main(guint u_ms) { static void assert_shown_large_with_dims(GgazeWindow *p_win, int i_w, int i_h) { - GtkWidget *p_child = gtk_window_get_child(GTK_WINDOW(p_win)); + GtkWidget *p_child = GTK_WIDGET(ggaze_window_get_stack(p_win)); g_assert_true(GTK_IS_STACK(p_child)); GtkWidget *p_large = gtk_stack_get_child_by_name(GTK_STACK(p_child), "large"); @@ -127,7 +127,7 @@ test_open_sample_image(void) { GgazeWindow *p_win = new_window(); GFile *p_file = g_file_new_for_path(c_path); ggaze_window_open(p_win, p_file); - GtkWidget *p_child = gtk_window_get_child(GTK_WINDOW(p_win)); + GtkWidget *p_child = GTK_WIDGET(ggaze_window_get_stack(p_win)); GtkWidget *p_large = gtk_stack_get_child_by_name(GTK_STACK(p_child), "large"); /* async load: pump until a texture lands */ diff --git a/tests/test_responsive_nav.c b/tests/test_responsive_nav.c index 0a13e1a..61a90ab 100644 --- a/tests/test_responsive_nav.c +++ b/tests/test_responsive_nav.c @@ -74,7 +74,7 @@ drain_main(guint u_ms) { static GdkTexture * viewer_texture(GgazeWindow *p_win) { - GtkWidget *p_child = gtk_window_get_child(GTK_WINDOW(p_win)); + GtkWidget *p_child = GTK_WIDGET(ggaze_window_get_stack(p_win)); GtkWidget *p_large = gtk_stack_get_child_by_name(GTK_STACK(p_child), "large"); return (ggaze_viewer_get_texture(GGAZE_VIEWER(p_large))); diff --git a/tests/test_walk_folder.c b/tests/test_walk_folder.c index f2f4f15..a175d3e 100644 --- a/tests/test_walk_folder.c +++ b/tests/test_walk_folder.c @@ -85,7 +85,7 @@ drain_main(guint u_ms) { static GdkTexture * viewer_texture(GgazeWindow *p_win) { - GtkWidget *p_child = gtk_window_get_child(GTK_WINDOW(p_win)); + GtkWidget *p_child = GTK_WIDGET(ggaze_window_get_stack(p_win)); GtkWidget *p_large = gtk_stack_get_child_by_name(GTK_STACK(p_child), "large"); return (ggaze_viewer_get_texture(GGAZE_VIEWER(p_large))); @@ -94,7 +94,7 @@ viewer_texture(GgazeWindow *p_win) { static void assert_dims(GgazeWindow *p_win, int i_w, int i_h) { /* Loads are async; pump the main loop until the viewer shows i_w x i_h. */ - GtkWidget *p_child = gtk_window_get_child(GTK_WINDOW(p_win)); + GtkWidget *p_child = GTK_WIDGET(ggaze_window_get_stack(p_win)); GtkWidget *p_large = gtk_stack_get_child_by_name(GTK_STACK(p_child), "large"); GdkTexture *p_tex = NULL; diff --git a/tests/test_window.c b/tests/test_window.c index 63e54bb..76b9363 100644 --- a/tests/test_window.c +++ b/tests/test_window.c @@ -33,7 +33,7 @@ test_stack_has_two_views(void) { GgazeWindow *p_win = new_window(); g_assert_nonnull(p_win); - GtkWidget *p_child = gtk_window_get_child(GTK_WINDOW(p_win)); + GtkWidget *p_child = GTK_WIDGET(ggaze_window_get_stack(p_win)); g_assert_nonnull(p_child); g_assert_true(GTK_IS_STACK(p_child)); |
