Add Legendary Moses card and production guidance
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# BP-001 — Moses — v03
|
||||
|
||||
**The Threshold — Legendary intricacy.** Astra medium direction; built-in targeted image edit. Retains v02’s stylized figure and composition while adding curated glass detail to cloud, curtain, robes, tent and selected environment forms. Artwork selected for bespoke layout review.
|
||||
|
||||
- `review/art-review.png`: 1000×1400 review.
|
||||
- `review/art-thumbnail.png`: 250×350 review.
|
||||
- `review/illustration-review.md`: assessment and tradeoffs.
|
||||
- `source/art-native.png`: unchanged generated output.
|
||||
- `source/art-master.png`: fitted 2000×2800 illustration.
|
||||
- `source/edit-prompt.txt`: exact edit prompt.
|
||||
- `source/edit-reference.jpg`: actual displayed v02 edit input.
|
||||
- `source/generation.json`: source/edit provenance and hashes.
|
||||
- `source/art-fit.json`: uniform fit record.
|
||||
- `source/refinement.md`: scope and input fallback.
|
||||
|
||||
Run python3 source/fit-art.py from this revision to reproduce the fitted art and review sizes. Image generation is stochastic. Earlier revisions remain preserved.
|
||||
|
||||
## Provisional Legendary composition
|
||||
|
||||
[Threshold composition preview](review/layout-preview-v1/README.md) tests a bespoke gold/lapis frame, upper-right title plaque, lower textile backing, real P052 lettering, and an authored attached-curtain foreground. This is an explicitly requested layout preview only. No production printings, finish masks, text masks, or harness fixture were generated.
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"cardId": "BP-001",
|
||||
"title": "Moses",
|
||||
"name": "MOSES",
|
||||
"rarity": "Legendary",
|
||||
"folderName": "BP-001-moses",
|
||||
"artApproval": {
|
||||
"by": "user",
|
||||
"note": "I like the art a lot.",
|
||||
"scope": "BP-001 Moses v03 illustration; layout preview authorized separately."
|
||||
},
|
||||
"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": "v03 illustration selected for bespoke layout review",
|
||||
"layoutStatus": "Provisional Legendary compact textile composition under review/layout-preview-v2; production assembly not authorized.",
|
||||
"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": "Preserve v02 glass stylization and composition; add curated hierarchical glass intricacy for Legendary presence.",
|
||||
"art": "in-progress/cards/BP-001-moses/revisions/v03/source/art-master.png",
|
||||
"artSHA256": "73f933cf34719dfda09b719376948bcf072bec504a88329b5a22e4ac1ed76d85",
|
||||
"layoutCandidate": {
|
||||
"id": "BP-001-moses-threshold-v2",
|
||||
"preview": "review/layout-preview-v2/composed-preview-1000.png",
|
||||
"governingIdea": "Tent threshold with compact woven screen",
|
||||
"foreground": "Attached burgundy curtain crossing the frame and meeting the stepped left edge of the verse backing",
|
||||
"typography": {
|
||||
"titleSize": 138,
|
||||
"verseSize": 72,
|
||||
"verseBaselineGap": 104,
|
||||
"referenceSize": 57,
|
||||
"verseCenterGaps": [
|
||||
90,
|
||||
91
|
||||
]
|
||||
},
|
||||
"reviewState": "pending user review"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"cardId": "BP-001",
|
||||
"folderName": "BP-001-moses",
|
||||
"revision": "v03",
|
||||
"stage": "art-review",
|
||||
"approval": null,
|
||||
"content": "card.json",
|
||||
"files": {
|
||||
"README.md": {
|
||||
"sha256": "d8174612d3d3455ad9a5ff972cba19908df343bab5d9a15d25f641be1f8fbef2"
|
||||
},
|
||||
"card.json": {
|
||||
"sha256": "678d59f142b18b6a589b524968c65eb06525bd6137c8de720f9090730ef74317"
|
||||
},
|
||||
"source/art-fit.json": {
|
||||
"sha256": "f06f3666c0add4fd52084a0998d9e48146ec84e0f04e51dddcfc5ca2aa396388"
|
||||
},
|
||||
"source/art-master.png": {
|
||||
"sha256": "73f933cf34719dfda09b719376948bcf072bec504a88329b5a22e4ac1ed76d85"
|
||||
},
|
||||
"source/art-native.png": {
|
||||
"sha256": "ab234b91b260dba3b1a6973d765fc987e59339c21ec790d0ce51528e8fd98157"
|
||||
},
|
||||
"source/edit-prompt.txt": {
|
||||
"sha256": "0bd8e3c5c40d59dd6f1f9caf9094371d0c3f52748e48a9cbd924609da1205e5a"
|
||||
},
|
||||
"source/edit-reference.jpg": {
|
||||
"sha256": "0f6754052e7e739cf00a8f8360cb922112b2aa8501b8adfe981d9544e314108f"
|
||||
},
|
||||
"source/fit-art.py": {
|
||||
"sha256": "f9aea5aa6b192591db9db7c8d26c6d0a1ee272ab977ed3940891ca32c17acd07"
|
||||
},
|
||||
"source/generation.json": {
|
||||
"sha256": "bf4cd2254dc45d51c79a5d29abe40613a9a8530fbf17a02d60448cf64a5a0e36"
|
||||
},
|
||||
"source/refinement.md": {
|
||||
"sha256": "ea3a4f27292be136543286d4d67d7780ceeb4fb986992f428736de749de6ff4d"
|
||||
},
|
||||
"low/manifest.json": {
|
||||
"sha256": "c6b4ddddd22832e4ea6e2201c12a90cf4a7a1d25594b5878f12c23f22987261b"
|
||||
},
|
||||
"med/manifest.json": {
|
||||
"sha256": "e9cd197e91ccc2477ec18ccd62d903a29b166bc23b431dad8aa25ca60ef59ac6"
|
||||
},
|
||||
"high/manifest.json": {
|
||||
"sha256": "7ec683bc1aaf3a6c3d941a9011c97c5681bd1f18a21a1ea359745f092bcf2b6a"
|
||||
}
|
||||
},
|
||||
"compatibilityAliases": []
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"kind": "compact-card-revision",
|
||||
"cardId": "BP-001",
|
||||
"folderName": "BP-001-moses",
|
||||
"revision": "v03",
|
||||
"originalStage": "art-review",
|
||||
"approval": null,
|
||||
"reason": "superseded when v05 was accepted",
|
||||
"originalManifest": "full-manifest.json",
|
||||
"originalManifestSHA256": "b0b5443349cab2bdc9705aa388e45565ab60ed467d4c134f936b87bd274d0029",
|
||||
"referenceResolution": "low",
|
||||
"referenceImages": [
|
||||
"review/art-reference.png"
|
||||
],
|
||||
"retention": "Text provenance plus one 500 x 700 card face per available printing; full current acceptance lives in artifacts/cards."
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 877 KiB |
@@ -0,0 +1,24 @@
|
||||
# Moses v03 — Legendary intricacy review
|
||||
|
||||
**Art-review; approval pending.** Astra medium direction, built-in image edit of v02. Inspected the returned full native image and the saved 250×350 thumbnail. This pass answers the user’s request for greater Legendary intricacy while retaining v02’s reduction in realism.
|
||||
|
||||
## Assessment
|
||||
|
||||
The refinement succeeds at making selected parts more finely crafted without returning to realism or changing the governing composition. Moses’s head, lowered gaze, solemn expression, folded hands, silhouette and robe arrangement remain substantially intact. Tent/cloud/camp positions and the complete attached curtain remain stable.
|
||||
|
||||
- Cloud gains nested ivory/champagne curves and secondary panes; its large bright silhouette still reads from a distance. It remains non-anthropomorphic.
|
||||
- Curtain has finer tapering divisions that follow its folds, plus a narrow geometric glass border and complete patterned hem. Its visible attachment to the tent is maintained, retaining future foreground-overlap feasibility.
|
||||
- Sapphire robes gain selected finer blue/turquoise planes within major folds. Warm trim has small triangular/elongated glass rhythms, stronger near the cuffs and opening. Broad blue and burgundy masses remain dominant.
|
||||
- Tent lintel and supports gain small facets and lead joints. Sky light transition, mountain planes and some foreground ground paths gain secondary divisions while the camp remains simple and subordinate.
|
||||
- Broad amber facial planes, expressive ivory beard locks and simplified hands are preserved. No realistic hair strands, veins or wrinkles return. Both wrists and hands remain visibly connected; no obvious new finger/anatomy defect appears.
|
||||
- At thumbnail size, the bright cloud, dark tent opening and sapphire figure still establish the hierarchy. New detail reads mainly as richer material rather than a change in scene or action.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
This is visibly denser than v02, especially along the cloud column and lower robe. The primary/secondary lead hierarchy exists, but some new lines are still fairly strong; cloud curves and triangular trim repeat. Surface variation is also lively in sky and cloth. These are the areas to quiet if the user finds the new richness excessive. The gold trim is more noticeable, though confined to existing edges rather than spread across the scene. Face and hands remain comparatively simple, as requested.
|
||||
|
||||
This is an art candidate for user review, not artwork approval. No composition, text placement, mask, finish, printing or moving-light validation was performed.
|
||||
|
||||
## Provenance and retention
|
||||
|
||||
The edit used only the most recent visible 900×1260 JPEG reference of v02 (`source/edit-reference.jpg`). Native-path reads remain unavailable because of the sandbox helper initialization error. Prompt, exact displayed reference, generated native bytes and hashes are retained. Fit and review resizing are deterministic via `source/fit-art.py`. v01 and v02 are preserved.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Moses — threshold of the Presence
|
||||
|
||||
Provisional Legendary Normal composition by Astra Medium. This candidate makes the **pillar of cloud** the shared gesture between illustration and card: its crown passes in front of the name field, and its substantial lower lobes enter the verse field. The original attached curtain continues down the left edge. The panels sit behind those materials instead of advertising a specially cut panel corner.
|
||||
|
||||
- [Full preview, 1000 px](composed-preview-1000.png)
|
||||
- [Delivery preview, 500 px](composed-preview-500.png)
|
||||
- [V2 / Astra v3 comparison](v2-v3-comparison.png)
|
||||
- [Cloud interaction details](cloud-interaction-detail.png)
|
||||
- [Layer-order comparison](curtain-comparison.png)
|
||||
- [Editable composition](composed-preview.svg)
|
||||
- [Builder](build-preview.py)
|
||||
- [Validation](preview-validation.json)
|
||||
|
||||
## Design
|
||||
|
||||
The story here is intimate access to the divine Presence at the tent threshold. The cloud supplies the luminous ivory, champagne and opal silhouette; the tent supplies the burgundy and gold material. Both belong to Moses's existing scene. A restrained gold enclosure and warm ivory fields let those illustrated forms carry the identity. No David mantle or leaf motif is borrowed.
|
||||
|
||||
The verse is set in three substantial lines to the right of the cloud, preserving its exact wording without quotation marks. The text deliberately occupies the unobstructed part of the backing. The title remains MOSES; the reference remains Deuteronomy 34:10 • ESV. All lettering uses the repository P052 faces and is generated deterministically.
|
||||
|
||||
## Source and scope
|
||||
|
||||
The selected `../../source/art-master.png` remains unchanged, at its original 2000 × 2800 coordinates. Foreground SVG clips re-show its existing cloud and curtain above the panels. A short seven-pixel dark lead arc closes the lowest cloud lobe where the original scene's rocky ground interrupted its silhouette. This is an editable contour treatment, not regenerated source art.
|
||||
|
||||
A separate generated curtain adaptation was attempted with built-in imagegen, but generation failed before reading the image because the environment's filesystem sandbox helper reported `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`. No generated donor exists or is used. This candidate is the completed SVG-native cloud direction.
|
||||
|
||||
Run `python3 in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v3/build-preview.py` from the repository. Inkscape, Fontconfig, Pillow and NumPy are required. The builder checks font hashes and glyph coverage, exact content, measured glyph bounds, opaque backing coverage, zero foreground/text overlap, and preservation of the source hash. Verse spacing is 77 master pixels from its upper rule and 86 pixels to the reference ink.
|
||||
|
||||
Static card-size and enlarged-join inspection completed. This remains a review candidate: no production printing exports, finish masks, harness fixtures, promotion or final approval were produced. The existing v1/v2 review folders and production assets are untouched.
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/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):ink(src,'--export-type=png',f'--export-filename={dst}','--export-width=2000','--export-height=2800')
|
||||
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='M260 2092 H1895 Q1942 2092 1942 2139 V2712 Q1942 2758 1895 2758 H260Z'
|
||||
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="M280 2114H1893 Q1920 2114 1920 2141 V2708 Q1920 2736 1892 2736 H280" fill="none" stroke="#bb9040" stroke-width="3"/><path d="M827 2172H1815 M827 2700H1815" stroke="#c5a257" stroke-width="2"/>'
|
||||
def panel(path,inner,ident):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>'
|
||||
panels=panel(title_path,title_inner,'title-backing')+panel(verse_path,verse_inner,'verse-backing')
|
||||
# Trace the visible source silhouette, including the whole cloud lower lobe.
|
||||
# Upper crown is separately registered; both layers stay at source coordinates.
|
||||
cloud_top='M0 0H714 Q752 43 748 83 Q805 43 852 48 C926 52 977 107 964 179 C1008 212 1010 268 983 320 Q964 353 929 370 L730 475 H0Z'
|
||||
cloud_low='M0 400H352 V1780H765 L758 1985 Q750 2040 726 2108 L709 2220 L709 2314 Q695 2340 660 2362 Q651 2405 612 2434 Q579 2462 538 2473 Q507 2485 467 2488 Q401 2473 353 2410 L350 2633 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></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>'''
|
||||
|
||||
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'])+'<path d="M467 2488 Q401 2473 353 2410" fill="none" stroke="#2e2417" stroke-width="7" stroke-linecap="round"/></g>'
|
||||
(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,2305,74,'Sanctification P052',500),
|
||||
('verse-1','prophet since in Israel like Moses,',1320,2413,74,'Sanctification P052',500),
|
||||
('verse-2','whom the LORD knew face to face',1320,2521,74,'Sanctification P052',500),
|
||||
('reference','Deuteronomy 34:10 • ESV',1320,2650,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,2180,1900,2690]
|
||||
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=2174;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':'Provisional Legendary Normal — threshold of the Presence',
|
||||
'canvas':[2000,2800],
|
||||
'sourceArtwork':{'path':str(ART.relative_to(REPO)),'sha256':art_hash,'unchanged':sha(ART)==art_hash},
|
||||
'template':{
|
||||
'governingIdea':'The pillar of cloud enters the text field and crowns the name; the tent curtain remains attached',
|
||||
'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':'Inspected at card size and enlarged cloud joins; provisional, pending user review',
|
||||
'generationAttempt':{'tool':'built-in imagegen','status':'failed before generation','reason':'filesystem sandbox helper: bwrap loopback Failed RTM_NEWADDR: Operation not permitted','fallback':'Native SVG contours around existing source artwork; no generated donor'},
|
||||
'foregroundAdaptation':'Cloud extracted in original coordinates. A 7px dark SVG lead arc closes the lowest lobe across its original ground occlusion; source file unchanged.'
|
||||
}
|
||||
(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))
|
||||
|
||||
# Reproducible v2/v3 presentation and focused interaction review.
|
||||
label_font=ImageFont.truetype(str(FONTS/'P052-Bold.otf'),26)
|
||||
board=Image.new('RGB',(1500,1100),'#11161c');d=ImageDraw.Draw(board)
|
||||
for i,(label,path) in enumerate([('V2 — textile edge',P.parent/'layout-preview-v2/composed-preview-1000.png'),('Astra v3 — pillar of cloud',P/'composed-preview-1000.png')]):
|
||||
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/'v2-v3-comparison.png')
|
||||
master=Image.open(P/'composed-preview-2000.png').convert('RGB')
|
||||
detail=Image.new('RGB',(1500,1050),'#11161c');d=ImageDraw.Draw(detail)
|
||||
for label,box,origin,size in [('Cloud crowns the name',(570,0,1140,470),(22,65),(650,536)),('Pillar enters the verse field',(220,1960,850,2580),(795,65),(650,640))]:
|
||||
d.text((origin[0],20),label,font=label_font,fill='#f4ead4')
|
||||
im=master.crop(box);im.thumbnail(size);detail.paste(im,origin)
|
||||
im=master.crop((250,2080,1980,2780));im.thumbnail((1450,320));detail.paste(im,(25,722))
|
||||
detail.save(P/'cloud-interaction-detail.png')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<fontconfig><dir>/home/dkzver/docker/sanctification/fonts</dir><cachedir>/home/dkzver/docker/sanctification/in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v3/font-cache</cachedir></fontconfig>
|
||||
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"scope": "Provisional Legendary Normal \u2014 threshold of the Presence",
|
||||
"canvas": [
|
||||
2000,
|
||||
2800
|
||||
],
|
||||
"sourceArtwork": {
|
||||
"path": "in-progress/cards/BP-001-moses/revisions/v03/source/art-master.png",
|
||||
"sha256": "73f933cf34719dfda09b719376948bcf072bec504a88329b5a22e4ac1ed76d85",
|
||||
"unchanged": true
|
||||
},
|
||||
"template": {
|
||||
"governingIdea": "The pillar of cloud enters the text field and crowns the name; the tent curtain remains attached",
|
||||
"titleBackingPath": "M700 62 H1900 Q1940 62 1940 102 V342 H700Z",
|
||||
"verseBackingPath": "M260 2092 H1895 Q1942 2092 1942 2139 V2712 Q1942 2758 1895 2758 H260Z",
|
||||
"compositionOrder": [
|
||||
"base-art",
|
||||
"frame-and-backings",
|
||||
"attached-curtain-foreground",
|
||||
"deterministic-text"
|
||||
],
|
||||
"curtainContour": "foreground-curtain.svg",
|
||||
"cloudUpperContour": "M0 0H714 Q752 43 748 83 Q805 43 852 48 C926 52 977 107 964 179 C1008 212 1010 268 983 320 Q964 353 929 370 L730 475 H0Z",
|
||||
"cloudLowerContour": "M0 400H352 V1780H765 L758 1985 Q750 2040 726 2108 L709 2220 L709 2314 Q695 2340 660 2362 Q651 2405 612 2434 Q579 2462 538 2473 Q507 2485 467 2488 Q401 2473 353 2410 L350 2633 Q286 2659 205 2635 Q101 2583 0 2612Z",
|
||||
"curtainAlphaBounds": [
|
||||
0,
|
||||
0,
|
||||
1001,
|
||||
2647
|
||||
],
|
||||
"curtainFrameOrBackingOverlapPixels": 347899
|
||||
},
|
||||
"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": 2305,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 74,
|
||||
"inkBounds": [
|
||||
890,
|
||||
2251,
|
||||
1749,
|
||||
2307
|
||||
],
|
||||
"safeBounds": [
|
||||
730,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-1",
|
||||
"text": "prophet since in Israel like Moses,",
|
||||
"baseline": 2413,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 74,
|
||||
"inkBounds": [
|
||||
769,
|
||||
2359,
|
||||
1869,
|
||||
2435
|
||||
],
|
||||
"safeBounds": [
|
||||
730,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-2",
|
||||
"text": "whom the LORD knew face to face",
|
||||
"baseline": 2521,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 74,
|
||||
"inkBounds": [
|
||||
752,
|
||||
2466,
|
||||
1886,
|
||||
2523
|
||||
],
|
||||
"safeBounds": [
|
||||
730,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "reference",
|
||||
"text": "Deuteronomy 34:10 \u2022 ESV",
|
||||
"baseline": 2650,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 57,
|
||||
"inkBounds": [
|
||||
991,
|
||||
2609,
|
||||
1649,
|
||||
2667
|
||||
],
|
||||
"safeBounds": [
|
||||
730,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
}
|
||||
],
|
||||
"verseInkUnion": [
|
||||
752,
|
||||
2251,
|
||||
1886,
|
||||
2523
|
||||
],
|
||||
"upperFlourishToVerseInk": 77,
|
||||
"verseInkToReferenceLineBox": 86,
|
||||
"centeringDifference": 9,
|
||||
"lineBaselineGap": 108
|
||||
},
|
||||
"structuralChecks": {
|
||||
"curtainTextIntersectionPixels": 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": "Inspected at card size and enlarged cloud joins; provisional, pending user review",
|
||||
"generationAttempt": {
|
||||
"tool": "built-in imagegen",
|
||||
"status": "failed before generation",
|
||||
"reason": "filesystem sandbox helper: bwrap loopback Failed RTM_NEWADDR: Operation not permitted",
|
||||
"fallback": "Native SVG contours around existing source artwork; no generated donor"
|
||||
},
|
||||
"foregroundAdaptation": "Cloud extracted in original coordinates. A 7px dark SVG lead arc closes the lowest lobe across its original ground occlusion; source file unchanged."
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Moses — refined cloud foreground
|
||||
|
||||
This provisional Normal candidate retains the v3 composition and refines the cloud where it crosses the name and verse backings.
|
||||
|
||||
- [Full preview](composed-preview-1000.png)
|
||||
- [500 px preview](composed-preview-500.png)
|
||||
- [V3 / V4 card comparison](v3-v4-comparison.png)
|
||||
- [V3 / V4 enlarged junction comparison](v3-v4-junction-comparison.png)
|
||||
- [V4 interaction details](cloud-interaction-detail.png)
|
||||
- [Editable foreground](foreground-curtain.svg)
|
||||
- [Reproducible builder](build-preview.py)
|
||||
- [Validation](preview-validation.json)
|
||||
|
||||
The prior contour passed through existing lead seams and retained small sky/terrain slivers, producing a clipped edge. V4 traces the cloud with fewer continuous cubic Bézier segments. A consistent eight-master-pixel dark lead perimeter integrates the exposed edges with the stained-glass construction. At the lower end, an authored rounded closure replaces the broken boundary inherited from the source's rocky ground. This foreground adaptation is deliberate; it does not need to retain that original occlusion.
|
||||
|
||||
The cloud interior stays registered to the original illustration. Geometry-heavy outputs render at 4000 × 5600, then downsample to the 2000 × 2800 master in premultiplied RGBA using Lanczos. The glass structure receives no blur. The full source art remains unchanged.
|
||||
|
||||
Exact title, verse and reference are retained. Font hashes, glyph coverage, measured safe bounds, zero foreground/text intersection, and opaque backing under all lettering are checked. The text and frame/backing raster components must match v3 pixel for pixel. Static inspection covers full card and enlarged upper/lower joins.
|
||||
|
||||
Rebuild from the repository with:
|
||||
|
||||
`python3 in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v4/build-preview.py`
|
||||
|
||||
Requires Inkscape, Fontconfig, Pillow and NumPy. This is a provisional review composition only. V3 is preserved. No production layers, printing exports, finish masks, harness fixture, GPU review or promotion are included.
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/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='M260 2092 H1895 Q1942 2092 1942 2139 V2712 Q1942 2758 1895 2758 H260Z'
|
||||
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="M280 2114H1893 Q1920 2114 1920 2141 V2708 Q1920 2736 1892 2736 H280" fill="none" stroke="#bb9040" stroke-width="3"/><path d="M827 2172H1815 M827 2700H1815" stroke="#c5a257" stroke-width="2"/>'
|
||||
def panel(path,inner,ident):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>'
|
||||
panels=panel(title_path,title_inner,'title-backing')+panel(verse_path,verse_inner,'verse-backing')
|
||||
# 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='M714 0 C741 25 750 55 746 82 C787 58 814 43 846 45 C926 50 980 112 952 191 C1008 238 1002 319 925 369'
|
||||
lower_edge='M736 2080 C728 2148 722 2213 710 2262 C704 2290 684 2310 660 2323 C650 2348 629 2367 608 2374 C599 2432 541 2477 480 2479 C435 2481 375 2418 354 2350'
|
||||
cloud_top=top_edge+' L730 475 H0 V0Z'
|
||||
cloud_low='M0 400H352 V1780H765 C765 1920 748 2010 736 2080 '+lower_edge.removeprefix('M736 2080 ')+' L350 2633 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></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}"/><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>'
|
||||
(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,2305,74,'Sanctification P052',500),
|
||||
('verse-1','prophet since in Israel like Moses,',1320,2413,74,'Sanctification P052',500),
|
||||
('verse-2','whom the LORD knew face to face',1320,2521,74,'Sanctification P052',500),
|
||||
('reference','Deuteronomy 34:10 • ESV',1320,2650,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,2180,1900,2690]
|
||||
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=2174;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':'Provisional Legendary Normal — threshold of the Presence',
|
||||
'canvas':[2000,2800],
|
||||
'sourceArtwork':{'path':str(ART.relative_to(REPO)),'sha256':art_hash,'unchanged':sha(ART)==art_hash},
|
||||
'template':{
|
||||
'governingIdea':'The pillar of cloud enters the text field and crowns the name; the tent curtain remains attached',
|
||||
'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':'Inspected at card size and enlarged cloud joins; provisional, pending user review',
|
||||
'edgeRendering':{'geometry':'source-traced cubic Bezier paths','leadWidthMasterPixels':8,'renderScale':2,'downsample':'premultiplied RGBA LANCZOS','blur':False},
|
||||
'foregroundAdaptation':'Source-registered cloud with retraced cubic perimeter and 8px dark lead; lower lobe closes before rocky-ground interruption. Source artwork, panel geometry and text unchanged.'
|
||||
}
|
||||
(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))
|
||||
|
||||
# Reproducible v3/v4 presentation and focused interaction review.
|
||||
label_font=ImageFont.truetype(str(FONTS/'P052-Bold.otf'),26)
|
||||
board=Image.new('RGB',(1500,1100),'#11161c');d=ImageDraw.Draw(board)
|
||||
for i,(label,path) in enumerate([('Astra v3 — initial contours',P.parent/'layout-preview-astra-v3/composed-preview-1000.png'),('Astra v4 — refined contours',P/'composed-preview-1000.png')]):
|
||||
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/'v3-v4-comparison.png')
|
||||
master=Image.open(P/'composed-preview-2000.png').convert('RGB')
|
||||
detail=Image.new('RGB',(1500,1050),'#11161c');d=ImageDraw.Draw(detail)
|
||||
for label,box,origin,size in [('Cloud crowns the name',(570,0,1140,470),(22,65),(650,536)),('Pillar enters the verse field',(220,1960,850,2580),(795,65),(650,640))]:
|
||||
d.text((origin[0],20),label,font=label_font,fill='#f4ead4')
|
||||
im=master.crop(box);im.thumbnail(size);detail.paste(im,origin)
|
||||
im=master.crop((250,2080,1980,2780));im.thumbnail((1450,320));detail.paste(im,(25,722))
|
||||
detail.save(P/'cloud-interaction-detail.png')
|
||||
|
||||
# Verify the refinement has no layout/type drift from v3.
|
||||
previous=P.parent/'layout-preview-astra-v3'
|
||||
for filename in ['text-preview.png','border-backing-preview.png']:
|
||||
assert np.array_equal(np.asarray(Image.open(P/filename)),np.asarray(Image.open(previous/filename))), filename
|
||||
report['structuralChecks']['textPixelsIdenticalToV3']=True
|
||||
report['structuralChecks']['frameAndBackingPixelsIdenticalToV3']=True
|
||||
report['edgeRendering']['alphaEdge']='Premultiplied before downsampling; no transparent-RGB halo'
|
||||
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n')
|
||||
# Matched enlarged crops make contour changes directly reviewable.
|
||||
board=Image.new('RGB',(1480,1430),'#11161c');d=ImageDraw.Draw(board)
|
||||
for col,(label,folder) in enumerate([('V3 — initial contour',previous),('V4 — refined lead contour',P)]):
|
||||
d.text((col*740+22,18),label,font=label_font,fill='#f4ead4')
|
||||
im=Image.open(folder/'composed-preview-2000.png').convert('RGB')
|
||||
for box,y,size in [((680,0,1030,400),65,(690,600)),((320,2070,765,2520),725,(690,670))]:
|
||||
crop=im.crop(box);crop.thumbnail(size);crop=crop.resize((round(crop.width*1.45),round(crop.height*1.45)),Image.Resampling.LANCZOS);crop.thumbnail(size);board.paste(crop,(col*740+22,y))
|
||||
board.save(P/'v3-v4-junction-comparison.png')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<fontconfig><dir>/home/dkzver/docker/sanctification/fonts</dir><cachedir>/home/dkzver/docker/sanctification/in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v4/font-cache</cachedir></fontconfig>
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"scope": "Provisional Legendary Normal \u2014 threshold of the Presence",
|
||||
"canvas": [
|
||||
2000,
|
||||
2800
|
||||
],
|
||||
"sourceArtwork": {
|
||||
"path": "in-progress/cards/BP-001-moses/revisions/v03/source/art-master.png",
|
||||
"sha256": "73f933cf34719dfda09b719376948bcf072bec504a88329b5a22e4ac1ed76d85",
|
||||
"unchanged": true
|
||||
},
|
||||
"template": {
|
||||
"governingIdea": "The pillar of cloud enters the text field and crowns the name; the tent curtain remains attached",
|
||||
"titleBackingPath": "M700 62 H1900 Q1940 62 1940 102 V342 H700Z",
|
||||
"verseBackingPath": "M260 2092 H1895 Q1942 2092 1942 2139 V2712 Q1942 2758 1895 2758 H260Z",
|
||||
"compositionOrder": [
|
||||
"base-art",
|
||||
"frame-and-backings",
|
||||
"attached-curtain-foreground",
|
||||
"deterministic-text"
|
||||
],
|
||||
"curtainContour": "foreground-curtain.svg",
|
||||
"cloudUpperContour": "M714 0 C741 25 750 55 746 82 C787 58 814 43 846 45 C926 50 980 112 952 191 C1008 238 1002 319 925 369 L730 475 H0 V0Z",
|
||||
"cloudLowerContour": "M0 400H352 V1780H765 C765 1920 748 2010 736 2080 C728 2148 722 2213 710 2262 C704 2290 684 2310 660 2323 C650 2348 629 2367 608 2374 C599 2432 541 2477 480 2479 C435 2481 375 2418 354 2350 L350 2633 Q286 2659 205 2635 Q101 2583 0 2612Z",
|
||||
"curtainAlphaBounds": [
|
||||
0,
|
||||
0,
|
||||
996,
|
||||
2649
|
||||
],
|
||||
"curtainFrameOrBackingOverlapPixels": 342547
|
||||
},
|
||||
"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": 2305,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 74,
|
||||
"inkBounds": [
|
||||
890,
|
||||
2251,
|
||||
1749,
|
||||
2307
|
||||
],
|
||||
"safeBounds": [
|
||||
730,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-1",
|
||||
"text": "prophet since in Israel like Moses,",
|
||||
"baseline": 2413,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 74,
|
||||
"inkBounds": [
|
||||
769,
|
||||
2359,
|
||||
1869,
|
||||
2435
|
||||
],
|
||||
"safeBounds": [
|
||||
730,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-2",
|
||||
"text": "whom the LORD knew face to face",
|
||||
"baseline": 2521,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 74,
|
||||
"inkBounds": [
|
||||
752,
|
||||
2466,
|
||||
1886,
|
||||
2523
|
||||
],
|
||||
"safeBounds": [
|
||||
730,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "reference",
|
||||
"text": "Deuteronomy 34:10 \u2022 ESV",
|
||||
"baseline": 2650,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 57,
|
||||
"inkBounds": [
|
||||
991,
|
||||
2609,
|
||||
1649,
|
||||
2667
|
||||
],
|
||||
"safeBounds": [
|
||||
730,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
}
|
||||
],
|
||||
"verseInkUnion": [
|
||||
752,
|
||||
2251,
|
||||
1886,
|
||||
2523
|
||||
],
|
||||
"upperFlourishToVerseInk": 77,
|
||||
"verseInkToReferenceLineBox": 86,
|
||||
"centeringDifference": 9,
|
||||
"lineBaselineGap": 108
|
||||
},
|
||||
"structuralChecks": {
|
||||
"curtainTextIntersectionPixels": 0,
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"artworkSHA256Unchanged": true,
|
||||
"fontGlyphCoverage": "passed",
|
||||
"fontManifestHashes": "passed",
|
||||
"textPixelsIdenticalToV3": true,
|
||||
"frameAndBackingPixelsIdenticalToV3": true
|
||||
},
|
||||
"notPerformed": [
|
||||
"Borderless/Textless/Boundless composition",
|
||||
"Production finish masks",
|
||||
"Production text masks",
|
||||
"Harness fixture installation",
|
||||
"GPU moving-light validation",
|
||||
"Final card approval"
|
||||
],
|
||||
"staticReview": "Inspected at card size and enlarged cloud joins; provisional, 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": "Source-registered cloud with retraced cubic perimeter and 8px dark lead; lower lobe closes before rocky-ground interruption. Source artwork, panel geometry and text unchanged."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# Moses — suspended tent textile and coherent entrance
|
||||
|
||||
This provisional Normal candidate preserves the v4 nameplate and cloud crown. Only the lower composition and near entrance structure are revised.
|
||||
|
||||
- [Full preview](composed-preview-1000.png)
|
||||
- [500 px preview](composed-preview-500.png)
|
||||
- [V4 / V5 full comparison](v4-v5-comparison.png)
|
||||
- [V4 / V5 lower junction comparison](v4-v5-junction-comparison.png)
|
||||
- [Editable composition](composed-preview.svg)
|
||||
- [Builder](build-preview.py)
|
||||
- [Validation](preview-validation.json)
|
||||
|
||||
## Lower textile
|
||||
|
||||
The rigid plaque becomes a suspended warm linen field. Its shallow sag reveals more of Moses's robe; its left edge curves inward behind the cloud instead of running to the curtain pole as a wide blank rectangle. A fine woven hem replaces the metal plaque rules. The right corners bind to the existing frame. Its upper-left corner extends behind the cloud to a binding on the near entrance post, so the field belongs to the tent structure.
|
||||
|
||||
The v4 cloud contour is retained, but the narrower backing changes the relationship: the cloud occupies and follows the textile edge rather than dangling over an expansive blank field. The text retains the exact words, font faces, sizes and horizontal alignment; verse baselines move down 55 master pixels and the reference moves down 40 pixels. Measured spacing is 66 pixels above the verse and 71 pixels between verse and reference ink.
|
||||
|
||||
## Near entrance post
|
||||
|
||||
A separate authored foreground jamb follows the existing curtain-side socket and vertical. Its dark outline, warm gold face and segmented joints match the structural language of the far post. It stays visible in front of the cloud and continues to the ground. Its slight lean and stronger near-side width make the entrance read as a pair of supports. A small binding ties the textile's concealed left corner to this post.
|
||||
|
||||
This is an SVG-native foreground adaptation; `../../source/art-master.png` is unchanged. No donor image or source repaint is used. The approved nameplate/crown region and title glyph pixels must remain identical to v4, verified programmatically.
|
||||
|
||||
## Verification and scope
|
||||
|
||||
The builder verifies the source hash, exact text, shared font hashes and glyph coverage, safe glyph bounds, opaque backing beneath lettering, zero foreground/text intersection, and preservation of the nameplate/crown. Compositing remains at 2× resolution with premultiplied-alpha Lanczos downsampling. No glass-blurring filter is used. Static review includes the full card and matched lower-junction crops.
|
||||
|
||||
Rebuild from the repository:
|
||||
|
||||
`python3 in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v5/build-preview.py`
|
||||
|
||||
Requires Inkscape, Fontconfig, Pillow and NumPy. Earlier candidates remain intact. This is a review candidate only: no production layers, printing exports, finish masks, harness fixture, GPU review or promotion.
|
||||
@@ -0,0 +1,242 @@
|
||||
#!/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='M714 0 C741 25 750 55 746 82 C787 58 814 43 846 45 C926 50 980 112 952 191 C1008 238 1002 319 925 369'
|
||||
lower_edge='M736 2080 C728 2148 722 2213 710 2262 C704 2290 684 2310 660 2323 C650 2348 629 2367 608 2374 C599 2432 541 2477 480 2479 C435 2481 375 2418 354 2350'
|
||||
cloud_top=top_edge+' L730 475 H0 V0Z'
|
||||
cloud_low='M0 400H352 V1780H765 C765 1920 748 2010 736 2080 '+lower_edge.removeprefix('M736 2080 ')+' L350 2633 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></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}"/><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>'
|
||||
# Near entrance jamb: aligned to the source curtain-side upper socket.
|
||||
# The cloud sits behind this continuous dark/gold glass structure.
|
||||
defs=defs.replace('</defs>','<linearGradient id="post-gold" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#654016"/><stop offset=".25" stop-color="#b78829"/><stop offset=".52" stop-color="#f5dc75"/><stop offset=".72" stop-color="#d8ae45"/><stop offset="1" stop-color="#80601c"/></linearGradient></defs>')
|
||||
post_path='M326 250 L360 250 L350 2640 L316 2637Z'
|
||||
post=f'<g id="near-entrance-post"><path d="{post_path}" fill="url(#post-gold)" stroke="#241c12" stroke-width="7" stroke-linejoin="round"/>'
|
||||
# Uneven joints follow the counterpart post's segmented construction.
|
||||
for y in [310,493,720,944,1186,1432,1669,1915,2167,2406,2590]:
|
||||
x=326-(y-250)*10/2390
|
||||
post+=f'<path d="M{x} {y}l34 1v15l-34-1Z" fill="#372916" stroke="#17170f" stroke-width="3"/><path d="M{x+7} {y+22}l0 101" fill="none" stroke="#f4d373" stroke-width="3" opacity=".5"/>'
|
||||
post+='<path d="M326 254Q342 242 359 254 M319 2625Q333 2633 350 2629" fill="none" stroke="#edcf70" stroke-width="4"/>'
|
||||
# A small woven binding holds the concealed left textile corner to the jamb.
|
||||
post+='<path d="M317 2108 C324 2101 345 2101 357 2110 L355 2124 C341 2114 326 2115 317 2121Z" fill="#b88b3f" stroke="#2b2216" stroke-width="4"/><path d="M319 2113 C333 2109 344 2112 355 2117" fill="none" stroke="#f2d484" stroke-width="3"/></g>'
|
||||
curtain+=post
|
||||
(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':'Provisional Legendary Normal — 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':'Inspected at card size and enlarged cloud joins; provisional, pending user review',
|
||||
'edgeRendering':{'geometry':'source-traced cubic Bezier paths','leadWidthMasterPixels':8,'renderScale':2,'downsample':'premultiplied RGBA LANCZOS','blur':False},
|
||||
'foregroundAdaptation':'V4 cloud and nameplate preserved. Lower linen field now curves behind the cloud along its side; shallow sag exposes more robe. Lower text moves down with exact words, faces, sizes and horizontal alignment retained. A continuous near entrance post is authored over the cloud, with the textile left corner bound to it behind the cloud.'
|
||||
}
|
||||
(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))
|
||||
|
||||
# Reproducible v3/v4 presentation and focused interaction review.
|
||||
label_font=ImageFont.truetype(str(FONTS/'P052-Bold.otf'),26)
|
||||
board=Image.new('RGB',(1500,1100),'#11161c');d=ImageDraw.Draw(board)
|
||||
for i,(label,path) in enumerate([('Astra v4 — rigid lower plaque',P.parent/'layout-preview-astra-v4/composed-preview-1000.png'),('Astra v5 — suspended textile',P/'composed-preview-1000.png')]):
|
||||
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/'v4-v5-comparison.png')
|
||||
master=Image.open(P/'composed-preview-2000.png').convert('RGB')
|
||||
detail=Image.new('RGB',(1500,1050),'#11161c');d=ImageDraw.Draw(detail)
|
||||
for label,box,origin,size in [('Cloud crowns the name',(570,0,1140,470),(22,65),(650,536)),('Pillar enters the verse field',(220,1960,850,2580),(795,65),(650,640))]:
|
||||
d.text((origin[0],20),label,font=label_font,fill='#f4ead4')
|
||||
im=master.crop(box);im.thumbnail(size);detail.paste(im,origin)
|
||||
im=master.crop((250,2080,1980,2780));im.thumbnail((1450,320));detail.paste(im,(25,722))
|
||||
detail.save(P/'cloud-interaction-detail.png')
|
||||
|
||||
# The approved nameplate and cloud crown remain pixel-identical.
|
||||
previous=P.parent/'layout-preview-astra-v4'
|
||||
current_image=np.asarray(Image.open(P/'composed-preview-2000.png'))
|
||||
previous_image=np.asarray(Image.open(previous/'composed-preview-2000.png'))
|
||||
assert np.array_equal(current_image[:200],previous_image[:200])
|
||||
assert np.array_equal(current_image[:500,600:],previous_image[:500,600:])
|
||||
assert np.array_equal(np.asarray(Image.open(P/'title-ink.png')),np.asarray(Image.open(previous/'title-ink.png')))
|
||||
report['structuralChecks']['titleAndCloudCrownRegionIdenticalToV4']=True
|
||||
report['structuralChecks']['titlePixelsIdenticalToV4']=True
|
||||
report['structuralChecks']['nearEntrancePostAuthored']=True
|
||||
report['template']['nearEntrancePostPath']=post_path
|
||||
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')
|
||||
# Matched lower card crops show the textile silhouette and interaction.
|
||||
board=Image.new('RGB',(1480,930),'#11161c');d=ImageDraw.Draw(board)
|
||||
for col,(label,folder) in enumerate([('V4 — rigid plaque',previous),('V5 — suspended textile',P)]):
|
||||
d.text((col*740+22,18),label,font=label_font,fill='#f4ead4')
|
||||
im=Image.open(folder/'composed-preview-2000.png').convert('RGB')
|
||||
crop=im.crop((190,1940,2000,2800));crop.thumbnail((710,850));board.paste(crop,(col*740+15,75))
|
||||
detail=im.crop((340,2050,830,2790));detail.thumbnail((380,485));board.paste(detail,(col*740+185,425))
|
||||
board.save(P/'v4-v5-junction-comparison.png')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<fontconfig><dir>/home/dkzver/docker/sanctification/fonts</dir><cachedir>/home/dkzver/docker/sanctification/in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v5/font-cache</cachedir></fontconfig>
|
||||
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"scope": "Provisional Legendary Normal \u2014 suspended tent textile",
|
||||
"canvas": [
|
||||
2000,
|
||||
2800
|
||||
],
|
||||
"sourceArtwork": {
|
||||
"path": "in-progress/cards/BP-001-moses/revisions/v03/source/art-master.png",
|
||||
"sha256": "73f933cf34719dfda09b719376948bcf072bec504a88329b5a22e4ac1ed76d85",
|
||||
"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": "M714 0 C741 25 750 55 746 82 C787 58 814 43 846 45 C926 50 980 112 952 191 C1008 238 1002 319 925 369 L730 475 H0 V0Z",
|
||||
"cloudLowerContour": "M0 400H352 V1780H765 C765 1920 748 2010 736 2080 C728 2148 722 2213 710 2262 C704 2290 684 2310 660 2323 C650 2348 629 2367 608 2374 C599 2432 541 2477 480 2479 C435 2481 375 2418 354 2350 L350 2633 Q286 2659 205 2635 Q101 2583 0 2612Z",
|
||||
"curtainAlphaBounds": [
|
||||
0,
|
||||
0,
|
||||
996,
|
||||
2714
|
||||
],
|
||||
"curtainFrameOrBackingOverlapPixels": 189298,
|
||||
"nearEntrancePostPath": "M326 250 L360 250 L350 2640 L316 2637Z",
|
||||
"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",
|
||||
"titleAndCloudCrownRegionIdenticalToV4": true,
|
||||
"titlePixelsIdenticalToV4": true,
|
||||
"nearEntrancePostAuthored": true
|
||||
},
|
||||
"notPerformed": [
|
||||
"Borderless/Textless/Boundless composition",
|
||||
"Production finish masks",
|
||||
"Production text masks",
|
||||
"Harness fixture installation",
|
||||
"GPU moving-light validation",
|
||||
"Final card approval"
|
||||
],
|
||||
"staticReview": "Inspected at card size and enlarged cloud joins; provisional, 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": "V4 cloud and nameplate preserved. Lower linen field now curves behind the cloud along its side; shallow sag exposes more robe. Lower text moves down with exact words, faces, sizes and horizontal alignment retained. A continuous near entrance post is authored over the cloud, with the textile left corner bound to it behind the cloud."
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Moses — provisional Legendary threshold composition
|
||||
|
||||
This requested preview tests the selected v03 illustration with real P052 typography and a bespoke threshold-driven Normal layout. It is a composition candidate, not production assembly.
|
||||
|
||||
- [Composed preview](composed-preview-1000.png)
|
||||
- [500 × 700 preview](composed-preview-500.png)
|
||||
- [Curtain comparison](curtain-comparison.png)
|
||||
- [Enlarged curtain/backing junction](curtain-junction-detail.png)
|
||||
- [Authored curtain foreground](foreground-curtain.svg)
|
||||
- [Frame and backing source](border-backing-preview.svg)
|
||||
- [Deterministic text source](text-preview.svg)
|
||||
- [Validation](preview-validation.json)
|
||||
- [Reproducible builder](build-preview.py)
|
||||
|
||||
## Governing idea
|
||||
|
||||
The outer frame uses narrow antique gold, lapis, ivory and tent-derived geometric bands. The upper-right plaque occupies open sky without covering Moses. The lower ivory textile backing begins below his folded hands and reserves a wide central-right field for the four-line verse.
|
||||
|
||||
The complete attached burgundy curtain is selected from the original illustration with an authored full-canvas contour. It composes above the frame and backing, crossing the left perimeter and far-left backing corner while remaining visibly attached to the tent. It does not intersect the lettering. Moses stays behind the backing, avoiding a second character-mantle treatment like David.
|
||||
|
||||
The narrative verse is presented without quotation marks:
|
||||
|
||||
And there has not arisen<br>
|
||||
a prophet since in Israel<br>
|
||||
like Moses, whom the LORD<br>
|
||||
knew face to face
|
||||
|
||||
Deuteronomy 34:10 • ESV
|
||||
|
||||
P052 Bold 700 renders MOSES at 138px. Sanctification P052 Medium 500 renders the verse at 80px with 94px baselines and the reference at 57px. The measured verse ink has 54 master pixels above and 53 below within its reserved band.
|
||||
|
||||
## Scope
|
||||
|
||||
Only the provisional Normal composition is rendered. The proposed printing behavior remains a hypothesis until this layout is accepted: Normal would use frame, backings, curtain, and text; Borderless would use backings, curtain, and text; Textless would use frame and curtain; Boundless would use the unmodified illustration.
|
||||
|
||||
No production printings, finish masks, text masks, harness fixtures, moving-light review, or final approval were created.
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/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):ink(src,'--export-type=png',f'--export-filename={dst}','--export-width=2000','--export-height=2800')
|
||||
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>'''
|
||||
title_path='M1110 66 H1888 L1938 116 V320 L1895 364 H1110 L1060 314 V116 Z'
|
||||
verse_path='M205 2075 H1892 Q1942 2075 1942 2125 V2715 Q1942 2765 1892 2765 H205 Q164 2765 164 2724 V2116 Q164 2075 205 2075 Z'
|
||||
title_inner='<path d="M1125 84 H1878 L1918 124 V310 L1884 344 H1124 L1080 302 V126 Z" fill="none" stroke="#9b732b" stroke-width="3"/><path d="M1326 312H1438 M1562 312H1674" stroke="#98702a" stroke-width="4"/><path d="M1500 299l13 13-13 13-13-13Z" fill="url(#gold)" stroke="#654516" stroke-width="2"/>'
|
||||
verse_inner='<path d="M222 2095 H1882 Q1922 2095 1922 2135 V2705 Q1922 2745 1882 2745 H220 Q184 2745 184 2709 V2131 Q184 2095 222 2095Z" fill="none" stroke="#9b732b" stroke-width="3"/><path d="M1138 2150H1268 M1372 2150H1502" stroke="#98702a" stroke-width="4"/><path d="M1320 2137l13 13-13 13-13-13Z" fill="url(#gold)" stroke="#654516" stroke-width="2"/><path d="M1148 2705H1273 M1367 2705H1492" stroke="#98702a" stroke-width="4"/><path d="M1320 2692l13 13-13 13-13-13Z" fill="url(#gold)" stroke="#654516" stroke-width="2"/><path d="M1880 2180v120 M1880 2538v120" stroke="#173e59" stroke-width="7"/><path d="M1863 2180h34v120h-34z M1863 2538h34v120h-34z" fill="url(#lapis-band)" stroke="#8c6827" stroke-width="3"/>'
|
||||
def panel(path,inner,ident):return f'<g id="{ident}"><path d="{path}" fill="url(#paper)" stroke="#5f431d" stroke-width="10"/><path d="{path}" fill="url(#weave)" stroke="url(#gold)" stroke-width="5"/>{inner}</g>'
|
||||
panels=panel(title_path,title_inner,'title-backing')+panel(verse_path,verse_inner,'verse-backing')
|
||||
|
||||
curtain=f'<g id="curtain-foreground" clip-path="url(#curtain-clip)"><image xlink:href="../../source/art-master.png" width="2000" height="2800"/></g>'
|
||||
(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',1500,244,138,'P052',700),
|
||||
('verse-0','And there has not arisen',1320,2276,80,'Sanctification P052',500),
|
||||
('verse-1','a prophet since in Israel',1320,2370,80,'Sanctification P052',500),
|
||||
('verse-2','like Moses, whom the LORD',1320,2464,80,'Sanctification P052',500),
|
||||
('verse-3','knew face to face',1320,2558,80,'Sanctification P052',500),
|
||||
('reference','Deuteronomy 34:10 • ESV',1320,2655,57,'Sanctification P052',500),
|
||||
]
|
||||
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=[1100,90,1905,340] if ident=='title' else [570,2165,1900,2690]
|
||||
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=2163;ref_top=2613;above=union[1]-upper;below=ref_top-union[3]
|
||||
assert min(above,below)>=35 and abs(above-below)<=4,(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([('Backing and frame alone',P/'without-curtain-1000.png'),('Threshold curtain overlap',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'),('Curtain over frame/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':'Provisional Legendary Normal composition only',
|
||||
'canvas':[2000,2800],
|
||||
'sourceArtwork':{'path':str(ART.relative_to(REPO)),'sha256':art_hash,'unchanged':sha(ART)==art_hash},
|
||||
'template':{
|
||||
'governingIdea':'Tent threshold',
|
||||
'titleBackingPath':title_path,
|
||||
'verseBackingPath':verse_path,
|
||||
'compositionOrder':['base-art','frame-and-backings','attached-curtain-foreground','deterministic-text'],
|
||||
'curtainContour':'foreground-curtain.svg',
|
||||
'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':94,
|
||||
},
|
||||
'structuralChecks':{
|
||||
'curtainTextIntersectionPixels':int(np.count_nonzero((ta>0)&(fa>0))),
|
||||
'glyphPixelsOutsideOpaqueBacking':0,
|
||||
'artworkSHA256Unchanged':True,
|
||||
},
|
||||
'notPerformed':['Borderless/Textless/Boundless composition','Production finish masks','Production text masks','Harness fixture installation','GPU moving-light validation','Final card approval'],
|
||||
'staticReview':'pending user review'
|
||||
}
|
||||
(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))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<fontconfig><dir>/home/dkzver/docker/sanctification/fonts</dir><cachedir>/home/dkzver/docker/sanctification/in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-v1/font-cache</cachedir></fontconfig>
|
||||
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"scope": "Provisional Legendary Normal composition only",
|
||||
"canvas": [
|
||||
2000,
|
||||
2800
|
||||
],
|
||||
"sourceArtwork": {
|
||||
"path": "in-progress/cards/BP-001-moses/revisions/v03/source/art-master.png",
|
||||
"sha256": "73f933cf34719dfda09b719376948bcf072bec504a88329b5a22e4ac1ed76d85",
|
||||
"unchanged": true
|
||||
},
|
||||
"template": {
|
||||
"governingIdea": "Tent threshold",
|
||||
"titleBackingPath": "M1110 66 H1888 L1938 116 V320 L1895 364 H1110 L1060 314 V116 Z",
|
||||
"verseBackingPath": "M205 2075 H1892 Q1942 2075 1942 2125 V2715 Q1942 2765 1892 2765 H205 Q164 2765 164 2724 V2116 Q164 2075 205 2075 Z",
|
||||
"compositionOrder": [
|
||||
"base-art",
|
||||
"frame-and-backings",
|
||||
"attached-curtain-foreground",
|
||||
"deterministic-text"
|
||||
],
|
||||
"curtainContour": "foreground-curtain.svg",
|
||||
"curtainAlphaBounds": [
|
||||
0,
|
||||
0,
|
||||
769,
|
||||
2800
|
||||
],
|
||||
"curtainFrameOrBackingOverlapPixels": 283408
|
||||
},
|
||||
"typography": {
|
||||
"fontManifestSHA256": "ef68ee6393981d54ca068a950f21c53b374d20e6e46c21c788f36380d3aae178",
|
||||
"checks": [
|
||||
{
|
||||
"id": "title",
|
||||
"text": "MOSES",
|
||||
"baseline": 244,
|
||||
"font": "P052",
|
||||
"weight": 700,
|
||||
"size": 138,
|
||||
"inkBounds": [
|
||||
1251,
|
||||
148,
|
||||
1746,
|
||||
247
|
||||
],
|
||||
"safeBounds": [
|
||||
1100,
|
||||
90,
|
||||
1905,
|
||||
340
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-0",
|
||||
"text": "And there has not arisen",
|
||||
"baseline": 2276,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 80,
|
||||
"inkBounds": [
|
||||
886,
|
||||
2217,
|
||||
1755,
|
||||
2278
|
||||
],
|
||||
"safeBounds": [
|
||||
570,
|
||||
2165,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-1",
|
||||
"text": "a prophet since in Israel",
|
||||
"baseline": 2370,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 80,
|
||||
"inkBounds": [
|
||||
902,
|
||||
2311,
|
||||
1739,
|
||||
2393
|
||||
],
|
||||
"safeBounds": [
|
||||
570,
|
||||
2165,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-2",
|
||||
"text": "like Moses, whom the LORD",
|
||||
"baseline": 2464,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 80,
|
||||
"inkBounds": [
|
||||
813,
|
||||
2405,
|
||||
1827,
|
||||
2477
|
||||
],
|
||||
"safeBounds": [
|
||||
570,
|
||||
2165,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-3",
|
||||
"text": "knew face to face",
|
||||
"baseline": 2558,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 80,
|
||||
"inkBounds": [
|
||||
1019,
|
||||
2499,
|
||||
1620,
|
||||
2560
|
||||
],
|
||||
"safeBounds": [
|
||||
570,
|
||||
2165,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "reference",
|
||||
"text": "Deuteronomy 34:10 \u2022 ESV",
|
||||
"baseline": 2655,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 57,
|
||||
"inkBounds": [
|
||||
991,
|
||||
2614,
|
||||
1649,
|
||||
2672
|
||||
],
|
||||
"safeBounds": [
|
||||
570,
|
||||
2165,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
}
|
||||
],
|
||||
"verseInkUnion": [
|
||||
813,
|
||||
2217,
|
||||
1827,
|
||||
2560
|
||||
],
|
||||
"upperFlourishToVerseInk": 54,
|
||||
"verseInkToReferenceLineBox": 53,
|
||||
"centeringDifference": 1,
|
||||
"lineBaselineGap": 94
|
||||
},
|
||||
"structuralChecks": {
|
||||
"curtainTextIntersectionPixels": 0,
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"artworkSHA256Unchanged": true
|
||||
},
|
||||
"notPerformed": [
|
||||
"Borderless/Textless/Boundless composition",
|
||||
"Production finish masks",
|
||||
"Production text masks",
|
||||
"Harness fixture installation",
|
||||
"GPU moving-light validation",
|
||||
"Final card approval"
|
||||
],
|
||||
"staticReview": "pending user review"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Moses — provisional Legendary compact textile composition
|
||||
|
||||
This preview revises the selected v03 illustration with a narrower, tent-derived backing and a three-line verse setting. It is a composition candidate, not production assembly.
|
||||
|
||||
- [Composed preview](composed-preview-1000.png)
|
||||
- [500 × 700 preview](composed-preview-500.png)
|
||||
- [v1 / v2 backing comparison](backing-comparison.png)
|
||||
- [Curtain comparison](curtain-comparison.png)
|
||||
- [Enlarged curtain/backing junction](curtain-junction-detail.png)
|
||||
- [Authored curtain foreground](foreground-curtain.svg)
|
||||
- [Frame and backing source](border-backing-preview.svg)
|
||||
- [Deterministic text source](text-preview.svg)
|
||||
- [Validation](preview-validation.json)
|
||||
- [Reproducible builder](build-preview.py)
|
||||
|
||||
## Governing idea
|
||||
|
||||
The outer frame uses narrow antique gold, lapis, ivory and tent-derived geometric bands. The upper-right plaque now reads as a sloped canopy rather than a chamfered royal plaque. The lower ivory backing enters beneath the curtain, then steps inward to form a compact woven screen. Burgundy, lapis, and restrained gold bands replace the centered lozenge flourish language used on David.
|
||||
|
||||
The attached burgundy curtain remains above the frame and backing. The backing no longer spans the unused lower-left bay: its visible edge follows the threshold transition, while the three-line verse expands across the usable textile field. Moses stays behind the backing.
|
||||
|
||||
The narrative verse is presented without quotation marks:
|
||||
|
||||
And there has not arisen a prophet since<br>
|
||||
in Israel like Moses, whom the LORD<br>
|
||||
knew face to face
|
||||
|
||||
Deuteronomy 34:10 • ESV
|
||||
|
||||
P052 Bold 700 renders MOSES at 138px. Sanctification P052 Medium 500 renders the verse at 72px with 104px baselines and the reference at 57px. Validation measures the actual verse ink between the upper textile band and the top of the reference.
|
||||
|
||||
## Scope
|
||||
|
||||
Only the provisional Normal composition is rendered. The proposed printing behavior remains a hypothesis until this layout is accepted: Normal would use frame, backings, curtain, and text; Borderless would use backings, curtain, and text; Textless would use frame and curtain; Boundless would use the unmodified illustration.
|
||||
|
||||
No production printings, finish masks, text masks, harness fixtures, moving-light review, or final approval were created.
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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):ink(src,'--export-type=png',f'--export-filename={dst}','--export-width=2000','--export-height=2800')
|
||||
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>'''
|
||||
title_path='M1080 72 H1888 L1940 124 V342 H1045 V148 Z'
|
||||
verse_path='M445 2088 H1896 Q1942 2088 1942 2134 V2719 Q1942 2765 1896 2765 H495 Q460 2765 460 2730 V2260 L405 2190 V2128 Q405 2088 445 2088 Z'
|
||||
title_inner='<path d="M1092 92 H1878 L1918 132 V320 H1067 V156 Z" fill="none" stroke="#9b732b" stroke-width="3"/><path d="M1110 299H1880" stroke="#7f2934" stroke-width="4"/><path d="M1110 309H1880" stroke="#173e59" stroke-width="5"/><path d="M1120 317H1870" stroke="#b98a32" stroke-width="2"/><g fill="#b98a32"><path d="M1190 297l9 9-9 9-9-9Z"/><path d="M1500 297l9 9-9 9-9-9Z"/><path d="M1810 297l9 9-9 9-9-9Z"/></g>'
|
||||
verse_inner='<path d="M458 2108 H1882 Q1922 2108 1922 2148 V2705 Q1922 2745 1882 2745 H510 Q480 2745 480 2715 V2252 L425 2182 V2141 Q425 2108 458 2108Z" fill="none" stroke="#9b732b" stroke-width="3"/><path d="M650 2144H1840" stroke="#7f2934" stroke-width="4"/><path d="M650 2154H1840" stroke="#173e59" stroke-width="5"/><path d="M650 2162H1840" stroke="#b98a32" stroke-width="2"/><path d="M650 2691H1840" stroke="#7f2934" stroke-width="4"/><path d="M650 2701H1840" stroke="#173e59" stroke-width="5"/><path d="M650 2709H1840" stroke="#b98a32" stroke-width="2"/><g fill="#b98a32"><path d="M760 2142l10 10-10 10-10-10Z"/><path d="M1245 2142l10 10-10 10-10-10Z"/><path d="M1730 2142l10 10-10 10-10-10Z"/><path d="M760 2689l10 10-10 10-10-10Z"/><path d="M1245 2689l10 10-10 10-10-10Z"/><path d="M1730 2689l10 10-10 10-10-10Z"/></g>'
|
||||
def panel(path,inner,ident):return f'<g id="{ident}"><path d="{path}" fill="url(#paper)" stroke="#5f431d" stroke-width="10"/><path d="{path}" fill="url(#weave)" stroke="url(#gold)" stroke-width="5"/>{inner}</g>'
|
||||
panels=panel(title_path,title_inner,'title-backing')+panel(verse_path,verse_inner,'verse-backing')
|
||||
|
||||
curtain=f'<g id="curtain-foreground" clip-path="url(#curtain-clip)"><image xlink:href="../../source/art-master.png" width="2000" height="2800"/></g>'
|
||||
(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',1500,244,138,'P052',700),
|
||||
('verse-0','And there has not arisen a prophet since',1190,2308,72,'Sanctification P052',500),
|
||||
('verse-1','in Israel like Moses, whom the LORD',1190,2412,72,'Sanctification P052',500),
|
||||
('verse-2','knew face to face',1190,2516,72,'Sanctification P052',500),
|
||||
('reference','Deuteronomy 34:10 • ESV',1190,2650,57,'Sanctification P052',500),
|
||||
]
|
||||
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=[1080,90,1905,330] if ident=='title' else [500,2180,1900,2690]
|
||||
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=2165;ref_top=checks[-1]['inkBounds'][1];above=union[1]-upper;below=ref_top-union[3]
|
||||
assert min(above,below)>=35 and abs(above-below)<=4,(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([('Backing and frame alone',P/'without-curtain-1000.png'),('Threshold curtain overlap',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'),('Curtain over frame/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':'Provisional Legendary Normal compact textile composition only',
|
||||
'canvas':[2000,2800],
|
||||
'sourceArtwork':{'path':str(ART.relative_to(REPO)),'sha256':art_hash,'unchanged':sha(ART)==art_hash},
|
||||
'template':{
|
||||
'governingIdea':'Tent threshold with compact woven screen',
|
||||
'titleBackingPath':title_path,
|
||||
'verseBackingPath':verse_path,
|
||||
'compositionOrder':['base-art','frame-and-backings','attached-curtain-foreground','deterministic-text'],
|
||||
'curtainContour':'foreground-curtain.svg',
|
||||
'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':104,
|
||||
},
|
||||
'structuralChecks':{
|
||||
'curtainTextIntersectionPixels':int(np.count_nonzero((ta>0)&(fa>0))),
|
||||
'glyphPixelsOutsideOpaqueBacking':0,
|
||||
'artworkSHA256Unchanged':True,
|
||||
},
|
||||
'notPerformed':['Borderless/Textless/Boundless composition','Production finish masks','Production text masks','Harness fixture installation','GPU moving-light validation','Final card approval'],
|
||||
'staticReview':'pending user review'
|
||||
}
|
||||
(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))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<fontconfig><dir>/home/dkzver/docker/sanctification/fonts</dir><cachedir>/home/dkzver/docker/sanctification/in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-v2/font-cache</cachedir></fontconfig>
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"scope": "Provisional Legendary Normal compact textile composition only",
|
||||
"canvas": [
|
||||
2000,
|
||||
2800
|
||||
],
|
||||
"sourceArtwork": {
|
||||
"path": "in-progress/cards/BP-001-moses/revisions/v03/source/art-master.png",
|
||||
"sha256": "73f933cf34719dfda09b719376948bcf072bec504a88329b5a22e4ac1ed76d85",
|
||||
"unchanged": true
|
||||
},
|
||||
"template": {
|
||||
"governingIdea": "Tent threshold with compact woven screen",
|
||||
"titleBackingPath": "M1080 72 H1888 L1940 124 V342 H1045 V148 Z",
|
||||
"verseBackingPath": "M445 2088 H1896 Q1942 2088 1942 2134 V2719 Q1942 2765 1896 2765 H495 Q460 2765 460 2730 V2260 L405 2190 V2128 Q405 2088 445 2088 Z",
|
||||
"compositionOrder": [
|
||||
"base-art",
|
||||
"frame-and-backings",
|
||||
"attached-curtain-foreground",
|
||||
"deterministic-text"
|
||||
],
|
||||
"curtainContour": "foreground-curtain.svg",
|
||||
"curtainAlphaBounds": [
|
||||
0,
|
||||
0,
|
||||
769,
|
||||
2800
|
||||
],
|
||||
"curtainFrameOrBackingOverlapPixels": 181809
|
||||
},
|
||||
"typography": {
|
||||
"fontManifestSHA256": "ef68ee6393981d54ca068a950f21c53b374d20e6e46c21c788f36380d3aae178",
|
||||
"checks": [
|
||||
{
|
||||
"id": "title",
|
||||
"text": "MOSES",
|
||||
"baseline": 244,
|
||||
"font": "P052",
|
||||
"weight": 700,
|
||||
"size": 138,
|
||||
"inkBounds": [
|
||||
1251,
|
||||
148,
|
||||
1746,
|
||||
247
|
||||
],
|
||||
"safeBounds": [
|
||||
1080,
|
||||
90,
|
||||
1905,
|
||||
330
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-0",
|
||||
"text": "And there has not arisen a prophet since",
|
||||
"baseline": 2308,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 72,
|
||||
"inkBounds": [
|
||||
548,
|
||||
2255,
|
||||
1831,
|
||||
2329
|
||||
],
|
||||
"safeBounds": [
|
||||
500,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-1",
|
||||
"text": "in Israel like Moses, whom the LORD",
|
||||
"baseline": 2412,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 72,
|
||||
"inkBounds": [
|
||||
598,
|
||||
2359,
|
||||
1782,
|
||||
2424
|
||||
],
|
||||
"safeBounds": [
|
||||
500,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "verse-2",
|
||||
"text": "knew face to face",
|
||||
"baseline": 2516,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 72,
|
||||
"inkBounds": [
|
||||
919,
|
||||
2463,
|
||||
1460,
|
||||
2518
|
||||
],
|
||||
"safeBounds": [
|
||||
500,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
},
|
||||
{
|
||||
"id": "reference",
|
||||
"text": "Deuteronomy 34:10 \u2022 ESV",
|
||||
"baseline": 2650,
|
||||
"font": "Sanctification P052",
|
||||
"weight": 500,
|
||||
"size": 57,
|
||||
"inkBounds": [
|
||||
861,
|
||||
2609,
|
||||
1519,
|
||||
2667
|
||||
],
|
||||
"safeBounds": [
|
||||
500,
|
||||
2180,
|
||||
1900,
|
||||
2690
|
||||
],
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"foregroundIntersectionPixels": 0
|
||||
}
|
||||
],
|
||||
"verseInkUnion": [
|
||||
548,
|
||||
2255,
|
||||
1831,
|
||||
2518
|
||||
],
|
||||
"upperFlourishToVerseInk": 90,
|
||||
"verseInkToReferenceLineBox": 91,
|
||||
"centeringDifference": 1,
|
||||
"lineBaselineGap": 104
|
||||
},
|
||||
"structuralChecks": {
|
||||
"curtainTextIntersectionPixels": 0,
|
||||
"glyphPixelsOutsideOpaqueBacking": 0,
|
||||
"artworkSHA256Unchanged": true
|
||||
},
|
||||
"notPerformed": [
|
||||
"Borderless/Textless/Boundless composition",
|
||||
"Production finish masks",
|
||||
"Production text masks",
|
||||
"Harness fixture installation",
|
||||
"GPU moving-light validation",
|
||||
"Final card approval"
|
||||
],
|
||||
"staticReview": "pending user review"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"source": "art-native.png",
|
||||
"sourceDimensions": [
|
||||
1060,
|
||||
1484
|
||||
],
|
||||
"canvas": [
|
||||
2000,
|
||||
2800
|
||||
],
|
||||
"method": "uniform centered cover fit",
|
||||
"scale": 1.8867924528301887,
|
||||
"scaledDimensions": [
|
||||
2000.0,
|
||||
2800.0
|
||||
],
|
||||
"cropOffset": [
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"resampling": "Pillow LANCZOS",
|
||||
"sourceSHA256": "ab234b91b260dba3b1a6973d765fc987e59339c21ec790d0ce51528e8fd98157",
|
||||
"outputSHA256": "73f933cf34719dfda09b719376948bcf072bec504a88329b5a22e4ac1ed76d85"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
Use case: style-transfer
|
||||
Asset type: targeted v03 refinement of the supplied Moses stained-glass illustration for a Legendary Sanctification card.
|
||||
Primary request: Keep this exact composition and its decisive non-realistic glass language, but make the craftsmanship richer and more intricate in carefully chosen regions. Simple and iconic at thumbnail size, finely worked and jewel-like up close. This is an EDIT of this image, not a new composition.
|
||||
|
||||
Strict invariants: preserve Moses's position, silhouette, size, left-facing lowered gaze, solemn expression, broad stylized amber facial planes, expressive ivory beard locks, calm posture and exact folded-hand gesture/anatomy/ownership. Preserve face and hands as broad legible planes, never realistic wrinkles, veins, pores, individual hair strands, or smooth anatomical gradients. Keep tent, attached hanging burgundy curtain and its full hem, cloud, camp, mountain horizon, robe colors and folds, and overall framing in the same positions. Curtain must stay visibly connected to its tent attachment for a future overlap. Keep non-anthropomorphic cloud and warm cloud-light/cool landscape relationship. Keep full-bleed 5:7.
|
||||
|
||||
Refinement: Introduce a clear hierarchy of primary structural leading and thinner secondary leading. Existing large pane silhouettes remain the composition's foundation. Selectively subdivide some of them into beautifully designed curving, tapered and angular smaller glass pieces which follow the forms. Do not simply add a uniform crack network.
|
||||
1. Cloud at tent entrance and upper left: the main broad lobed forms stay readable, with interlocking smaller ivory, champagne, pale amber and cool opal panes nested selectively along bright transitions and curled edges. Some large quiet glowing pieces remain. This should feel skillfully assembled from precious glass, not cottony realism, glitter or repeated tiny tiles.
|
||||
2. Attached burgundy curtain: preserve its long substantial drape and dark fold valleys. Add fine flowing secondary pane divisions following chosen folds, and restrained inset glass patterning along a narrow edge/hem band. Patterns belong to the cloth's construction; no new symbols, words or ornate fantasy brocade. Keep broad wine-colored rests between detailed passages.
|
||||
3. Moses's robe: preserve the broad sapphire and burgundy masses. Refine select major fold ridges into nested elongated jewel panes and subtly contrasting blue/turquoise sub-panes. Add a restrained small geometric glass rhythm to the existing warm trim near sleeves and robe opening, sparse and subordinate. Face, beard and hands remain simpler than the cloth, retaining their present stylization.
|
||||
4. Tent structure: finer lead joints and small warm glass facets around the existing supports and upper angled cloth edge, without changing architecture or adding decoration that competes with the figure.
|
||||
5. Sky and mountains: selected thin layered panes at the gold-to-blue light transition and a few thoughtfully split violet mountain planes, while most sky remains broad and restful. No extra rays, halos or sun symbols.
|
||||
6. Camp and terrain: keep simplified tent silhouettes and grouped vegetation. Add modest secondary pane rhythms to a few tent folds and stepping ground planes near the foreground, not realistic tiny life or extra objects. Keep distant camp quieter than Moses.
|
||||
|
||||
Material: jewel-colored worked glass, delicate pane-contained brushed/etched satin texture, richer selective bounded color transitions and purposeful form-following highlights. Occasional fine painted-glass accents within panes, not photorealistic shading. Craft detail rather than all-over grain. Gold is restrained warm glass trim, not metallic decoration everywhere.
|
||||
Avoid: same shard size/density throughout, uniform tessellation, micro-mosaic, random cracks, omnipresent speckles or sparkles, noisy ornament, gaudy gold, realistic face/hands/landscape, clutter, altered pose, altered hand anatomy, new props or narrative events. No text, border, card frame, backing panels, watermark or literal divine figure.
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reproduce only the fitted art and review sizes; never assemble a card."""
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageOps
|
||||
import hashlib,json
|
||||
root=Path(__file__).resolve().parent.parent
|
||||
native=root/'source/art-native.png'
|
||||
with Image.open(native) as source:
|
||||
im=source.convert('RGB');w,h=im.size
|
||||
scale=max(2000/w,2800/h)
|
||||
art=ImageOps.fit(im,(2000,2800),Image.Resampling.LANCZOS,centering=(.5,.5))
|
||||
art.save(root/'source/art-master.png')
|
||||
for name,size in [('art-review.png',(1000,1400)),('art-thumbnail.png',(250,350))]:
|
||||
art.resize(size,Image.Resampling.LANCZOS).save(root/'review'/name)
|
||||
fit={'source':'art-native.png','sourceDimensions':[w,h],'canvas':[2000,2800],'method':'uniform centered cover fit','scale':scale,'scaledDimensions':[w*scale,h*scale],'cropOffset':[(w*scale-2000)/2,(h*scale-2800)/2],'resampling':'Pillow LANCZOS','sourceSHA256':hashlib.sha256(native.read_bytes()).hexdigest(),'outputSHA256':hashlib.sha256((root/'source/art-master.png').read_bytes()).hexdigest()}
|
||||
(root/'source/art-fit.json').write_text(json.dumps(fit,indent=2)+'\n')
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mode": "built-in imagegen edit",
|
||||
"artDirectionModel": "gpt-6-astra",
|
||||
"artDirectionReasoning": "medium",
|
||||
"prompt": "edit-prompt.txt",
|
||||
"native": "art-native.png",
|
||||
"originalGeneratedPath": "/home/dkzver/.codex/generated_images/01a0acd6-0b11-7932-a7f3-c69f609e9974/exec-1638b62b-56e6-4a1a-8198-8420dfb9f761.png",
|
||||
"reference": "edit-reference.jpg",
|
||||
"referenceSHA256": "0f6754052e7e739cf00a8f8360cb922112b2aa8501b8adfe981d9544e314108f",
|
||||
"referenceProvenance": "900x1260 JPEG quality 90 made from v02 source/art-native.png and shown in conversation; native-path tool reads unavailable due to sandbox initialization failure, so num_last_images_to_include=1 was used.",
|
||||
"priorNative": "../../v02/source/art-native.png",
|
||||
"priorNativeSHA256": "145d11849d97e9007fb4066b167b1bfa53e608aeb025ef8783ab1a79fc59f71d",
|
||||
"fit": "art-fit.json",
|
||||
"generationReproducibility": "Stochastic targeted edit; exact prompt, actual displayed reference and native output retained; fit and review sizes deterministic.",
|
||||
"approval": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Moses v03 — curated Legendary intricacy
|
||||
|
||||
The user requested greater intricacy for Legendary presentation after the v02 realism reduction. Retain the exact governing composition and broad stylized face, hands and beard. Add form-following secondary leading and finer nested glass shapes selectively in the cloud, attached curtain, robe folds/trim, tent joints, sky transition and some terrain rhythms. Retain large quiet regions and camp subordination.
|
||||
|
||||
Astra medium authored the edit. Native-path image input is unavailable due to sandbox-helper initialization failures, so the successful conversation-image path uses a retained 900×1260 JPEG quality 90 made from v02 native output, shown immediately before the edit call. No other image was included. Earlier revisions remain intact. No assembly assets are generated.
|
||||
Reference in New Issue
Block a user