From 53d1077a222b46f0d68e15eb1b4e00c4eef1fcaf Mon Sep 17 00:00:00 2001 From: scootz Date: Fri, 1 May 2026 13:25:01 +0100 Subject: [PATCH] Auto-select digit template font --- README.md | 8 ++++ reforger_queue_read.py | 101 +++++++++++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3692925..2321258 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ make No sudo install is required for the default Portal/PipeWire capture path. +The digit matcher uses ImageMagick to render reference digits. By default it auto-selects an installed font, preferring Roboto Condensed when present and falling back to common system fonts such as DejaVu Sans. + ## Use Preferred command: @@ -61,6 +63,12 @@ Save the captured input image while debugging: uv run reforger_queue_read.py --portal-window --save-input /tmp/apt-portal-window.png --show-crop --debug ``` +If font matching is weird on a specific machine, pass an installed ImageMagick font explicitly: + +```bash +uv run reforger_queue_read.py --portal-window --font DejaVu-Sans-Condensed --show-crop --debug +``` + ## Optional KDE/KWin Mode KWin mode can automatically find the Arma window by KWin metadata and can rebind after the game restarts. It is KDE-specific and requires installing an authorized helper. diff --git a/reforger_queue_read.py b/reforger_queue_read.py index fd5128b..5c7789a 100644 --- a/reforger_queue_read.py +++ b/reforger_queue_read.py @@ -16,8 +16,20 @@ import uuid DEFAULT_REFERENCE_SIZE = (2560, 1440) DEFAULT_REFERENCE_CROP = (1050, 620, 100, 60) -DEFAULT_FONT = "Roboto-Condensed" +DEFAULT_FONT = "auto" DEFAULT_POINTSIZE = 43 +AUTO_FONT_CANDIDATES = ( + "Roboto-Condensed", + "RobotoCondensed-Regular", + "Roboto-Condensed-Regular", + "Noto-Sans-Condensed", + "NotoSans-Condensed", + "Liberation-Sans-Narrow", + "DejaVu-Sans-Condensed", + "DejaVu-Sans", + "Arial", + "Helvetica", +) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) SYSTEM_KWIN_HELPER = "/usr/local/bin/reforger-kwin-capture" DEFAULT_PORTAL_RESTORE_TOKEN = os.path.expanduser( @@ -507,8 +519,7 @@ def save_obs_scene_screenshot_with_setup( time.sleep(0.5) -def ppm_from_magick(args): - data = run_bytes(["magick", *args, "-alpha", "off", "-depth", "8", "ppm:-"]) +def parse_ppm(data): idx = 0 def next_token(): @@ -540,6 +551,44 @@ def ppm_from_magick(args): return width, height, data[idx:] +def ppm_from_magick(args): + return parse_ppm(run_bytes(["magick", *args, "-alpha", "off", "-depth", "8", "ppm:-"])) + + +def try_ppm_from_magick(args): + command = ["magick", *args, "-alpha", "off", "-depth", "8", "ppm:-"] + try: + result = subprocess.run(command, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except FileNotFoundError: + sys.exit("missing required command: magick") + + if result.returncode != 0: + return None, result.stderr.decode("utf-8", "replace").strip() + + try: + return parse_ppm(result.stdout), "" + except Exception as exc: + return None, str(exc) + + +def magick_fonts(): + fonts = set() + for line in run_text(["magick", "-list", "font"]).splitlines(): + match = re.match(r"\s*Font:\s*(\S+)", line) + if match: + fonts.add(match.group(1)) + return fonts + + +def template_font_candidates(font): + if font != "auto": + return [font] + + available = magick_fonts() + candidates = [candidate for candidate in AUTO_FONT_CANDIDATES if candidate in available] + return [*candidates, None] + + def image_size(image_path): output = run_bytes(["magick", "identify", "-format", "%w %h", image_path]).decode("ascii") width, height = output.split() @@ -657,24 +706,46 @@ def template_distance(left, right): return hamming, iou -def build_templates(font, pointsize): - width, height, raw = ppm_from_magick( +def build_template_args(font, pointsize): + args = [ + "-background", + "black", + "-fill", + "white", + ] + if font: + args.extend(["-font", font]) + args.extend( [ - "-background", - "black", - "-fill", - "white", - "-font", - font, "-pointsize", str(pointsize), "label:0123456789", ] ) - components = connected_components(white_mask(width, height, raw)) - if len(components) < 10: - sys.exit(f"template rendering produced only {len(components)} digit components") - return {str(index): normalize(component) for index, component in enumerate(components[:10])} + return args + + +def build_templates(font, pointsize): + errors = [] + for candidate in template_font_candidates(font): + ppm, error = try_ppm_from_magick(build_template_args(candidate, pointsize)) + if ppm is None: + errors.append(f"{candidate or 'ImageMagick default'}: {error}") + continue + + width, height, raw = ppm + components = connected_components(white_mask(width, height, raw)) + if len(components) < 10: + errors.append(f"{candidate or 'ImageMagick default'}: rendered only {len(components)} digit components") + continue + return {str(index): normalize(component) for index, component in enumerate(components[:10])} + + if font == "auto": + sys.exit("could not render digit templates with any available ImageMagick font:\n " + "\n ".join(errors)) + sys.exit( + f"could not render digit templates with font {font!r}. " + "Install that font, use --font auto, or pass another ImageMagick font name." + ) def classify(component, templates):