#!/usr/bin/env python3 """T02 v2 process C: template text + real product photo. No network, no generative model. Layout as ../../control-v1/render.py: text band y<360 (Golos Text 70/52 px at x=55, step 100), product area 900x840 below. Photo is scaled uniformly (contain), never cropped, stretched or cut out. Never overwrites runs.""" import csv, hashlib, json, platform, time from datetime import datetime, timezone from pathlib import Path from PIL import Image, ImageDraw, ImageFont, ImageCms, __version__ HERE = Path(__file__).resolve().parent V2 = HERE.parent FONT = V2.parent / 'control-v1/assets/GolosText.ttf' ICC = ImageCms.ImageCmsProfile(ImageCms.createProfile('sRGB')).tobytes() BAND, W, H, PAD = 360, 900, 1200, 30 def sha(p): return hashlib.sha256(Path(p).read_bytes()).hexdigest() def stamp(): return datetime.now(timezone.utc).isoformat() def main(): dest = HERE / 'out'; dest.mkdir(exist_ok=False) items = json.loads((V2 / 'inputs-frozen.json').read_text())['inputs'] rows = [] for it in items: started = stamp(); tick = time.monotonic() src = V2 / it['file']; assert sha(src) == it['sha256'], src photo = Image.open(src).convert('RGB') im = Image.new('RGB', (W, H), 'white'); d = ImageDraw.Draw(im) boxes, sizes = [], [] for i, line in enumerate(it['passport_lines']): size = 70 if i == 0 else 52 while True: # shrink only if the line does not fit; logged in params f = ImageFont.truetype(str(FONT), size); box = d.textbbox((55, 65 + i * 100), line, font=f) if box[2] < 845: break size -= 2 assert box[3] < BAND, (line, box) d.text((55, 65 + i * 100), line, font=f, fill='#172438'); boxes.append(box); sizes.append(size) aw, ah = W - 2 * PAD, H - BAND - PAD k = min(aw / photo.width, ah / photo.height) pw, ph = round(photo.width * k), round(photo.height * k) x, y = (W - pw) // 2, BAND + (ah - ph) // 2 im.paste(photo.resize((pw, ph), Image.Resampling.LANCZOS), (x, y)) out = dest / (it['id'] + '-C.png'); im.save(out, icc_profile=ICC) prev = dest / (it['id'] + '-C-300.png'); im.resize((300, 400), Image.Resampling.LANCZOS).save(prev, icc_profile=ICC) params = dest / (it['id'] + '-C.json') params.write_text(json.dumps({'lines': it['passport_lines'], 'font_px': sizes, 'text_boxes': boxes, 'photo_box': [x, y, x + pw, y + ph], 'scale': round(k, 4), 'source': it['file'], 'source_sha256': it['sha256'], 'renderer_sha256': sha(__file__), 'font_sha256': sha(FONT), 'pillow': __version__, 'python': platform.python_version(), 'product_transform': 'uniform scale (LANCZOS), whole photo, no crop, no cutout, background of photo kept', 'output_sha256': sha(out), 'preview_sha256': sha(prev)}, ensure_ascii=False, indent=1) + '\n') rows.append({'run_id': it['id'] + '-C', 'phase': 'primary', 'process_id': 'C', 'sku_id': it['id'], 'repeat': 1, 'started_at': started, 'finished_at': stamp(), 'status': 'rendered', 'model_version': 'none; Pillow ' + __version__, 'input_sha256': it['sha256'], 'parameters_path': str(params.relative_to(V2)), 'request_id': 'local', 'output_path': str(out.relative_to(V2)), 'output_sha256': sha(out), 'wait_seconds': round(time.monotonic() - tick, 3), 'charged_credits': 0, 'charged_rub': 0, 'cost_status': 'local, no paid API; labour unmeasured', 'accepted': 'not-checked', 'notes': 'font_px ' + '/'.join(map(str, sizes))}) fields = next(csv.reader((V2 / 'runs.csv').open())) with (dest / 'runs-C.csv').open('w') as f: w = csv.DictWriter(f, fieldnames=fields); w.writeheader(); w.writerows(rows) print(json.dumps({'rendered': len(rows), 'shrunk': [r['sku_id'] for r in rows if r['notes'] != 'font_px 70/52/52']})) if __name__ == '__main__': main()