Files
abrown38andClaude Opus 5.5 1afed741bf Use the JARL wordmark in the header and as the README banner
The header now shows the login page's wordmark letters (without the rule)
instead of "Jarl" set in Cinzel, so the bundled Cinzel font, its preload
and its licence stanza in the .deb's copyright file are removed: the web
UI needs no web font.

The README opens with the login page's banner, the logo above the
wordmark (artwork/banner.svg). scripts/banner.py builds it from the web
UI's logo.js and brand.js, with light and dark colors, and logo.py runs
it after tracing the logo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:09:41 +00:00

80 lines
3.6 KiB
Python
Executable File

#!/usr/bin/env python3
# Builds artwork/banner.svg, the README's banner: the logo above the "JARL"
# wordmark, laid out as on the web UI's login page. It reads both from the
# web UI's own sources, so it matches the login page:
#
# internal/web/static/assets/logo.js the logo's paths (from scripts/logo.py)
# internal/web/static/assets/brand.js the wordmark's letters and rule
#
# The letters and rule take the UI's text and accent colors, switching to
# the dark theme's when the viewer prefers dark. Needs only Python; run from
# the repository root after changing either file (scripts/logo.py runs it):
#
# python3 scripts/banner.py
import re
import sys
ASSETS = "internal/web/static/assets"
OUT = "artwork/banner.svg"
# The UI's --text and --accent (style.css), light and dark.
STYLE = (".t{fill:#1d2330}.a{fill:#2f5fb3}.l{fill:none;stroke:#2f5fb3;stroke-width:2.6}"
".d{fill:#2f5fb3;font:700 12.5px system-ui,sans-serif;letter-spacing:.5px}"
"@media (prefers-color-scheme:dark){.t{fill:#e3e7ee}.a,.d{fill:#6d9be8}.l{stroke:#6d9be8}}")
CLASSES = {"fill": "a", "line": "l", "dns": "d"}
# The login page's sizes in pixels (style.css): a 104px logo, 10px above a
# 220px wordmark whose viewBox is 288 units wide.
UNIT = 288 / 220
LOGO = round(104 * UNIT, 1)
GAP = round(10 * UNIT, 1)
def attrs(src):
"""Turns a JS object literal's body ({ cx: 106, "text-anchor": "middle" })
into SVG attributes."""
out = []
for key, val in re.findall(r'"?([\w-]+)"?:\s*("[^"]*"|[\d.]+)', src):
val = val.strip('"')
out.append(f'class="{CLASSES[val]}"' if key == "class" else f'{key}="{val}"')
return " ".join(out)
def main():
logo = open(f"{ASSETS}/logo.js").read()
view_box = re.search(r'viewBox = "([^"]*)"', logo).group(1)
transform = re.search(r'transform = "([^"]*)"', logo).group(1)
layers = re.findall(r'\["(#\w+)", "([^"]*)"\]', logo)
brand = open(f"{ASSETS}/brand.js").read()
letters = re.findall(r'\{ x: (\d+), d: "([^"]*)" \}', brand)
rule = brand[brand.index("const ruleG"):]
rule = rule[:rule.index(");\n")]
rule_at = re.search(r'class: "wordmark-rule", transform: "([^"]*)"', rule).group(1)
shapes = re.findall(r'el\("(path|circle|text)", \{ ([^}]*) \}(?:, "([^"]*)")?\)', rule)
mark_box = re.search(r'viewBox: "(-4 -4 288 162)"', brand).group(1)
if len(layers) < 2 or len(letters) != 4 or len(shapes) < 5:
sys.exit("banner.py: logo.js or brand.js no longer looks as expected; update the script")
width, height = 288, LOGO + GAP + 162
svg = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height:g}" role="img" aria-label="Jarl">',
f"<!-- Generated by scripts/banner.py from {ASSETS}/logo.js and brand.js. -->",
f"<title>Jarl</title><style>{STYLE}</style>",
f'<svg x="{(width - LOGO) / 2:g}" width="{LOGO:g}" height="{LOGO:g}" viewBox="{view_box}">'
f'<g transform="{transform}">'
+ "".join(f'<path fill="{fill}" d="{d}"/>' for fill, d in layers) + "</g></svg>",
f'<svg y="{LOGO + GAP:g}" width="288" height="162" viewBox="{mark_box}">'
'<g class="t" fill-rule="evenodd">'
+ "".join(f'<path transform="translate({x} 0)" d="{d}"/>' for x, d in letters) + "</g>"
f'<g transform="{rule_at}">'
+ "".join(f"<{tag} {attrs(a)}>{text}</{tag}>" if text else f"<{tag} {attrs(a)}/>" for tag, a, text in shapes)
+ "</g></svg>",
"</svg>"]
with open(OUT, "w") as f:
f.write("\n".join(svg) + "\n")
print(f"{OUT}: {width}x{height:g}")
if __name__ == "__main__":
sys.exit(main())