130 lines
4.7 KiB
Python
130 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Build before/after (source vs clone) screenshot composites for a tier.
|
|
|
|
For each site: a single PNG with a desktop row (1280px) and a mobile row (375px),
|
|
each showing SOURCE on the left and CLONE on the right, with labels.
|
|
"""
|
|
import json, os, sys, glob, re
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
RUNS = os.path.join(ROOT, "runs")
|
|
TIER = sys.argv[1] if len(sys.argv) > 1 else "easy"
|
|
OUT = os.path.join(RUNS, f"compare-{TIER}")
|
|
os.makedirs(OUT, exist_ok=True)
|
|
|
|
def font(sz):
|
|
for p in ["/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"]:
|
|
if os.path.exists(p):
|
|
return ImageFont.truetype(p, sz)
|
|
return ImageFont.load_default()
|
|
|
|
F_TITLE = font(30)
|
|
F_LABEL = font(22)
|
|
|
|
def host_of(url):
|
|
# Mirror siteIdFromUrl() in cli.ts: host (minus www) + slugified path.
|
|
from urllib.parse import urlparse
|
|
u = urlparse(url)
|
|
host = re.sub(r"^www\.", "", u.hostname or "")
|
|
path = re.sub(r"^-|-$", "", re.sub(r"[^a-zA-Z0-9]+", "-", u.path.rstrip("/")))
|
|
sid = host + (("-" + path) if path else "")
|
|
return re.sub(r"[^a-zA-Z0-9.-]", "-", sid)[:80]
|
|
|
|
def latest_run(host):
|
|
dirs = sorted(glob.glob(os.path.join(RUNS, host, "*")), reverse=True)
|
|
for d in dirs:
|
|
if (os.path.exists(os.path.join(d, "source", "screenshots")) and
|
|
os.path.exists(os.path.join(d, "rendered", "screenshots"))):
|
|
return d
|
|
return None
|
|
|
|
def load(path, scale, max_h):
|
|
if not os.path.exists(path):
|
|
return None
|
|
im = Image.open(path).convert("RGB")
|
|
w = int(im.width * scale)
|
|
h = int(im.height * scale)
|
|
im = im.resize((w, h), Image.LANCZOS)
|
|
if im.height > max_h: # cap very tall pages
|
|
im = im.crop((0, 0, im.width, max_h))
|
|
return im
|
|
|
|
PAD = 16
|
|
HEADER = 46 # per-row label strip
|
|
TITLE = 56 # site title strip
|
|
GAP = 10 # gap between source and clone
|
|
BG = (250, 250, 250)
|
|
INK = (20, 20, 20)
|
|
RULE = (210, 210, 210)
|
|
|
|
def row(src_path, clone_path, scale, max_h, label):
|
|
"""One labeled row: [SOURCE | CLONE] side by side, top-aligned."""
|
|
s = load(src_path, scale, max_h)
|
|
c = load(clone_path, scale, max_h)
|
|
if s is None and c is None:
|
|
return None
|
|
sw = s.width if s else (c.width if c else 0)
|
|
cw = c.width if c else sw
|
|
sh = s.height if s else 0
|
|
ch = c.height if c else 0
|
|
body_h = max(sh, ch)
|
|
W = sw + GAP + cw
|
|
H = HEADER + body_h
|
|
img = Image.new("RGB", (W, H), BG)
|
|
d = ImageDraw.Draw(img)
|
|
d.text((4, 10), label, font=F_LABEL, fill=INK)
|
|
d.text((4, HEADER - 4), "SOURCE", font=F_LABEL, fill=(90, 90, 90))
|
|
d.text((sw + GAP + 4, HEADER - 4), "CLONE", font=F_LABEL, fill=(90, 90, 90))
|
|
if s: img.paste(s, (0, HEADER))
|
|
if c: img.paste(c, (sw + GAP, HEADER))
|
|
# divider line between the pair
|
|
d.line([(sw + GAP // 2, HEADER), (sw + GAP // 2, H)], fill=RULE, width=2)
|
|
return img
|
|
|
|
def stack(title, rows):
|
|
rows = [r for r in rows if r is not None]
|
|
if not rows:
|
|
return None
|
|
W = max(r.width for r in rows) + PAD * 2
|
|
H = TITLE + sum(r.height for r in rows) + PAD * (len(rows) + 1)
|
|
img = Image.new("RGB", (W, H), BG)
|
|
d = ImageDraw.Draw(img)
|
|
d.text((PAD, 14), title, font=F_TITLE, fill=INK)
|
|
d.line([(0, TITLE - 4), (W, TITLE - 4)], fill=RULE, width=2)
|
|
y = TITLE + PAD
|
|
for r in rows:
|
|
img.paste(r, (PAD, y))
|
|
y += r.height + PAD
|
|
return img
|
|
|
|
def main():
|
|
sites = json.load(open(os.path.join(ROOT, "compiler", "benchmarks", f"{TIER}.json")))
|
|
made = []
|
|
for site in sites:
|
|
host = host_of(site["url"])
|
|
run = latest_run(host)
|
|
if not run:
|
|
print(f"SKIP {site['id']} {host}: no run with screenshots")
|
|
continue
|
|
ss = os.path.join(run, "source", "screenshots")
|
|
rs = os.path.join(run, "rendered", "screenshots")
|
|
title = f"{site['id']} — {host}"
|
|
desktop = row(os.path.join(ss, "1280.png"), os.path.join(rs, "1280.png"),
|
|
0.5, 1600, "Desktop · 1280px viewport (shown at 50%)")
|
|
mobile = row(os.path.join(ss, "375.png"), os.path.join(rs, "375.png"),
|
|
1.0, 1800, "Mobile · 375px viewport (100%)")
|
|
img = stack(title, [desktop, mobile])
|
|
if img is None:
|
|
print(f"SKIP {site['id']}: no images")
|
|
continue
|
|
out = os.path.join(OUT, f"{site['id']}-{host}.png")
|
|
img.save(out, optimize=True)
|
|
made.append(out)
|
|
print(f"OK {out} ({img.width}x{img.height})")
|
|
print(f"\n{len(made)} composites -> {OUT}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|