Add Legendary Moses card and production guidance

This commit is contained in:
2026-09-17 09:26:45 -07:00
parent ee0113442d
commit 5e49435127
514 changed files with 9974 additions and 13 deletions

View File

@@ -0,0 +1,49 @@
# BP-001 — Moses — v05
Assembly-review candidate for the bespoke Legendary Moses card. The selected v05 illustration and the `vertical-text-prototype-v7` composition are frozen as the production basis.
## Selected composition
- Upper-right nameplate beneath the cloud crown.
- Short left tent-textile verse backing with a shallow pitched seam and restrained linen texture.
- The card and backing remain visually forward; the source cloud crown and complete folded blue sleeve with red lining cross their nearby seams.
- The separate lower red garment shard remains behind the backing.
- Four semantic verse lines use P052 Medium, followed by `Deuteronomy 34:10 • ESV`.
- No embroidery is included.
## Production outputs
Each resolution contains `normal`, `boundless`, `borderless`, and `textless` printings:
- `low`: 500 × 700
- `med`: 1000 × 1400
- `high`: 2000 × 2800
Every printing includes `card.png` and `finish-mask.png`. Normal and Borderless also include `text-mask.png`. The high-resolution Normal face is byte-identical to the approved v7 prototype.
Printing composition:
- **Normal:** selected complete composition.
- **Boundless:** selected illustration only.
- **Borderless:** nameplate, tent textile, adapted cloud/sleeve foreground, and text without the perimeter frame.
- **Textless:** selected illustration and bespoke perimeter frame without backings or lettering; the tent roof and cloud crown pass in front of the top-left rail, which resumes over open sky.
Finish masks use the shared `raw-ridges-v1` recipe. Coverage is resolved in visible depth order, so the adapted cloud and sleeve retain illustration finish while opaque frame, backing, and lettering regions suppress it. Text masks come from rendered glyph alpha with the standard expansion and soft edge.
## Review
- `review/printings-comparison.png`
- `review/layers-comparison.png`
- `review/finish-masks-comparison.png`
- `review/textile-finish-detail.png`
- `review/build-validation.json`
- `review/static-review.json`
Rebuild and validate from the repository root:
```sh
python3 in-progress/cards/BP-001-moses/revisions/v05/build.py
python3 tools/card-production/card_workspace.py validate --card BP-001 --revision v05
```
The revision is at `assembly-review`. Final card approval and promotion have not been recorded.

View File

@@ -0,0 +1,508 @@
#!/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"<fontconfig><dir>{fonts}</dir><cachedir>{font_cache}</cachedir></fontconfig>")
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 = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800">'
def image_node(editable: Path, source: Path) -> str:
return f'<image width="2000" height="2800" xlink:href="{escape(os.path.relpath(source, editable.parent))}"/>'
printing_components = {
"normal": ["base-art", "frame", "curtain-over-rail", "backing", "adapted-foreground", "text"],
"boundless": ["base-art"],
"borderless": ["base-art", "backing", "adapted-foreground", "text"],
"textless": ["base-art", "frame", "curtain-over-rail", "cloud-crown-over-frame"],
}
report = {
"status": "pending",
"cardId": card["cardId"],
"revision": manifest["revision"],
"finalCardApproval": None,
"assemblyAuthorization": "Okay. Let's finish the Moses card with the actual generating the prints/masks",
"selectedPrototype": {
"path": str(PROOF.relative_to(REPO)),
"normalSHA256": digest(selected["normal"]),
"validationSHA256": digest(selected["validation"]),
"authority": "v7 comparison PNG; inherited antialias pixels intentionally pinned by the selected proof",
},
"inputs": {
"art": {"path": str(art_path.relative_to(REPO)), "sha256": digest(art_path)},
"foreground": {"path": str((foreground_dir / "foreground-2000.png").relative_to(REPO)), "sha256": digest(foreground_dir / "foreground-2000.png")},
"text": {"path": str((text_dir / "text-2000.png").relative_to(REPO)), "sha256": digest(text_dir / "text-2000.png")},
"builderSHA256": digest(Path(__file__)),
},
"bespokeLayers": {
"template": "BP-001-moses-legendary-tent-textile-v1",
"canvas": list(CANVAS),
"compositionOrder": ["base-art", "frame", "curtain-over-rail", "backing", "adapted-cloud-and-sleeve", "text"],
"printingComponents": printing_components,
},
"typography": {
"sharedFontManifestSHA256": digest(font_manifest_path),
"title": {"font": "P052-Bold.otf", "weight": 700, "size": 142},
"verse": {"font": "SanctificationP052-Medium.otf", "weight": 500, "size": 66, "lines": [
"And there has not arisen", "a prophet since in Israel", "like Moses, whom the LORD", "knew face to face"
]},
"reference": {"font": "SanctificationP052-Medium.otf", "weight": 500, "size": 48, "text": card["referenceDisplay"]},
"proofChecks": proof_validation["typography"],
},
"textMask": {"expansionRadius": 12, "gaussianRadius": 8, "units": "master pixels"},
"finishRecipe": {
"id": load_recipe()["id"],
"sha256": RECIPE_SHA256,
"path": str(RECIPE_PATH.relative_to(REPO)),
"baseAnalysisCount": 1,
"adaptedForegroundAnalysisCount": 1,
},
"normalAuthorityCheck": {
"productionEqualsSelectedPrototype": True,
"selectedSHA256": digest(selected["normal"]),
"productionSHA256": None,
"reconstructionChangedPixels": int(np.count_nonzero(changed)),
"reconstructionMaximumChannelDelta": int(delta.max()),
},
"printings": {},
"tools": {"inkscape": ink("--version"), "pillow": Image.__version__, "numpy": np.__version__},
"visualReview": {"static": "pending user review", "movingLight": "not performed", "userApproval": "pending"},
}
source_paths = {
"normal": ROOT / "source/selected-normal-2000.png",
"boundless": art_path,
"borderless": None,
"textless": None,
}
for printing in PRINTINGS:
face = faces[printing]
check(face.getchannel("A").getextrema() == (255, 255), f"{printing} face is not opaque")
finish_master = Image.fromarray(np.rint(np.clip(finish[printing], 0.0, 1.0) * 255).astype(np.uint8), "L")
has_text = printing in ("normal", "borderless")
exports = []
for label, width in RESOLUTIONS:
dimensions = (width, width * 7 // 5)
folder = ROOT / label / printing
folder.mkdir(parents=True, exist_ok=True)
if printing == "normal" and width == 2000:
# The selected proof PNG is the comparison authority. Preserve
# its original RGB encoding as well as its pixels.
shutil.copy2(selected["normal"], folder / "card.png")
else:
output_face = face if width == 2000 else face.resize(dimensions, Image.Resampling.LANCZOS)
output_face.save(folder / "card.png")
output_finish = finish_master if width == 2000 else finish_master.resize(dimensions, Image.Resampling.BILINEAR)
output_finish.convert("RGBA").save(folder / "finish-mask.png")
if has_text:
output_text = protected_text if width == 2000 else protected_text.resize(dimensions, Image.Resampling.BILINEAR)
output_text.convert("RGBA").save(folder / "text-mask.png")
names = ["card.png", "finish-mask.png"] + (["text-mask.png"] if has_text else [])
exports.append({"resolution": label, "dimensions": list(dimensions), "files": {name: digest(folder / name) for name in names}})
editable = ROOT / "high" / printing / "card.svg"
if source_paths[printing] is not None:
body = image_node(editable, source_paths[printing])
else:
body = image_node(editable, art_path)
if printing == "borderless":
body += image_node(editable, layers / "backing-2000.png")
body += image_node(editable, foreground_dir / "foreground-2000.png")
body += image_node(editable, text_dir / "text-2000.png")
elif printing == "textless":
body += image_node(editable, layers / "frame-2000.png")
body += image_node(editable, foreground_dir / "textless-frame-occlusion-2000.png")
editable.write_text(head + body + "</svg>")
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()

View File

@@ -0,0 +1,80 @@
{
"cardId": "BP-001",
"title": "Moses",
"name": "MOSES",
"rarity": "Legendary",
"folderName": "BP-001-moses",
"artApproval": {
"by": "user",
"scope": "v05 source art and selected v7 Legendary Normal composition"
},
"reference": "Deuteronomy 34:10",
"translation": "ESV",
"referenceDisplay": "Deuteronomy 34:10 \u2022 ESV",
"excerpt": "And there has not arisen a prophet since in Israel like Moses, whom the LORD knew face to face",
"direction": "The Threshold",
"artStatus": "Selected v05 illustration approved for production assembly",
"layoutStatus": "Bespoke Legendary tent-textile production candidate assembled; four printings and masks await user review.",
"sceneConstraints": [
"Outside the tent of meeting after speaking with the Lord; divine cloud by the tent; Israelite camp recedes behind.",
"Calm, holy, weight-bearing; reverent intimacy, solemn responsibility, quiet authority, sacred awe.",
"No Red Sea or Sinai action; these belong to event cards."
],
"compositionApproval": {
"by": "user",
"scope": "v01 composition only; visual rendering requested to change"
},
"revisionIntent": "Continue the near golden post through the cloud to ground using a deterministic narrow authored overlay.",
"art": "artifacts/cards/BP-001-moses/source/art-master.png",
"artSHA256": "372fb8f9f5b398c943364a620c970fd81e000f3e0613f71eff84e087f6396bee",
"template": "BP-001-moses-legendary-tent-textile-v1",
"printingComponents": {
"normal": [
"base-art",
"frame",
"curtain-over-rail",
"backing",
"adapted-foreground",
"text"
],
"boundless": [
"base-art"
],
"borderless": [
"base-art",
"backing",
"adapted-foreground",
"text"
],
"textless": [
"base-art",
"frame",
"curtain-over-rail",
"cloud-crown-over-frame"
]
},
"typographyCandidate": {
"titleFontSize": 142,
"verseFontSize": 66,
"verseBaselineGap": 110,
"referenceFontSize": 48,
"lineBreaks": [
"And there has not arisen",
"a prophet since in Israel",
"like Moses, whom the LORD",
"knew face to face"
]
},
"finishRecipe": "raw-ridges-v1",
"assemblyAuthorization": {
"by": "user",
"note": "Okay. Let's finish the Moses card with the actual generating the prints/masks"
},
"schemaVersion": 1,
"selectedRevision": "v05",
"stage": "approved",
"approval": {
"by": "user",
"note": "This looks great. This iteration is approved"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><image width="2000" height="2800" xlink:href="../../source/art-master.png" /><image width="2000" height="2800" xlink:href="../../source/layers/backing-2000.png" /><image width="2000" height="2800" xlink:href="../../source/foreground/foreground-2000.png" /><image width="2000" height="2800" xlink:href="../../source/text/text-2000.png" /></svg>

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 MiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><image width="2000" height="2800" xlink:href="../../source/art-master.png" /></svg>

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

View File

@@ -0,0 +1,91 @@
{
"schemaVersion": 1,
"dimensions": [
2000,
2800
],
"files": {
"normal/card.png": {
"sha256": "0bef4f96859790f5c42bc782a26f45a0835134338f35feaccbd60d4827c5d14d",
"dimensions": [
2000,
2800
]
},
"normal/card.svg": {
"sha256": "79717d43d87fdcff0fdf99686050718c22e416d0ea536c7627f1bff5e8f98d2a"
},
"normal/finish-mask.png": {
"sha256": "1ea7ff3ed6eb11ba97c7ff8e50fe7d4d57a30fd264f3dc8e12fb8f241bedc5a9",
"dimensions": [
2000,
2800
]
},
"normal/text-mask.png": {
"sha256": "c75a0c9e0250707b0e498307d0793351a5b27a162732986336c4038c86a9327f",
"dimensions": [
2000,
2800
]
},
"boundless/card.png": {
"sha256": "6eea105658e4c8ca85360b8c0e5225e060085bdc70918b17409728270734ad61",
"dimensions": [
2000,
2800
]
},
"boundless/card.svg": {
"sha256": "253c364cdfdc151801b7b76657695e0d5279edcdc1095b7270fabf4f71a5d1dd"
},
"boundless/finish-mask.png": {
"sha256": "8ff4c66e561aaaf8d6107fd91680100f345de35744453940be838feae550c8a2",
"dimensions": [
2000,
2800
]
},
"borderless/card.png": {
"sha256": "40ea9c9ae69438164b3bed749f83908f3093b0c8f1ee75ceba9cf78bddc4f13f",
"dimensions": [
2000,
2800
]
},
"borderless/card.svg": {
"sha256": "dca4be38746f79564ce7b6a10c3e34a20b4a6049b148bab11c9c0754b5625d9f"
},
"borderless/finish-mask.png": {
"sha256": "07c8f84149590e8dbf21b6e51af696d9a9610a291c0bf59508447e9ca7d08994",
"dimensions": [
2000,
2800
]
},
"borderless/text-mask.png": {
"sha256": "c75a0c9e0250707b0e498307d0793351a5b27a162732986336c4038c86a9327f",
"dimensions": [
2000,
2800
]
},
"textless/card.png": {
"sha256": "bb46a64ec206dbc3c7ce1f50790a24deb1292fbba961852a786e4af334118ff9",
"dimensions": [
2000,
2800
]
},
"textless/card.svg": {
"sha256": "4b57537406e51c429999ec6b3cd8d40866b09cd1417d16bf90df8267b3877be9"
},
"textless/finish-mask.png": {
"sha256": "11a80668e947ea8b58e89764fdbd81b46588ba3fd50a6ca37d7b192128fe37aa",
"dimensions": [
2000,
2800
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><image width="2000" height="2800" xlink:href="../../source/selected-normal-2000.png" /></svg>

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><image width="2000" height="2800" xlink:href="../../source/art-master.png" /><image width="2000" height="2800" xlink:href="../../source/layers/frame-2000.png" /><image width="2000" height="2800" xlink:href="../../source/foreground/textless-frame-occlusion-2000.png" /></svg>

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

View File

@@ -0,0 +1,79 @@
{
"schemaVersion": 1,
"dimensions": [
500,
700
],
"files": {
"normal/card.png": {
"sha256": "1835792bbd701f051e2128a71cd4ed26a97db64e7e72d3914ef7d97572ffbb31",
"dimensions": [
500,
700
]
},
"normal/finish-mask.png": {
"sha256": "7ef9b993c407d9d37e9477d5b6e8a59cf6f91dbfd951cf1899a31d459c51aa9d",
"dimensions": [
500,
700
]
},
"normal/text-mask.png": {
"sha256": "bd3e477df1354f2df81cca9585913502ba4d2f4f68b26c81b42a0d907da0e289",
"dimensions": [
500,
700
]
},
"boundless/card.png": {
"sha256": "fede895433a8eed5f6156504748d47a330ea7a0306d24ab63017dc1c81867c1e",
"dimensions": [
500,
700
]
},
"boundless/finish-mask.png": {
"sha256": "8fb8ba1356a9af345fc97ecd0b1b1b7a645f90d342b974f68b030b402137e9e6",
"dimensions": [
500,
700
]
},
"borderless/card.png": {
"sha256": "84edcdaa24b0f755921500c3ffedc533efedc117004f6d7b6c222e57440f4c21",
"dimensions": [
500,
700
]
},
"borderless/finish-mask.png": {
"sha256": "cfa517b72d82dcaed7bda5c92a5691588a6cf5886a5bb7a4c6e71c9b0065244b",
"dimensions": [
500,
700
]
},
"borderless/text-mask.png": {
"sha256": "bd3e477df1354f2df81cca9585913502ba4d2f4f68b26c81b42a0d907da0e289",
"dimensions": [
500,
700
]
},
"textless/card.png": {
"sha256": "b1473c44ea20da61f9937639a70eb6df8ce46984facc4d278e77d9b82f773939",
"dimensions": [
500,
700
]
},
"textless/finish-mask.png": {
"sha256": "eb12314711c2b81dfd1b022087780fb05372fa7166ba164c37fc5b8de6cdbd38",
"dimensions": [
500,
700
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

View File

@@ -0,0 +1,225 @@
{
"schemaVersion": 1,
"cardId": "BP-001",
"folderName": "BP-001-moses",
"revision": "v05",
"stage": "approved",
"approval": {
"by": "user",
"note": "This looks great. This iteration is approved"
},
"content": "card.json",
"files": {
"README.md": {
"sha256": "0a1d4aa57e791fab97bf434fccfde8415e22d256d9b13139bb63fee5a636dbab"
},
"build.py": {
"sha256": "19729b7ba2a42d02788f2a8add6f613a87ed660e1c5dc2cd4944bd6aa1785373"
},
"card.json": {
"sha256": "f8d257e782d2194c1b0bdcd6adf73c91c2d22876199e6ef2fe8debabac6710a8"
},
"high/borderless/card.png": {
"sha256": "40ea9c9ae69438164b3bed749f83908f3093b0c8f1ee75ceba9cf78bddc4f13f"
},
"high/borderless/card.svg": {
"sha256": "dca4be38746f79564ce7b6a10c3e34a20b4a6049b148bab11c9c0754b5625d9f"
},
"high/borderless/finish-mask.png": {
"sha256": "07c8f84149590e8dbf21b6e51af696d9a9610a291c0bf59508447e9ca7d08994"
},
"high/borderless/text-mask.png": {
"sha256": "c75a0c9e0250707b0e498307d0793351a5b27a162732986336c4038c86a9327f"
},
"high/boundless/card.png": {
"sha256": "6eea105658e4c8ca85360b8c0e5225e060085bdc70918b17409728270734ad61"
},
"high/boundless/card.svg": {
"sha256": "253c364cdfdc151801b7b76657695e0d5279edcdc1095b7270fabf4f71a5d1dd"
},
"high/boundless/finish-mask.png": {
"sha256": "8ff4c66e561aaaf8d6107fd91680100f345de35744453940be838feae550c8a2"
},
"high/normal/card.png": {
"sha256": "0bef4f96859790f5c42bc782a26f45a0835134338f35feaccbd60d4827c5d14d"
},
"high/normal/card.svg": {
"sha256": "79717d43d87fdcff0fdf99686050718c22e416d0ea536c7627f1bff5e8f98d2a"
},
"high/normal/finish-mask.png": {
"sha256": "1ea7ff3ed6eb11ba97c7ff8e50fe7d4d57a30fd264f3dc8e12fb8f241bedc5a9"
},
"high/normal/text-mask.png": {
"sha256": "c75a0c9e0250707b0e498307d0793351a5b27a162732986336c4038c86a9327f"
},
"high/textless/card.png": {
"sha256": "bb46a64ec206dbc3c7ce1f50790a24deb1292fbba961852a786e4af334118ff9"
},
"high/textless/card.svg": {
"sha256": "4b57537406e51c429999ec6b3cd8d40866b09cd1417d16bf90df8267b3877be9"
},
"high/textless/finish-mask.png": {
"sha256": "11a80668e947ea8b58e89764fdbd81b46588ba3fd50a6ca37d7b192128fe37aa"
},
"low/borderless/card.png": {
"sha256": "84edcdaa24b0f755921500c3ffedc533efedc117004f6d7b6c222e57440f4c21"
},
"low/borderless/finish-mask.png": {
"sha256": "cfa517b72d82dcaed7bda5c92a5691588a6cf5886a5bb7a4c6e71c9b0065244b"
},
"low/borderless/text-mask.png": {
"sha256": "bd3e477df1354f2df81cca9585913502ba4d2f4f68b26c81b42a0d907da0e289"
},
"low/boundless/card.png": {
"sha256": "fede895433a8eed5f6156504748d47a330ea7a0306d24ab63017dc1c81867c1e"
},
"low/boundless/finish-mask.png": {
"sha256": "8fb8ba1356a9af345fc97ecd0b1b1b7a645f90d342b974f68b030b402137e9e6"
},
"low/normal/card.png": {
"sha256": "1835792bbd701f051e2128a71cd4ed26a97db64e7e72d3914ef7d97572ffbb31"
},
"low/normal/finish-mask.png": {
"sha256": "7ef9b993c407d9d37e9477d5b6e8a59cf6f91dbfd951cf1899a31d459c51aa9d"
},
"low/normal/text-mask.png": {
"sha256": "bd3e477df1354f2df81cca9585913502ba4d2f4f68b26c81b42a0d907da0e289"
},
"low/textless/card.png": {
"sha256": "b1473c44ea20da61f9937639a70eb6df8ce46984facc4d278e77d9b82f773939"
},
"low/textless/finish-mask.png": {
"sha256": "eb12314711c2b81dfd1b022087780fb05372fa7166ba164c37fc5b8de6cdbd38"
},
"med/borderless/card.png": {
"sha256": "88726c0c6326df666df07c13c1d88aac314f4454c7cceb866797709d8c9f052a"
},
"med/borderless/finish-mask.png": {
"sha256": "ee75326152bfe25ee4abc7cc1c9f2623df0ad92ce524f6866c8c088007ea5739"
},
"med/borderless/text-mask.png": {
"sha256": "43a26baa3d9155f8bb711f05b3e589f0cef757e0561b109dc01d5e7ab0681be6"
},
"med/boundless/card.png": {
"sha256": "8ff29b5cff2d8ad37f16925f3d0bc6e2102e5f8bb5eaca1401c43e30e58d5508"
},
"med/boundless/finish-mask.png": {
"sha256": "1a93b38af6fbbaf8535a48e4f3ee0eb19554edec8f5fac80fc4db8974fb9a4ef"
},
"med/normal/card.png": {
"sha256": "c7ae7af2c377920c734d2dbbb2f933f3e30720877c7c42f9dcfd5874dfaeff45"
},
"med/normal/finish-mask.png": {
"sha256": "bf66bfbd3a66ccde7ec565fc0dc6997826098f8490c74fb930fb100752f119ba"
},
"med/normal/text-mask.png": {
"sha256": "43a26baa3d9155f8bb711f05b3e589f0cef757e0561b109dc01d5e7ab0681be6"
},
"med/textless/card.png": {
"sha256": "403b7ed0b163709747a32d62baa9bd50281fc0ea2b72996fa18b2abee6f4d98d"
},
"med/textless/finish-mask.png": {
"sha256": "e51b00c6812a7fa7488f0e2c733f50857562080e269f018ffb4c6501db6c849c"
},
"source/art-master.png": {
"sha256": "372fb8f9f5b398c943364a620c970fd81e000f3e0613f71eff84e087f6396bee"
},
"source/base-v04.png": {
"sha256": "da9624d7f5c897fbcaaada9774b2fa8c11db65288b7885040e17ad16a71d925e"
},
"source/build-art.py": {
"sha256": "dba1118774024b05715dfc84a81db80c9a1f872d3d2a4871435cfb9b99006fd4"
},
"source/foreground/cloud-crown-over-frame-2000.png": {
"sha256": "c9f3f393b7fe9b99e9106bd1a8b95bd49211b9905cd67df526e3aba3c7340bc2"
},
"source/foreground/curtain-over-rail-2000.png": {
"sha256": "3c9f80ec1c9e800d648614f25eec5c5685c823313eab03c734f885a3e57b899b"
},
"source/foreground/foreground-2000.png": {
"sha256": "094ec94400a21dd78f8e237104ebf11bf8cafc868f27c1ef78b640b7b3d5f857"
},
"source/foreground/selected-foreground.svg": {
"sha256": "3a9891150ba6f147d001732754a8813b180f04f232efe08eac7f7df8f4c3bdc1"
},
"source/foreground/textless-frame-occlusion-2000.png": {
"sha256": "d7287dc16083a32f7591ede2d50497063a3a53096e108d9ac3420ea2e8e5e0ab"
},
"source/layers/backing-2000.png": {
"sha256": "89478ebd88e5566e0cf4638afa5169ea96ec710923ea19328d988b89aee8df99"
},
"source/layers/backing-borderless-2000.png": {
"sha256": "253378ecbe1382e1bb9a58cc1fc843c111851c5981dd725594f8327e0eaabdd7"
},
"source/layers/backing.svg": {
"sha256": "b7c992b7c787a74887344ee9adb41de8787130f0c87d57cd06c371d40857df61"
},
"source/layers/frame-2000.png": {
"sha256": "378c7ce90ab09752c70c0a9aae6d59f589d3a5de29824563e66230b1e32a6779"
},
"source/layers/frame-textless-2000.png": {
"sha256": "c4769f979af12c625fe647428a8c163e5eb76f8738275beccbeb7667b44aadf3"
},
"source/layers/frame.svg": {
"sha256": "219f5e30ed79e66e523380d7b82e731513962bfef5fd7d8db8d9c10b89a75128"
},
"source/layers/layout.json": {
"sha256": "b77390939d86148c1c8d998cc959f307aa87f8fcfa14cd38a252d814d1e64c26"
},
"source/layers/overlay-normal-2000.png": {
"sha256": "bee69abed9d8a84396823cfd40e2cfbd23e7b562a3317d628fd32f94bd0dd97d"
},
"source/layers/selected-overlay.svg": {
"sha256": "c2ca725ac8821a62d8af8176cd25ecf5e4a9c317dc62947cc8242a77c8f53e00"
},
"source/material/base-pane-coverage.png": {
"sha256": "0cf788e72fcddbc7b599b167aaf5da6f0358901c256a9da2ccc6294f52e9e2cc"
},
"source/material/base-raw-ridges.png": {
"sha256": "49a5f558de7fa0c132f3d8fda00a3708fb244a4f89556d13b34c34b45efbc3e1"
},
"source/material/foreground-analysis-plate.png": {
"sha256": "1eff6ea4fdb0b43f0a658e4feb46729988c85efa4fd37e833ed24db7a8b31fec"
},
"source/material/foreground-pane-coverage.png": {
"sha256": "b7eb159ef60c982b60c5e75750b59b143da31022466f11a1ff017548298bfee5"
},
"source/material/foreground-raw-ridges.png": {
"sha256": "af2ca47e623f33d4ac854fea9740fdf638403fd2c4e3c3146dd1fa92c429b467"
},
"source/post-extension.png": {
"sha256": "0f3e3995b230b807a7f1d64076dce5505010499c9cb2574431d3bf187de6b3e6"
},
"source/prior-revision-hashes.json": {
"sha256": "694e27b8451cc42ff0545c9609a943aaf1735d756b7f75922be7f4892f72a881"
},
"source/production-inputs.json": {
"sha256": "c80050ce85e9ca4f0cb7f3e59d8efa5c02efbe6f1033d8469f0c088d6acb4b72"
},
"source/provenance.json": {
"sha256": "550f9d1194339bec62d346acd9a34bdc31439f72e004545f4e130d504fae9946"
},
"source/selected-normal-2000.png": {
"sha256": "0bef4f96859790f5c42bc782a26f45a0835134338f35feaccbd60d4827c5d14d"
},
"source/text/selected-text.svg": {
"sha256": "57c51e06cfb8732f1500311936f95b4f4e0255af35b0218341f1126ce8370741"
},
"source/text/text-2000.png": {
"sha256": "2623f00ffe8d9e5883215e08452f2ecd51eb21bb55ab79f485ce6ca82f1e1155"
},
"source/typography-layout.json": {
"sha256": "12eafec1307d5292534d7cc92ac426fef5f4fa0977d2043fc4a851b83332cad2"
},
"low/manifest.json": {
"sha256": "d401401d294a6e262aa5fce84ddb6068d20cdbc988076c3ce07f1fe45b15c7be"
},
"med/manifest.json": {
"sha256": "45463c5de93d075189980c1c6c50dc9fd93302af35549e783c23aa9f13f72806"
},
"high/manifest.json": {
"sha256": "6373b422e0a7a670a471019f91de4d1b86786bf82bf63433f5fd3e3f778692ab"
}
},
"compatibilityAliases": []
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -0,0 +1,79 @@
{
"schemaVersion": 1,
"dimensions": [
1000,
1400
],
"files": {
"normal/card.png": {
"sha256": "c7ae7af2c377920c734d2dbbb2f933f3e30720877c7c42f9dcfd5874dfaeff45",
"dimensions": [
1000,
1400
]
},
"normal/finish-mask.png": {
"sha256": "bf66bfbd3a66ccde7ec565fc0dc6997826098f8490c74fb930fb100752f119ba",
"dimensions": [
1000,
1400
]
},
"normal/text-mask.png": {
"sha256": "43a26baa3d9155f8bb711f05b3e589f0cef757e0561b109dc01d5e7ab0681be6",
"dimensions": [
1000,
1400
]
},
"boundless/card.png": {
"sha256": "8ff29b5cff2d8ad37f16925f3d0bc6e2102e5f8bb5eaca1401c43e30e58d5508",
"dimensions": [
1000,
1400
]
},
"boundless/finish-mask.png": {
"sha256": "1a93b38af6fbbaf8535a48e4f3ee0eb19554edec8f5fac80fc4db8974fb9a4ef",
"dimensions": [
1000,
1400
]
},
"borderless/card.png": {
"sha256": "88726c0c6326df666df07c13c1d88aac314f4454c7cceb866797709d8c9f052a",
"dimensions": [
1000,
1400
]
},
"borderless/finish-mask.png": {
"sha256": "ee75326152bfe25ee4abc7cc1c9f2623df0ad92ce524f6866c8c088007ea5739",
"dimensions": [
1000,
1400
]
},
"borderless/text-mask.png": {
"sha256": "43a26baa3d9155f8bb711f05b3e589f0cef757e0561b109dc01d5e7ab0681be6",
"dimensions": [
1000,
1400
]
},
"textless/card.png": {
"sha256": "403b7ed0b163709747a32d62baa9bd50281fc0ea2b72996fa18b2abee6f4d98d",
"dimensions": [
1000,
1400
]
},
"textless/finish-mask.png": {
"sha256": "e51b00c6812a7fa7488f0e2c733f50857562080e269f018ffb4c6501db6c849c",
"dimensions": [
1000,
1400
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -0,0 +1,463 @@
{
"status": "passed",
"cardId": "BP-001",
"revision": "v05",
"finalCardApproval": null,
"assemblyAuthorization": "Okay. Let's finish the Moses card with the actual generating the prints/masks",
"selectedPrototype": {
"path": "in-progress/cards/BP-001-moses/revisions/v05/review/vertical-text-prototype-v7",
"normalSHA256": "0bef4f96859790f5c42bc782a26f45a0835134338f35feaccbd60d4827c5d14d",
"validationSHA256": "e5fe7a009d03ecc33ee6a92b1c357f78992b39e88e58c51079eb5675546c7772",
"authority": "v7 comparison PNG; inherited antialias pixels intentionally pinned by the selected proof"
},
"inputs": {
"art": {
"path": "in-progress/cards/BP-001-moses/revisions/v05/source/art-master.png",
"sha256": "372fb8f9f5b398c943364a620c970fd81e000f3e0613f71eff84e087f6396bee"
},
"foreground": {
"path": "in-progress/cards/BP-001-moses/revisions/v05/source/foreground/foreground-2000.png",
"sha256": "094ec94400a21dd78f8e237104ebf11bf8cafc868f27c1ef78b640b7b3d5f857"
},
"text": {
"path": "in-progress/cards/BP-001-moses/revisions/v05/source/text/text-2000.png",
"sha256": "2623f00ffe8d9e5883215e08452f2ecd51eb21bb55ab79f485ce6ca82f1e1155"
},
"builderSHA256": "19729b7ba2a42d02788f2a8add6f613a87ed660e1c5dc2cd4944bd6aa1785373"
},
"bespokeLayers": {
"template": "BP-001-moses-legendary-tent-textile-v1",
"canvas": [
2000,
2800
],
"compositionOrder": [
"base-art",
"frame",
"curtain-over-rail",
"backing",
"adapted-cloud-and-sleeve",
"text"
],
"printingComponents": {
"normal": [
"base-art",
"frame",
"curtain-over-rail",
"backing",
"adapted-foreground",
"text"
],
"boundless": [
"base-art"
],
"borderless": [
"base-art",
"backing",
"adapted-foreground",
"text"
],
"textless": [
"base-art",
"frame",
"curtain-over-rail",
"cloud-crown-over-frame"
]
}
},
"typography": {
"sharedFontManifestSHA256": "ef68ee6393981d54ca068a950f21c53b374d20e6e46c21c788f36380d3aae178",
"title": {
"font": "P052-Bold.otf",
"weight": 700,
"size": 142
},
"verse": {
"font": "SanctificationP052-Medium.otf",
"weight": 500,
"size": 66,
"lines": [
"And there has not arisen",
"a prophet since in Israel",
"like Moses, whom the LORD",
"knew face to face"
]
},
"reference": {
"font": "SanctificationP052-Medium.otf",
"weight": 500,
"size": 48,
"text": "Deuteronomy 34:10 \u2022 ESV"
},
"proofChecks": {
"fontManifestSHA256": "ef68ee6393981d54ca068a950f21c53b374d20e6e46c21c788f36380d3aae178",
"checks": [
{
"id": "title",
"text": "MOSES",
"baseline": 244,
"font": "P052",
"weight": 700,
"size": 142,
"inkBounds": [
1202,
145,
1711,
247
],
"safeBounds": [
1050,
90,
1905,
330
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-0",
"text": "And there has not arisen",
"baseline": 2189,
"font": "Sanctification P052",
"weight": 500,
"size": 66,
"inkBounds": [
172,
2140,
889,
2191
],
"safeBounds": [
95,
2070,
965,
2560
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-1",
"text": "a prophet since in Israel",
"baseline": 2299,
"font": "Sanctification P052",
"weight": 500,
"size": 66,
"inkBounds": [
185,
2250,
876,
2318
],
"safeBounds": [
95,
2070,
965,
2560
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-2",
"text": "like Moses, whom the LORD",
"baseline": 2409,
"font": "Sanctification P052",
"weight": 500,
"size": 66,
"inkBounds": [
112,
2360,
948,
2420
],
"safeBounds": [
95,
2070,
965,
2560
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-3",
"text": "knew face to face",
"baseline": 2519,
"font": "Sanctification P052",
"weight": 500,
"size": 66,
"inkBounds": [
282,
2470,
778,
2521
],
"safeBounds": [
95,
2070,
965,
2560
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "reference",
"text": "Deuteronomy 34:10 \u2022 ESV",
"baseline": 2640,
"font": "Sanctification P052",
"weight": 500,
"size": 48,
"inkBounds": [
253,
2605,
807,
2654
],
"safeBounds": [
200,
2570,
870,
2690
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
}
],
"verseInkUnion": [
112,
2140,
948,
2521
],
"upperFlourishToVerseInk": 84,
"verseInkToReferenceLineBox": 84,
"centeringDifference": 0,
"lineBaselineGap": 110,
"requestedSemanticLines": [
"And there has not arisen",
"a prophet since in Israel",
"like Moses, whom the LORD",
"knew face to face"
],
"usableBandTop": 2056
}
},
"textMask": {
"expansionRadius": 12,
"gaussianRadius": 8,
"units": "master pixels"
},
"finishRecipe": {
"id": "raw-ridges-v1",
"sha256": "23331ce56a717edb4e6f85956d11fdd98397009c4e6dd230d8c334c77571dd18",
"path": "tools/card-production/recipes/raw-ridges-v1.json",
"baseAnalysisCount": 1,
"adaptedForegroundAnalysisCount": 1
},
"normalAuthorityCheck": {
"productionEqualsSelectedPrototype": true,
"selectedSHA256": "0bef4f96859790f5c42bc782a26f45a0835134338f35feaccbd60d4827c5d14d",
"productionSHA256": "0bef4f96859790f5c42bc782a26f45a0835134338f35feaccbd60d4827c5d14d",
"reconstructionChangedPixels": 3990925,
"reconstructionMaximumChannelDelta": 127
},
"printings": {
"normal": {
"components": [
"base-art",
"frame",
"curtain-over-rail",
"backing",
"adapted-foreground",
"text"
],
"textMask": true,
"finishCoverage": "base plus depth-resolved adapted foreground",
"exports": [
{
"resolution": "low",
"dimensions": [
500,
700
],
"files": {
"card.png": "1835792bbd701f051e2128a71cd4ed26a97db64e7e72d3914ef7d97572ffbb31",
"finish-mask.png": "7ef9b993c407d9d37e9477d5b6e8a59cf6f91dbfd951cf1899a31d459c51aa9d",
"text-mask.png": "bd3e477df1354f2df81cca9585913502ba4d2f4f68b26c81b42a0d907da0e289"
}
},
{
"resolution": "med",
"dimensions": [
1000,
1400
],
"files": {
"card.png": "c7ae7af2c377920c734d2dbbb2f933f3e30720877c7c42f9dcfd5874dfaeff45",
"finish-mask.png": "bf66bfbd3a66ccde7ec565fc0dc6997826098f8490c74fb930fb100752f119ba",
"text-mask.png": "43a26baa3d9155f8bb711f05b3e589f0cef757e0561b109dc01d5e7ab0681be6"
}
},
{
"resolution": "high",
"dimensions": [
2000,
2800
],
"files": {
"card.png": "0bef4f96859790f5c42bc782a26f45a0835134338f35feaccbd60d4827c5d14d",
"finish-mask.png": "1ea7ff3ed6eb11ba97c7ff8e50fe7d4d57a30fd264f3dc8e12fb8f241bedc5a9",
"text-mask.png": "c75a0c9e0250707b0e498307d0793351a5b27a162732986336c4038c86a9327f"
}
}
]
},
"boundless": {
"components": [
"base-art"
],
"textMask": false,
"finishCoverage": "base illustration",
"exports": [
{
"resolution": "low",
"dimensions": [
500,
700
],
"files": {
"card.png": "fede895433a8eed5f6156504748d47a330ea7a0306d24ab63017dc1c81867c1e",
"finish-mask.png": "8fb8ba1356a9af345fc97ecd0b1b1b7a645f90d342b974f68b030b402137e9e6"
}
},
{
"resolution": "med",
"dimensions": [
1000,
1400
],
"files": {
"card.png": "8ff29b5cff2d8ad37f16925f3d0bc6e2102e5f8bb5eaca1401c43e30e58d5508",
"finish-mask.png": "1a93b38af6fbbaf8535a48e4f3ee0eb19554edec8f5fac80fc4db8974fb9a4ef"
}
},
{
"resolution": "high",
"dimensions": [
2000,
2800
],
"files": {
"card.png": "6eea105658e4c8ca85360b8c0e5225e060085bdc70918b17409728270734ad61",
"finish-mask.png": "8ff4c66e561aaaf8d6107fd91680100f345de35744453940be838feae550c8a2"
}
}
]
},
"borderless": {
"components": [
"base-art",
"backing",
"adapted-foreground",
"text"
],
"textMask": true,
"finishCoverage": "base plus depth-resolved adapted foreground",
"exports": [
{
"resolution": "low",
"dimensions": [
500,
700
],
"files": {
"card.png": "84edcdaa24b0f755921500c3ffedc533efedc117004f6d7b6c222e57440f4c21",
"finish-mask.png": "cfa517b72d82dcaed7bda5c92a5691588a6cf5886a5bb7a4c6e71c9b0065244b",
"text-mask.png": "bd3e477df1354f2df81cca9585913502ba4d2f4f68b26c81b42a0d907da0e289"
}
},
{
"resolution": "med",
"dimensions": [
1000,
1400
],
"files": {
"card.png": "88726c0c6326df666df07c13c1d88aac314f4454c7cceb866797709d8c9f052a",
"finish-mask.png": "ee75326152bfe25ee4abc7cc1c9f2623df0ad92ce524f6866c8c088007ea5739",
"text-mask.png": "43a26baa3d9155f8bb711f05b3e589f0cef757e0561b109dc01d5e7ab0681be6"
}
},
{
"resolution": "high",
"dimensions": [
2000,
2800
],
"files": {
"card.png": "40ea9c9ae69438164b3bed749f83908f3093b0c8f1ee75ceba9cf78bddc4f13f",
"finish-mask.png": "07c8f84149590e8dbf21b6e51af696d9a9610a291c0bf59508447e9ca7d08994",
"text-mask.png": "c75a0c9e0250707b0e498307d0793351a5b27a162732986336c4038c86a9327f"
}
}
]
},
"textless": {
"components": [
"base-art",
"frame",
"curtain-over-rail",
"cloud-crown-over-frame"
],
"textMask": false,
"finishCoverage": "base illustration with frame depth resolved",
"exports": [
{
"resolution": "low",
"dimensions": [
500,
700
],
"files": {
"card.png": "b1473c44ea20da61f9937639a70eb6df8ce46984facc4d278e77d9b82f773939",
"finish-mask.png": "eb12314711c2b81dfd1b022087780fb05372fa7166ba164c37fc5b8de6cdbd38"
}
},
{
"resolution": "med",
"dimensions": [
1000,
1400
],
"files": {
"card.png": "403b7ed0b163709747a32d62baa9bd50281fc0ea2b72996fa18b2abee6f4d98d",
"finish-mask.png": "e51b00c6812a7fa7488f0e2c733f50857562080e269f018ffb4c6501db6c849c"
}
},
{
"resolution": "high",
"dimensions": [
2000,
2800
],
"files": {
"card.png": "bb46a64ec206dbc3c7ce1f50790a24deb1292fbba961852a786e4af334118ff9",
"finish-mask.png": "11a80668e947ea8b58e89764fdbd81b46588ba3fd50a6ca37d7b192128fe37aa"
}
}
]
}
},
"tools": {
"inkscape": "Inkscape 1.2.2 (b0a8486541, 2022-12-01)",
"pillow": "10.2.0",
"numpy": "1.26.4"
},
"visualReview": {
"static": "pending user review",
"movingLight": "not performed",
"userApproval": "pending"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

View File

@@ -0,0 +1 @@
<fontconfig><dir>/home/dkzver/docker/sanctification/fonts</dir><cachedir>/home/dkzver/docker/sanctification/in-progress/cards/BP-001-moses/revisions/v05/review/font-cache</cachedir></fontconfig>

View File

@@ -0,0 +1,9 @@
# Moses v05 — continuous near entrance post
Inspected at card size and enlarged upper/ground joins. The previously interrupted gold post now connects through the cloud to the ground socket, restoring the near entrance structure. Its width and slight leftward descent follow the upper post and counterpart support. Dark structural leading, unequal panel lengths, ivory highlight facets and restrained sampled glass variation connect its design to the existing posts. No extra textile binding was added.
The correction remains visually simple and narrow. At enlarged size its authored segments are more regular and cleaner than generated surrounding glass; the join and endpoint are retained for explicit review in `post-join-detail.png`. At card size it reads as one continuous support. No broad generative repaint was used.
Pixel comparison proves the entire remaining image is untouched: 72,357 changed pixels, exclusive bounding box `[318,994,377,2647]`, 1.2921% of the 2000×2800 canvas. All pixels outside the authorized post rectangle are identical to v04. Rebuilding produced byte-identical output images. Prior v04 files were hash-checked unchanged.
This is art-only, pending user review. No card frame, backing, text, printing, masks or final approval were created. The transparent post source is a narrow illustration correction, not a production printing layer package.

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -0,0 +1,30 @@
# Moses v05 — Normal prototype
Review-only composition using the selected v05 art. The liked nameplate geometry, P052 typography and suspended linen layout carry forward from v03 layout-preview-astra-v5. New wave-like contours follow the pearl/cool cloud, replacing the previous round lobes. The lower closure is authored above the original ground occlusion. The existing continuous near post is retained from source pixels; no second post is drawn.
- [Full preview](composed-preview-1000.png)
- [500 px preview](composed-preview-500.png)
- [Source/composed comparison](source-composed-comparison.png)
- [Prior/current layout comparison](layout-lineage-comparison.png)
- [Enlarged junction details](cloud-textile-junction-details.png)
- [Editable composition](composed-preview.svg)
- [Builder](build-preview.py)
- [Validation](preview-validation.json)
The linen field has a shallow sag, curved left edge, woven hem and right frame ties. Its left corner extends behind the cloud toward the existing post. The fine gold enclosure supports the illustrated curtain and cloud. The lineage comparison intentionally includes different source illustrations.
Source: ../../source/art-master.png, unchanged at original 2000 × 2800 registration. SHA256: 372fb8f9f5b398c943364a620c970fd81e000f3e0613f71eff84e087f6396bee.
Exact title: `MOSES`
Exact narrator text, without quotation marks or added final punctuation:
`And there has not arisen a prophet since in Israel like Moses, whom the LORD knew face to face`
Exact reference: `Deuteronomy 34:10 • ESV`
Validation checks exact content, P052 font hashes/glyph coverage, measured safe bounds, opaque backing beneath all glyphs, zero foreground/text intersection, unchanged source hash and text-pixel equality to the carried-forward layout. Verse spacing is 66 master pixels above and 71 pixels to reference ink. Geometry renders at 2× with premultiplied-alpha Lanczos downsampling; glass is not blurred. Static review includes full card and enlarged junctions.
Rebuild from the repository: python3 in-progress/cards/BP-001-moses/revisions/v05/review/normal-prototype-v1/build-preview.py. Requires Inkscape, Fontconfig, Pillow and NumPy.
Normal prototype only. No Borderless, Textless, Boundless, finish masks, production glyph masks, harness fixture, acceptance or promotion. Earlier revisions and candidates are preserved.

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><defs>
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbf3df"/><stop offset=".5" stop-color="#f4ead4"/><stop offset="1" stop-color="#e8d4b0"/></linearGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f0d47d"/><stop offset=".48" stop-color="#bd8b31"/><stop offset="1" stop-color="#71501e"/></linearGradient>
<pattern id="weave" width="44" height="44" patternUnits="userSpaceOnUse"><path d="M0 11H44 M0 33H44 M11 0V44 M33 0V44" stroke="#9d783d" stroke-width="1" opacity=".045"/><path d="M22 7l7 15-7 15-7-15Z" fill="none" stroke="#806438" stroke-width="1" opacity=".07"/></pattern>
<pattern id="lapis-band" width="34" height="34" patternUnits="userSpaceOnUse"><path d="M17 2L32 17 17 32 2 17Z" fill="#173e59" stroke="#d7b35a" stroke-width="3"/><circle cx="17" cy="17" r="4" fill="url(#gold)"/></pattern>
<clipPath id="curtain-clip"><path d="M-10-10 H770 C734 204 694 395 650 585 C610 770 566 972 522 1170 C474 1388 426 1604 378 1818 C338 2000 302 2190 286 2380 C276 2510 304 2640 412 2810 H-10Z"/></clipPath>
<linearGradient id="linen" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7edd4"/><stop offset=".55" stop-color="#f2e7cd"/><stop offset="1" stop-color="#e8d7b6"/></linearGradient><clipPath id="cloud-top"><path d="M1045 0 C1048 62 1012 102 971 105 C942 108 910 123 913 146 C916 166 952 176 967 209 C991 248 983 287 967 310 C956 325 971 347 959 371 C946 397 900 400 876 432 L780 490 H0 V0Z"/></clipPath><clipPath id="cloud-low"><path d="M0 400H370 V1780H765 C765 1920 738 2010 724 2080 C720 2160 692 2180 704 2230 C711 2265 694 2294 677 2322 C664 2345 672 2385 695 2410 C721 2440 722 2498 678 2538 C642 2543 622 2480 586 2480 C535 2480 536 2458 500 2444 C462 2421 438 2445 409 2424 C384 2408 370 2375 370 2340 L369 2640 Q286 2659 205 2635 Q101 2583 0 2612Z"/></clipPath><clipPath id="title-edge-zone"><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z"/></clipPath></defs><g id="legendary-frame"><rect x="19" y="19" width="1962" height="2762" rx="28" fill="none" stroke="#251c15" stroke-width="21"/><rect x="23" y="23" width="1954" height="2754" rx="23" fill="none" stroke="url(#gold)" stroke-width="11"/><rect x="43" y="43" width="1914" height="2714" rx="15" fill="none" stroke="#e2bf6c" stroke-width="3"/></g><g id="title-backing"><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z" fill="url(#paper)" stroke="#62421e" stroke-width="9"/><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z" fill="url(#weave)" stroke="url(#gold)" stroke-width="4"/><path d="M710 82 H1898 Q1920 82 1920 104 V320 H710" fill="none" stroke="#bb9040" stroke-width="3"/><path d="M1070 301H1870" stroke="#c5a257" stroke-width="2"/></g><g id="verse-backing"><path d="M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z" transform="translate(3 6)" fill="#211b15" opacity=".6"/><path d="M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z" fill="url(#linen)" stroke="#79552b" stroke-width="4"/><path d="M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z" fill="url(#weave)"/><path d="M690 2170 C1000 2250 1470 2250 1925 2138 M490 2677 C516 2726 551 2728 591 2725 C1060 2702 1530 2757 1925 2696" fill="none" stroke="#a17533" stroke-width="3"/><path d="M688 2182 C1000 2262 1470 2262 1923 2150 M489 2665 C516 2714 551 2716 591 2713 C1060 2690 1530 2745 1925 2684" fill="none" stroke="#8c3d3c" stroke-width="2" opacity=".65"/><path d="M1925 2140V2695" fill="none" stroke="#b7924a" stroke-width="3" stroke-dasharray="7 9"/></g><g fill="none" stroke-linecap="round"><path d="M1940 2119 C1950 2106 1964 2105 1977 2109" stroke="#362719" stroke-width="13"/><path d="M1940 2119 C1950 2106 1964 2105 1977 2109" stroke="#d6b260" stroke-width="7"/><path d="M1940 2709 C1950 2696 1964 2695 1977 2699" stroke="#362719" stroke-width="13"/><path d="M1940 2709 C1950 2696 1964 2695 1977 2699" stroke="#d6b260" stroke-width="7"/></g></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""Provisional BP-001 Moses Legendary composition; no production assembly."""
from pathlib import Path
import hashlib,json,os,subprocess
from xml.sax.saxutils import escape
import numpy as np
from PIL import Image,ImageDraw,ImageFont
P=Path(__file__).resolve().parent
REV=P.parents[1]
REPO=next(p for p in P.parents if (p/'docs/card-layer-pipeline.md').is_file())
ART=REV/'source/art-master.png'
FONTS=REPO/'fonts'
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
art_hash=sha(ART)
font_manifest=json.loads((FONTS/'manifest.json').read_text())
font_files={'title':'P052-Bold.otf','verse':'SanctificationP052-Medium.otf','reference':'SanctificationP052-Medium.otf'}
for name in set(font_files.values()):
assert sha(FONTS/name)==font_manifest['files'][name]['sha256']
(P/'font-cache').mkdir(exist_ok=True)
(P/'fonts.conf').write_text(f'<fontconfig><dir>{FONTS}</dir><cachedir>{P/"font-cache"}</cachedir></fontconfig>')
env=dict(os.environ,FONTCONFIG_FILE=str(P/'fonts.conf'))
def ink(*args):return subprocess.check_output(['inkscape',*map(str,args)],env=env,text=True,stderr=subprocess.PIPE).strip()
def render(src,dst):
# Supersample only geometry-heavy composite/foreground outputs; text measurements
# remain native. LANCZOS uses premultiplied RGBA via Pillow's RGBa path.
factor=2 if src.stem in {'foreground-curtain','composed-preview','without-curtain'} else 1
ink(src,'--export-type=png',f'--export-filename={dst}',f'--export-width={2000*factor}',f'--export-height={2800*factor}')
if factor>1:
im=Image.open(dst).convert('RGBA')
im.convert('RGBa').resize((2000,2800),Image.Resampling.LANCZOS).convert('RGBA').save(dst)
def resolve(family,style):return Path(subprocess.check_output(['fc-match','-f','%{file}',f'{family}:style={style}'],env=env,text=True).strip()).resolve()
assert resolve('P052','Bold')==(FONTS/'P052-Bold.otf').resolve()
assert resolve('Sanctification P052','Medium')==(FONTS/'SanctificationP052-Medium.otf').resolve()
head='<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800">'
defs='''<defs>
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbf3df"/><stop offset=".5" stop-color="#f4ead4"/><stop offset="1" stop-color="#e8d4b0"/></linearGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f0d47d"/><stop offset=".48" stop-color="#bd8b31"/><stop offset="1" stop-color="#71501e"/></linearGradient>
<pattern id="weave" width="44" height="44" patternUnits="userSpaceOnUse"><path d="M0 11H44 M0 33H44 M11 0V44 M33 0V44" stroke="#9d783d" stroke-width="1" opacity=".045"/><path d="M22 7l7 15-7 15-7-15Z" fill="none" stroke="#806438" stroke-width="1" opacity=".07"/></pattern>
<pattern id="lapis-band" width="34" height="34" patternUnits="userSpaceOnUse"><path d="M17 2L32 17 17 32 2 17Z" fill="#173e59" stroke="#d7b35a" stroke-width="3"/><circle cx="17" cy="17" r="4" fill="url(#gold)"/></pattern>
<clipPath id="curtain-clip"><path d="M-10-10 H770 C734 204 694 395 650 585 C610 770 566 972 522 1170 C474 1388 426 1604 378 1818 C338 2000 302 2190 286 2380 C276 2510 304 2640 412 2810 H-10Z"/></clipPath>
</defs>'''
base='<image xlink:href="../../source/art-master.png" width="2000" height="2800"/>'
frame='''<g id="legendary-frame">
<rect x="14" y="14" width="1972" height="2772" rx="34" fill="none" stroke="#2c2115" stroke-width="28"/>
<rect x="25" y="25" width="1950" height="2750" rx="27" fill="none" stroke="url(#gold)" stroke-width="14"/>
<rect x="43" y="43" width="1914" height="2714" rx="20" fill="none" stroke="#173e59" stroke-width="10"/>
<rect x="55" y="55" width="1890" height="2690" rx="15" fill="none" stroke="#d7b35a" stroke-width="4"/>
<path d="M68 260V68H260 M1740 68H1932V260 M68 2540V2732H260 M1740 2732H1932V2540" fill="none" stroke="#9b6d26" stroke-width="6"/>
<g fill="url(#gold)" stroke="#533810" stroke-width="3"><path d="M68 68l14 14-14 14-14-14Z"/><path d="M1932 68l14 14-14 14-14-14Z"/><path d="M68 2732l14 14-14 14-14-14Z"/><path d="M1932 2732l14 14-14 14-14-14Z"/></g>
<rect x="1906" y="430" width="27" height="255" fill="url(#lapis-band)" opacity=".95"/><rect x="1906" y="2115" width="27" height="255" fill="url(#lapis-band)" opacity=".95"/>
</g>'''
# The panels tuck behind the original tent and the pillar of cloud.
# No alteration, retouch, rescaling or regeneration of the selected source.
title_path='M700 62 H1900 Q1940 62 1940 102 V342 H700Z'
verse_path='M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z'
title_inner='<path d="M710 82 H1898 Q1920 82 1920 104 V320 H710" fill="none" stroke="#bb9040" stroke-width="3"/><path d="M1070 301H1870" stroke="#c5a257" stroke-width="2"/>'
verse_inner='''<path d="M690 2170 C1000 2250 1470 2250 1925 2138 M490 2677 C516 2726 551 2728 591 2725 C1060 2702 1530 2757 1925 2696" fill="none" stroke="#a17533" stroke-width="3"/><path d="M688 2182 C1000 2262 1470 2262 1923 2150 M489 2665 C516 2714 551 2716 591 2713 C1060 2690 1530 2745 1925 2684" fill="none" stroke="#8c3d3c" stroke-width="2" opacity=".65"/><path d="M1925 2140V2695" fill="none" stroke="#b7924a" stroke-width="3" stroke-dasharray="7 9"/>'''
def panel(path,inner,ident):
if ident=='verse-backing':
# A linen field with a woven seam, rather than a metallic plaque.
return f'<g id="{ident}"><path d="{path}" transform="translate(3 6)" fill="#211b15" opacity=".6"/><path d="{path}" fill="url(#linen)" stroke="#79552b" stroke-width="4"/><path d="{path}" fill="url(#weave)"/>{inner}</g>'
return f'<g id="{ident}"><path d="{path}" fill="url(#paper)" stroke="#62421e" stroke-width="9"/><path d="{path}" fill="url(#weave)" stroke="url(#gold)" stroke-width="4"/>{inner}</g>'
defs=defs.replace('</defs>','<linearGradient id="linen" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7edd4"/><stop offset=".55" stop-color="#f2e7cd"/><stop offset="1" stop-color="#e8d7b6"/></linearGradient></defs>')
# Ties sit at the existing right rail and visibly support the hanging field.
ties='<g fill="none" stroke-linecap="round">'+''.join(f'<path d="M1940 {y+9} C1950 {y-4} 1964 {y-5} 1977 {y-1}" stroke="#362719" stroke-width="13"/><path d="M1940 {y+9} C1950 {y-4} 1964 {y-5} 1977 {y-1}" stroke="#d6b260" stroke-width="7"/>' for y in [2110,2700])+'</g>'
panels=panel(title_path,title_inner,'title-backing')+panel(verse_path,verse_inner,'verse-backing')+ties
# Trace the visible source silhouette, including the whole cloud lower lobe.
# Upper crown is separately registered; both layers stay at source coordinates.
# Cubic contours follow the source lead, not the rough v3 clipping guesses.
# The lower closing arc ends above the old terrain occlusion.
top_edge='M1045 0 C1048 62 1012 102 971 105 C942 108 910 123 913 146 C916 166 952 176 967 209 C991 248 983 287 967 310 C956 325 971 347 959 371 C946 397 900 400 876 432'
lower_edge='M724 2080 C720 2160 692 2180 704 2230 C711 2265 694 2294 677 2322 C664 2345 672 2385 695 2410 C721 2440 722 2498 678 2538 C642 2543 622 2480 586 2480 C535 2480 536 2458 500 2444 C462 2421 438 2445 409 2424 C384 2408 370 2375 370 2340'
cloud_top=top_edge+' L780 490 H0 V0Z'
cloud_low='M0 400H370 V1780H765 C765 1920 738 2010 724 2080 '+lower_edge.removeprefix('M724 2080 ')+' L369 2640 Q286 2659 205 2635 Q101 2583 0 2612Z'
defs=defs.replace('</defs>',f'<clipPath id="cloud-top"><path d="{cloud_top}"/></clipPath><clipPath id="cloud-low"><path d="{cloud_low}"/></clipPath><clipPath id="title-edge-zone"><path d="{title_path}"/></clipPath></defs>')
# Quiet fine-gold enclosure: motifs reside in the original curtain and cloud.
frame='''<g id="legendary-frame"><rect x="19" y="19" width="1962" height="2762" rx="28" fill="none" stroke="#251c15" stroke-width="21"/><rect x="23" y="23" width="1954" height="2754" rx="23" fill="none" stroke="url(#gold)" stroke-width="11"/><rect x="43" y="43" width="1914" height="2714" rx="15" fill="none" stroke="#e2bf6c" stroke-width="3"/></g>'''
# A narrow opaque lead edge removes blue/terrain fringe without softening glass.
lead=f'<g fill="none" stroke="#2b2116" stroke-width="8" stroke-linejoin="round" stroke-linecap="round"><path d="{top_edge}" clip-path="url(#title-edge-zone)"/><path d="{lower_edge}"/></g>'
curtain='<g id="presence-foreground">'+''.join(f'<g clip-path="url(#{clip})"><image xlink:href="../../source/art-master.png" width="2000" height="2800"/></g>' for clip in ['cloud-top','cloud-low'])+lead+'</g>'
# The continuous near post already belongs to v05 art. The registered source
# foreground retains it; no second geometry, gradient, or post overlay is drawn.
(P/'foreground-curtain.svg').write_text(head+defs+curtain+'</svg>');render(P/'foreground-curtain.svg',P/'foreground-curtain.png')
fg=Image.open(P/'foreground-curtain.png').convert('RGBA')
fa=np.asarray(fg.getchannel('A'))
assert fa.max()==255 and fa.min()==0
rows=[
('title','MOSES',1458,244,142,'P052',700),
('verse-0','And there has not arisen a',1320,2360,74,'Sanctification P052',500),
('verse-1','prophet since in Israel like Moses,',1320,2468,74,'Sanctification P052',500),
('verse-2','whom the LORD knew face to face',1320,2576,74,'Sanctification P052',500),
('reference','Deuteronomy 34:10 • ESV',1320,2690,57,'Sanctification P052',500),
]
for ident,content,*_ in rows:
fontfile=font_files['title' if ident=='title' else 'reference' if ident=='reference' else 'verse']
charset=subprocess.check_output(['fc-query','--format=%{charset}',str(FONTS/fontfile)],text=True).split()
ranges=[tuple(int(v,16) for v in token.split('-')) for token in charset]
assert all(any(r[0]<=ord(char)<=r[-1] for r in ranges) for char in content), (ident, 'missing glyph')
texts=''.join(f'<text id="{ident}" x="{x}" y="{y}" font-family="{family}" font-weight="{weight}" font-size="{size}" text-anchor="middle" fill="#263f50">{escape(text)}</text>' for ident,text,x,y,size,family,weight in rows)
(P/'text-preview.svg').write_text(head+texts+'</svg>');render(P/'text-preview.svg',P/'text-preview.png')
text_image=Image.open(P/'text-preview.png').convert('RGBA');ta=np.asarray(text_image.getchannel('A'))
assert not np.any((ta>0)&(fa>0))
(P/'border-backing-preview.svg').write_text(head+defs+frame+panels+'</svg>');render(P/'border-backing-preview.svg',P/'border-backing-preview.png')
backing=np.asarray(Image.open(P/'border-backing-preview.png').convert('RGBA').getchannel('A'))
checks=[];verse_bounds=[]
for ident,text,x,y,size,family,weight in rows:
node=next(v for v in texts.split('</text>') if f'id="{ident}"' in v)+'</text>'
src=P/(ident+'-ink.svg');dst=P/(ident+'-ink.png');src.write_text(head+node+'</svg>');render(src,dst)
alpha=np.asarray(Image.open(dst).convert('RGBA').getchannel('A'));ys,xs=np.where(alpha>0);assert len(xs)
bounds=[int(xs.min()),int(ys.min()),int(xs.max()+1),int(ys.max()+1)]
safe=[1050,90,1905,330] if ident=='title' else [730,2275,1900,2730]
assert safe[0]<=bounds[0] and safe[1]<=bounds[1] and bounds[2]<=safe[2] and bounds[3]<=safe[3],(ident,bounds,safe)
assert np.all(backing[alpha>0]==255),ident
assert not np.any((alpha>0)&(fa>0)),ident
checks.append({'id':ident,'text':text,'baseline':y,'font':family,'weight':weight,'size':size,'inkBounds':bounds,'safeBounds':safe,'glyphPixelsOutsideOpaqueBacking':0,'foregroundIntersectionPixels':0})
if ident.startswith('verse-'):verse_bounds.append(bounds)
assert ' '.join(c['text'] for c in checks if c['id'].startswith('verse-'))==json.loads((REV/'card.json').read_text())['excerpt']
assert checks[-1]['text']==json.loads((REV/'card.json').read_text())['referenceDisplay']
union=[min(b[0] for b in verse_bounds),min(b[1] for b in verse_bounds),max(b[2] for b in verse_bounds),max(b[3] for b in verse_bounds)]
upper=2240;ref_top=checks[-1]['inkBounds'][1];above=union[1]-upper;below=ref_top-union[3]
assert min(above,below)>=35 and abs(above-below)<=20,(above,below)
for name,include_curtain in [('composed-preview',True),('without-curtain',False)]:
body=base+frame+panels+(curtain if include_curtain else '')+texts
src=P/(name+'.svg');src.write_text(head+defs+body+'</svg>');render(src,P/(name+'-2000.png'))
master=Image.open(P/(name+'-2000.png')).convert('RGB')
master.resize((1000,1400),Image.Resampling.LANCZOS).save(P/(name+'-1000.png'))
master.resize((500,700),Image.Resampling.LANCZOS).save(P/(name+'-500.png'))
# Compare the structural effect of the attached curtain.
board=Image.new('RGB',(1000,750),'#11161c');draw=ImageDraw.Draw(board)
for i,(label,path) in enumerate([('Panels before foreground',P/'without-curtain-1000.png'),('Pillar of cloud over panels',P/'composed-preview-1000.png')]):
draw.text((i*500+12,12),label,fill='#f4ead4')
image=Image.open(path).convert('RGB');image.thumbnail((480,672));board.paste(image,(i*500+10,45))
board.save(P/'curtain-comparison.png')
# A focused junction comparison at master crop.
detail=Image.new('RGB',(1200,620),'#11161c');draw=ImageDraw.Draw(detail)
for i,(label,path) in enumerate([('Without foreground',P/'without-curtain-2000.png'),('Cloud and curtain over backing',P/'composed-preview-2000.png')]):
draw.text((i*600+12,12),label,fill='#f4ead4')
image=Image.open(path).convert('RGB').crop((0,1780,900,2800));image.thumbnail((575,545));detail.paste(image,(i*600+12,52))
detail.save(P/'curtain-junction-detail.png')
frame_alpha=np.asarray(Image.open(P/'border-backing-preview.png').convert('RGBA').getchannel('A'))
foreground_frame_overlap=int(np.count_nonzero((fa>0)&(frame_alpha>0)))
report={
'status':'passed',
'scope':'v05 Normal-only prototype — pearl cloud and suspended tent textile',
'canvas':[2000,2800],
'sourceArtwork':{'path':str(ART.relative_to(REPO)),'sha256':art_hash,'unchanged':sha(ART)==art_hash},
'template':{
'governingIdea':'Nameplate unchanged; a suspended linen field tucks behind the pillar of cloud and ties to the right frame',
'titleBackingPath':title_path,
'verseBackingPath':verse_path,
'compositionOrder':['base-art','frame-and-backings','attached-curtain-foreground','deterministic-text'],
'curtainContour':'foreground-curtain.svg',
'cloudUpperContour':cloud_top,
'cloudLowerContour':cloud_low,
'curtainAlphaBounds':list(Image.fromarray(fa).getbbox()),
'curtainFrameOrBackingOverlapPixels':foreground_frame_overlap,
},
'typography':{
'fontManifestSHA256':sha(FONTS/'manifest.json'),
'checks':checks,
'verseInkUnion':union,
'upperFlourishToVerseInk':above,
'verseInkToReferenceLineBox':below,
'centeringDifference':abs(above-below),
'lineBaselineGap':108,
},
'structuralChecks':{
'curtainTextIntersectionPixels':int(np.count_nonzero((ta>0)&(fa>0))),
'glyphPixelsOutsideOpaqueBacking':0,
'artworkSHA256Unchanged':True,
'fontGlyphCoverage':'passed',
'fontManifestHashes':'passed',
},
'notPerformed':['Borderless/Textless/Boundless composition','Production finish masks','Production text masks','Harness fixture installation','GPU moving-light validation','Final card approval'],
'staticReview':'Provisional Normal-only prototype; static inspection at card size and enlarged junctions; pending user review',
'edgeRendering':{'geometry':'source-traced cubic Bezier paths','leadWidthMasterPixels':8,'renderScale':2,'downsample':'premultiplied RGBA LANCZOS','blur':False},
'foregroundAdaptation':'Liked nameplate geometry and suspended linen layout retained. New v05 pearl/cool cloud uses authored wave-like cubic contours; lower closure stays above source ground occlusion. The continuous entrance post is reused from the baked source and never redrawn.'
}
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n')
assert sha(ART)==art_hash
print(json.dumps({'status':'passed','preview':str((P/'composed-preview-1000.png').relative_to(REPO)),'curtainOverlapPixels':foreground_frame_overlap,'spacing':[above,below]},indent=2))
# Source/composed review and comparison to the carried-forward layout.
label_font=ImageFont.truetype(str(FONTS/'P052-Bold.otf'),26)
previous=REPO/'in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v5'
for name,items in [
('source-composed-comparison',[('Selected v05 source',ART),('v05 Normal prototype',P/'composed-preview-1000.png')]),
('layout-lineage-comparison',[('Prior v03 layout / prior art',previous/'composed-preview-1000.png'),('v05 layout / current art',P/'composed-preview-1000.png')]),
]:
board=Image.new('RGB',(1500,1100),'#11161c');d=ImageDraw.Draw(board)
for i,(label,path) in enumerate(items):
d.text((i*750+24,18),label,font=label_font,fill='#f4ead4')
im=Image.open(path).convert('RGB');im.thumbnail((730,1022));board.paste(im,(i*750+10,63))
board.save(P/(name+'.png'))
master=Image.open(P/'composed-preview-2000.png').convert('RGB')
detail=Image.new('RGB',(1500,1300),'#11161c');d=ImageDraw.Draw(detail)
for label,box,origin,size in [
('Pearl cloud / nameplate',(620,0,1140,480),(22,65),(700,550)),
('Cloud / textile / existing post',(295,2020,800,2660),(795,65),(660,690)),
]:
d.text((origin[0],20),label,font=label_font,fill='#f4ead4')
im=master.crop(box);im=im.resize((round(im.width*1.15),round(im.height*1.15)),Image.Resampling.LANCZOS);im.thumbnail(size);detail.paste(im,origin)
im=master.crop((290,2070,2000,2800));im.thumbnail((1450,520));detail.paste(im,(25,775))
detail.save(P/'cloud-textile-junction-details.png')
# Preserve approved typography and nameplate geometry without claiming the
# changed source/cloud pixels are identical to old artwork.
assert sha(ART)==json.loads((REV/'card.json').read_text())['artSHA256']
assert np.array_equal(np.asarray(Image.open(P/'text-preview.png')),np.asarray(Image.open(previous/'text-preview.png')))
report['structuralChecks']['textPixelsIdenticalToCarriedForwardLayout']=True
report['structuralChecks']['sourceMatchesV05CardSHA256']=True
report['structuralChecks']['secondPostDrawn']=False
report['template']['layoutReference']=str(previous.relative_to(REPO))
report['template']['postPolicy']='Use the continuous entrance post already baked into v05 artwork; no new post layer'
report['template']['leftTextileAnchor']=[340,2106]
report['edgeRendering']['alphaEdge']='Premultiplied before downsampling; no transparent-RGB halo'
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n')

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

View File

@@ -0,0 +1 @@
<fontconfig><dir>/home/dkzver/docker/sanctification/fonts</dir><cachedir>/home/dkzver/docker/sanctification/in-progress/cards/BP-001-moses/revisions/v05/review/normal-prototype-v1/font-cache</cachedir></fontconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><defs>
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbf3df" /><stop offset=".5" stop-color="#f4ead4" /><stop offset="1" stop-color="#e8d4b0" /></linearGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f0d47d" /><stop offset=".48" stop-color="#bd8b31" /><stop offset="1" stop-color="#71501e" /></linearGradient>
<pattern id="weave" width="44" height="44" patternUnits="userSpaceOnUse"><path d="M0 11H44 M0 33H44 M11 0V44 M33 0V44" stroke="#9d783d" stroke-width="1" opacity=".045" /><path d="M22 7l7 15-7 15-7-15Z" fill="none" stroke="#806438" stroke-width="1" opacity=".07" /></pattern>
<pattern id="lapis-band" width="34" height="34" patternUnits="userSpaceOnUse"><path d="M17 2L32 17 17 32 2 17Z" fill="#173e59" stroke="#d7b35a" stroke-width="3" /><circle cx="17" cy="17" r="4" fill="url(#gold)" /></pattern>
<clipPath id="curtain-clip"><path d="M-10-10 H770 C734 204 694 395 650 585 C610 770 566 972 522 1170 C474 1388 426 1604 378 1818 C338 2000 302 2190 286 2380 C276 2510 304 2640 412 2810 H-10Z" /></clipPath>
<linearGradient id="linen" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7edd4" /><stop offset=".55" stop-color="#f2e7cd" /><stop offset="1" stop-color="#e8d7b6" /></linearGradient><clipPath id="cloud-top"><path d="M1045 0 C1048 62 1012 102 971 105 C942 108 910 123 913 146 C916 166 952 176 967 209 C991 248 983 287 967 310 C956 325 971 347 959 371 C946 397 900 400 876 432 L780 490 H0 V0Z" /></clipPath><clipPath id="cloud-low"><path d="M0 400H370 V1780H765 C765 1920 738 2010 724 2080 C720 2160 692 2180 704 2230 C711 2265 694 2294 677 2322 C664 2345 672 2385 695 2410 C721 2440 722 2498 678 2538 C642 2543 622 2480 586 2480 C535 2480 536 2458 500 2444 C462 2421 438 2445 409 2424 C384 2408 370 2375 370 2340 L369 2640 Q286 2659 205 2635 Q101 2583 0 2612Z" /></clipPath><clipPath id="title-edge-zone"><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z" /></clipPath></defs><g id="presence-foreground"><g clip-path="url(#cloud-top)"><image xlink:href="../../source/art-master.png" width="2000" height="2800" /></g><g clip-path="url(#cloud-low)"><image xlink:href="../../source/art-master.png" width="2000" height="2800" /></g><g fill="none" stroke="#2b2116" stroke-width="8" stroke-linejoin="round" stroke-linecap="round"><path d="M1045 0 C1048 62 1012 102 971 105 C942 108 910 123 913 146 C916 166 952 176 967 209 C991 248 983 287 967 310 C956 325 971 347 959 371 C946 397 900 400 876 432" clip-path="url(#title-edge-zone)" /><path d="M724 2080 C720 2160 692 2180 704 2230 C711 2265 694 2294 677 2322 C664 2345 672 2385 695 2410 C721 2440 722 2498 678 2538 C642 2543 622 2480 586 2480 C535 2480 536 2458 500 2444 C462 2421 438 2445 409 2424 C384 2408 370 2375 370 2340" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@@ -0,0 +1,193 @@
{
"status": "passed",
"scope": "v05 Normal-only prototype \u2014 pearl cloud and suspended tent textile",
"canvas": [
2000,
2800
],
"sourceArtwork": {
"path": "in-progress/cards/BP-001-moses/revisions/v05/source/art-master.png",
"sha256": "372fb8f9f5b398c943364a620c970fd81e000f3e0613f71eff84e087f6396bee",
"unchanged": true
},
"template": {
"governingIdea": "Nameplate unchanged; a suspended linen field tucks behind the pillar of cloud and ties to the right frame",
"titleBackingPath": "M700 62 H1900 Q1940 62 1940 102 V342 H700Z",
"verseBackingPath": "M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z",
"compositionOrder": [
"base-art",
"frame-and-backings",
"attached-curtain-foreground",
"deterministic-text"
],
"curtainContour": "foreground-curtain.svg",
"cloudUpperContour": "M1045 0 C1048 62 1012 102 971 105 C942 108 910 123 913 146 C916 166 952 176 967 209 C991 248 983 287 967 310 C956 325 971 347 959 371 C946 397 900 400 876 432 L780 490 H0 V0Z",
"cloudLowerContour": "M0 400H370 V1780H765 C765 1920 738 2010 724 2080 C720 2160 692 2180 704 2230 C711 2265 694 2294 677 2322 C664 2345 672 2385 695 2410 C721 2440 722 2498 678 2538 C642 2543 622 2480 586 2480 C535 2480 536 2458 500 2444 C462 2421 438 2445 409 2424 C384 2408 370 2375 370 2340 L369 2640 Q286 2659 205 2635 Q101 2583 0 2612Z",
"curtainAlphaBounds": [
0,
0,
1048,
2651
],
"curtainFrameOrBackingOverlapPixels": 216058,
"layoutReference": "in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v5",
"postPolicy": "Use the continuous entrance post already baked into v05 artwork; no new post layer",
"leftTextileAnchor": [
340,
2106
]
},
"typography": {
"fontManifestSHA256": "ef68ee6393981d54ca068a950f21c53b374d20e6e46c21c788f36380d3aae178",
"checks": [
{
"id": "title",
"text": "MOSES",
"baseline": 244,
"font": "P052",
"weight": 700,
"size": 142,
"inkBounds": [
1202,
145,
1711,
247
],
"safeBounds": [
1050,
90,
1905,
330
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-0",
"text": "And there has not arisen a",
"baseline": 2360,
"font": "Sanctification P052",
"weight": 500,
"size": 74,
"inkBounds": [
890,
2306,
1749,
2362
],
"safeBounds": [
730,
2275,
1900,
2730
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-1",
"text": "prophet since in Israel like Moses,",
"baseline": 2468,
"font": "Sanctification P052",
"weight": 500,
"size": 74,
"inkBounds": [
769,
2414,
1869,
2490
],
"safeBounds": [
730,
2275,
1900,
2730
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-2",
"text": "whom the LORD knew face to face",
"baseline": 2576,
"font": "Sanctification P052",
"weight": 500,
"size": 74,
"inkBounds": [
752,
2521,
1886,
2578
],
"safeBounds": [
730,
2275,
1900,
2730
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "reference",
"text": "Deuteronomy 34:10 \u2022 ESV",
"baseline": 2690,
"font": "Sanctification P052",
"weight": 500,
"size": 57,
"inkBounds": [
991,
2649,
1649,
2707
],
"safeBounds": [
730,
2275,
1900,
2730
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
}
],
"verseInkUnion": [
752,
2306,
1886,
2578
],
"upperFlourishToVerseInk": 66,
"verseInkToReferenceLineBox": 71,
"centeringDifference": 5,
"lineBaselineGap": 108
},
"structuralChecks": {
"curtainTextIntersectionPixels": 0,
"glyphPixelsOutsideOpaqueBacking": 0,
"artworkSHA256Unchanged": true,
"fontGlyphCoverage": "passed",
"fontManifestHashes": "passed",
"textPixelsIdenticalToCarriedForwardLayout": true,
"sourceMatchesV05CardSHA256": true,
"secondPostDrawn": false
},
"notPerformed": [
"Borderless/Textless/Boundless composition",
"Production finish masks",
"Production text masks",
"Harness fixture installation",
"GPU moving-light validation",
"Final card approval"
],
"staticReview": "Provisional Normal-only prototype; static inspection at card size and enlarged junctions; pending user review",
"edgeRendering": {
"geometry": "source-traced cubic Bezier paths",
"leadWidthMasterPixels": 8,
"renderScale": 2,
"downsample": "premultiplied RGBA LANCZOS",
"blur": false,
"alphaEdge": "Premultiplied before downsampling; no transparent-RGB halo"
},
"foregroundAdaptation": "Liked nameplate geometry and suspended linen layout retained. New v05 pearl/cool cloud uses authored wave-like cubic contours; lower closure stays above source ground occlusion. The continuous entrance post is reused from the baked source and never redrawn."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><text id="reference" x="1320" y="2690" font-family="Sanctification P052" font-weight="500" font-size="57" text-anchor="middle" fill="#263f50">Deuteronomy 34:10 • ESV</text></svg>

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><text id="title" x="1458" y="244" font-family="P052" font-weight="700" font-size="142" text-anchor="middle" fill="#263f50">MOSES</text><text id="verse-0" x="1320" y="2360" font-family="Sanctification P052" font-weight="500" font-size="74" text-anchor="middle" fill="#263f50">And there has not arisen a</text><text id="verse-1" x="1320" y="2468" font-family="Sanctification P052" font-weight="500" font-size="74" text-anchor="middle" fill="#263f50">prophet since in Israel like Moses,</text><text id="verse-2" x="1320" y="2576" font-family="Sanctification P052" font-weight="500" font-size="74" text-anchor="middle" fill="#263f50">whom the LORD knew face to face</text><text id="reference" x="1320" y="2690" font-family="Sanctification P052" font-weight="500" font-size="57" text-anchor="middle" fill="#263f50">Deuteronomy 34:10 • ESV</text></svg>

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><text id="title" x="1458" y="244" font-family="P052" font-weight="700" font-size="142" text-anchor="middle" fill="#263f50">MOSES</text></svg>

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><text id="verse-0" x="1320" y="2360" font-family="Sanctification P052" font-weight="500" font-size="74" text-anchor="middle" fill="#263f50">And there has not arisen a</text></svg>

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><text id="verse-1" x="1320" y="2468" font-family="Sanctification P052" font-weight="500" font-size="74" text-anchor="middle" fill="#263f50">prophet since in Israel like Moses,</text></svg>

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><text id="verse-2" x="1320" y="2576" font-family="Sanctification P052" font-weight="500" font-size="74" text-anchor="middle" fill="#263f50">whom the LORD knew face to face</text></svg>

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><defs>
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbf3df" /><stop offset=".5" stop-color="#f4ead4" /><stop offset="1" stop-color="#e8d4b0" /></linearGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f0d47d" /><stop offset=".48" stop-color="#bd8b31" /><stop offset="1" stop-color="#71501e" /></linearGradient>
<pattern id="weave" width="44" height="44" patternUnits="userSpaceOnUse"><path d="M0 11H44 M0 33H44 M11 0V44 M33 0V44" stroke="#9d783d" stroke-width="1" opacity=".045" /><path d="M22 7l7 15-7 15-7-15Z" fill="none" stroke="#806438" stroke-width="1" opacity=".07" /></pattern>
<pattern id="lapis-band" width="34" height="34" patternUnits="userSpaceOnUse"><path d="M17 2L32 17 17 32 2 17Z" fill="#173e59" stroke="#d7b35a" stroke-width="3" /><circle cx="17" cy="17" r="4" fill="url(#gold)" /></pattern>
<clipPath id="curtain-clip"><path d="M-10-10 H770 C734 204 694 395 650 585 C610 770 566 972 522 1170 C474 1388 426 1604 378 1818 C338 2000 302 2190 286 2380 C276 2510 304 2640 412 2810 H-10Z" /></clipPath>
<linearGradient id="linen" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7edd4" /><stop offset=".55" stop-color="#f2e7cd" /><stop offset="1" stop-color="#e8d7b6" /></linearGradient><clipPath id="cloud-top"><path d="M1045 0 C1048 62 1012 102 971 105 C942 108 910 123 913 146 C916 166 952 176 967 209 C991 248 983 287 967 310 C956 325 971 347 959 371 C946 397 900 400 876 432 L780 490 H0 V0Z" /></clipPath><clipPath id="cloud-low"><path d="M0 400H370 V1780H765 C765 1920 738 2010 724 2080 C720 2160 692 2180 704 2230 C711 2265 694 2294 677 2322 C664 2345 672 2385 695 2410 C721 2440 722 2498 678 2538 C642 2543 622 2480 586 2480 C535 2480 536 2458 500 2444 C462 2421 438 2445 409 2424 C384 2408 370 2375 370 2340 L369 2640 Q286 2659 205 2635 Q101 2583 0 2612Z" /></clipPath><clipPath id="title-edge-zone"><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z" /></clipPath></defs><image xlink:href="../../source/art-master.png" width="2000" height="2800" /><g id="legendary-frame"><rect x="19" y="19" width="1962" height="2762" rx="28" fill="none" stroke="#251c15" stroke-width="21" /><rect x="23" y="23" width="1954" height="2754" rx="23" fill="none" stroke="url(#gold)" stroke-width="11" /><rect x="43" y="43" width="1914" height="2714" rx="15" fill="none" stroke="#e2bf6c" stroke-width="3" /></g><g id="title-backing"><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z" fill="url(#paper)" stroke="#62421e" stroke-width="9" /><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z" fill="url(#weave)" stroke="url(#gold)" stroke-width="4" /><path d="M710 82 H1898 Q1920 82 1920 104 V320 H710" fill="none" stroke="#bb9040" stroke-width="3" /><path d="M1070 301H1870" stroke="#c5a257" stroke-width="2" /></g><g id="verse-backing"><path d="M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z" transform="translate(3 6)" fill="#211b15" opacity=".6" /><path d="M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z" fill="url(#linen)" stroke="#79552b" stroke-width="4" /><path d="M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z" fill="url(#weave)" /><path d="M690 2170 C1000 2250 1470 2250 1925 2138 M490 2677 C516 2726 551 2728 591 2725 C1060 2702 1530 2757 1925 2696" fill="none" stroke="#a17533" stroke-width="3" /><path d="M688 2182 C1000 2262 1470 2262 1923 2150 M489 2665 C516 2714 551 2716 591 2713 C1060 2690 1530 2745 1925 2684" fill="none" stroke="#8c3d3c" stroke-width="2" opacity=".65" /><path d="M1925 2140V2695" fill="none" stroke="#b7924a" stroke-width="3" stroke-dasharray="7 9" /></g><g fill="none" stroke-linecap="round"><path d="M1940 2119 C1950 2106 1964 2105 1977 2109" stroke="#362719" stroke-width="13" /><path d="M1940 2119 C1950 2106 1964 2105 1977 2109" stroke="#d6b260" stroke-width="7" /><path d="M1940 2709 C1950 2696 1964 2695 1977 2699" stroke="#362719" stroke-width="13" /><path d="M1940 2709 C1950 2696 1964 2695 1977 2699" stroke="#d6b260" stroke-width="7" /></g><text id="title" x="1458" y="244" font-family="P052" font-weight="700" font-size="142" text-anchor="middle" fill="#263f50">MOSES</text><text id="verse-0" x="1320" y="2360" font-family="Sanctification P052" font-weight="500" font-size="74" text-anchor="middle" fill="#263f50">And there has not arisen a</text><text id="verse-1" x="1320" y="2468" font-family="Sanctification P052" font-weight="500" font-size="74" text-anchor="middle" fill="#263f50">prophet since in Israel like Moses,</text><text id="verse-2" x="1320" y="2576" font-family="Sanctification P052" font-weight="500" font-size="74" text-anchor="middle" fill="#263f50">whom the LORD knew face to face</text><text id="reference" x="1320" y="2690" font-family="Sanctification P052" font-weight="500" font-size="57" text-anchor="middle" fill="#263f50">Deuteronomy 34:10 • ESV</text></svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,24 @@
# Moses v05 — Normal prototype v2
Targeted refinement of [v1](../normal-prototype-v1/README.md), which remains intact.
- [Full preview](composed-preview-1000.png)
- [500 px preview](composed-preview-500.png)
- [V1 / V2 full comparison](v1-v2-full-comparison.png)
- [V1 / V2 lower junction comparison](v1-v2-junction-comparison.png)
- [Enlarged cloud/textile details](cloud-textile-junction-details.png)
- [Source/composed comparison](source-composed-comparison.png)
- [Builder](build-preview.py)
- [Validation](preview-validation.json)
The foreground now follows the actual curtain hem through its full lower edge, hiding the frame behind the fabric. The old shortened extraction exposed a gold frame rule inside the hem. A dedicated check verifies zero exposed frame pixels within the affected cloth region.
The lowest cloud overflow is now a broader rounded roll. Continuous cubic tangents replace the previous pointed closure; its pearl and cool glass interior remains registered to the source. No blur, new post or source repaint is introduced.
All text and backing pixels match v1 exactly. The upper composition, including the liked nameplate and cloud crown, is preserved. Checks cover exact text, font hashes/glyph coverage, safe glyph bounds, opaque backing, zero foreground/text intersection, source SHA256 and targeted v1/v2 preservation. Rendering uses 2× resolution and premultiplied-alpha downsampling.
Source: ../../source/art-master.png, unchanged SHA256 372fb8f9f5b398c943364a620c970fd81e000f3e0613f71eff84e087f6396bee. The continuous entrance post is already in this source and is not drawn again.
Rebuild: python3 in-progress/cards/BP-001-moses/revisions/v05/review/normal-prototype-v2/build-preview.py. Requires Inkscape, Fontconfig, Pillow and NumPy.
Normal review prototype only. No other printing types, finish masks, production exports, harness fixture, acceptance or promotion.

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800"><defs>
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbf3df"/><stop offset=".5" stop-color="#f4ead4"/><stop offset="1" stop-color="#e8d4b0"/></linearGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f0d47d"/><stop offset=".48" stop-color="#bd8b31"/><stop offset="1" stop-color="#71501e"/></linearGradient>
<pattern id="weave" width="44" height="44" patternUnits="userSpaceOnUse"><path d="M0 11H44 M0 33H44 M11 0V44 M33 0V44" stroke="#9d783d" stroke-width="1" opacity=".045"/><path d="M22 7l7 15-7 15-7-15Z" fill="none" stroke="#806438" stroke-width="1" opacity=".07"/></pattern>
<pattern id="lapis-band" width="34" height="34" patternUnits="userSpaceOnUse"><path d="M17 2L32 17 17 32 2 17Z" fill="#173e59" stroke="#d7b35a" stroke-width="3"/><circle cx="17" cy="17" r="4" fill="url(#gold)"/></pattern>
<clipPath id="curtain-clip"><path d="M-10-10 H770 C734 204 694 395 650 585 C610 770 566 972 522 1170 C474 1388 426 1604 378 1818 C338 2000 302 2190 286 2380 C276 2510 304 2640 412 2810 H-10Z"/></clipPath>
<linearGradient id="linen" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7edd4"/><stop offset=".55" stop-color="#f2e7cd"/><stop offset="1" stop-color="#e8d7b6"/></linearGradient><clipPath id="cloud-top"><path d="M1045 0 C1048 62 1012 102 971 105 C942 108 910 123 913 146 C916 166 952 176 967 209 C991 248 983 287 967 310 C956 325 971 347 959 371 C946 397 900 400 876 432 L780 490 H0 V0Z"/></clipPath><clipPath id="cloud-low"><path d="M0 400H370 V1780H765 C765 1920 738 2010 724 2080 C720 2160 692 2180 704 2230 C711 2265 694 2294 677 2322 C664 2345 672 2385 695 2410 C721 2440 722 2478 701 2501 C680 2524 647 2527 619 2509 C594 2492 566 2496 537 2485 C510 2474 499 2459 471 2455 C436 2450 413 2442 393 2423 C377 2408 370 2375 370 2340 L369 2650 C318 2651 271 2644 224 2641 C173 2638 122 2650 76 2646 C45 2640 24 2634 0 2640Z"/></clipPath><clipPath id="title-edge-zone"><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z"/></clipPath></defs><g id="legendary-frame"><rect x="19" y="19" width="1962" height="2762" rx="28" fill="none" stroke="#251c15" stroke-width="21"/><rect x="23" y="23" width="1954" height="2754" rx="23" fill="none" stroke="url(#gold)" stroke-width="11"/><rect x="43" y="43" width="1914" height="2714" rx="15" fill="none" stroke="#e2bf6c" stroke-width="3"/></g><g id="title-backing"><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z" fill="url(#paper)" stroke="#62421e" stroke-width="9"/><path d="M700 62 H1900 Q1940 62 1940 102 V342 H700Z" fill="url(#weave)" stroke="url(#gold)" stroke-width="4"/><path d="M710 82 H1898 Q1920 82 1920 104 V320 H710" fill="none" stroke="#bb9040" stroke-width="3"/><path d="M1070 301H1870" stroke="#c5a257" stroke-width="2"/></g><g id="verse-backing"><path d="M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z" transform="translate(3 6)" fill="#211b15" opacity=".6"/><path d="M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z" fill="url(#linen)" stroke="#79552b" stroke-width="4"/><path d="M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z" fill="url(#weave)"/><path d="M690 2170 C1000 2250 1470 2250 1925 2138 M490 2677 C516 2726 551 2728 591 2725 C1060 2702 1530 2757 1925 2696" fill="none" stroke="#a17533" stroke-width="3"/><path d="M688 2182 C1000 2262 1470 2262 1923 2150 M489 2665 C516 2714 551 2716 591 2713 C1060 2690 1530 2745 1925 2684" fill="none" stroke="#8c3d3c" stroke-width="2" opacity=".65"/><path d="M1925 2140V2695" fill="none" stroke="#b7924a" stroke-width="3" stroke-dasharray="7 9"/></g><g fill="none" stroke-linecap="round"><path d="M1940 2119 C1950 2106 1964 2105 1977 2109" stroke="#362719" stroke-width="13"/><path d="M1940 2119 C1950 2106 1964 2105 1977 2109" stroke="#d6b260" stroke-width="7"/><path d="M1940 2709 C1950 2696 1964 2695 1977 2699" stroke="#362719" stroke-width="13"/><path d="M1940 2709 C1950 2696 1964 2695 1977 2699" stroke="#d6b260" stroke-width="7"/></g></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""Provisional BP-001 Moses Legendary composition; no production assembly."""
from pathlib import Path
import hashlib,json,os,subprocess
from xml.sax.saxutils import escape
import numpy as np
from PIL import Image,ImageDraw,ImageFont
P=Path(__file__).resolve().parent
REV=P.parents[1]
REPO=next(p for p in P.parents if (p/'docs/card-layer-pipeline.md').is_file())
ART=REV/'source/art-master.png'
FONTS=REPO/'fonts'
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
art_hash=sha(ART)
font_manifest=json.loads((FONTS/'manifest.json').read_text())
font_files={'title':'P052-Bold.otf','verse':'SanctificationP052-Medium.otf','reference':'SanctificationP052-Medium.otf'}
for name in set(font_files.values()):
assert sha(FONTS/name)==font_manifest['files'][name]['sha256']
(P/'font-cache').mkdir(exist_ok=True)
(P/'fonts.conf').write_text(f'<fontconfig><dir>{FONTS}</dir><cachedir>{P/"font-cache"}</cachedir></fontconfig>')
env=dict(os.environ,FONTCONFIG_FILE=str(P/'fonts.conf'))
def ink(*args):return subprocess.check_output(['inkscape',*map(str,args)],env=env,text=True,stderr=subprocess.PIPE).strip()
def render(src,dst):
# Supersample only geometry-heavy composite/foreground outputs; text measurements
# remain native. LANCZOS uses premultiplied RGBA via Pillow's RGBa path.
factor=2 if src.stem in {'foreground-curtain','composed-preview','without-curtain'} else 1
ink(src,'--export-type=png',f'--export-filename={dst}',f'--export-width={2000*factor}',f'--export-height={2800*factor}')
if factor>1:
im=Image.open(dst).convert('RGBA')
im.convert('RGBa').resize((2000,2800),Image.Resampling.LANCZOS).convert('RGBA').save(dst)
def resolve(family,style):return Path(subprocess.check_output(['fc-match','-f','%{file}',f'{family}:style={style}'],env=env,text=True).strip()).resolve()
assert resolve('P052','Bold')==(FONTS/'P052-Bold.otf').resolve()
assert resolve('Sanctification P052','Medium')==(FONTS/'SanctificationP052-Medium.otf').resolve()
head='<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800">'
defs='''<defs>
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbf3df"/><stop offset=".5" stop-color="#f4ead4"/><stop offset="1" stop-color="#e8d4b0"/></linearGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f0d47d"/><stop offset=".48" stop-color="#bd8b31"/><stop offset="1" stop-color="#71501e"/></linearGradient>
<pattern id="weave" width="44" height="44" patternUnits="userSpaceOnUse"><path d="M0 11H44 M0 33H44 M11 0V44 M33 0V44" stroke="#9d783d" stroke-width="1" opacity=".045"/><path d="M22 7l7 15-7 15-7-15Z" fill="none" stroke="#806438" stroke-width="1" opacity=".07"/></pattern>
<pattern id="lapis-band" width="34" height="34" patternUnits="userSpaceOnUse"><path d="M17 2L32 17 17 32 2 17Z" fill="#173e59" stroke="#d7b35a" stroke-width="3"/><circle cx="17" cy="17" r="4" fill="url(#gold)"/></pattern>
<clipPath id="curtain-clip"><path d="M-10-10 H770 C734 204 694 395 650 585 C610 770 566 972 522 1170 C474 1388 426 1604 378 1818 C338 2000 302 2190 286 2380 C276 2510 304 2640 412 2810 H-10Z"/></clipPath>
</defs>'''
base='<image xlink:href="../../source/art-master.png" width="2000" height="2800"/>'
frame='''<g id="legendary-frame">
<rect x="14" y="14" width="1972" height="2772" rx="34" fill="none" stroke="#2c2115" stroke-width="28"/>
<rect x="25" y="25" width="1950" height="2750" rx="27" fill="none" stroke="url(#gold)" stroke-width="14"/>
<rect x="43" y="43" width="1914" height="2714" rx="20" fill="none" stroke="#173e59" stroke-width="10"/>
<rect x="55" y="55" width="1890" height="2690" rx="15" fill="none" stroke="#d7b35a" stroke-width="4"/>
<path d="M68 260V68H260 M1740 68H1932V260 M68 2540V2732H260 M1740 2732H1932V2540" fill="none" stroke="#9b6d26" stroke-width="6"/>
<g fill="url(#gold)" stroke="#533810" stroke-width="3"><path d="M68 68l14 14-14 14-14-14Z"/><path d="M1932 68l14 14-14 14-14-14Z"/><path d="M68 2732l14 14-14 14-14-14Z"/><path d="M1932 2732l14 14-14 14-14-14Z"/></g>
<rect x="1906" y="430" width="27" height="255" fill="url(#lapis-band)" opacity=".95"/><rect x="1906" y="2115" width="27" height="255" fill="url(#lapis-band)" opacity=".95"/>
</g>'''
# The panels tuck behind the original tent and the pillar of cloud.
# No alteration, retouch, rescaling or regeneration of the selected source.
title_path='M700 62 H1900 Q1940 62 1940 102 V342 H700Z'
verse_path='M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z'
title_inner='<path d="M710 82 H1898 Q1920 82 1920 104 V320 H710" fill="none" stroke="#bb9040" stroke-width="3"/><path d="M1070 301H1870" stroke="#c5a257" stroke-width="2"/>'
verse_inner='''<path d="M690 2170 C1000 2250 1470 2250 1925 2138 M490 2677 C516 2726 551 2728 591 2725 C1060 2702 1530 2757 1925 2696" fill="none" stroke="#a17533" stroke-width="3"/><path d="M688 2182 C1000 2262 1470 2262 1923 2150 M489 2665 C516 2714 551 2716 591 2713 C1060 2690 1530 2745 1925 2684" fill="none" stroke="#8c3d3c" stroke-width="2" opacity=".65"/><path d="M1925 2140V2695" fill="none" stroke="#b7924a" stroke-width="3" stroke-dasharray="7 9"/>'''
def panel(path,inner,ident):
if ident=='verse-backing':
# A linen field with a woven seam, rather than a metallic plaque.
return f'<g id="{ident}"><path d="{path}" transform="translate(3 6)" fill="#211b15" opacity=".6"/><path d="{path}" fill="url(#linen)" stroke="#79552b" stroke-width="4"/><path d="{path}" fill="url(#weave)"/>{inner}</g>'
return f'<g id="{ident}"><path d="{path}" fill="url(#paper)" stroke="#62421e" stroke-width="9"/><path d="{path}" fill="url(#weave)" stroke="url(#gold)" stroke-width="4"/>{inner}</g>'
defs=defs.replace('</defs>','<linearGradient id="linen" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7edd4"/><stop offset=".55" stop-color="#f2e7cd"/><stop offset="1" stop-color="#e8d7b6"/></linearGradient></defs>')
# Ties sit at the existing right rail and visibly support the hanging field.
ties='<g fill="none" stroke-linecap="round">'+''.join(f'<path d="M1940 {y+9} C1950 {y-4} 1964 {y-5} 1977 {y-1}" stroke="#362719" stroke-width="13"/><path d="M1940 {y+9} C1950 {y-4} 1964 {y-5} 1977 {y-1}" stroke="#d6b260" stroke-width="7"/>' for y in [2110,2700])+'</g>'
panels=panel(title_path,title_inner,'title-backing')+panel(verse_path,verse_inner,'verse-backing')+ties
# Trace the visible source silhouette, including the whole cloud lower lobe.
# Upper crown is separately registered; both layers stay at source coordinates.
# Cubic contours follow the source lead, not the rough v3 clipping guesses.
# The lower closing arc ends above the old terrain occlusion.
top_edge='M1045 0 C1048 62 1012 102 971 105 C942 108 910 123 913 146 C916 166 952 176 967 209 C991 248 983 287 967 310 C956 325 971 347 959 371 C946 397 900 400 876 432'
lower_edge='M724 2080 C720 2160 692 2180 704 2230 C711 2265 694 2294 677 2322 C664 2345 672 2385 695 2410 C721 2440 722 2478 701 2501 C680 2524 647 2527 619 2509 C594 2492 566 2496 537 2485 C510 2474 499 2459 471 2455 C436 2450 413 2442 393 2423 C377 2408 370 2375 370 2340'
cloud_top=top_edge+' L780 490 H0 V0Z'
cloud_low='M0 400H370 V1780H765 C765 1920 738 2010 724 2080 '+lower_edge.removeprefix('M724 2080 ')+' L369 2650 C318 2651 271 2644 224 2641 C173 2638 122 2650 76 2646 C45 2640 24 2634 0 2640Z'
defs=defs.replace('</defs>',f'<clipPath id="cloud-top"><path d="{cloud_top}"/></clipPath><clipPath id="cloud-low"><path d="{cloud_low}"/></clipPath><clipPath id="title-edge-zone"><path d="{title_path}"/></clipPath></defs>')
# Quiet fine-gold enclosure: motifs reside in the original curtain and cloud.
frame='''<g id="legendary-frame"><rect x="19" y="19" width="1962" height="2762" rx="28" fill="none" stroke="#251c15" stroke-width="21"/><rect x="23" y="23" width="1954" height="2754" rx="23" fill="none" stroke="url(#gold)" stroke-width="11"/><rect x="43" y="43" width="1914" height="2714" rx="15" fill="none" stroke="#e2bf6c" stroke-width="3"/></g>'''
# A narrow opaque lead edge removes blue/terrain fringe without softening glass.
lead=f'<g fill="none" stroke="#2b2116" stroke-width="8" stroke-linejoin="round" stroke-linecap="round"><path d="{top_edge}" clip-path="url(#title-edge-zone)"/><path d="{lower_edge}"/></g>'
curtain='<g id="presence-foreground">'+''.join(f'<g clip-path="url(#{clip})"><image xlink:href="../../source/art-master.png" width="2000" height="2800"/></g>' for clip in ['cloud-top','cloud-low'])+lead+'</g>'
# The continuous near post already belongs to v05 art. The registered source
# foreground retains it; no second geometry, gradient, or post overlay is drawn.
(P/'foreground-curtain.svg').write_text(head+defs+curtain+'</svg>');render(P/'foreground-curtain.svg',P/'foreground-curtain.png')
fg=Image.open(P/'foreground-curtain.png').convert('RGBA')
fa=np.asarray(fg.getchannel('A'))
assert fa.max()==255 and fa.min()==0
rows=[
('title','MOSES',1458,244,142,'P052',700),
('verse-0','And there has not arisen a',1320,2360,74,'Sanctification P052',500),
('verse-1','prophet since in Israel like Moses,',1320,2468,74,'Sanctification P052',500),
('verse-2','whom the LORD knew face to face',1320,2576,74,'Sanctification P052',500),
('reference','Deuteronomy 34:10 • ESV',1320,2690,57,'Sanctification P052',500),
]
for ident,content,*_ in rows:
fontfile=font_files['title' if ident=='title' else 'reference' if ident=='reference' else 'verse']
charset=subprocess.check_output(['fc-query','--format=%{charset}',str(FONTS/fontfile)],text=True).split()
ranges=[tuple(int(v,16) for v in token.split('-')) for token in charset]
assert all(any(r[0]<=ord(char)<=r[-1] for r in ranges) for char in content), (ident, 'missing glyph')
texts=''.join(f'<text id="{ident}" x="{x}" y="{y}" font-family="{family}" font-weight="{weight}" font-size="{size}" text-anchor="middle" fill="#263f50">{escape(text)}</text>' for ident,text,x,y,size,family,weight in rows)
(P/'text-preview.svg').write_text(head+texts+'</svg>');render(P/'text-preview.svg',P/'text-preview.png')
text_image=Image.open(P/'text-preview.png').convert('RGBA');ta=np.asarray(text_image.getchannel('A'))
assert not np.any((ta>0)&(fa>0))
(P/'border-backing-preview.svg').write_text(head+defs+frame+panels+'</svg>');render(P/'border-backing-preview.svg',P/'border-backing-preview.png')
backing=np.asarray(Image.open(P/'border-backing-preview.png').convert('RGBA').getchannel('A'))
checks=[];verse_bounds=[]
for ident,text,x,y,size,family,weight in rows:
node=next(v for v in texts.split('</text>') if f'id="{ident}"' in v)+'</text>'
src=P/(ident+'-ink.svg');dst=P/(ident+'-ink.png');src.write_text(head+node+'</svg>');render(src,dst)
alpha=np.asarray(Image.open(dst).convert('RGBA').getchannel('A'));ys,xs=np.where(alpha>0);assert len(xs)
bounds=[int(xs.min()),int(ys.min()),int(xs.max()+1),int(ys.max()+1)]
safe=[1050,90,1905,330] if ident=='title' else [730,2275,1900,2730]
assert safe[0]<=bounds[0] and safe[1]<=bounds[1] and bounds[2]<=safe[2] and bounds[3]<=safe[3],(ident,bounds,safe)
assert np.all(backing[alpha>0]==255),ident
assert not np.any((alpha>0)&(fa>0)),ident
checks.append({'id':ident,'text':text,'baseline':y,'font':family,'weight':weight,'size':size,'inkBounds':bounds,'safeBounds':safe,'glyphPixelsOutsideOpaqueBacking':0,'foregroundIntersectionPixels':0})
if ident.startswith('verse-'):verse_bounds.append(bounds)
assert ' '.join(c['text'] for c in checks if c['id'].startswith('verse-'))==json.loads((REV/'card.json').read_text())['excerpt']
assert checks[-1]['text']==json.loads((REV/'card.json').read_text())['referenceDisplay']
union=[min(b[0] for b in verse_bounds),min(b[1] for b in verse_bounds),max(b[2] for b in verse_bounds),max(b[3] for b in verse_bounds)]
upper=2240;ref_top=checks[-1]['inkBounds'][1];above=union[1]-upper;below=ref_top-union[3]
assert min(above,below)>=35 and abs(above-below)<=20,(above,below)
for name,include_curtain in [('composed-preview',True),('without-curtain',False)]:
body=base+frame+panels+(curtain if include_curtain else '')+texts
src=P/(name+'.svg');src.write_text(head+defs+body+'</svg>');render(src,P/(name+'-2000.png'))
master=Image.open(P/(name+'-2000.png')).convert('RGB')
master.resize((1000,1400),Image.Resampling.LANCZOS).save(P/(name+'-1000.png'))
master.resize((500,700),Image.Resampling.LANCZOS).save(P/(name+'-500.png'))
# Compare the structural effect of the attached curtain.
board=Image.new('RGB',(1000,750),'#11161c');draw=ImageDraw.Draw(board)
for i,(label,path) in enumerate([('Panels before foreground',P/'without-curtain-1000.png'),('Pillar of cloud over panels',P/'composed-preview-1000.png')]):
draw.text((i*500+12,12),label,fill='#f4ead4')
image=Image.open(path).convert('RGB');image.thumbnail((480,672));board.paste(image,(i*500+10,45))
board.save(P/'curtain-comparison.png')
# A focused junction comparison at master crop.
detail=Image.new('RGB',(1200,620),'#11161c');draw=ImageDraw.Draw(detail)
for i,(label,path) in enumerate([('Without foreground',P/'without-curtain-2000.png'),('Cloud and curtain over backing',P/'composed-preview-2000.png')]):
draw.text((i*600+12,12),label,fill='#f4ead4')
image=Image.open(path).convert('RGB').crop((0,1780,900,2800));image.thumbnail((575,545));detail.paste(image,(i*600+12,52))
detail.save(P/'curtain-junction-detail.png')
frame_alpha=np.asarray(Image.open(P/'border-backing-preview.png').convert('RGBA').getchannel('A'))
foreground_frame_overlap=int(np.count_nonzero((fa>0)&(frame_alpha>0)))
report={
'status':'passed',
'scope':'v05 Normal-only prototype — pearl cloud and suspended tent textile',
'canvas':[2000,2800],
'sourceArtwork':{'path':str(ART.relative_to(REPO)),'sha256':art_hash,'unchanged':sha(ART)==art_hash},
'template':{
'governingIdea':'Nameplate unchanged; a suspended linen field tucks behind the pillar of cloud and ties to the right frame',
'titleBackingPath':title_path,
'verseBackingPath':verse_path,
'compositionOrder':['base-art','frame-and-backings','attached-curtain-foreground','deterministic-text'],
'curtainContour':'foreground-curtain.svg',
'cloudUpperContour':cloud_top,
'cloudLowerContour':cloud_low,
'curtainAlphaBounds':list(Image.fromarray(fa).getbbox()),
'curtainFrameOrBackingOverlapPixels':foreground_frame_overlap,
},
'typography':{
'fontManifestSHA256':sha(FONTS/'manifest.json'),
'checks':checks,
'verseInkUnion':union,
'upperFlourishToVerseInk':above,
'verseInkToReferenceLineBox':below,
'centeringDifference':abs(above-below),
'lineBaselineGap':108,
},
'structuralChecks':{
'curtainTextIntersectionPixels':int(np.count_nonzero((ta>0)&(fa>0))),
'glyphPixelsOutsideOpaqueBacking':0,
'artworkSHA256Unchanged':True,
'fontGlyphCoverage':'passed',
'fontManifestHashes':'passed',
},
'notPerformed':['Borderless/Textless/Boundless composition','Production finish masks','Production text masks','Harness fixture installation','GPU moving-light validation','Final card approval'],
'staticReview':'Provisional Normal-only prototype; static inspection at card size and enlarged junctions; pending user review',
'edgeRendering':{'geometry':'source-traced cubic Bezier paths','leadWidthMasterPixels':8,'renderScale':2,'downsample':'premultiplied RGBA LANCZOS','blur':False},
'foregroundAdaptation':'Liked nameplate geometry and suspended linen layout retained. New v05 pearl/cool cloud uses authored wave-like cubic contours; lower closure stays above source ground occlusion. The continuous entrance post is reused from the baked source and never redrawn.'
}
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n')
assert sha(ART)==art_hash
print(json.dumps({'status':'passed','preview':str((P/'composed-preview-1000.png').relative_to(REPO)),'curtainOverlapPixels':foreground_frame_overlap,'spacing':[above,below]},indent=2))
# Source/composed review and comparison to the carried-forward layout.
label_font=ImageFont.truetype(str(FONTS/'P052-Bold.otf'),26)
previous=REPO/'in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v5'
for name,items in [
('source-composed-comparison',[('Selected v05 source',ART),('v05 Normal prototype',P/'composed-preview-1000.png')]),
('layout-lineage-comparison',[('Prior v03 layout / prior art',previous/'composed-preview-1000.png'),('v05 layout / current art',P/'composed-preview-1000.png')]),
]:
board=Image.new('RGB',(1500,1100),'#11161c');d=ImageDraw.Draw(board)
for i,(label,path) in enumerate(items):
d.text((i*750+24,18),label,font=label_font,fill='#f4ead4')
im=Image.open(path).convert('RGB');im.thumbnail((730,1022));board.paste(im,(i*750+10,63))
board.save(P/(name+'.png'))
master=Image.open(P/'composed-preview-2000.png').convert('RGB')
detail=Image.new('RGB',(1500,1300),'#11161c');d=ImageDraw.Draw(detail)
for label,box,origin,size in [
('Pearl cloud / nameplate',(620,0,1140,480),(22,65),(700,550)),
('Cloud / textile / existing post',(295,2020,800,2660),(795,65),(660,690)),
]:
d.text((origin[0],20),label,font=label_font,fill='#f4ead4')
im=master.crop(box);im=im.resize((round(im.width*1.15),round(im.height*1.15)),Image.Resampling.LANCZOS);im.thumbnail(size);detail.paste(im,origin)
im=master.crop((290,2070,2000,2800));im.thumbnail((1450,520));detail.paste(im,(25,775))
detail.save(P/'cloud-textile-junction-details.png')
# Preserve approved typography and nameplate geometry without claiming the
# changed source/cloud pixels are identical to old artwork.
assert sha(ART)==json.loads((REV/'card.json').read_text())['artSHA256']
assert np.array_equal(np.asarray(Image.open(P/'text-preview.png')),np.asarray(Image.open(previous/'text-preview.png')))
report['structuralChecks']['textPixelsIdenticalToCarriedForwardLayout']=True
report['structuralChecks']['sourceMatchesV05CardSHA256']=True
report['structuralChecks']['secondPostDrawn']=False
report['template']['layoutReference']=str(previous.relative_to(REPO))
report['template']['postPolicy']='Use the continuous entrance post already baked into v05 artwork; no new post layer'
report['template']['leftTextileAnchor']=[340,2106]
report['edgeRendering']['alphaEdge']='Premultiplied before downsampling; no transparent-RGB halo'
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n')
# Targeted v1/v2 regression checks and comparison assets.
v1=P.parent/'normal-prototype-v1'
assert np.array_equal(np.asarray(Image.open(P/'text-preview.png')),np.asarray(Image.open(v1/'text-preview.png')))
assert np.array_equal(np.asarray(Image.open(P/'border-backing-preview.png')),np.asarray(Image.open(v1/'border-backing-preview.png')))
current=np.asarray(Image.open(P/'composed-preview-2000.png'))
prior=np.asarray(Image.open(v1/'composed-preview-2000.png'))
assert np.array_equal(current[:2300],prior[:2300])
hem_frame=(frame_alpha[2460:2630,:65]>0)
hem_foreground=fa[2460:2630,:65]
assert np.all(hem_foreground[hem_frame]==255)
report['structuralChecks']['textAndBackingPixelsIdenticalToV1']=True
report['structuralChecks']['upper2300RowsIdenticalToV1']=True
report['structuralChecks']['framePixelsExposedInsideCurtainHem']=int(np.count_nonzero(hem_frame & (hem_foreground<255)))
report['template']['v2Refinement']='Registered curtain hem occludes the frame through its actual lower edge; broad rounded cloud roll replaces the pointed tail, with continuous cubic tangents.'
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n')
for name,box,size,canvas in [
('v1-v2-full-comparison',(0,0,2000,2800),(700,980),(1460,1060)),
('v1-v2-junction-comparison',(0,2310,900,2780),(700,600),(1460,450)),
]:
board=Image.new('RGB',canvas,'#11161c');d=ImageDraw.Draw(board)
for col,(label,folder) in enumerate([('V1 — prior junction',v1),('V2 — hem and cloud roll',P)]):
d.text((col*730+20,18),label,font=label_font,fill='#f4ead4')
im=Image.open(folder/'composed-preview-2000.png').convert('RGB').crop(box);im.thumbnail(size);board.paste(im,(col*730+15,65))
board.save(P/(name+'.png'))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 KiB

Some files were not shown because too many files have changed in this diff Show More