#!/usr/bin/env python3 """Build the bespoke BP-002 David Legendary card and all printing variants.""" from __future__ import annotations from pathlib import Path import hashlib import json import os import struct import subprocess import sys from xml.sax.saxutils import escape import numpy as np from PIL import Image, ImageDraw, ImageFilter, ImageFont ROOT = Path(__file__).resolve().parent REPO = next(p for p in ROOT.parents if (p / "docs/card-layer-pipeline.md").is_file()) sys.path.insert(0, str(REPO / "tools/card-production")) from card_workspace import refresh, update_index from finish_masks import RECIPE_PATH, RECIPE_SHA256, load_recipe, prepare_illustration CANVAS = (2000, 2800) PRINTINGS = ("normal", "boundless", "borderless", "textless") RESOLUTIONS = (("low", 500), ("med", 1000), ("high", 2000)) def digest(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def save_json(path: Path, value: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value, indent=2) + "\n") def check(condition: bool, message: str) -> None: if not condition: raise ValueError(message) def alpha_array(image: Image.Image) -> np.ndarray: check("A" in image.getbands(), "Expected an alpha-bearing component") return np.asarray(image.getchannel("A"), dtype=np.uint8) def font_metric_box(font_path: Path, size: int) -> dict: data = font_path.read_bytes() tables = {} count = struct.unpack_from(">H", data, 4)[0] for index in range(count): tag, _, offset, length = struct.unpack_from(">4sIII", data, 12 + 16 * index) tables[tag.decode()] = data[offset:offset + length] units = struct.unpack_from(">H", tables["head"], 18)[0] ascent, descent, _ = struct.unpack_from(">hhh", tables["hhea"], 4) ymin = struct.unpack_from(">h", tables["head"], 38)[0] import math return { "unitsPerEm": units, "hheaAscender": ascent, "hheaDescender": descent, "headYMin": ymin, "size": size, "ascent": math.ceil(ascent * size / units), "descent": math.ceil(max(-descent, -ymin) * size / units), "lineGapUsed": 0, } def luminance(rgb: np.ndarray) -> np.ndarray: values = np.asarray(rgb, dtype=np.float64) / 255.0 values = np.where(values <= 0.04045, values / 12.92, ((values + 0.055) / 1.055) ** 2.4) return values @ np.array([0.2126, 0.7152, 0.0722]) def contrast_ratio(a: np.ndarray, b: np.ndarray) -> np.ndarray: la = luminance(a) lb = luminance(b) return (np.maximum(la, lb) + 0.05) / (np.minimum(la, lb) + 0.05) def main() -> None: card_path = ROOT / "card.json" manifest_path = ROOT / "manifest.json" card = json.loads(card_path.read_text()) manifest = json.loads(manifest_path.read_text()) art_path = ROOT / "source/art-master.png" foreground_path = ROOT / "source/foreground/foreground-2000.png" art = Image.open(art_path).convert("RGBA") foreground = Image.open(foreground_path).convert("RGBA") check(art.size == CANVAS and art.getchannel("A").getextrema() == (255, 255), "Base art must be opaque 2000x2800") check(foreground.size == CANVAS and foreground.getchannel("A").getextrema()[1] == 255, "Foreground must be registered RGBA") check(digest(art_path) == card["artSHA256"], "Card art hash does not match selected source") fonts_dir = REPO / "fonts" font_manifest_path = fonts_dir / "manifest.json" font_manifest = json.loads(font_manifest_path.read_text()) title_font = fonts_dir / "P052-Bold.otf" verse_font = fonts_dir / "SanctificationP052-Medium.otf" bold_font = title_font for font in (title_font, verse_font): check(digest(font) == font_manifest["files"][font.name]["sha256"], f"Font hash mismatch: {font.name}") review = ROOT / "review" review.mkdir(exist_ok=True) font_cache = review / "font-cache" font_cache.mkdir(exist_ok=True) fonts_conf = review / "fonts.conf" fonts_conf.write_text(f"{fonts_dir}{font_cache}") env = dict(os.environ, FONTCONFIG_FILE=str(fonts_conf)) def ink(*args: object) -> str: return subprocess.check_output(["inkscape", *map(str, args)], env=env, text=True, stderr=subprocess.PIPE).strip() def render(source: Path, destination: Path) -> None: destination.parent.mkdir(parents=True, exist_ok=True) ink(source, "--export-type=png", f"--export-filename={destination}", "--export-width=2000", "--export-height=2800") def resolved_font(family: str, style: str) -> Path: return Path(subprocess.check_output(["fc-match", "-f", "%{file}", f"{family}:style={style}"], env=env, text=True).strip()).resolve() check(resolved_font("P052", "Bold") == title_font.resolve(), "P052 Bold fallback detected") check(resolved_font("Sanctification P052", "Medium") == verse_font.resolve(), "P052 Medium fallback detected") head = '' defs = """ """ frame = """ """ title_path = "M1090 58 H1897 L1938 99 V326 L1899 365 H1090 L1048 323 V100 Z" verse_path = "M500 2075 H1898 Q1942 2075 1942 2119 V2720 Q1942 2764 1898 2764 H500 Q466 2764 466 2730 V2109 Q466 2075 500 2075 Z" def panel(path: str, inner: str, ident: str) -> str: return f'{inner}' title_inner = '' verse_inner = '' backing = panel(title_path, title_inner, "title-backing") + panel(verse_path, verse_inner, "verse-backing") layers = ROOT / "source/layers" layers.mkdir(parents=True, exist_ok=True) frame_svg = layers / "frame.svg" backing_svg = layers / "backing.svg" frame_svg.write_text(head + defs + frame + "") backing_svg.write_text(head + defs + backing + "") render(frame_svg, layers / "frame-2000.png") render(backing_svg, layers / "backing-2000.png") frame_image = Image.open(layers / "frame-2000.png").convert("RGBA") backing_image = Image.open(layers / "backing-2000.png").convert("RGBA") # A two-pixel lead guard sits below the foreground only where the lower backing is active. foreground_alpha_image = foreground.getchannel("A") expanded = foreground_alpha_image.filter(ImageFilter.MaxFilter(5)) ga = np.asarray(expanded, dtype=np.uint16) ba = np.asarray(backing_image.getchannel("A"), dtype=np.uint16) guard_alpha = np.where((ga > 0) & (ba > 0), 255, 0).astype(np.uint8) guard_alpha[:2060, :] = 0 guard_alpha[:, :450] = 0 guard_alpha[:, 1950:] = 0 guard_rgba = np.zeros((2800, 2000, 4), dtype=np.uint8) guard_rgba[:, :, :3] = [29, 23, 18] guard_rgba[:, :, 3] = guard_alpha guard_image = Image.fromarray(guard_rgba, "RGBA") guard_path = layers / "foreground-occlusion-guard.png" guard_image.save(guard_path) normal_overlay = Image.alpha_composite(Image.alpha_composite(Image.new("RGBA", CANVAS), frame_image), backing_image) normal_overlay = Image.alpha_composite(normal_overlay, guard_image) borderless_overlay = Image.alpha_composite(backing_image, guard_image) normal_overlay.save(layers / "overlay-normal-2000.png") borderless_overlay.save(layers / "backing-borderless-2000.png") frame_image.save(layers / "frame-textless-2000.png") (layers / "overlay-normal.svg").write_text(head + defs + frame + backing + f'') (layers / "backing-borderless.svg").write_text(head + defs + backing + f'') (layers / "frame-textless.svg").write_text(head + defs + frame + "") text_dir = ROOT / "source/text" text_dir.mkdir(parents=True, exist_ok=True) rows = [ {"id": "title", "role": "title", "parts": [("DAVID", False)], "x": 1490, "baseline": 244, "size": 138, "family": "P052", "weight": 700}, {"id": "verse-0", "role": "verse", "parts": [("“", False), ("I", True), (" have found David", False)], "x": 1320, "baseline": 2264, "size": 80, "family": "Sanctification P052", "weight": 500}, {"id": "verse-1", "role": "verse", "parts": [("the son of Jesse, a man", False)], "x": 1320, "baseline": 2358, "size": 80, "family": "Sanctification P052", "weight": 500}, {"id": "verse-2", "role": "verse", "parts": [("after ", False), ("My", True), (" own heart,", False)], "x": 1320, "baseline": 2452, "size": 80, "family": "Sanctification P052", "weight": 500}, {"id": "verse-3", "role": "verse", "parts": [("who will do all ", False), ("My", True), (" will.”", False)], "x": 1320, "baseline": 2546, "size": 80, "family": "Sanctification P052", "weight": 500}, {"id": "reference", "role": "reference", "parts": [("Acts 13:22 • NKJV", False)], "x": 1320, "baseline": 2655, "size": 57, "family": "Sanctification P052", "weight": 500}, ] def text_nodes(only_emphasis: bool = False) -> str: output = [] for row in rows: spans = "" for value, emphasized in row["parts"]: if emphasized: style = 'fill="#263f50" font-family="P052" font-weight="700"' elif only_emphasis: style = 'fill="#000" fill-opacity="0" stroke="none"' else: style = 'fill="#263f50"' spans += f'{escape(value)}' output.append( f'{spans}' ) return "".join(output) text_svg = text_dir / "text.svg" text_svg.write_text(head + text_nodes() + "") render(text_svg, text_dir / "text-2000.png") text_image = Image.open(text_dir / "text-2000.png").convert("RGBA") text_alpha = alpha_array(text_image) (text_dir / "bold-emphasis.svg").write_text(head + text_nodes(True) + "") render(text_dir / "bold-emphasis.svg", text_dir / "bold-emphasis-2000.png") # Validate glyph coverage, actual ink bounds, panel coverage, and foreground clearance. def charset(font: Path) -> set[int]: result = subprocess.check_output(["fc-query", "--format", "%{charset}", str(font)], text=True) points: set[int] = set() for span in result.split(): ends = [int(value, 16) for value in span.split("-")] points.update(range(ends[0], ends[-1] + 1)) return points medium_chars = charset(verse_font) bold_chars = charset(bold_font) glyph_checks = [] bounds_by_id = {} backing_alpha = alpha_array(backing_image) foreground_alpha = alpha_array(foreground) for row in rows: row_text = "".join(value for value, _ in row["parts"]) for value, emphasized in row["parts"]: coverage = bold_chars if emphasized or row["role"] == "title" else medium_chars check(all(ord(character) in coverage for character in value), f"Missing glyph in {row['id']}") node = next(part for part in text_nodes().split("") if f'id="{row["id"]}"' in part) + "" row_svg = text_dir / f'{row["id"]}-unclipped.svg' row_png = text_dir / f'{row["id"]}-unclipped.png' row_svg.write_text(head + node + "") render(row_svg, row_png) row_alpha = alpha_array(Image.open(row_png).convert("RGBA")) ys, xs = np.where(row_alpha > 0) check(len(xs) > 0, f"No rendered ink: {row['id']}") bounds = [int(xs.min()), int(ys.min()), int(xs.max() + 1), int(ys.max() + 1)] bounds_by_id[row["id"]] = bounds safe = [1090, 58, 1940, 365] if row["role"] == "title" else [500, 2163, 1900, 2693] check(safe[0] <= bounds[0] and safe[1] <= bounds[1] and bounds[2] <= safe[2] and bounds[3] <= safe[3], f"Text overflow: {row['id']}") check(np.all(backing_alpha[row_alpha > 0] == 255), f"Glyph outside opaque backing: {row['id']}") check(not np.any((row_alpha > 0) & (foreground_alpha > 0)), f"Foreground intersects lettering: {row['id']}") glyph_checks.append({ "id": row["id"], "role": row["role"], "text": row_text, "baseline": row["baseline"], "fontFamily": row["family"], "weight": row["weight"], "size": row["size"], "unclippedInkBounds": bounds, "safeBounds": safe, "glyphPixelsOutsideOpaqueBacking": 0, "foregroundIntersectionPixels": 0, "pass": True, }) verse_text = " ".join(item["text"] for item in glyph_checks if item["role"] == "verse") check(verse_text == "“" + card["excerpt"] + "”", "Rendered verse does not match card content") check(glyph_checks[-1]["text"] == card["referenceDisplay"], "Rendered reference does not match card content") verse_bounds = [bounds_by_id[f"verse-{index}"] for index in range(4)] verse_union = [ min(box[0] for box in verse_bounds), min(box[1] for box in verse_bounds), max(box[2] for box in verse_bounds), max(box[3] for box in verse_bounds), ] upper_flourish_bottom = 2163 reference_metrics = font_metric_box(verse_font, 57) reference_line_top = rows[-1]["baseline"] - reference_metrics["ascent"] reference_line_bottom = rows[-1]["baseline"] + reference_metrics["descent"] above = verse_union[1] - upper_flourish_bottom below = reference_line_top - verse_union[3] check(min(above, below) >= 40 and abs(above - below) <= 3, "Verse is not optically centered in its reserved band") check(bounds_by_id["reference"][1] >= reference_line_top and bounds_by_id["reference"][3] <= reference_line_bottom, "Reference escapes its fixed line box") check(2693 - reference_line_bottom >= 20, "Reference line box lacks lower flourish clearance") actual_backing_rgb = np.asarray(backing_image.convert("RGB")) ink_rgb = np.array([38, 63, 80]) minimum_text_contrast = float(np.min(contrast_ratio(ink_rgb, actual_backing_rgb[text_alpha >= 128]))) check(minimum_text_contrast >= 7.0, "Text/backing contrast is below 7:1") # Text protection is independent of finish coverage. protected_text = text_image.getchannel("A").filter(ImageFilter.MaxFilter(25)).filter(ImageFilter.GaussianBlur(8)) minimum_protection = int(np.min(np.asarray(protected_text)[text_alpha >= 128])) check(minimum_protection >= 216, "Text protection does not cover rendered glyphs") # Analyze the base and the opaque adapted-art plate exactly once each. base_analysis = prepare_illustration(art) adapted_plate = Image.alpha_composite(art, foreground) check(adapted_plate.getchannel("A").getextrema() == (255, 255), "Adapted analysis plate must be opaque") adapted_analysis = prepare_illustration(adapted_plate) material_dir = ROOT / "source/material" material_dir.mkdir(parents=True, exist_ok=True) adapted_plate.save(material_dir / "foreground-analysis-plate.png") Image.fromarray(base_analysis.raw_ridges).save(material_dir / "base-raw-ridges.png") Image.fromarray(np.rint(base_analysis.pane_weights * 255).astype(np.uint8)).save(material_dir / "base-pane-coverage.png") Image.fromarray(adapted_analysis.raw_ridges).save(material_dir / "foreground-raw-ridges.png") Image.fromarray(np.rint(adapted_analysis.pane_weights * 255).astype(np.uint8)).save(material_dir / "foreground-pane-coverage.png") def coverage(analysis) -> np.ndarray: return analysis.pane_weights * (1.0 - analysis.raw_ridges.astype(np.float32) / 255.0) base_coverage = coverage(base_analysis) adapted_coverage = coverage(adapted_analysis) fa = foreground_alpha.astype(np.float32) / 255.0 ta = text_alpha.astype(np.float32) / 255.0 overlays = { "normal": normal_overlay, "boundless": None, "borderless": borderless_overlay, "textless": frame_image, } foregrounds = {"normal": foreground, "borderless": foreground, "boundless": None, "textless": None} has_text = {"normal": True, "borderless": True, "boundless": False, "textless": False} report = { "status": "pending", "cardId": card["cardId"], "revision": manifest["revision"], "artApproval": { "by": "user", "note": "The selected David artwork and corrected mantle composition provide a strong basis for full card production.", "scope": "Selected v03 illustration and adapted foreground; full assembly authorized.", }, "finalCardApproval": None, "inputs": { "art": {"path": str(art_path.relative_to(REPO)), "sha256": digest(art_path)}, "foreground": { "path": str(foreground_path.relative_to(REPO)), "sha256": digest(foreground_path), "contour": str((ROOT / "source/foreground/mantle-contour.json").relative_to(REPO)), "provenance": str((ROOT / "source/foreground/art-provenance.json").relative_to(REPO)), }, "cardSHA256BeforeBuild": digest(card_path), "builderSHA256": digest(Path(__file__)), }, "bespokeLayers": { "template": "BP-002-david-legendary-v1", "canvas": list(CANVAS), "compositionOrder": ["base-art", "printing-overlay", "adapted-foreground-if-selected", "text-if-selected"], "assets": {}, }, "typography": { "sharedFontManifestSHA256": digest(font_manifest_path), "fonts": [ {"role": "title-and-emphasis", "path": str(title_font.relative_to(REPO)), "sha256": digest(title_font), "weight": 700, "fallbackUsed": False}, {"role": "verse-and-reference", "path": str(verse_font.relative_to(REPO)), "sha256": digest(verse_font), "weight": 500, "fallbackUsed": False}, ], "glyphs": glyph_checks, "boldTokens": ["I", "My", "My"], "boldColor": "#263f50", "fontMetrics": reference_metrics, "referenceLineBox": [reference_line_top, reference_line_bottom], "referenceLineBoxToLowerFlourish": 2693 - reference_line_bottom, "verseInkUnion": verse_union, "upperFlourishToVerseInk": above, "verseInkToReferenceLineBox": below, "centeringError": abs(above - below), "minimumTextContrast": round(minimum_text_contrast, 2), "textLayerSHA256": digest(text_dir / "text-2000.png"), }, "textMask": { "expansionRadius": 12, "gaussianRadius": 8, "units": "master pixels", "minimumProtectionAtGlyphAlpha128": minimum_protection, }, "finishRecipe": { "id": load_recipe()["id"], "sha256": RECIPE_SHA256, "path": str(RECIPE_PATH.relative_to(REPO)), "moduleSHA256": digest(REPO / "tools/card-production/finish_masks.py"), "illustrationAnalyses": 2, "baseRawRidgesSHA256": digest(material_dir / "base-raw-ridges.png"), "basePaneCoverageSHA256": digest(material_dir / "base-pane-coverage.png"), "foregroundPlateSHA256": digest(material_dir / "foreground-analysis-plate.png"), "foregroundRawRidgesSHA256": digest(material_dir / "foreground-raw-ridges.png"), "foregroundPaneCoverageSHA256": digest(material_dir / "foreground-pane-coverage.png"), }, "tools": {"inkscape": ink("--version"), "pillow": Image.__version__, "numpy": np.__version__}, "printings": {}, "visualReview": {"static": "pending user review", "movingLight": "not performed", "userApproval": "pending"}, } for name in ["frame-2000.png", "backing-2000.png", "overlay-normal-2000.png", "backing-borderless-2000.png", "frame-textless-2000.png", "foreground-occlusion-guard.png"]: path = layers / name report["bespokeLayers"]["assets"][name] = {"path": str(path.relative_to(REPO)), "sha256": digest(path)} for printing in PRINTINGS: overlay = overlays[printing] selected_foreground = foregrounds[printing] face = art.copy() if overlay is not None: face = Image.alpha_composite(face, overlay) if selected_foreground is not None: face = Image.alpha_composite(face, selected_foreground) if has_text[printing]: face = Image.alpha_composite(face, text_image) check(face.getchannel("A").getextrema() == (255, 255), f"{printing} face is not opaque") if printing == "boundless": check(np.array_equal(np.asarray(face), np.asarray(art)), "Boundless must equal the base art") if printing == "textless": expected = Image.alpha_composite(art, frame_image) check(np.array_equal(np.asarray(face), np.asarray(expected)), "Textless must contain only base art and frame") overlay_alpha = np.zeros((2800, 2000), dtype=np.float32) if overlay is None else alpha_array(overlay).astype(np.float32) / 255.0 underlying = base_coverage * (1.0 - overlay_alpha) if selected_foreground is not None: resolved = underlying * (1.0 - fa) + adapted_coverage * fa else: resolved = underlying if has_text[printing]: resolved = resolved * (1.0 - ta) finish_array = np.rint(np.clip(resolved, 0.0, 1.0) * 255).astype(np.uint8) finish = Image.fromarray(finish_array, "L") if selected_foreground is not None: opaque_foreground = foreground_alpha == 255 expected_foreground = np.rint(adapted_coverage * 255).astype(np.uint8) check(np.array_equal(finish_array[opaque_foreground], expected_foreground[opaque_foreground]), f"{printing} loses adapted foreground finish") visible_overlay_only = (overlay_alpha == 1.0) & (foreground_alpha == 0 if selected_foreground is not None else True) check(np.all(finish_array[visible_overlay_only] == 0), f"{printing} finish leaks onto visible opaque overlay") text_mask = protected_text if has_text[printing] else Image.new("L", CANVAS, 0) exports = [] for label, width in RESOLUTIONS: dimensions = (width, width * 7 // 5) folder = ROOT / label / printing folder.mkdir(parents=True, exist_ok=True) face_output = face if width == 2000 else face.resize(dimensions, Image.Resampling.LANCZOS) face_output.save(folder / "card.png") for filename, source in (("finish-mask.png", finish), ("text-mask.png", text_mask)): output = source if width == 2000 else source.resize(dimensions, Image.Resampling.BILINEAR) rgba = output.convert("RGBA") rgba.putalpha(255) rgba.save(folder / filename) exports.append({ "resolution": label, "dimensions": list(dimensions), "files": {filename: digest(folder / filename) for filename in ("card.png", "finish-mask.png", "text-mask.png")}, }) editable = ROOT / "high" / printing / "card.svg" def link(path: Path) -> str: return f'' body = link(art_path) if overlay is not None: overlay_path = layers / ("overlay-normal-2000.png" if printing == "normal" else "backing-borderless-2000.png" if printing == "borderless" else "frame-textless-2000.png") body += link(overlay_path) if selected_foreground is not None: body += link(foreground_path) if has_text[printing]: body += text_nodes() editable.write_text(head + body + "") report["printings"][printing] = { "overlay": None if overlay is None else ("normal-overlay" if printing == "normal" else "backing-only" if printing == "borderless" else "frame-only"), "adaptedForeground": selected_foreground is not None, "text": has_text[printing], "finishCoverage": "base plus alpha-resolved adapted foreground" if selected_foreground is not None else "base illustration", "exports": exports, } save_json(ROOT / "source/typography-layout.json", { "canvas": list(CANVAS), "policy": "Bespoke Legendary fixed coordinates; actual verse ink centered in reserved band; fixed reference line box.", "titleAnchor": [1490, 244], "verseAnchorX": 1320, "verseBaselines": [2264, 2358, 2452, 2546], "referenceBaseline": 2655, "fontSizes": {"title": 138, "verse": 80, "reference": 57}, "boldTokens": ["I", "My", "My"], "rows": [{**row, "parts": [{"text": text, "bold": bold} for text, bold in row["parts"]]} for row in rows], "measured": { "verseInkUnion": verse_union, "upperFlourishToVerseInk": above, "verseInkToReferenceLineBox": below, "referenceLineBox": [reference_line_top, reference_line_bottom], }, }) save_json(layers / "layout.json", { "id": "BP-002-david-legendary-v1", "canvas": list(CANVAS), "titleBackingPath": title_path, "verseBackingPath": verse_path, "titleTextRegion": [1090, 58, 850, 307], "verseTextRegion": [500, 2163, 1400, 530], "compositionOrder": ["base-art", "active-overlay", "adapted-foreground-normal-borderless-only", "active-text"], "occlusionGuard": {"radius": 2, "rgb": [29, 23, 18], "region": [450, 2060, 1500, 740]}, "printingComponents": { "normal": ["base-art", "frame", "backing", "occlusion-guard", "adapted-foreground", "text"], "borderless": ["base-art", "backing", "occlusion-guard", "adapted-foreground", "text"], "textless": ["base-art", "frame"], "boundless": ["base-art"], }, }) # Review sheets. label_font = ImageFont.truetype(str(title_font), 24) sheet = Image.new("RGB", (1600, 610), "#11161c") draw = ImageDraw.Draw(sheet) for index, printing in enumerate(PRINTINGS): draw.text((index * 400 + 14, 12), printing.title(), font=label_font, fill="#f4ead4") image = Image.open(ROOT / "med" / printing / "card.png").convert("RGB") image.thumbnail((370, 518), Image.Resampling.LANCZOS) sheet.paste(image, (index * 400 + 14, 56)) sheet.save(review / "printings-comparison.png") finish_sheet = Image.new("RGB", (1600, 610), "#11161c") draw = ImageDraw.Draw(finish_sheet) for index, printing in enumerate(PRINTINGS): draw.text((index * 400 + 14, 12), printing.title(), font=label_font, fill="#f4ead4") image = Image.open(ROOT / "med" / printing / "finish-mask.png").convert("RGB") image.thumbnail((370, 518), Image.Resampling.BILINEAR) finish_sheet.paste(image, (index * 400 + 14, 56)) finish_sheet.save(review / "finish-masks-comparison.png") detail_sheet = Image.new("RGB", (1500, 760), "#11161c") detail_draw = ImageDraw.Draw(detail_sheet) detail_items = [ ("Normal card", ROOT / "high/normal/card.png"), ("Foreground finish coverage", ROOT / "high/normal/finish-mask.png"), ("Lettering protection", ROOT / "high/normal/text-mask.png"), ] for index, (label, path) in enumerate(detail_items): detail_draw.text((index * 500 + 12, 12), label, font=label_font, fill="#f4ead4") image = Image.open(path).convert("RGB").crop((250, 1900, 1250, 2800)) image.thumbnail((480, 690), Image.Resampling.LANCZOS) detail_sheet.paste(image, (index * 500 + 10, 48)) detail_sheet.save(review / "foreground-finish-detail.png") checker = Image.new("RGBA", CANVAS, (30, 34, 40, 255)) checker_arr = np.asarray(checker).copy() tile = 100 for y in range(0, 2800, tile): for x in range(0, 2000, tile): if (x // tile + y // tile) % 2: checker_arr[y:y + tile, x:x + tile, :3] = [56, 61, 69] checker = Image.fromarray(checker_arr, "RGBA") layer_items = [ ("Base art", art), ("Frame", frame_image), ("Backings", backing_image), ("Adapted foreground", foreground), ("Text", text_image), ] layer_sheet = Image.new("RGB", (1500, 500), "#11161c") draw = ImageDraw.Draw(layer_sheet) for index, (label, layer) in enumerate(layer_items): draw.text((index * 300 + 10, 8), label, font=label_font, fill="#f4ead4") composed = checker.copy() composed.alpha_composite(layer) composed.thumbnail((280, 392), Image.Resampling.LANCZOS) layer_sheet.paste(composed.convert("RGB"), (index * 300 + 10, 50)) layer_sheet.save(review / "layers-comparison.png") report["status"] = "passed" save_json(review / "build-validation.json", report) save_json(review / "static-review.json", { "status": "passed-structural-user-visual-review-pending", "cardId": card["cardId"], "revision": manifest["revision"], "checks": { "fourPrintingComposition": "passed", "layerRegistration": "passed", "opaqueFaces": "passed", "textOnBacking": "passed", "foregroundTextIntersection": 0, "foregroundFinishReplacement": "passed at all fully opaque foreground pixels", "seamGuard": "passed structurally; selected review seam retained", "lowMedHighExports": "passed", }, "reviewAssets": ["printings-comparison.png", "layers-comparison.png", "finish-masks-comparison.png", "foreground-finish-detail.png"], "staticVisualAssessment": "Pending user review of all four printings and finish masks.", "movingLightReview": "Not performed.", "finalCardApproval": None, }) card.update({ "artApproval": report["artApproval"], "artStatus": "Selected v03 illustration approved for assembly", "sceneConstraints": ["No lineage or Jesus-related imagery."], "layoutStatus": "Bespoke Legendary v1 production candidate assembled; four printings and low/med/high exports await user review.", "template": "BP-002-david-legendary-v1", "printingComponents": json.loads((layers / "layout.json").read_text())["printingComponents"], "typographyCandidate": { "verseFontSize": 80, "verseBaselineGap": 94, "referenceFontSize": 57, "verseCenterGaps": [above, below], "boldTokens": ["I", "My", "My"], "emphasisTreatment": "P052 Bold 700 in standard teal #263f50; no special finish.", }, "finishRecipe": "raw-ridges-v1", "assemblyAuthorization": { "by": "user", "note": "Let's build out the other parts of the card (layers, prints, etc.)", }, }) save_json(card_path, card) manifest["stage"] = "assembly-review" manifest["approval"] = None save_json(manifest_path, manifest) profile_path = ROOT.parents[1] / "card.json" profile = json.loads(profile_path.read_text()) profile.update({"stage": "assembly-review", "selectedRevision": manifest["revision"], "approval": None}) save_json(profile_path, profile) refresh(ROOT) update_index() print(f'{card["cardId"]} {manifest["revision"]}: bespoke Legendary layers and four printings exported at low/med/high; structural validation passed.') if __name__ == "__main__": main()