diff --git a/docs/chafa.xml b/docs/chafa.xml index dd1f2552..8eed1bd2 100644 --- a/docs/chafa.xml +++ b/docs/chafa.xml @@ -137,6 +137,18 @@ Clear screen before processing each file. + + + +Try to match the input's size exactly [on, off, auto]. When on, this will +override other sizing options and produce output images at the exact pixel size +of the inputs. In auto mode, scaling will be avoided (in exchange for padding) +if the output size is equal to or slightly bigger than the input. When off, +padding will never be added, and the image is scaled to fit the containing +cell extent. Defaults to auto. + + + diff --git a/tools/chafa/chafa.c b/tools/chafa/chafa.c index 47ea299c..0b0de633 100644 --- a/tools/chafa/chafa.c +++ b/tools/chafa/chafa.c @@ -64,6 +64,14 @@ #define CELL_EXTENT_AUTO_MAX 65535 #define PIXEL_EXTENT_MAX 32767 +typedef enum +{ + TRISTATE_FALSE = 0, + TRISTATE_TRUE, + TRISTATE_AUTO +} +Tristate; + typedef struct { gchar *executable_name; @@ -126,6 +134,8 @@ typedef struct * eliminate interframe delay altogether. */ gdouble anim_speed_multiplier; + Tristate use_exact_size; + /* Automatically set if terminal size is detected and there is * zero bottom margin. */ gboolean have_parking_row; @@ -418,6 +428,7 @@ print_summary (void) " -C, --center=BOOL Center images horizontally in view [on, off]. Default off.\n" " --clear Clear screen before processing each file.\n" + " --exact-size=MODE Try to match the input's size exactly [on, off, auto].\n" " --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" @@ -523,10 +534,21 @@ static gboolean fuzz_seed_get_bool (gconstpointer seed, gint seed_len, gint *ofs) { const guchar *seed_u8 = seed; - + return seed_u8 [(*ofs)++ % seed_len] < 128 ? TRUE : FALSE; } +static Tristate +fuzz_seed_get_tristate (gconstpointer seed, gint seed_len, gint *ofs) +{ + const guchar *seed_u8 = seed; + guint v = seed_u8 [(*ofs)++ % seed_len]; + + return (v < 256 / 3) ? TRISTATE_FALSE + : (v < (256 * 2) / 3) ? TRISTATE_TRUE + : TRISTATE_AUTO; +} + static guint32 fuzz_seed_get_u32 (gconstpointer seed, gint seed_len, gint *ofs) { @@ -610,6 +632,7 @@ fuzz_options_with_seed (GlobalOptions *opt, gconstpointer seed, gint seed_len) opt->passthrough_set = TRUE; opt->transparency_threshold = fuzz_seed_get_double (seed, seed_len, &ofs, 0.0, 1.0); opt->transparency_threshold_set = TRUE; + opt->use_exact_size = fuzz_seed_get_tristate (seed, seed_len, &ofs); } static void @@ -632,6 +655,60 @@ fuzz_options_with_file (GlobalOptions *opt, const gchar *filename) fuzz_options_with_seed (opt, seed, seed_len); } +static gboolean +parse_boolean_token (const gchar *token, gboolean *value_out) +{ + gboolean success = FALSE; + + if (!g_ascii_strcasecmp (token, "on") + || !g_ascii_strcasecmp (token, "yes") + || !g_ascii_strcasecmp (token, "true")) + { + *value_out = TRUE; + } + else if (!g_ascii_strcasecmp (token, "off") + || !g_ascii_strcasecmp (token, "no") + || !g_ascii_strcasecmp (token, "false")) + { + *value_out = FALSE; + } + else + { + goto out; + } + + success = TRUE; + +out: + return success; +} + +static gboolean +parse_tristate_token (const gchar *token, Tristate *value_out) +{ + gboolean success = FALSE; + gboolean value_bool = FALSE; + + if (!g_ascii_strcasecmp (token, "auto") + || !g_ascii_strcasecmp (token, "default")) + { + *value_out = TRISTATE_AUTO; + } + else + { + success = parse_boolean_token (token, &value_bool); + if (!success) + goto out; + + *value_out = value_bool ? TRISTATE_TRUE : TRISTATE_FALSE; + } + + success = TRUE; + +out: + return success; +} + static gboolean parse_colors_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_GNUC_UNUSED gpointer data, GError **error) { @@ -977,6 +1054,21 @@ parse_size_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_GN 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) +{ + gboolean result; + + result = parse_tristate_token (value, &options.use_exact_size); + if (!result) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Exact size selector must be one of [on, off, auto]."); + } + + return result; +} + static gboolean parse_dither_grain_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_GNUC_UNUSED gpointer data, GError **error) { @@ -1207,34 +1299,6 @@ parse_dump_glyph_file_arg (G_GNUC_UNUSED const gchar *option_name, const gchar * return result; } -static gboolean -parse_boolean_token (const gchar *token, gboolean *value_out) -{ - gboolean success = FALSE; - - if (!g_ascii_strcasecmp (token, "on") - || !g_ascii_strcasecmp (token, "yes") - || !g_ascii_strcasecmp (token, "true")) - { - *value_out = TRUE; - } - else if (!g_ascii_strcasecmp (token, "off") - || !g_ascii_strcasecmp (token, "no") - || !g_ascii_strcasecmp (token, "false")) - { - *value_out = FALSE; - } - else - { - goto out; - } - - success = TRUE; - -out: - return success; -} - static gboolean parse_animate_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_GNUC_UNUSED gpointer data, GError **error) { @@ -1783,6 +1847,7 @@ parse_options (int *argc, char **argv []) { "dither-intensity", '\0', 0, G_OPTION_ARG_DOUBLE, &options.dither_intensity, "Dither intensity", NULL }, { "dump-glyph-file", '\0', 0, G_OPTION_ARG_CALLBACK, parse_dump_glyph_file_arg, "Dump glyph file", NULL }, { "duration", 'd', 0, G_OPTION_ARG_CALLBACK, parse_duration_arg, "Duration", NULL }, + { "exact-size", '\0', 0, G_OPTION_ARG_CALLBACK, parse_exact_size_arg, "Whether to prefer the original image size", NULL }, { "fg", '\0', 0, G_OPTION_ARG_CALLBACK, parse_fg_color_arg, "Foreground color of display", NULL }, { "fg-only", '\0', 0, G_OPTION_ARG_NONE, &options.fg_only, "Foreground only", NULL }, { "fill", '\0', 0, G_OPTION_ARG_CALLBACK, parse_fill_arg, "Fill symbols", NULL }, @@ -1877,6 +1942,7 @@ parse_options (int *argc, char **argv []) options.file_duration_s = -1.0; /* Unset */ options.anim_fps = -1.0; options.anim_speed_multiplier = 1.0; + options.use_exact_size = TRISTATE_AUTO; options.output_utf_16_on_windows = FALSE; options.is_conhost_mode = FALSE; @@ -1931,7 +1997,7 @@ parse_options (int *argc, char **argv []) && options.cell_width > 0 && options.cell_height > 0) { - options.font_ratio = (gfloat) options.cell_width / (gfloat) options.cell_height; + options.font_ratio = (gdouble) options.cell_width / (gdouble) options.cell_height; } } @@ -2218,6 +2284,38 @@ parse_options (int *argc, char **argv []) return result; } +static void +pixel_to_cell_dimensions (gdouble scale, + gint cell_width, gint cell_height, + gint width, gint height, + gint *width_out, gint *height_out) +{ + gint extra_height; + + /* Scale can't be zero or negative */ + scale = MAX (scale, 0.00001); + + /* Zero or negative cell dimensions -> presumably unknown, use 8x8 */ + if (cell_width < 1) + cell_width = 8; + if (cell_height < 1) + cell_height = 8; + + extra_height = options.pixel_mode == CHAFA_PIXEL_MODE_SIXELS ? 5 : 0; + + if (width_out) + { + *width_out = ((int) (width * scale) + cell_width - 1) / cell_width; + *width_out = MAX (*width_out, 1); + } + + if (height_out) + { + *height_out = ((int) (height * scale) + cell_height - 1 + extra_height) / cell_height; + *height_out = MAX (*height_out, 1); + } +} + static gchar * emit_vertical_space (gchar *dest) { @@ -2506,10 +2604,12 @@ 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) + gint placement_id, + ChafaTuck tuck) { ChafaFrame *frame; ChafaImage *image; + ChafaPlacement *placement; ChafaCanvas *canvas; canvas = chafa_canvas_new (config); @@ -2517,40 +2617,18 @@ build_canvas (ChafaPixelType pixel_type, const guint8 *pixels, src_width, src_height, src_rowstride); image = chafa_image_new (); chafa_image_set_frame (image, frame); - chafa_canvas_set_image (canvas, image, placement_id); + + placement = chafa_placement_new (image, placement_id); + chafa_placement_set_tuck (placement, tuck); + chafa_placement_set_halign (placement, options.center ? CHAFA_ALIGN_CENTER : CHAFA_ALIGN_START); + chafa_canvas_set_placement (canvas, placement); + + chafa_placement_unref (placement); chafa_image_unref (image); chafa_frame_unref (frame); return canvas; } -static void -pixel_to_cell_dimensions (gdouble scale, - gint cell_width, gint cell_height, - gint width, gint height, - gint *width_out, gint *height_out) -{ - /* Scale can't be zero or negative */ - scale = MAX (scale, 0.00001); - - /* Zero or negative cell dimensions -> presumably unknown, use 8x8 */ - if (cell_width < 1) - cell_width = 8; - if (cell_height < 1) - cell_height = 8; - - if (width_out) - { - *width_out = (gdouble) width * scale / cell_width + 0.5; - *width_out = MAX (*width_out, 1); - } - - if (height_out) - { - *height_out = (gdouble) height * scale / cell_width + 0.5; - *height_out = MAX (*height_out, 1); - } -} - typedef enum { FILE_FAILED, @@ -2618,14 +2696,34 @@ run_generic (const gchar *filename, gboolean is_first_file, gboolean is_first_fr gint delay_ms; ChafaPixelType pixel_type; gint src_width, src_height, src_rowstride; + gint uncorrected_src_width, uncorrected_src_height; gint virt_src_width, virt_src_height; gint dest_width, dest_height; const guint8 *pixels; ChafaCanvasConfig *config; ChafaCanvas *canvas; + ChafaTuck tuck; g_timer_start (timer); + if (options.use_exact_size == TRISTATE_TRUE) + { + /* True */ + tuck = CHAFA_TUCK_SHRINK_TO_FIT; + } + else + { + /* False/auto */ + if (options.stretch) + { + tuck = CHAFA_TUCK_STRETCH; + } + else + { + tuck = CHAFA_TUCK_FIT; + } + } + pixels = media_loader_get_frame_data (media_loader, &pixel_type, &src_width, @@ -2647,16 +2745,30 @@ run_generic (const gchar *filename, gboolean is_first_file, gboolean is_first_fr pixel_to_cell_dimensions (options.scale, options.cell_width, options.cell_height, src_width, src_height, - &virt_src_width, &virt_src_height); + &uncorrected_src_width, &uncorrected_src_height); + + virt_src_width = uncorrected_src_width; + if (options.cell_width > 0 && options.cell_height > 0) + virt_src_height = uncorrected_src_height / options.font_ratio; + else + virt_src_height = uncorrected_src_height; } else { - virt_src_width = src_width; - virt_src_height = src_height; + virt_src_width = uncorrected_src_width = src_width; + virt_src_height = uncorrected_src_height = src_height; } - dest_width = options.width; - dest_height = options.height; + if (options.use_exact_size == TRISTATE_TRUE) + { + dest_width = virt_src_width; + dest_height = virt_src_height; + } + else + { + dest_width = options.width; + dest_height = options.height; + } chafa_calc_canvas_geometry (virt_src_width, virt_src_height, @@ -2666,10 +2778,28 @@ run_generic (const gchar *filename, gboolean is_first_file, gboolean is_first_fr options.scale >= SCALE_MAX - 0.1 ? TRUE : FALSE, options.stretch); + if (options.use_exact_size == TRISTATE_AUTO + && dest_width == uncorrected_src_width + && dest_height == uncorrected_src_height) + { + tuck = CHAFA_TUCK_SHRINK_TO_FIT; + } + +#if 0 + /* The size calculations are too convoluted, so we may need this to + * debug --exact-size. */ + g_printerr ("src=(%dx%d) unc=(%dx%d) virt=(%dx%d) dest=(%dx%d)\n", + src_width, src_height, + uncorrected_src_width, uncorrected_src_height, + virt_src_width, virt_src_height, + dest_width, dest_height); +#endif + config = build_config (dest_width, dest_height, is_animation); canvas = build_canvas (pixel_type, pixels, src_width, src_height, src_rowstride, config, - placement_id >= 0 ? placement_id + ((frame_count++) % 2) : -1); + placement_id >= 0 ? placement_id + ((frame_count++) % 2) : -1, + tuck); #ifdef G_OS_WIN32 if (options.is_conhost_mode)