From 6528f64594d6021606c9088d6693184211b4a9a1 Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 14 Jul 2026 13:04:12 +0200 Subject: [PATCH] add REAL_VIEWFINDER setting to size and center the live preview Sizes the curses viewfinder box to match the print output's character grid (A4/roll-paper target dims) instead of always filling the terminal, and always centers the box. Defaults to true. --- ascii.conf | 8 ++++++++ photobooth.py | 49 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/ascii.conf b/ascii.conf index ec183ff..b08f1d6 100644 --- a/ascii.conf +++ b/ascii.conf @@ -18,6 +18,14 @@ NEGATIVE=false FLIP_X=false FLIP_Y=false +# Size the live-preview viewfinder box to the same character grid as the +# final print output (see PRINTER_A4/PRINTER_MAX_LINES/PRINTER_MAX_COLS in +# printer.conf) instead of just filling the terminal. The preview frame is +# still cropped/converted on its own, upright, before any print-time +# rotation - only the box's cols/rows match the output. The viewfinder is +# always centered in the terminal either way. (true/false) +REAL_VIEWFINDER=true + # Color and braille output are intentionally not exposed here: both rely on # ANSI/multi-byte Unicode that a parallel-port needle printer can't render. # Only add --color/--braille yourself if you don't plan to print via diff --git a/photobooth.py b/photobooth.py index 4decc1b..bbd514c 100755 --- a/photobooth.py +++ b/photobooth.py @@ -79,6 +79,7 @@ class Config: self.negative = as_bool(ascii_.get("NEGATIVE")) self.flip_x = as_bool(ascii_.get("FLIP_X")) self.flip_y = as_bool(ascii_.get("FLIP_Y")) + self.real_viewfinder = as_bool(ascii_.get("REAL_VIEWFINDER")) self.printer_a4 = as_bool(printer.get("PRINTER_A4")) self.printer_max_lines = int(printer.get("PRINTER_MAX_LINES", "64") or 64) @@ -177,8 +178,28 @@ def capture_label(cfg): return "space" if cfg.key_capture == " " else cfg.key_capture -def box_dims_for(term_rows, term_cols): - return max(term_rows - 2, 3), max(term_cols, 10) +def output_target_dims(cfg, inner_cols, inner_rows): + """cols/rows the print/output pipeline sizes a capture to (see prepare_print_frame).""" + if cfg.printer_max_lines == 0: + return cfg.printer_max_cols, None + if cfg.printer_a4: + return cfg.printer_max_cols, cfg.printer_max_lines + return inner_cols, inner_rows + + +def box_dims_for(term_rows, term_cols, cfg=None): + max_rows, max_cols = max(term_rows - 2, 3), max(term_cols, 10) + if cfg is None or not cfg.real_viewfinder: + return max_rows, max_cols + target_cols, target_rows = output_target_dims(cfg, max_cols - 2, max_rows - 2) + rows = max_rows if target_rows is None else max(min(target_rows + 2, max_rows), 3) + cols = max(min(target_cols + 2, max_cols), 10) + return rows, cols + + +def box_origin(term_rows, term_cols, box_rows, box_cols): + """Top-left corner that centers a box_rows x box_cols window in the terminal.""" + return max((term_rows - box_rows) // 2, 0), max((term_cols - box_cols) // 2, 0) _CHAR_ASPECT = 0.5 # width:height ratio of a typical monospace glyph cell @@ -210,23 +231,21 @@ def prepare_print_frame(frame, cfg, inner_cols, inner_rows): first and mangling already-rendered ascii text, which doesn't account for non-square character cells and reads as visibly distorted. """ - if cfg.printer_max_lines == 0: + cols, rows = output_target_dims(cfg, inner_cols, inner_rows) + if rows is None: # Roll printer: paper feeds continuously, so there's no page height # to crop to - only the carriage width (PRINTER_MAX_COLS) limits the # print. Leave the frame uncropped and pass rows=None downstream so # ascii-image-converter derives height from the image's own aspect # ratio instead of forcing a row count. - cols, rows = cfg.printer_max_cols, None if cfg.printer_a4: frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) elif cfg.printer_a4: - cols, rows = cfg.printer_max_cols, cfg.printer_max_lines # Cropping happens before the 90-degree rotation below, which swaps # width and height, so crop to the inverse of the final aspect ratio. frame = crop_to_aspect(frame, 1 / visual_aspect(cols, rows)) frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) else: - cols, rows = inner_cols, inner_rows frame = crop_to_aspect(frame, visual_aspect(cols, rows)) return frame, cols, rows @@ -253,8 +272,9 @@ def main(stdscr): frame_times = [] message, message_until = "", 0.0 rows, cols = stdscr.getmaxyx() - box_dims = box_dims_for(rows, cols) - box_win = curses.newwin(box_dims[0], box_dims[1], 0, 0) + box_dims = box_dims_for(rows, cols, cfg) + box_pos = box_origin(rows, cols, *box_dims) + box_win = curses.newwin(*box_dims, *box_pos) try: while True: @@ -268,12 +288,14 @@ def main(stdscr): fps = len(frame_times) rows, cols = stdscr.getmaxyx() - box_rows, box_cols = box_dims_for(rows, cols) + box_rows, box_cols = box_dims_for(rows, cols, cfg) inner_rows, inner_cols = max(box_rows - 2, 1), max(box_cols - 2, 1) + box_pos_now = box_origin(rows, cols, box_rows, box_cols) - if (box_rows, box_cols) != box_dims: - box_win = curses.newwin(box_rows, box_cols, 0, 0) + if (box_rows, box_cols) != box_dims or box_pos_now != box_pos: + box_win = curses.newwin(box_rows, box_cols, *box_pos_now) box_dims = (box_rows, box_cols) + box_pos = box_pos_now stdscr.erase() # Live preview: always upright, cropped to roughly the box's own @@ -306,9 +328,10 @@ def main(stdscr): f"[{cfg.key_reset}] reset [{capture_label(cfg)}] capture " f"[{cfg.key_quit}] quit " ) + status_y = box_pos[0] + box_rows try: - stdscr.addstr(box_rows, 0, status[:cols].ljust(cols)) - stdscr.addstr(box_rows + 1, 0, help_line[:cols].ljust(cols - 1)) + stdscr.addstr(status_y, 0, status[:cols].ljust(cols)) + stdscr.addstr(status_y + 1, 0, help_line[:cols].ljust(cols - 1)) except curses.error: pass stdscr.refresh()