#!/usr/bin/env python3 """Generate the logo-poll showcase assets for /logo/ from one geometry source. python3 website/tools/logo-assets.py Writes website/static/img/logo/{m,r}/*.png and website/data/logo/{m,r}.json. Needs: cairosvg, Pillow, fontTools (pip). Run it on the dev box and commit the output — the site build itself needs nothing beyond Hugo. Mark M: lowercase f knocked out of a gate, 64-unit grid (even-odd fill). Mark R: window, wheel and F, 104x114 grid, stroke 8 (from Robert's sketch). """ import base64 import io import json import os import sys import cairosvg from fontTools.pens.svgPathPen import SVGPathPen from fontTools.ttLib import TTFont from PIL import Image, ImageDraw HERE = os.path.dirname(os.path.abspath(__file__)) SITE = os.path.dirname(HERE) FONT = os.path.join(HERE, "FamiljenGrotesk-SemiBold.ttf") XMLNS = 'xmlns="http://www.w3.org/2000/svg"' # ---------------------------------------------------------------- geometry M_FLAT = ( "M6 60V28a26 26 0 0 1 52 0v32Z M20.0 60V26.5A10.5 10.5 0 0 1 30.5 16H45.0v8.6" "H32.5a3.5 3.5 0 0 0-3.5 3.5V32h16v8.6H29.0V60Z" ) M_MIN = ( "M5 60V27a27 27 0 0 1 54 0v33Z M19.5 60V26A10 10 0 0 1 29.5 16H46v10H32" "a2.5 2.5 0 0 0-2.5 2.5V32H46v10H29.5v18Z" ) R_FRAME = "M6 108V42A36 36 0 0 1 42 6H62A36 36 0 0 1 98 42V108Z" R_OUTER = "M2 112V42A40 40 0 0 1 42 2H62A40 40 0 0 1 102 42V112Z" R_HOLE = "M63 65a11 11 0 1 0-22 0a11 11 0 1 0 22 0Z" R_SPOKES = "M52 8V106 M8 65H96 M52 65L4 31.1 M52 65L4 98.9 M52 65L100 98.9" R_ARMS = "M52 22H79M52 37H69" MARKS = { "m": { "vb": (0, 0, 64, 64), "box": (6, 2, 52, 58), # x, y, w, h of the drawn mark inside the viewBox "color": "#c03a28", "ink": "#0d0d0f", "grad": ("#d95a45", "#c03a28", "#8a2618"), }, "r": { "vb": (0, 0, 104, 114), "box": (2, 2, 100, 110), "color": "#af6428", "ink": "#151515", "grad": ("#c9803f", "#af6428", "#9a561f"), }, } def body(k, tier, color="currentColor", uid=""): """SVG inner markup for mark k at tier display | flat | min.""" if k == "m": if tier == "display": g = MARKS["m"]["grad"] return ( f'' f'' f'' f'' f'' f'' ) path = M_MIN if tier == "min" else M_FLAT return f'' if tier == "min": return ( f'' f'' f'' f'' ) defs = ( f'' ) if tier == "display": g = MARKS["r"]["grad"] defs += ( f'' f'' f'' ) color = f"url(#g{uid})" return ( f"{defs}" f'' f'' f'' ) def svg(k, tier, color="currentColor", uid="", cls="pm", standalone=False): x, y, w, h = MARKS[k]["vb"] ns = f" {XMLNS}" if standalone else "" return ( f'" ) # ---------------------------------------------------------------- raster def raster(svgtxt, w, h): png = cairosvg.svg2png(bytestring=svgtxt.encode(), output_width=w, output_height=h) return Image.open(io.BytesIO(png)).convert("RGBA") def mark_png(k, tier, height, color=None): _, _, w, h = MARKS[k]["vb"] return raster(svg(k, tier, color or MARKS[k]["color"], standalone=True), round(height * w / h), height) # ---------------------------------------------------------------- wordmark _font = TTFont(FONT) _upm = _font["head"].unitsPerEm _cap = _font["OS/2"].sCapHeight or int(_upm * 0.7) _glyphs = _font.getGlyphSet() _cmap = _font.getBestCmap() def wordmark(text, size, x0=0.0, baseline=0.0, tracking=-0.028): """Outline text as one SVG path; returns (path_d, advance_width).""" s = size / _upm parts, x = [], x0 for ch in text: name = _cmap[ord(ch)] pen = SVGPathPen(_glyphs) g = _glyphs[name] g.draw(pen) d = pen.getCommands() if d: parts.append(f'') x += g.width * s + tracking * size return "".join(parts), x - x0 - tracking * size def lockup(k, stacked=False): """Mark + 'furtka', outlined, currentColor. Returns an inline SVG string.""" bx, by, bw, bh = MARKS[k]["box"] H = 58.0 # mark height in lockup units sc = H / bh mw = bw * sc size = H * 0.72 * _upm / _cap # cap height = 72 % of the mark height if stacked: paths, tw = wordmark("furtka", size) gap = 12 total_w = max(mw, tw) total_h = H + gap + size * _cap / _upm + size * 0.22 mx = (total_w - mw) / 2 tx = (total_w - tw) / 2 base = H + gap + size * _cap / _upm paths, _ = wordmark("furtka", size, x0=tx, baseline=base) inner = ( f'{body(k, "flat")}' f'{paths}' ) return f'' gap = H * 0.28 paths, tw = wordmark("furtka", size, x0=mw + gap, baseline=H) total_w = mw + gap + tw inner = ( f'{body(k, "flat")}' f'{paths}' ) return f'' def lockup_png(k, W, Hpx, stacked, mark_h): """Rasterise the lockup on the mark's ink ground (splash / social card).""" ink = MARKS[k]["ink"] col = MARKS[k]["color"] inner = lockup(k, stacked).replace("currentColor", col) inner = inner.replace(' 128 b = a.getpixel((x, y + 1)) > 128 s += "█" if t and b else "▀" if t else "▄" if b else " " lines.append(s.rstrip()) return "\n".join(lines) def b64png(im): buf = io.BytesIO() im.save(buf, "PNG", optimize=True) return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode() def main(): for k in MARKS: out = os.path.join(SITE, "static", "img", "logo", k) os.makedirs(out, exist_ok=True) tile(k, 192, radius=42).save(os.path.join(out, "tile.png"), optimize=True) tile(k, 192, radius=96).save(os.path.join(out, "tile-round.png"), optimize=True) lockup_png(k, 960, 540, True, 190).save(os.path.join(out, "splash.png"), optimize=True) lockup_png(k, 600, 315, False, 130).save(os.path.join(out, "social.png"), optimize=True) data = { "display": svg(k, "display", uid=f"{k}d"), "flat": svg(k, "flat"), "min": svg(k, "min"), "lockup_h": lockup(k), "lockup_s": lockup(k, stacked=True), "tab16": b64png(mark_png(k, "min", 16)), "full16": b64png(mark_png(k, "flat", 16)), "banner": banner(k), "color": MARKS[k]["color"], "ink": MARKS[k]["ink"], } with open(os.path.join(SITE, "data", "logo", f"{k}.json"), "w") as f: json.dump(data, f, indent=1, ensure_ascii=False) print(k, "ok") if __name__ == "__main__": sys.exit(main())