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.
master
Amir Alexander Abdelbaki 2026-07-14 13:04:12 +02:00
parent 8ecb5cd558
commit 6528f64594
2 changed files with 44 additions and 13 deletions

View File

@ -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

View File

@ -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()