← back to Wallco Ai
scripts/bake-og-cw.py
116 lines
#!/usr/bin/env python3
"""bake-og-cw.py — bake a colorway-recolored og:image JPG for
/design/:id?cw=<N>.
Reads the source design PNG, applies the colorway's ink_map (nearest-color
substitution in RGB euclidean), resizes to 1200x630 (Open Graph default
aspect, ~50KB target), encodes JPEG q=85, writes to the path the server
passes us.
CLI (called via spawnSync from server.js /design/:id route):
bake-og-cw.py <src_png> <out_jpg> <inkmap_json>
inkmap_json is the same shape stored in wallco_user_colorways.ink_map:
[{"from": "#rrggbb", "to": "#rrggbb"}, ...]
Exit 0 on success, non-zero on any failure (server falls back to parent
og:image on non-zero). Prints brief status to stderr.
Pattern mirrors scripts/fix-seam.py (PIL-only, no extra deps). On Mac2 +
Kamatera prod both have Pillow installed system-wide. ~150ms cold-start +
~200ms recolor for a 1024x1024 input; cached to disk so only on first hit.
"""
import sys, os, json, time
def hex_to_rgb(h):
h = h.strip().lstrip('#')
if len(h) != 6:
raise ValueError(f"bad hex: {h!r}")
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
def main():
if len(sys.argv) != 4:
print("usage: bake-og-cw.py <src_png> <out_jpg> <inkmap_json>", file=sys.stderr)
return 2
src, out, ink_json = sys.argv[1], sys.argv[2], sys.argv[3]
if not os.path.exists(src):
print(f"src missing: {src}", file=sys.stderr)
return 3
try:
ink_map = json.loads(ink_json)
pairs = []
for m in ink_map:
f = hex_to_rgb(m['from']); t = hex_to_rgb(m['to'])
pairs.append((f, t))
if not pairs:
print("ink_map empty", file=sys.stderr)
return 4
except Exception as e:
print(f"ink_map parse fail: {e}", file=sys.stderr)
return 5
try:
from PIL import Image
except ImportError:
print("Pillow not installed (pip3 install Pillow)", file=sys.stderr)
return 6
t0 = time.time()
try:
im = Image.open(src).convert('RGB')
except Exception as e:
print(f"open fail: {e}", file=sys.stderr)
return 7
# Resize to 1200x630 Open Graph default (preserve aspect, cover-crop).
# Most src tiles are square (1024x1024) — cover-crop centers horizontally.
OG_W, OG_H = 1200, 630
sw, sh = im.size
src_ar = sw / sh; og_ar = OG_W / OG_H
if src_ar > og_ar:
# source wider than og — crop width
new_w = int(sh * og_ar); off = (sw - new_w) // 2
im = im.crop((off, 0, off + new_w, sh))
elif src_ar < og_ar:
# source narrower than og — crop height
new_h = int(sw / og_ar); off = (sh - new_h) // 2
im = im.crop((0, off, sw, off + new_h))
im = im.resize((OG_W, OG_H), Image.LANCZOS)
# Per-pixel nearest-color recolor. For each input pixel, find the
# nearest 'from' color in pairs by RGB-euclidean; if within tolerance,
# remap to 'to'. Tolerance 48 mirrors the typical wallco palette
# spacing (solid screen-print inks, ~80-100 between distinct colors).
TOL_SQ = 48 * 48
px = im.load()
w, h = im.size
fr = [p[0] for p in pairs]; to = [p[1] for p in pairs]
for y in range(h):
for x in range(w):
r, g, b = px[x, y]
best_i = -1; best_d = TOL_SQ + 1
for i, (fr_r, fr_g, fr_b) in enumerate(fr):
d = (r - fr_r) ** 2 + (g - fr_g) ** 2 + (b - fr_b) ** 2
if d < best_d:
best_d = d; best_i = i
if best_i >= 0 and best_d <= TOL_SQ:
px[x, y] = to[best_i]
os.makedirs(os.path.dirname(out), exist_ok=True)
tmp = out + '.tmp'
try:
im.save(tmp, 'JPEG', quality=85, optimize=True, progressive=True)
os.replace(tmp, out)
except Exception as e:
print(f"save fail: {e}", file=sys.stderr)
try: os.unlink(tmp)
except: pass
return 8
dt = time.time() - t0
print(f"baked {out} in {dt*1000:.0f}ms", file=sys.stderr)
return 0
if __name__ == '__main__':
sys.exit(main())