f0dc6a9d4e
gates / gates (push) Successful in 4m24s
build.py (Pillow, site tokens + Plus Jakarta Sans) with in-build checks: profile circle (today's shape 6823 pixels outside, new 0), cover safe area and profile-circle clearance, sizes read back. COPY.md: intro 99/100, long description, Messenger welcome, three first posts, each claim traced to the website. R-914 gets the listing-based removal proof and the picture-API tasks; R-915 the footer-link note; R-916 the logo's vector master.
431 lines
19 KiB
Python
431 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""build.py — the Felhom.eu Facebook Page pictures: two profile pictures, three covers, one preview page.
|
||
|
||
Run: python3 marketing/facebook/build.py
|
||
Exit: 0 every image built and every check passed · 1 a check failed · 2 a render failed
|
||
|
||
Renderer: Pillow only (shapes, text, compositing). Text is set in the website's own Plus Jakarta Sans
|
||
(website/assets/fonts/*.woff2), which Pillow reads through FreeType. NOT librsvg: tried 2026-10-08 — pango fell back
|
||
to DejaVu for the woff2 fonts, and logo.svg's own lettering is live <text> in 'M+ 2c' / 'Vremena Grotesk', fonts
|
||
DooPlex does not have, so the SVG renders with the wrong letters. The logo master is therefore website/assets/logo.png
|
||
(645 x 408, the file the site itself shows), placed unscaled.
|
||
|
||
Checks (each prints its numbers; a failure exits 1):
|
||
circle every logo pixel of a profile picture lies inside radius 0.42 x width of the centre (8 % margin).
|
||
Shown failing first on the shape the Page has today (logo.png, full size, on a square).
|
||
safe every pixel of a cover's CONTENT layer (text, logo, drawings — not the background) lies inside the
|
||
area both desktop and phone show, and outside the places the profile circle covers.
|
||
size each PNG's size is read back from the file, never taken from the constants here.
|
||
|
||
Sizes (graded READ, second-hand — Meta's own help page needs a browser; see README.md for the URLs):
|
||
cover shown 820 x 312 on computers, 640 x 360 on phones; built at 2 x = 1640 x 624.
|
||
profile shown 176 px (computer) / 196 px (phone, older layouts) — built at the size where logo.png fits unscaled.
|
||
profile circle over the cover: computer 16 px from the left, 176 px from the top (176 px circle);
|
||
phone 24 px from the left, 196 px from the top (140 px circle).
|
||
"""
|
||
import base64
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||
sys.path.insert(0, HERE)
|
||
|
||
ASSETS = os.path.join(ROOT, "website", "assets")
|
||
OUT = os.path.join(HERE, "out")
|
||
LOGO_PNG = os.path.join(ASSETS, "logo.png")
|
||
SHOT = os.path.join(ASSETS, "dashboard-backups-hu.webp")
|
||
PJS = os.path.join(ASSETS, "fonts", "pjs-latin.woff2")
|
||
PJS_EXT = os.path.join(ASSETS, "fonts", "pjs-latin-ext.woff2")
|
||
|
||
|
||
def hexc(h, a=255):
|
||
h = h.lstrip("#")
|
||
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), a)
|
||
|
||
|
||
# website/assets/site.css :root — design system v2 tokens, verbatim (the site is dark-only).
|
||
BG0, BG1, BG2 = hexc("#0A1220"), hexc("#0F1B2E"), hexc("#16263F")
|
||
LINE = hexc("#22344F")
|
||
TEXT1, TEXT2, TEXT3 = hexc("#EDF2F9"), hexc("#94A6BF"), hexc("#5E7392")
|
||
BLUE, BLUE_BRIGHT = hexc("#0083D8"), hexc("#2EA8F5")
|
||
WHITE = hexc("#FFFFFF")
|
||
|
||
# The site's headline (index.html hero), from escapes (brief section 9.7).
|
||
L1 = "Saj\u00e1t felh\u0151d,"
|
||
L2 = "saj\u00e1t szab\u00e1lyaid"
|
||
SLOGAN = L1 + " " + L2
|
||
DOMAIN = "felhom.eu"
|
||
|
||
CIRCLE_R = 0.42 # brief §7 A: the check radius, as a fraction of the width
|
||
LOGO_R = 0.40 # where the logo's farthest pixel is placed (inside the check radius)
|
||
CW, CH = 1640, 624 # cover, 2 x the 820 x 312 computer display
|
||
SCALE = CW / 820.0
|
||
# Phone shows the full height at 640:360 → the centre CW_PHONE pixels of the width.
|
||
CW_PHONE = CH * 640.0 / 360.0
|
||
PHONE_X0 = (CW - CW_PHONE) / 2.0
|
||
MARGIN = 40
|
||
SAFE = (int(math.ceil(PHONE_X0 + MARGIN)), MARGIN, int(CW - PHONE_X0 - MARGIN), CH - MARGIN)
|
||
PAD = 24
|
||
DESK_CIRCLE = (16 * SCALE, 176 * SCALE, 176 * SCALE) # x, y, diameter in cover pixels
|
||
PHONE_K = CH / 360.0
|
||
PHONE_CIRCLE = (PHONE_X0 + 24 * PHONE_K, 196 * PHONE_K, 140 * PHONE_K)
|
||
QUIET = [(0, c[1] - PAD, c[0] + c[2] + PAD, CH) for c in (DESK_CIRCLE, PHONE_CIRCLE)]
|
||
|
||
FAILS = []
|
||
|
||
|
||
def check(ok, msg):
|
||
print((" ok " if ok else " FAIL ") + msg)
|
||
if not ok:
|
||
FAILS.append(msg)
|
||
|
||
|
||
# ------------------------------------------------------------------ text in the site font
|
||
|
||
_fonts = {}
|
||
|
||
|
||
def font(size, weight, ext):
|
||
key = (size, weight, ext)
|
||
if key not in _fonts:
|
||
f = ImageFont.truetype(PJS_EXT if ext else PJS, size)
|
||
f.set_variation_by_name(weight)
|
||
_fonts[key] = f
|
||
return _fonts[key]
|
||
|
||
|
||
def runs(text):
|
||
"""Split into runs by the CSS unicode-range of the two woff2 files (latin-ext = U+0100–02BA)."""
|
||
out = []
|
||
for ch in text:
|
||
ext = 0x100 <= ord(ch) <= 0x2BA
|
||
if out and out[-1][0] == ext:
|
||
out[-1][1] += ch
|
||
else:
|
||
out.append([ext, ch])
|
||
return out
|
||
|
||
|
||
def text_width(text, size, weight):
|
||
return sum(font(size, weight, e).getlength(s) for e, s in runs(text))
|
||
|
||
|
||
def draw_text(img, x, y, text, size, weight, fill, align="left"):
|
||
"""Draw at baseline y; x is the left edge, or the centre with align='center'. Returns the width."""
|
||
w = text_width(text, size, weight)
|
||
if align == "center":
|
||
x -= w / 2.0
|
||
d = ImageDraw.Draw(img)
|
||
for e, s in runs(text):
|
||
f = font(size, weight, e)
|
||
d.text((x, y), s, font=f, fill=fill, anchor="ls")
|
||
x += f.getlength(s)
|
||
return w
|
||
|
||
|
||
def fit_size(text, weight, max_w, start):
|
||
size = start
|
||
while text_width(text, size, weight) > max_w:
|
||
size -= 1
|
||
return size
|
||
|
||
|
||
# ------------------------------------------------------------------ the logo
|
||
|
||
def alpha_points(img, thr=16):
|
||
a = img.getchannel("A")
|
||
w, h = img.size
|
||
px = a.load()
|
||
return [(x, y) for y in range(h) for x in range(w) if px[x, y] > thr]
|
||
|
||
|
||
def outside_circle(img, bg, r_frac):
|
||
"""Count pixels that differ from the background and lie outside radius r_frac x width of the centre."""
|
||
w, h = img.size
|
||
cx, cy, r = (w - 1) / 2.0, (h - 1) / 2.0, r_frac * w
|
||
rgb = img.convert("RGB").load()
|
||
n = 0
|
||
for y in range(h):
|
||
dy2 = (y - cy) ** 2
|
||
for x in range(w):
|
||
p = rgb[x, y]
|
||
if max(abs(p[0] - bg[0]), abs(p[1] - bg[1]), abs(p[2] - bg[2])) > 24 and (x - cx) ** 2 + dy2 > r * r:
|
||
n += 1
|
||
return n
|
||
|
||
|
||
def today_shape():
|
||
"""The Page today: logo.png at full size, unscaled, centred on a white square of its own width."""
|
||
src = Image.open(LOGO_PNG).convert("RGBA")
|
||
w, h = src.size
|
||
sq = Image.new("RGBA", (w, w), WHITE)
|
||
sq.alpha_composite(src, (0, (w - h) // 2))
|
||
return sq
|
||
|
||
|
||
def profile_width():
|
||
"""The square size at which logo.png fits radius LOGO_R x width UNSCALED (never upsampled), at least 720."""
|
||
src = Image.open(LOGO_PNG).convert("RGBA")
|
||
pts = alpha_points(src)
|
||
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
|
||
cx, cy = (min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0
|
||
dmax = max(math.hypot(x - cx, y - cy) for x, y in pts) + 1
|
||
return max(720, int(math.ceil(dmax / LOGO_R / 2.0)) * 2), (cx, cy)
|
||
|
||
|
||
def build_profile(bg, name):
|
||
w, (cx, cy) = profile_width()
|
||
logo = Image.open(LOGO_PNG).convert("RGBA")
|
||
img = Image.new("RGBA", (w, w), bg)
|
||
img.alpha_composite(logo, (int(round(w / 2.0 - cx)), int(round(w / 2.0 - cy))))
|
||
path = os.path.join(OUT, name)
|
||
img.convert("RGB").save(path, optimize=True)
|
||
return path
|
||
|
||
|
||
# ------------------------------------------------------------------ covers
|
||
|
||
def background(glow_at=(0.78, 0.25)):
|
||
"""bg-0 with the site's 50 px grid (3 % blue, index.html) and the og-image's soft blue glow; top blue rule."""
|
||
img = Image.new("RGBA", (CW, CH), BG0)
|
||
glow = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||
gd = ImageDraw.Draw(glow)
|
||
gx, gy, gr = glow_at[0] * CW, glow_at[1] * CH, 520
|
||
gd.ellipse((gx - gr, gy - gr, gx + gr, gy + gr), fill=(0, 131, 216, 46))
|
||
img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(180)))
|
||
grid = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||
d = ImageDraw.Draw(grid)
|
||
step = int(50 * SCALE)
|
||
for x in range(0, CW, step):
|
||
d.line((x, 0, x, CH), fill=(0, 131, 216, 9), width=2)
|
||
for y in range(0, CH, step):
|
||
d.line((0, y, CW, y), fill=(0, 131, 216, 9), width=2)
|
||
img.alpha_composite(grid)
|
||
ImageDraw.Draw(img).rectangle((0, 0, CW, 8), fill=BLUE)
|
||
return img
|
||
|
||
|
||
def content_a():
|
||
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||
mid = (SAFE[0] + SAFE[2]) / 2.0
|
||
size = fit_size(SLOGAN, "ExtraBold", (SAFE[2] - SAFE[0]) - 120, 66)
|
||
w1 = text_width(L1 + " ", size, "ExtraBold")
|
||
w = text_width(SLOGAN, size, "ExtraBold")
|
||
x0 = mid - w / 2.0
|
||
draw_text(c, x0, 238, L1 + " ", size, "ExtraBold", TEXT1)
|
||
draw_text(c, x0 + w1, 238, L2, size, "ExtraBold", BLUE_BRIGHT)
|
||
draw_text(c, mid, 300, DOMAIN, 34, "SemiBold", TEXT2, align="center")
|
||
return c
|
||
|
||
|
||
def rack(d, x, y, w, h, fill, dot):
|
||
"""Three stacked server units with status dots — the logo's own motif, drawn as plain shapes."""
|
||
uh = (h - 2 * 8) / 3.0
|
||
for i in range(3):
|
||
ty = y + i * (uh + 8)
|
||
d.rectangle((x, ty, x + w, ty + uh), fill=fill)
|
||
for j in range(3):
|
||
cx = x + 20 + j * 22
|
||
d.ellipse((cx - 5, ty + uh / 2 - 5, cx + 5, ty + uh / 2 + 5), fill=dot)
|
||
|
||
|
||
def tile(d, cx, cy, s, kind):
|
||
"""A plain app tile (no third-party logo): a square with a simple generic glyph."""
|
||
x0, y0, x1, y1 = cx - s / 2, cy - s / 2, cx + s / 2, cy + s / 2
|
||
d.rectangle((x0, y0, x1, y1), fill=BG2, outline=LINE, width=3)
|
||
g = s * 0.28
|
||
if kind == "photo":
|
||
d.polygon([(cx - g, cy + g * 0.7), (cx - g * 0.2, cy - g * 0.5), (cx + g * 0.3, cy + g * 0.2),
|
||
(cx + g * 0.6, cy - g * 0.1), (cx + g, cy + g * 0.7)], fill=BLUE_BRIGHT)
|
||
d.ellipse((cx + g * 0.35, cy - g, cx + g * 0.85, cy - g * 0.5), fill=TEXT2)
|
||
elif kind == "doc":
|
||
d.rectangle((cx - g * 0.75, cy - g, cx + g * 0.75, cy + g), outline=TEXT1, width=5)
|
||
for i in range(3):
|
||
yy = cy - g * 0.45 + i * g * 0.45
|
||
d.line((cx - g * 0.45, yy, cx + g * 0.45, yy), fill=BLUE_BRIGHT, width=5)
|
||
elif kind == "play":
|
||
d.polygon([(cx - g * 0.6, cy - g), (cx + g, cy), (cx - g * 0.6, cy + g)], fill=BLUE_BRIGHT)
|
||
elif kind == "lock":
|
||
d.arc((cx - g * 0.6, cy - g * 1.2, cx + g * 0.6, cy), 180, 360, fill=TEXT1, width=6)
|
||
d.rectangle((cx - g * 0.85, cy - g * 0.6, cx + g * 0.85, cy + g * 0.9), fill=TEXT1)
|
||
d.ellipse((cx - g * 0.18, cy - g * 0.1, cx + g * 0.18, cy + g * 0.26), fill=BG2)
|
||
elif kind == "folder":
|
||
d.polygon([(cx - g, cy - g * 0.7), (cx - g * 0.2, cy - g * 0.7), (cx, cy - g * 0.4), (cx + g, cy - g * 0.4),
|
||
(cx + g, cy + g * 0.8), (cx - g, cy + g * 0.8)], fill=BLUE_BRIGHT)
|
||
|
||
|
||
def content_b():
|
||
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||
d = ImageDraw.Draw(c)
|
||
tx = SAFE[0] + 24 # text stays above the profile circles (y < 316)
|
||
draw_text(c, tx, 170, L1, 64, "ExtraBold", TEXT1)
|
||
draw_text(c, tx, 244, L2, 64, "ExtraBold", BLUE_BRIGHT)
|
||
draw_text(c, tx, 290, DOMAIN, 32, "SemiBold", TEXT2)
|
||
# the home with its server, on the right
|
||
hx, hy, hw = 1010, 300, 220 # house body: left, top, width
|
||
tiles = [(975, 110, "photo"), (1140, 92, "doc"), (1260, 210, "play"), (1260, 400, "lock"), (970, 520, "folder")]
|
||
for x, y, _ in tiles:
|
||
d.line((hx + hw / 2, hy + 90, x, y), fill=(46, 168, 245, 110), width=4)
|
||
d.rectangle((hx, hy, hx + hw, hy + 190), fill=BG1, outline=BLUE_BRIGHT, width=6)
|
||
d.polygon([(hx - 30, hy + 4), (hx + hw / 2, hy - 120), (hx + hw + 30, hy + 4)], fill=BG1, outline=BLUE_BRIGHT)
|
||
d.line([(hx - 30, hy + 4), (hx + hw / 2, hy - 120), (hx + hw + 30, hy + 4)], fill=BLUE_BRIGHT, width=6, joint="curve")
|
||
rack(d, hx + 40, hy + 40, hw - 80, 110, BLUE, TEXT1)
|
||
for x, y, kind in tiles:
|
||
tile(d, x, y, 92, kind)
|
||
return c
|
||
|
||
|
||
def content_c():
|
||
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||
d = ImageDraw.Draw(c)
|
||
tx = SAFE[0] + 24
|
||
draw_text(c, tx, 180, L1, 52, "ExtraBold", TEXT1)
|
||
draw_text(c, tx, 246, L2, 52, "ExtraBold", BLUE_BRIGHT)
|
||
draw_text(c, tx, 292, DOMAIN, 30, "SemiBold", TEXT2)
|
||
shot = Image.open(SHOT).convert("RGBA")
|
||
# The domain chip next to the title names the demo box; covered with the page colour (copy only, source unchanged).
|
||
ImageDraw.Draw(shot).rectangle((684, 32, 826, 70), fill=hexc("#0B1322"))
|
||
sw = 470
|
||
sh = int(round(sw * shot.height / shot.width))
|
||
shot = shot.resize((sw, sh), Image.LANCZOS)
|
||
fx, fy, b = 800, 110, 14 # frame left/top, bezel
|
||
d.rectangle((fx, fy, fx + sw + 2 * b, fy + sh + 2 * b), fill=BG2, outline=LINE, width=3)
|
||
c.alpha_composite(shot, (fx + b, fy + b))
|
||
base_y = fy + sh + 2 * b
|
||
d.polygon([(fx - 30, base_y + 22), (fx + sw + 2 * b + 30, base_y + 22), (fx + sw + 2 * b, base_y),
|
||
(fx, base_y)], fill=LINE)
|
||
return c
|
||
|
||
|
||
def check_safe(name, content):
|
||
pts = alpha_points(content.resize((CW // 4, CH // 4)), thr=8)
|
||
xs = [p[0] * 4 for p in pts]
|
||
ys = [p[1] * 4 for p in pts]
|
||
bbox = (min(xs), min(ys), max(xs) + 3, max(ys) + 3)
|
||
inside = bbox[0] >= SAFE[0] and bbox[1] >= SAFE[1] and bbox[2] <= SAFE[2] and bbox[3] <= SAFE[3]
|
||
check(inside, "%s: content box %s inside the safe area %s" % (name, bbox, SAFE))
|
||
hits = 0
|
||
for q in QUIET:
|
||
hits += sum(1 for x, y in zip(xs, ys) if q[0] <= x <= q[2] and q[1] <= y <= q[3])
|
||
check(hits == 0, "%s: %d content pixels where the profile circle sits (computer or phone)" % (name, hits))
|
||
|
||
|
||
def build_cover(name, content, glow):
|
||
img = background(glow)
|
||
img.alpha_composite(content)
|
||
path = os.path.join(OUT, name)
|
||
img.convert("RGB").save(path, optimize=True)
|
||
return path
|
||
|
||
|
||
# ------------------------------------------------------------------ preview
|
||
|
||
def b64(path):
|
||
with open(path, "rb") as fh:
|
||
return "data:image/png;base64," + base64.b64encode(fh.read()).decode()
|
||
|
||
|
||
def preview(profiles, covers):
|
||
dk, ph = DESK_CIRCLE, PHONE_CIRCLE
|
||
sx0, sy0, sx1, sy1 = SAFE
|
||
pct = lambda v, tot: "%.3f%%" % (100.0 * v / tot) # noqa: E731
|
||
prof_html = "".join(
|
||
'<figure><div class="row"><img class="pc" src="%s" alt=""><img class="pc sm" src="%s" alt="">'
|
||
'<img class="pc xs" src="%s" alt=""></div><figcaption>%s — 176 px, 96 px, 40 px</figcaption></figure>'
|
||
% (b64(p), b64(p), b64(p), label) for label, p in profiles)
|
||
cov = []
|
||
rec = profiles[0][1]
|
||
for label, p in covers:
|
||
src = b64(p)
|
||
safe = ('<div class="safe" style="left:%s;top:%s;width:%s;height:%s"></div>'
|
||
% (pct(sx0, CW), pct(sy0, CH), pct(sx1 - sx0, CW), pct(sy1 - sy0, CH)))
|
||
desk = ('<div class="desk"><img src="%s" alt="">%s<img class="ov" src="%s" alt="" '
|
||
'style="left:%s;top:%s;width:%s"></div>'
|
||
% (src, safe, b64(rec), pct(dk[0], CW), pct(dk[1], CH), pct(dk[2], CW)))
|
||
crop_l = PHONE_X0 / CW_PHONE * 100
|
||
phone = ('<div class="phone"><div class="crop"><img src="%s" alt="" style="width:%.3f%%;left:-%.3f%%">'
|
||
'</div><img class="ov" src="%s" alt="" style="left:%s;top:%s;width:%s"></div>'
|
||
% (src, CW / CW_PHONE * 100, crop_l, b64(rec), pct(ph[0] - PHONE_X0, CW_PHONE),
|
||
pct(ph[1], CH), pct(ph[2], CW_PHONE)))
|
||
cov.append('<section><h2>%s</h2><p class="k">Computer (820 x 312), dashed = safe area (preview only)</p>%s'
|
||
'<p class="k">Phone (640 x 360 crop)</p>%s</section>' % (label, desk, phone))
|
||
html = """<!doctype html>
|
||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Facebook Page pictures</title>
|
||
<style>
|
||
:root{--bg-0:#0A1220;--bg-1:#0F1B2E;--line:#22344F;--text-1:#EDF2F9;--text-2:#94A6BF;--blue:#0083D8}
|
||
body{margin:0;background:var(--bg-0);color:var(--text-1);font:15px/1.5 -apple-system,'Segoe UI',sans-serif;padding:24px 16px}
|
||
main{max-width:860px;margin:0 auto}h1{font-size:22px;margin:0 0 4px}h2{font-size:17px;margin:32px 0 8px}
|
||
.k{color:var(--text-2);margin:12px 0 6px;font-size:13px}figure{margin:16px 0}.row{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
|
||
.pc{width:176px;height:176px;border-radius:50%%;border:4px solid #fff;display:block}.sm{width:96px;height:96px;border-width:3px}
|
||
.xs{width:40px;height:40px;border-width:1px}figcaption{color:var(--text-2);font-size:13px;margin-top:6px}
|
||
.desk{position:relative;max-width:820px;margin-bottom:56px}.desk>img:first-child{width:100%%;display:block}
|
||
.ov{position:absolute;border-radius:50%%;border:3px solid #fff;box-sizing:border-box}
|
||
.safe{position:absolute;border:2px dashed #E0A93E;box-sizing:border-box;pointer-events:none}
|
||
.phone{position:relative;width:min(360px,100%%);aspect-ratio:640/360;margin-bottom:48px}
|
||
.crop{position:absolute;inset:0;overflow:hidden}.crop img{position:absolute;top:0;height:100%%}
|
||
</style></head><body><main>
|
||
<h1>Felhom.eu - Facebook pictures</h1>
|
||
<p class="k">Profile picture: the DARK background is recommended. The logo's lettering is white with a thin navy edge,
|
||
so on white it fades at 40 px; on dark it reads solid, as on the website. The covers below show the dark one.</p>
|
||
<h2>Profile picture</h2>%s
|
||
%s
|
||
</main></body></html>
|
||
""" % (prof_html, "".join(cov))
|
||
path = os.path.join(OUT, "preview.html")
|
||
with open(path, "w", encoding="utf-8") as fh:
|
||
fh.write(html)
|
||
return path
|
||
|
||
|
||
# ------------------------------------------------------------------ main
|
||
|
||
def main():
|
||
os.makedirs(OUT, exist_ok=True)
|
||
print("circle check (radius %.2f x width)" % CIRCLE_R)
|
||
try:
|
||
before = today_shape()
|
||
n0 = outside_circle(before, WHITE, CIRCLE_R)
|
||
print(" today's shape (logo.png %dx%d on a %d square): %d logo pixels outside the circle — expected > 0"
|
||
% (Image.open(LOGO_PNG).width, Image.open(LOGO_PNG).height, before.width, n0))
|
||
check(n0 > 0, "the check convicts today's shape (control)")
|
||
prof = [("profile-white.png", WHITE), ("profile-dark.png", BG0)]
|
||
built = []
|
||
for name, bg in prof:
|
||
p = build_profile(bg, name)
|
||
n = outside_circle(Image.open(p), bg, CIRCLE_R)
|
||
check(n == 0, "%s: %d logo pixels outside the circle" % (name, n))
|
||
built.append(p)
|
||
print("safe-area check (computer 820x312 + phone 640x360; profile circles kept clear)")
|
||
covers = [("cover-a.png", content_a(), (0.78, 0.25)), ("cover-b.png", content_b(), (0.70, 0.45)),
|
||
("cover-c.png", content_c(), (0.66, 0.40))]
|
||
cpaths = []
|
||
for name, content, glow in covers:
|
||
check_safe(name, content)
|
||
cpaths.append(build_cover(name, content, glow))
|
||
except RuntimeError as e:
|
||
print("RENDER FAILED: %s" % e)
|
||
return 2
|
||
print("sizes read back from the files")
|
||
for p in built + cpaths:
|
||
w, h = Image.open(p).size
|
||
want = (profile_width()[0],) * 2 if "profile" in p else (CW, CH)
|
||
check((w, h) == want, "%s: %d x %d" % (os.path.basename(p), w, h))
|
||
pv = preview([("Dark background (recommended)", built[1]), ("White background", built[0])],
|
||
[("A - calm", cpaths[0]), ("B - the idea", cpaths[1]), ("C - the product", cpaths[2])])
|
||
print("preview: %s (%d KB)" % (os.path.relpath(pv, ROOT), os.path.getsize(pv) // 1024))
|
||
if FAILS:
|
||
print("FAILED: %d check(s)" % len(FAILS))
|
||
return 1
|
||
print("all checks passed")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|