from pathlib import Path from .assets import load_json, load_rgb from .settings import BOARD_DECK, DECKS_DIR, BuildConfig ORIGINAL_BOARD_MM = (88.9, 139.7) TRIMMED_BOARD_ASPECT_RATIO = ORIGINAL_BOARD_MM[0] / ORIGINAL_BOARD_MM[1] BOARD_SIZE_OPTIONS_MM = { "original": ORIGINAL_BOARD_MM, "a6": (105.0, 148.0), "a5": (148.0, 210.0), "a4": (210.0, 297.0), "half-letter": (139.7, 215.9), "letter": (215.9, 279.4), } BOARD_MODE_CHOICES = tuple( [name for name in BOARD_SIZE_OPTIONS_MM] + [f"{name}-fit" for name in BOARD_SIZE_OPTIONS_MM] + ["custom", "custom-fit"] ) VISIBLE_BOARD_MODE_CHOICES = ("original", "a6", "a6-fit", "a5", "a5-fit", "half-letter", "half-letter-fit", "custom") def fit_size_inside(box_mm: tuple[float, float], ratio: float = TRIMMED_BOARD_ASPECT_RATIO) -> tuple[float, float]: max_w, max_h = box_mm if ratio > max_w / max_h: return max_w, max_w / ratio return max_h * ratio, max_h def board_target_mm_for_mode( board_path: Path, board_mode: str, board_custom_mm: tuple[float, float] | None = None, ) -> tuple[float, float]: if board_custom_mm is not None: if board_mode == "custom-fit": return fit_size_inside(board_custom_mm) return board_custom_mm if board_mode.endswith("-fit"): base_mode = board_mode.removesuffix("-fit") if base_mode in BOARD_SIZE_OPTIONS_MM and base_mode != "original": return fit_size_inside(BOARD_SIZE_OPTIONS_MM[base_mode]) return BOARD_SIZE_OPTIONS_MM.get(board_mode, ORIGINAL_BOARD_MM) def board_aspect_ratio(board_path: Path) -> float: deck_json = board_path.parent / "deck.json" if deck_json.exists(): meta = load_json(deck_json) if meta.get("cardWidth") and meta.get("cardHeight"): return float(meta["cardWidth"]) / float(meta["cardHeight"]) with load_rgb(board_path) as img: return img.width / img.height def board_target_mm(board_path: Path, config: BuildConfig) -> tuple[float, float]: return board_target_mm_for_mode(board_path, config.board_mode, config.board_custom_mm) def board_size_fits_paper( board_mode: str, board_custom_mm: tuple[float, float] | None, paper_mm: tuple[float, float], margin_mm: float, ) -> bool: usable_w = paper_mm[0] - 2 * margin_mm usable_h = paper_mm[1] - 2 * margin_mm board_paths = sorted((DECKS_DIR / BOARD_DECK).glob("*-jumbo-front.png")) if not board_paths: return True for board_path in board_paths: board_w, board_h = board_target_mm_for_mode(board_path, board_mode, board_custom_mm) fits_upright = board_w <= usable_w and board_h <= usable_h fits_rotated = board_h <= usable_w and board_w <= usable_h if not (fits_upright or fits_rotated): return False return True def board_mode_fits_paper(board_mode: str, paper_mm: tuple[float, float], margin_mm: float) -> bool: return board_size_fits_paper(board_mode, None, paper_mm, margin_mm) def crop_board_bleed(img): target_w = round(img.width * (3.5 / 3.75)) target_h = round(img.height * (5.5 / 5.75)) left = max(0, (img.width - target_w) // 2) top = max(0, (img.height - target_h) // 2) return img.crop((left, top, left + target_w, top + target_h))