"""Command-line SVG QR code generator.""" from __future__ import annotations import argparse from pathlib import Path import re from typing import Sequence import qrcode import yaml SVG_SUFFIX = ".svg" MAX_FILENAME_LENGTH = 32 INVALID_FILENAME_CHARACTERS = re.compile(r'[<>:"/\\|?*\x00-\x1f]') def svg_path(text: str) -> str: """Build an SVG containing the QR code as filled convex module paths.""" code = qrcode.QRCode( version=None, error_correction=qrcode.constants.ERROR_CORRECT_L, border=4, ) code.add_data(text) code.make(fit=True) matrix = code.get_matrix() size = len(matrix) modules = [] for row_index, row in enumerate(matrix): for column_index, is_dark in enumerate(row): if is_dark: modules.append( f"M{column_index} {row_index}H{column_index + 1}" f"V{row_index + 1}H{column_index}Z" ) path_data = "".join(modules) return ( '\n' f'\n' f' \n' "\n" ) def write_svg(text: str, output_path: Path) -> None: output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(svg_path(text), encoding="utf-8") def with_svg_suffix(path: Path) -> Path: if path.suffix.lower() == SVG_SUFFIX: return path return path.with_name(f"{path.name}{SVG_SUFFIX}") def automatic_output_path(text: str, directory: Path = Path(".")) -> Path: filename = INVALID_FILENAME_CHARACTERS.sub("_", text).rstrip(". ") if filename in {"", ".", ".."}: filename = "_" stem_length = MAX_FILENAME_LENGTH - len(SVG_SUFFIX) return directory / f"{filename[:stem_length]}{SVG_SUFFIX}" def read_yaml_items(input_path: Path) -> dict[str, str]: try: items = yaml.safe_load(input_path.read_text(encoding="utf-8")) or {} except yaml.YAMLError as error: raise ValueError(f"{input_path}: invalid YAML: {error}") from None if not isinstance(items, dict) or not all( isinstance(key, str) and isinstance(value, str) for key, value in items.items() ): raise ValueError(f"{input_path}: YAML must be a flat mapping of text values") return items def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Generate SVG QR codes from text.") parser.add_argument("text", nargs="?", help="Text to encode (the default input mode).") parser.add_argument("-t", "--text", dest="option_text", help="Text to encode.") parser.add_argument("-o", "--output", type=Path, help="Path for the generated SVG.") parser.add_argument("-i", "--input", type=Path, help="File containing one QR value per line.") parser.add_argument("--yaml", type=Path, help="Flat YAML file containing output names and text.") return parser def run(arguments: Sequence[str] | None = None) -> None: parser = build_parser() args = parser.parse_args(arguments) if args.input is not None or args.yaml is not None: if args.input is not None and args.yaml is not None: parser.error("--input and --yaml cannot be used together") if args.text is not None or args.option_text is not None: parser.error("text input cannot be used with batch input") if args.input is not None: for line in args.input.read_text(encoding="utf-8").splitlines(): write_svg(line, automatic_output_path(line, Path("output"))) else: try: items = read_yaml_items(args.yaml) except ValueError as error: parser.error(str(error)) for name, text in items.items(): write_svg(text, automatic_output_path(name, Path("output"))) return if args.text is not None and args.option_text is not None: parser.error("provide text either positionally or with --text, not both") text = args.option_text if args.option_text is not None else args.text if text is None: parser.error("text is required unless --input is provided") output_path = with_svg_suffix(args.output) if args.output else automatic_output_path(text) write_svg(text, output_path) if __name__ == "__main__": run()