From 4ef09f92b2677d5faadca1c5960c7706d331b5e8 Mon Sep 17 00:00:00 2001 From: Hans Petter Jansson Date: Sun, 22 Dec 2024 18:38:14 +0100 Subject: [PATCH] chafa: Implement grid layout Works, but still needs a little polish. Fixes #102 (GitHub). --- docs/chafa.xml | 11 ++ tools/chafa/Makefile.am | 2 + tools/chafa/chafa.c | 95 ++++++++- tools/chafa/grid-layout.c | 406 ++++++++++++++++++++++++++++++++++++++ tools/chafa/grid-layout.h | 43 ++++ 5 files changed, 554 insertions(+), 3 deletions(-) create mode 100644 tools/chafa/grid-layout.c create mode 100644 tools/chafa/grid-layout.h diff --git a/docs/chafa.xml b/docs/chafa.xml index 1241689..64ee2a4 100644 --- a/docs/chafa.xml +++ b/docs/chafa.xml @@ -181,6 +181,17 @@ fraction. Defaults to 1/2. This will only be applied in symbol mode. + + + +Lay out images in a grid of cols columns and +rows rows per screenful. Either +cols or rows may be +omitted, e.g. --grid 4 or --grid x4, in which case cell allocations +will be approximately square. + + + diff --git a/tools/chafa/Makefile.am b/tools/chafa/Makefile.am index 3cdfb82..8fd3ebd 100644 --- a/tools/chafa/Makefile.am +++ b/tools/chafa/Makefile.am @@ -11,6 +11,8 @@ chafa_SOURCES = \ font-loader.h \ gif-loader.c \ gif-loader.h \ + grid-layout.c \ + grid-layout.h \ media-loader.c \ media-loader.h \ placement-counter.c \ diff --git a/tools/chafa/chafa.c b/tools/chafa/chafa.c index 0b45177..6813f60 100644 --- a/tools/chafa/chafa.c +++ b/tools/chafa/chafa.c @@ -42,6 +42,7 @@ #include #include "font-loader.h" +#include "grid-layout.h" #include "media-loader.h" #include "named-colors.h" #include "placement-counter.h" @@ -115,6 +116,7 @@ typedef struct gint view_width, view_height; gint width, height; gint cell_width, cell_height; + gint grid_width, grid_height; gint margin_bottom, margin_right; gdouble scale; gdouble font_ratio; @@ -439,6 +441,8 @@ print_summary (void) " --fit-width Fit images to view's width, possibly exceeding its height.\n" " --font-ratio=W/H Target font's width/height ratio. Can be specified as\n" " a real number or a fraction. Defaults to 1/2.\n" + " --grid=CxR Lay out images in a grid of C columns and R rows per\n" + " screenful. Either C or R may be omitted, e.g. --grid 4.\n" " --margin-bottom=NUM When terminal size is detected, reserve at least NUM\n" " rows at the bottom as a safety margin. Can be used to\n" " prevent images from scrolling out. Defaults to 1.\n" @@ -1212,6 +1216,41 @@ parse_size_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_GN return result; } +static gboolean +parse_grid_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_GNUC_UNUSED gpointer data, GError **error) +{ + gboolean result = TRUE; + gint width, height; + + parse_2d_size (value, &width, &height); + + if (width < 0 && height < 0) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Grid size must be specified as [width]x[height], [width]x or x[height], e.g 4x4, 4x or x4."); + result = FALSE; + } + else if (width == 0 || height == 0) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Grid size must be at least 1x1."); + result = FALSE; + } + else if (width < 0) + { + width = height; + } + else if (height < 0) + { + height = width; + } + + options.grid_width = width; + options.grid_height = height; + + return result; +} + static gboolean parse_exact_size_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_GNUC_UNUSED gpointer data, GError **error) { @@ -2049,6 +2088,7 @@ parse_options (int *argc, char **argv []) { "font-ratio", '\0', 0, G_OPTION_ARG_CALLBACK, parse_font_ratio_arg, "Font ratio", NULL }, { "fuzz-options", '\0', 0, G_OPTION_ARG_NONE, &options.fuzz_options, "Fuzz the options", NULL }, { "glyph-file", '\0', 0, G_OPTION_ARG_CALLBACK, parse_glyph_file_arg, "Glyph file", NULL }, + { "grid", '\0', 0, G_OPTION_ARG_CALLBACK, parse_grid_arg, "Grid", NULL }, { "invert", '\0', 0, G_OPTION_ARG_NONE, &options.invert, "Invert foreground/background", NULL }, { "margin-bottom", '\0', 0, G_OPTION_ARG_INT, &options.margin_bottom, "Bottom margin", NULL }, { "margin-right", '\0', 0, G_OPTION_ARG_INT, &options.margin_right, "Right margin", NULL }, @@ -2112,6 +2152,8 @@ parse_options (int *argc, char **argv []) options.view_height = -1; /* Unset */ options.width = -1; /* Unset */ options.height = -1; /* Unset */ + options.grid_width = -1; /* Unset */ + options.grid_height = -1; /* Unset */ options.fit_to_width = FALSE; options.font_ratio = -1.0; /* Unset */ options.margin_bottom = -1; /* Unset */ @@ -3218,6 +3260,50 @@ run_all (GList *filenames) return (n_processed - n_failed < 1) ? 2 : (n_failed > 0) ? 1 : 0; } +static gint +run_grid (GList *filenames) +{ + ChafaCanvasConfig *canvas_config; + GridLayout *grid_layout; + gint n_processed = 0; + gint n_failed = 0; + GList *l; + gchar *out_chunk; + + /* This can only happen with --help and --version, so no error */ + if (!filenames) + return 0; + + tty_options_init (); + + canvas_config = build_config (options.width, options.height, FALSE); + + grid_layout = grid_layout_new (); + grid_layout_set_view_size (grid_layout, options.width, options.height); + grid_layout_set_grid_size (grid_layout, options.grid_width, options.grid_height); + grid_layout_set_canvas_config (grid_layout, canvas_config); + grid_layout_set_term_info (grid_layout, options.term_info); + + for (l = filenames; l; l = g_list_next (l)) + { + grid_layout_push_path (grid_layout, l->data); + n_processed++; + } + + while (!interrupted_by_user + && (out_chunk = grid_layout_format_next_chunk (grid_layout))) + { + write_to_stdout (out_chunk, strlen (out_chunk)); + g_free (out_chunk); + } + + chafa_canvas_config_unref (canvas_config); + grid_layout_destroy (grid_layout); + + tty_options_deinit (); + return n_processed; +} + static void proc_init (void) { @@ -3255,9 +3341,12 @@ main (int argc, char *argv []) if (!parse_options (&argc, &argv)) exit (2); - ret = options.watch - ? run_watch (options.args->data) - : run_all (options.args); + if (options.grid_width > 1 || options.grid_height > 1) + ret = run_grid (options.args); + else if (options.watch) + ret = run_watch (options.args->data); + else + ret = run_all (options.args); retire_passthrough_workarounds_tmux (); diff --git a/tools/chafa/grid-layout.c b/tools/chafa/grid-layout.c new file mode 100644 index 0000000..750de8c --- /dev/null +++ b/tools/chafa/grid-layout.c @@ -0,0 +1,406 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2024 Hans Petter Jansson + * + * This file is part of Chafa, a program that shows pictures on text terminals. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#include "config.h" +#include +#include "grid-layout.h" +#include "media-loader.h" + +#define MAX_COLS 1024 + +struct GridLayout +{ + gint view_width, view_height; + gint n_cols, n_rows; + ChafaCanvasConfig *canvas_config; + ChafaTermInfo *term_info; + GList *paths, *next_path; + gint n_items; + guint finished_push : 1; +}; + +static void +update_geometry (GridLayout *grid) +{ + gint view_width, view_height; + gint n_cols, n_rows; + gint item_width, item_height; + + view_width = MAX (grid->view_width, 1); + view_height = MAX (grid->view_height, 1); + n_cols = MAX (grid->n_cols, 1); + n_rows = MAX (grid->n_rows, 1); + + item_width = MAX (view_width / n_cols - 1, 1); + item_height = MAX (view_height / n_rows - 1, 1); + + if (grid->canvas_config) + chafa_canvas_config_set_geometry (grid->canvas_config, item_width, item_height); +} + +static ChafaCanvas * +build_canvas (ChafaPixelType pixel_type, const guint8 *pixels, + gint src_width, gint src_height, gint src_rowstride, + const ChafaCanvasConfig *config, + gint placement_id, + ChafaTuck tuck) +{ + ChafaFrame *frame; + ChafaImage *image; + ChafaPlacement *placement; + ChafaCanvas *canvas; + + canvas = chafa_canvas_new (config); + frame = chafa_frame_new_borrow ((gpointer) pixels, pixel_type, + src_width, src_height, src_rowstride); + image = chafa_image_new (); + chafa_image_set_frame (image, frame); + + placement = chafa_placement_new (image, placement_id); + chafa_placement_set_tuck (placement, tuck); + chafa_placement_set_halign (placement, CHAFA_ALIGN_CENTER); + chafa_placement_set_valign (placement, CHAFA_ALIGN_END); + chafa_canvas_set_placement (canvas, placement); + + chafa_placement_unref (placement); + chafa_image_unref (image); + chafa_frame_unref (frame); + return canvas; +} + +static gboolean +format_item (GridLayout *grid, const gchar *path, GString ***gsa) +{ + MediaLoader *media_loader = NULL; + ChafaPixelType pixel_type; + gconstpointer pixels; + ChafaCanvas *canvas = NULL; + GError *error = NULL; + gint src_width, src_height, src_rowstride; + gboolean success = FALSE; + + media_loader = media_loader_new (path, &error); + if (!media_loader) + { + /* FIXME: Use a placeholder image */ + goto out; + } + + pixels = media_loader_get_frame_data (media_loader, + &pixel_type, + &src_width, + &src_height, + &src_rowstride); + if (!pixels) + { + /* FIXME: Use a placeholder image */ + goto out; + } + + canvas = build_canvas (pixel_type, pixels, + src_width, src_height, src_rowstride, grid->canvas_config, + -1, + CHAFA_TUCK_FIT); + chafa_canvas_print_rows (canvas, grid->term_info, gsa, NULL); + success = TRUE; + +out: + if (canvas) + chafa_canvas_unref (canvas); + if (media_loader) + media_loader_destroy (media_loader); + return success; +} + +static gchar * +format_grid_row_symbols (GridLayout *grid, GString ***item_gsa, gint n_cols_produced) +{ + gint col_width, row_height; + gchar *row_data, *p0; + gint row_data_len = 0; + gint item_height [MAX_COLS]; + gint i, j; + + chafa_canvas_config_get_geometry (grid->canvas_config, &col_width, &row_height); + + for (i = 0; i < MAX_COLS; i++) + item_height [i] = G_MAXINT; + + for (i = 0; i < row_height; i++) + { + for (j = 0; j < n_cols_produced; j++) + { + if (i >= item_height [j]) + { + /* Pad with spaces */ + row_data_len += col_width + 1; + } + else if (!item_gsa [j] [i]) + { + /* Pad with spaces */ + row_data_len += col_width + 1; + item_height [j] = i; + } + else + { + row_data_len += item_gsa [j] [i]->len + 1; + } + } + } + + row_data = p0 = g_malloc (row_data_len + 2); + + for (i = 0; i < row_height; i++) + { + for (j = 0; j < n_cols_produced; j++) + { + if (i >= item_height [j]) + { + /* Pad with spaces */ + memset (p0, ' ', col_width + 1); + p0 += col_width + 1; + } + else + { + gint col_row_data_len = item_gsa [j] [i]->len; + + if (j > 0) + *(p0++) = ' '; + + memcpy (p0, item_gsa [j] [i]->str, col_row_data_len); + p0 += col_row_data_len; + } + } + + *(p0++) = '\n'; + } + + *(p0++) = '\n'; + *(p0++) = '\0'; + return row_data; +} + +static gchar * +format_grid_row_images (GridLayout *grid, GString ***item_gsa, gint n_cols_produced) +{ + gchar save_cursor_seq [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; + gchar restore_cursor_and_skip_seq [CHAFA_TERM_SEQ_LENGTH_MAX * 2 + 2]; + gint col_width, row_height; + gchar *row_data, *p0; + gint row_data_len = 0; + gint i, j; + + chafa_canvas_config_get_geometry (grid->canvas_config, &col_width, &row_height); + + *chafa_term_info_emit_save_cursor_pos (grid->term_info, save_cursor_seq) = '\0'; + *chafa_term_info_emit_cursor_right (grid->term_info, + chafa_term_info_emit_restore_cursor_pos (grid->term_info, + restore_cursor_and_skip_seq), + col_width + 1) = '\0'; + + /* Allocate space for string */ + + for (i = 0; i < n_cols_produced; i++) + { + row_data_len += strlen (restore_cursor_and_skip_seq) + strlen (save_cursor_seq); + + for (j = 0; item_gsa [i] [j]; j++) + { + row_data_len += item_gsa [i] [j]->len; + } + } + + row_data = p0 = g_malloc (row_data_len + 2 * (row_height + 1) * CHAFA_TERM_SEQ_LENGTH_MAX + 2); + + /* Reserve space on terminal, scrolling if necessary */ + + for (i = 0; i < row_height + 1; i++) + { + p0 = chafa_term_info_emit_cursor_down_scroll (grid->term_info, p0); + } + p0 = chafa_term_info_emit_cursor_up (grid->term_info, p0, row_height + 1); + + /* Format image row */ + + for (i = 0; i < n_cols_produced; i++) + { + strcpy (p0, save_cursor_seq); + p0 += strlen (p0); + + for (j = 0; item_gsa [i] [j]; j++) + { + gint col_row_data_len = item_gsa [i] [j]->len; + + memcpy (p0, item_gsa [i] [j]->str, col_row_data_len); + p0 += col_row_data_len; + } + + strcpy (p0, restore_cursor_and_skip_seq); + p0 += strlen (p0); + } + + /* FIXME: Make relative */ + *(p0++) = '\r'; + + for (i = 0; i < row_height + 1; i++) + { + p0 = chafa_term_info_emit_cursor_down_scroll (grid->term_info, p0); + } + + *(p0++) = '\0'; + return row_data; +} + +static gchar * +format_grid_row (GridLayout *grid) +{ + ChafaPixelMode pixel_mode; + GString **item_gsa [MAX_COLS]; + gchar *chunk; + gint n_cols_produced; + gint i; + + for (i = 0; i < grid->n_cols && grid->next_path; ) + { + if (format_item (grid, grid->next_path->data, &item_gsa [i])) + { + /* FIXME: Use a placeholder image */ + i++; + } + + grid->next_path = g_list_next (grid->next_path); + } + + n_cols_produced = i; + if (n_cols_produced < 1) + return NULL; + + pixel_mode = chafa_canvas_config_get_pixel_mode (grid->canvas_config); + + if (pixel_mode == CHAFA_PIXEL_MODE_SYMBOLS) + chunk = format_grid_row_symbols (grid, item_gsa, n_cols_produced); + else + chunk = format_grid_row_images (grid, item_gsa, n_cols_produced); + + for (i = 0; i < n_cols_produced; i++) + chafa_free_gstring_array (item_gsa [i]); + + return chunk; +} + +GridLayout * +grid_layout_new (void) +{ + GridLayout *grid; + + grid = g_new0 (GridLayout, 1); + return grid; +} + +void +grid_layout_destroy (GridLayout *grid) +{ + if (grid->canvas_config) + chafa_canvas_config_unref (grid->canvas_config); + if (grid->term_info) + chafa_term_info_unref (grid->term_info); + g_list_free_full (g_steal_pointer (&grid->paths), g_free); + g_free (grid); +} + +void +grid_layout_set_canvas_config (GridLayout *grid, ChafaCanvasConfig *canvas_config) +{ + g_return_if_fail (grid != NULL); + + chafa_canvas_config_ref (canvas_config); + if (grid->canvas_config) + chafa_canvas_config_unref (grid->canvas_config); + grid->canvas_config = canvas_config; + + update_geometry (grid); +} + +void +grid_layout_set_term_info (GridLayout *grid, ChafaTermInfo *term_info) +{ + g_return_if_fail (grid != NULL); + + chafa_term_info_ref (term_info); + if (grid->term_info) + chafa_term_info_unref (grid->term_info); + grid->term_info = term_info; + + update_geometry (grid); +} + +void +grid_layout_set_view_size (GridLayout *grid, gint width, gint height) +{ + g_return_if_fail (grid != NULL); + + grid->view_width = width; + grid->view_height = height; + + update_geometry (grid); +} + +void +grid_layout_set_grid_size (GridLayout *grid, gint n_cols, gint n_rows) +{ + g_return_if_fail (grid != NULL); + + grid->n_cols = MIN (n_cols, MAX_COLS); + grid->n_rows = n_rows; + + update_geometry (grid); +} + +void +grid_layout_push_path (GridLayout *grid, const gchar *path) +{ + g_return_if_fail (grid != NULL); + g_return_if_fail (path != NULL); + g_return_if_fail (grid->finished_push == FALSE); + + grid->paths = g_list_prepend (grid->paths, g_strdup (path)); +} + +gchar * +grid_layout_format_next_chunk (GridLayout *grid) +{ + g_return_val_if_fail (grid != NULL, NULL); + + if (!grid->finished_push) + { + grid->paths = g_list_reverse (grid->paths); + grid->n_items = g_list_length (grid->paths); + grid->next_path = grid->paths; + grid->finished_push = TRUE; + + if (!grid->canvas_config) + grid->canvas_config = chafa_canvas_config_new (); + if (!grid->term_info) + grid->term_info = chafa_term_db_get_fallback_info (chafa_term_db_get_default ()); + + update_geometry (grid); + } + + return format_grid_row (grid); +} diff --git a/tools/chafa/grid-layout.h b/tools/chafa/grid-layout.h new file mode 100644 index 0000000..3655ae3 --- /dev/null +++ b/tools/chafa/grid-layout.h @@ -0,0 +1,43 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2024 Hans Petter Jansson + * + * This file is part of Chafa, a program that shows pictures on text terminals. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#ifndef __GRID_LAYOUT_H__ +#define __GRID_LAYOUT_H__ + +#include + +G_BEGIN_DECLS + +typedef struct GridLayout GridLayout; + +GridLayout *grid_layout_new (void); +void grid_layout_destroy (GridLayout *grid); + +void grid_layout_set_canvas_config (GridLayout *grid, ChafaCanvasConfig *canvas_config); +void grid_layout_set_term_info (GridLayout *grid, ChafaTermInfo *term_info); +void grid_layout_set_view_size (GridLayout *grid, gint width, gint height); +void grid_layout_set_grid_size (GridLayout *grid, gint n_cols, gint n_rows); + +void grid_layout_push_path (GridLayout *grid, const gchar *path); + +gchar *grid_layout_format_next_chunk (GridLayout *grid); + +G_END_DECLS + +#endif /* __GRID_LAYOUT_H__ */