#!/usr/bin/env python3
"""Build BP-001 Moses v05 as a bespoke Legendary card."""
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
import hashlib
import json
import os
import shutil
import subprocess
import sys
import xml.etree.ElementTree as ET
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))
PROOF = ROOT / "review/vertical-text-prototype-v7"
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(image: Image.Image) -> np.ndarray:
check("A" in image.getbands(), "Component must carry alpha")
return np.asarray(image.getchannel("A"), dtype=np.uint8)
def split_overlay_svg(source: Path, frame_output: Path, backing_output: Path) -> None:
"""Split the selected proof's combined SVG without redrawing its geometry."""
tree = ET.parse(source)
root = tree.getroot()
children = list(root)
check(children and children[0].tag.endswith("defs"), "Selected overlay SVG lacks defs")
def write(destination: Path, want_frame: bool) -> None:
result = ET.Element(root.tag, root.attrib)
result.append(deepcopy(children[0]))
for child in children[1:]:
is_frame = child.attrib.get("id") == "legendary-frame"
if is_frame == want_frame:
result.append(deepcopy(child))
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
destination.parent.mkdir(parents=True, exist_ok=True)
ET.ElementTree(result).write(destination, encoding="unicode")
write(frame_output, True)
write(backing_output, False)
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"
art = Image.open(art_path).convert("RGBA")
check(art.size == CANVAS and art.getchannel("A").getextrema() == (255, 255), "Selected art must be opaque 2000x2800")
check(digest(art_path) == card["artSHA256"], "Selected art hash changed")
selected = {
"normal": PROOF / "composed-preview-2000.png",
"overlaySvg": PROOF / "border-backing-preview.svg",
"foreground": PROOF / "foreground-curtain.png",
"foregroundSvg": PROOF / "foreground-curtain.svg",
"text": PROOF / "text-preview.png",
"textSvg": PROOF / "text-preview.svg",
"validation": PROOF / "preview-validation.json",
}
for path in selected.values():
check(path.is_file(), f"Missing selected v7 proof input: {path}")
proof_validation = json.loads(selected["validation"].read_text())
check(proof_validation["status"] == "passed", "Selected v7 proof did not pass")
check(proof_validation["structuralChecks"]["curtainTextIntersectionPixels"] == 0, "Selected foreground intersects text")
check(proof_validation["structuralChecks"]["glyphPixelsOutsideOpaqueBacking"] == 0, "Selected text escapes backing")
fonts = REPO / "fonts"
font_manifest_path = fonts / "manifest.json"
font_manifest = json.loads(font_manifest_path.read_text())
title_font = fonts / "P052-Bold.otf"
body_font = fonts / "SanctificationP052-Medium.otf"
for font in (title_font, body_font):
check(digest(font) == font_manifest["files"][font.name]["sha256"], f"Font hash mismatch: {font.name}")
review = ROOT / "review"
layers = ROOT / "source/layers"
text_dir = ROOT / "source/text"
foreground_dir = ROOT / "source/foreground"
material_dir = ROOT / "source/material"
for directory in (review, layers, text_dir, foreground_dir, material_dir):
directory.mkdir(parents=True, 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}{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")
resolved_title = Path(subprocess.check_output(["fc-match", "-f", "%{file}", "P052:style=Bold"], env=env, text=True).strip()).resolve()
resolved_body = Path(subprocess.check_output(["fc-match", "-f", "%{file}", "Sanctification P052:style=Medium"], env=env, text=True).strip()).resolve()
check(resolved_title == title_font.resolve(), "P052 Bold fallback detected")
check(resolved_body == body_font.resolve(), "P052 Medium fallback detected")
# Freeze the selected proof components as production inputs.
shutil.copy2(selected["normal"], ROOT / "source/selected-normal-2000.png")
shutil.copy2(selected["foreground"], foreground_dir / "foreground-2000.png")
shutil.copy2(selected["foregroundSvg"], foreground_dir / "selected-foreground.svg")
shutil.copy2(selected["text"], text_dir / "text-2000.png")
shutil.copy2(selected["textSvg"], text_dir / "selected-text.svg")
shutil.copy2(selected["overlaySvg"], layers / "selected-overlay.svg")
frame_svg = layers / "frame.svg"
backing_svg = layers / "backing.svg"
split_overlay_svg(selected["overlaySvg"], frame_svg, backing_svg)
render(frame_svg, layers / "frame-2000.png")
render(backing_svg, layers / "backing-2000.png")
frame = Image.open(layers / "frame-2000.png").convert("RGBA")
backing = Image.open(layers / "backing-2000.png").convert("RGBA")
foreground = Image.open(foreground_dir / "foreground-2000.png").convert("RGBA")
text = Image.open(text_dir / "text-2000.png").convert("RGBA")
for name, component in (("frame", frame), ("backing", backing), ("foreground", foreground), ("text", text)):
check(component.size == CANVAS, f"{name} is not registered to the master canvas")
# The source curtain sits in front of the outer left rail, but below the backing.
rail_mask = Image.new("L", CANVAS, 0)
ImageDraw.Draw(rail_mask).polygon([(0, 0), (80, 0), (80, 2646), (0, 2640)], fill=255)
rail = art.copy()
rail.putalpha(rail_mask)
rail.save(foreground_dir / "curtain-over-rail-2000.png")
# Textless keeps the scene's depth at the top-left corner: the tent roof
# and cloud crown occlude the perimeter rail until it reaches open sky.
# The selected foreground already carries that approved source contour;
# crop its alpha to the upper crown so the sleeve is not duplicated here.
crown_alpha_image = foreground.getchannel("A")
crown_alpha_values = np.asarray(crown_alpha_image).copy()
crown_alpha_values[600:, :] = 0
cloud_crown = foreground.copy()
cloud_crown.putalpha(Image.fromarray(crown_alpha_values, "L"))
cloud_crown.save(foreground_dir / "cloud-crown-over-frame-2000.png")
textless_occlusion = Image.alpha_composite(rail, cloud_crown)
textless_occlusion.save(foreground_dir / "textless-frame-occlusion-2000.png")
combined_overlay = Image.alpha_composite(frame, backing)
combined_overlay.save(layers / "overlay-normal-2000.png")
backing.save(layers / "backing-borderless-2000.png")
frame.save(layers / "frame-textless-2000.png")
frame_a = alpha(frame).astype(np.float32) / 255.0
backing_a = alpha(backing).astype(np.float32) / 255.0
foreground_a = alpha(foreground).astype(np.float32) / 255.0
rail_a = alpha(rail).astype(np.float32) / 255.0
crown_a = alpha(cloud_crown).astype(np.float32) / 255.0
text_a = alpha(text).astype(np.float32) / 255.0
check(not np.any((text_a > 0) & (foreground_a > 0)), "Production foreground intersects text")
check(np.all(alpha(backing)[text_a > 0] == 255), "Production text escapes opaque backing")
selected_normal = Image.open(ROOT / "source/selected-normal-2000.png").convert("RGBA")
check(selected_normal.size == CANVAS and selected_normal.getchannel("A").getextrema() == (255, 255), "Selected Normal is invalid")
# Reconstruct the selected order as a diagnostic. The selected PNG remains
# authoritative because the approved proof pins inherited antialias pixels.
reconstructed = Image.alpha_composite(art, frame)
reconstructed = Image.alpha_composite(reconstructed, rail)
reconstructed = Image.alpha_composite(reconstructed, backing)
reconstructed = Image.alpha_composite(reconstructed, foreground)
reconstructed = Image.alpha_composite(reconstructed, text)
reconstructed.save(review / "normal-reconstructed.png")
delta = np.abs(np.asarray(selected_normal, dtype=np.int16)[:, :, :3] - np.asarray(reconstructed, dtype=np.int16)[:, :, :3])
changed = np.any(delta > 0, axis=2)
faces = {
"normal": selected_normal,
"boundless": art.copy(),
"borderless": Image.alpha_composite(Image.alpha_composite(Image.alpha_composite(art, backing), foreground), text),
"textless": Image.alpha_composite(Image.alpha_composite(art, frame), textless_occlusion),
}
check(np.array_equal(np.asarray(faces["boundless"]), np.asarray(art)), "Boundless must equal selected art")
expected_textless = Image.alpha_composite(Image.alpha_composite(art, frame), textless_occlusion)
check(np.array_equal(np.asarray(faces["textless"]), np.asarray(expected_textless)), "Textless component error")
crown_opaque = crown_a == 1.0
check(np.array_equal(np.asarray(faces["textless"])[crown_opaque], np.asarray(cloud_crown)[crown_opaque]), "Tent/cloud crown does not fully occlude the Textless frame")
check(np.any(frame_a[:100, 1200:] > 0), "Textless top rail no longer resumes over open sky")
# Expand and soften actual glyph alpha for the runtime readability mask.
protected_text = text.getchannel("A").filter(ImageFilter.MaxFilter(25)).filter(ImageFilter.GaussianBlur(8))
check(int(np.min(np.asarray(protected_text)[text_a >= 0.5])) >= 216, "Text protection is too weak over rendered glyphs")
# Analyze the illustration and the source-registered adapted foreground once.
base_analysis = prepare_illustration(art)
adapted_plate = Image.alpha_composite(art, foreground)
adapted_analysis = prepare_illustration(adapted_plate)
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)
def suppress(values: np.ndarray, component_alpha: np.ndarray) -> np.ndarray:
return values * (1.0 - component_alpha)
def restore(values: np.ndarray, component_alpha: np.ndarray, component_coverage: np.ndarray) -> np.ndarray:
return values * (1.0 - component_alpha) + component_coverage * component_alpha
finish = {}
# Resolve each printing in its actual front-to-back order.
values = suppress(base_coverage, frame_a)
values = restore(values, rail_a, base_coverage)
values = suppress(values, backing_a)
values = restore(values, foreground_a, adapted_coverage)
finish["normal"] = suppress(values, text_a)
values = suppress(base_coverage, backing_a)
values = restore(values, foreground_a, adapted_coverage)
finish["borderless"] = suppress(values, text_a)
values = restore(suppress(base_coverage, frame_a), rail_a, base_coverage)
finish["textless"] = restore(values, crown_a, adapted_coverage)
finish["boundless"] = base_coverage
head = '")
report["printings"][printing] = {
"components": printing_components[printing],
"textMask": has_text,
"finishCoverage": "base plus depth-resolved adapted foreground" if printing in ("normal", "borderless") else "base illustration with frame depth resolved" if printing == "textless" else "base illustration",
"exports": exports,
}
report["normalAuthorityCheck"]["productionSHA256"] = digest(ROOT / "high/normal/card.png")
check(report["normalAuthorityCheck"]["productionSHA256"] == report["normalAuthorityCheck"]["selectedSHA256"], "Normal export changed from selected v7 proof")
save_json(layers / "layout.json", {
"id": "BP-001-moses-legendary-tent-textile-v1",
"canvas": list(CANVAS),
"selectedProof": str(PROOF.relative_to(REPO)),
"titleBacking": "upper-right paper plaque beneath the source cloud crown",
"verseBacking": "shortened left tent textile with shallow pitched seam",
"depthOrder": ["base-art", "frame", "curtain-over-rail", "backings", "cloud-crown-and-complete-blue-red-sleeve", "text"],
"printingComponents": printing_components,
"foregroundScope": "Normal and Borderless restore the source cloud crown plus complete blue outer sleeve and red inner lining ending at the low V. Textless restores the tent/cloud crown over the top-left rail. The separate lower red shard stays behind the backing.",
})
save_json(ROOT / "source/typography-layout.json", {
"canvas": list(CANVAS),
"policy": "Bespoke Legendary coordinates; semantic four-line verse; actual ink centered in the reserved band.",
"titleAnchor": [1458, 244],
"verseAnchorX": 530,
"verseBaselines": [2189, 2299, 2409, 2519],
"referenceBaseline": 2640,
"fontSizes": {"title": 142, "verse": 66, "reference": 48},
"verseLines": report["typography"]["verse"]["lines"],
"reference": card["referenceDisplay"],
"measured": {
"verseInkUnion": proof_validation["typography"]["verseInkUnion"],
"upperFlourishToVerseInk": proof_validation["typography"]["upperFlourishToVerseInk"],
"verseInkToReferenceLineBox": proof_validation["typography"]["verseInkToReferenceLineBox"],
"centeringDifference": proof_validation["typography"]["centeringDifference"],
},
})
save_json(ROOT / "source/production-inputs.json", {
"selectedProof": str(PROOF.relative_to(REPO)),
"files": {name: {"path": str(path.relative_to(REPO)), "sha256": digest(path)} for name, path in selected.items()},
"fonts": {font.name: digest(font) for font in (title_font, body_font)},
"finishRecipeSHA256": RECIPE_SHA256,
})
# Review sheets.
label_font = ImageFont.truetype(str(title_font), 24)
for filename, item_name in (("printings-comparison.png", "card.png"), ("finish-masks-comparison.png", "finish-mask.png")):
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 / item_name).convert("RGB")
image.thumbnail((370, 518), Image.Resampling.LANCZOS if item_name == "card.png" else Image.Resampling.BILINEAR)
sheet.paste(image, (index * 400 + 14, 56))
sheet.save(review / filename)
checker = Image.new("RGBA", CANVAS, (30, 34, 40, 255))
checker_values = np.asarray(checker).copy()
for y in range(0, 2800, 100):
for x in range(0, 2000, 100):
if (x // 100 + y // 100) % 2:
checker_values[y:y + 100, x:x + 100, :3] = [56, 61, 69]
checker = Image.fromarray(checker_values, "RGBA")
items = [("Frame", frame), ("Backings", backing), ("Textless depth", textless_occlusion), ("Cloud + sleeve", foreground), ("Text", text)]
sheet = Image.new("RGB", (1500, 500), "#11161c")
draw = ImageDraw.Draw(sheet)
for index, (label, layer) in enumerate(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)
sheet.paste(composed.convert("RGB"), (index * 300 + 10, 50))
sheet.save(review / "layers-comparison.png")
detail = Image.new("RGB", (1500, 760), "#11161c")
draw = ImageDraw.Draw(detail)
for index, (label, path) in enumerate([
("Normal card", ROOT / "high/normal/card.png"),
("Normal finish", ROOT / "high/normal/finish-mask.png"),
("Text protection", ROOT / "high/normal/text-mask.png"),
]):
draw.text((index * 500 + 12, 12), label, font=label_font, fill="#f4ead4")
image = Image.open(path).convert("RGB").crop((0, 1500, 1200, 2800))
image.thumbnail((480, 690), Image.Resampling.LANCZOS)
detail.paste(image, (index * 500 + 10, 48))
detail.save(review / "textile-finish-detail.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": {
"selectedNormalPreservedByteForByte": True,
"fourPrintingComposition": "passed",
"layerRegistration": "passed",
"opaqueFaces": "passed",
"textOnOpaqueBacking": "passed",
"foregroundTextIntersectionPixels": 0,
"depthResolvedFinishCoverage": "passed",
"textlessTentCloudOccludesTopLeftRail": "passed",
"textlessTopRailResumesOverOpenSky": "passed",
"lowMedHighExports": "passed",
},
"reviewAssets": ["printings-comparison.png", "layers-comparison.png", "finish-masks-comparison.png", "textile-finish-detail.png"],
"staticVisualAssessment": "Pending user review of the four production printings and masks.",
"movingLightReview": "Not performed.",
"finalCardApproval": None,
})
card.update({
"artApproval": {"by": "user", "scope": "v05 source art and selected v7 Legendary Normal composition"},
"artStatus": "Selected v05 illustration approved for production assembly",
"layoutStatus": "Bespoke Legendary tent-textile production candidate assembled; four printings and masks await user review.",
"template": "BP-001-moses-legendary-tent-textile-v1",
"printingComponents": printing_components,
"typographyCandidate": {
"titleFontSize": 142,
"verseFontSize": 66,
"verseBaselineGap": 110,
"referenceFontSize": 48,
"lineBreaks": report["typography"]["verse"]["lines"],
},
"finishRecipe": "raw-ridges-v1",
"assemblyAuthorization": {"by": "user", "note": report["assemblyAuthorization"]},
})
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']}: four Legendary printings and production masks exported; structural validation passed.")
if __name__ == "__main__":
main()