78 lines
5.3 KiB
Python
78 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Four controlled font proofs using retained SOL v2 assembly and validation."""
|
|
from pathlib import Path
|
|
import hashlib, json, subprocess, sys
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
OUT = ROOT/'output'
|
|
(OUT/'review').mkdir(parents=True, exist_ok=True)
|
|
choices = json.loads((ROOT/'font-choices.json').read_text())
|
|
def run(*args):
|
|
subprocess.run([str(x) for x in args], check=True)
|
|
def pixels(path):
|
|
return np.asarray(Image.open(path).convert('RGBA'))
|
|
def pixel_sha(path):
|
|
return hashlib.sha256(pixels(path).tobytes()).hexdigest()
|
|
reports = {}
|
|
for item in choices:
|
|
folder = ROOT/'variants'/item['slug']
|
|
if '--review-only' not in sys.argv:
|
|
run(sys.executable, folder/'build.py')
|
|
reports[item['slug']] = json.loads((folder/'output'/f"{item['prefix']}-validation.json").read_text())
|
|
|
|
# A controlled experiment: exactly the same source inputs and material layers.
|
|
preserved = {}
|
|
provenance = json.loads((ROOT/'source/provenance.json').read_text())
|
|
for relative in ['source/calling-disciples-SOL-art.png', 'source/calling-disciples-SOL-frame-runtime.png']:
|
|
hashes = [hashlib.sha256((ROOT/'variants'/i['slug']/relative).read_bytes()).hexdigest() for i in choices]
|
|
assert len(set(hashes)) == 1
|
|
preserved[relative] = {'byteIdenticalAcrossVariants': True, 'sha256': hashes[0]}
|
|
for suffix in ['art-master', 'frame-runtime', 'backing-normal', 'backing-borderless', 'normal-runtime-overlay']:
|
|
hashes = [pixel_sha(ROOT/'variants'/i['slug']/'output/layers'/f"{i['prefix']}-{suffix}.png") for i in choices]
|
|
assert len(set(hashes)) == 1, suffix
|
|
assert hashes[0] == provenance['preservedLayerDecodedRGBA_SHA256'][suffix], suffix
|
|
preserved[suffix] = {'pixelIdenticalAcrossVariants': True, 'pixelIdenticalToSOLv2': True, 'decodedRGBA_SHA256': hashes[0]}
|
|
for item in choices:
|
|
layout = json.loads((ROOT/'variants'/item['slug']/'layout.json').read_text())
|
|
assert layout['fontSizes'] == {'title':112, 'verse':80, 'reference':57}
|
|
assert layout['backingTreatment']['baseColor'] == '#f4ead4'
|
|
assert layout['backingTreatment']['palette']['type'] == '#263f50'
|
|
assert reports[item['slug']]['status'] == 'passed'
|
|
|
|
label_font = ROOT/'variants/noto-sans/fonts/NotoSans-Regular.ttf'
|
|
for size in [500,1000]:
|
|
tiles = []
|
|
for item in choices:
|
|
source = ROOT/'variants'/item['slug']/'output/cards'/f"{item['prefix']}-normal-{size}.png"
|
|
tile = OUT/'review'/f"{item['slug']}-{size}-tile.png"
|
|
label = 'Helvetica* / Nimbus Sans' if item['slug']=='helvetica' else item['renderedFamily']
|
|
run('magick',source,'-background','#ede9df','-gravity','north','-splice',f'0x{size//7}','-font',label_font,'-fill','#263f50','-pointsize',str(size//20),'-gravity','north','-annotate',f'+0+{size//35}',label,tile)
|
|
tiles.append(tile)
|
|
run('magick','montage',*tiles,'-tile','4x1','-geometry',f'{size}x{size*7//5+size//7}+18+18','-background','#ede9df',OUT/'review'/f'calling-disciples-SOL-font-comparison-{size}.png')
|
|
|
|
# Real 320px-wide digital view, separately labeled so it is clear what is shown.
|
|
tiles=[]
|
|
for item in choices:
|
|
source=ROOT/'variants'/item['slug']/'output/cards'/f"{item['prefix']}-normal-2000.png"
|
|
tile=OUT/'review'/f"{item['slug']}-320-tile.png"
|
|
label='Helvetica* / Nimbus Sans' if item['slug']=='helvetica' else item['renderedFamily']
|
|
run('magick',source,'-filter','Lanczos','-resize','320x448','-background','#ede9df','-gravity','north','-splice','0x48','-font',label_font,'-fill','#263f50','-pointsize','16','-gravity','north','-annotate','+0+12',label,tile)
|
|
tiles.append(tile)
|
|
run('magick','montage',*tiles,'-tile','4x1','-geometry','320x496+14+14','-background','#ede9df',OUT/'review/calling-disciples-SOL-font-comparison-320.png')
|
|
|
|
# Header and verse crops preserve native size, simplifying stroke/glyph inspection.
|
|
for section,geometry in [('headers','2000x300+0+60'),('verses','2000x560+0+2180')]:
|
|
tiles=[]
|
|
for item in choices:
|
|
source=ROOT/'variants'/item['slug']/'output/cards'/f"{item['prefix']}-normal-2000.png"
|
|
tile=OUT/'review'/f"{item['slug']}-{section}.png"
|
|
run('magick',source,'-crop',geometry,'+repage','-background','#ede9df','-gravity','north','-splice','0x70','-font',label_font,'-fill','#263f50','-pointsize','36','-annotate','+0+16',item['label'],tile)
|
|
tiles.append(tile)
|
|
run('magick','montage',*tiles,'-tile','1x4','-geometry','+10+10','-background','#ede9df',OUT/'review'/f'calling-disciples-SOL-font-comparison-{section}.png')
|
|
|
|
report = {'status':'passed','proof':'Controlled font comparison; only font family varies','fontChoices':choices,'palette':{'text':'#263f50','backingBase':'#f4ead4'},'preserved':preserved,'variantValidation':reports,'checks':{'sameFontSizesBaselinesAndLineBreaks':True,'samePaletteAndBackingAcrossVariants':True,'exactReference':'Matthew 4:19 • NKJV','noVisibleCategoryOrSubtitle':True},'limitations':['Helvetica is not installed; its proof explicitly uses pinned Nimbus Sans rather than genuine Helvetica glyphs.','Visual preference and readability require user review; automated glyph fit does not establish optical quality.','No human or moving-light harness approval is claimed.']}
|
|
(OUT/'validation.json').write_text(json.dumps(report,indent=2)+'\n')
|
|
print(f'Four-font comparison built and validated: {ROOT}')
|