Source code for yasuki_gui.ui.deck_builder.custom_art

from pathlib import Path

from PIL import Image

from yasuki_core.card_art import (
    CustomPrint,
    art_rect,
    classify,
    cover_crop,
    custom_print_id,
    mon_overlays,
    overlays_for,
    patches_for,
)
from yasuki_core.paths import OVERLAYS_DIR, resolve_set_image_path


def _box(image: Image.Image, rect: tuple[float, float, float, float]) -> tuple[int, int, int, int]:
    w, h = image.size
    left, top, right, bottom = rect
    return (round(left * w), round(top * h), round(right * w), round(bottom * h))


[docs] def composite_art( recipient_path: Path, donor_path: Path, recipient_key: tuple[str, str], donor_key: tuple[str, str], recipient_keywords: list[str] = (), ) -> Image.Image: """Crop the donor's art (its layout's cut rect) into the recipient's art window. The donor crop is reduced to the window's aspect ratio before scaling, so the art fills the window edge-to-edge without distortion (a thin strip of the donor's outer edge is trimmed). The recipient's frame overlays (holding flair) and keyword mons are stamped back over the new art.""" recipient = Image.open(recipient_path).convert("RGB") donor = Image.open(donor_path).convert("RGB") window = _box(recipient, art_rect(recipient_key)) target_w, target_h = window[2] - window[0], window[3] - window[1] source = cover_crop(_box(donor, art_rect(donor_key)), target_w, target_h) art = donor.crop(source).resize((target_w, target_h), Image.LANCZOS) out = recipient.copy() out.paste(art, (window[0], window[1])) overlays = overlays_for(recipient_key) + mon_overlays(recipient_keywords, recipient_key[0]) _stamp_patches(out, recipient, recipient_key) _stamp_overlays(out, overlays) # last, so the mons sit on top of any patch return out
def _stamp_overlays(card: Image.Image, overlays: list[dict]) -> None: """Stamp frame overlays (holding flair, keyword mons) over the donor art. Each overlay carries a baked transparent hole where a card-specific element (the gold-cost coin) sits, so that element shows through unaltered.""" for overlay in overlays: left, top, right, bottom = _box(card, overlay["rect"]) asset = Image.open(OVERLAYS_DIR / overlay["asset"]).convert("RGBA") asset = asset.resize((right - left, bottom - top), Image.LANCZOS) card.paste(asset, (left, top), asset) def _stamp_patches(card: Image.Image, recipient: Image.Image, key: tuple[str, str]) -> None: """Re-stamp recipient patches (stat icons, banner corners, frame edges) over the donor art. Each region is harvested from the pristine ``recipient`` at its rect. A masked patch keeps only the silhouette shape (the stat icons, so the donor art shows around them); an unmasked patch restores the whole rect (banner corners, frame edges).""" for patch in patches_for(key): left, top, right, bottom = _box(card, patch["rect"]) if right - left < 1 or bottom - top < 1: continue crop = recipient.crop((left, top, right, bottom)) mask_asset = patch.get("mask") if mask_asset: mask = Image.open(OVERLAYS_DIR / mask_asset).convert("RGBA") mask = mask.resize((right - left, bottom - top), Image.LANCZOS) card.paste(crop, (left, top), mask.getchannel("A")) else: card.paste(crop, (left, top))
[docs] def custom_print_record(recipe: CustomPrint, repository) -> dict: """A synthetic print dict for a recipe, shaped like a DB print so it flows through the UI.""" donor = repository.get_card(recipe.donor_card_id) or {} donor_name = donor.get("extended_title") or donor.get("name") or recipe.donor_card_id return { "print_id": custom_print_id(recipe), "card_id": recipe.recipient_card_id, "set_name": f"Custom · {donor_name}", "image_path": None, "back_image_path": None, "flavor_text": "", "is_custom": True, "recipe": recipe, }
[docs] def render_custom_image(recipe: CustomPrint, repository) -> Image.Image | None: """Recompose a recipe's art from its recipient and donor printings, or None if either is missing.""" recipient_card = repository.get_card(recipe.recipient_card_id) donor_card = repository.get_card(recipe.donor_card_id) if not recipient_card or not donor_card: return None recipient_print = _find_print(repository, recipe.recipient_card_id, recipe.recipient_print_id) donor_print = _find_print(repository, recipe.donor_card_id, recipe.donor_print_id) if not recipient_print or not donor_print: return None recipient_path = resolve_set_image_path(recipient_print.get("image_path") or "") donor_path = resolve_set_image_path(donor_print.get("image_path") or "") if not (recipient_path and recipient_path.exists() and donor_path and donor_path.exists()): return None recipient_key = classify(recipient_card, recipient_print.get("set_name", "")) donor_key = classify(donor_card, donor_print.get("set_name", "")) return composite_art( recipient_path, donor_path, recipient_key, donor_key, recipient_card.get("keywords") or [] )
def _find_print(repository, card_id: str, print_id: int) -> dict | None: return next((p for p in repository.get_prints(card_id) if p["print_id"] == print_id), None)