#!/usr/bin/env python3
"""
Generates the banner of the start page and of the README:
app/MindWork AI Studio/wwwroot/svg/banner.svg
The app shows the banner through an element, where neither the app's CSS nor its web fonts
apply. Therefore, every text is converted into outlines, so the banner looks the same on Windows,
macOS, Linux, and GitHub. The text uses Inter (SIL Open Font License 1.1); the icons are the
Material Icons the app itself uses, read from the MudBlazor package (Apache License 2.0).
See README.md in this folder for the setup.
"""
import argparse
import hashlib
import os
import re
import sys
from pathlib import Path
import dnfile
import uharfbuzz as hb
from fontTools.pens.basePen import BasePen
from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib import TTFont
HERE = Path(__file__).resolve().parent
REPO_ROOT = HERE.parents[1]
APP_PROJECT = REPO_ROOT / "app" / "MindWork AI Studio" / "MindWork AI Studio.csproj"
DEFAULT_OUTPUT = REPO_ROOT / "app" / "MindWork AI Studio" / "wwwroot" / "svg" / "banner.svg"
# The variable font Inter[opsz,wght].ttf from google/fonts, pinned to one commit. Another version
# of the font changes the outlines and therefore the generated file.
FONT_PATH = HERE / "Inter.ttf"
FONT_URL = "https://raw.githubusercontent.com/google/fonts/e1d6480102fed30739fead0faee463101f892c8f/ofl/inter/Inter%5Bopsz,wght%5D.ttf"
FONT_SHA256 = "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031"
#
# Content
#
EYEBROW = "MINDWORK"
NAME = "AI Studio"
TAGLINE = "Any model. Your data. Your rules."
# Two rows of four chips; the chips of one column share their width, so pair labels of similar
# length in a column. Each entry: (id, label, name of the icon in Icons.Material.Filled).
CHIPS = [
("chat", "Chat", "Chat"),
("assistants", "Assistants", "Apps"),
("builder", "No-code builder", "AutoMode"),
("tools", "Agentic tools", "Build"),
("research", "Research", "Language"),
("transcribe", "Transcribe", "Mic"),
("documents", "Your documents", "Description"),
("emails", "Your e-mails", "Email"),
]
#
# Look
#
WIDTH, HEIGHT, CORNER_RADIUS = 1920, 320, 24
CREAM = "#f6f0d8"
# When the window is wider than 1536 px, the start page crops up to 32 units at the top and at the
# bottom. Keep text and chips between y = 48 and y = 272.
LAYOUT = dict(
sun=(1860, -50, 660), # centre x, centre y, radius
left=104, eyebrow_y=98, eyebrow_size=30, name_y=208, name_size=120, tag_y=258, tag_size=34,
chip_font=29, chip_icon=30, chip_pad=22, chip_icon_gap=11, chip_h=62, chip_gap=14, chip_row_gap=14, chip_cy=164,
right=104, chip_fill="#0b2e22", chip_fill_op=.35, chip_stroke_op=.3,
)
# Weights and optical sizes of the variable font:
EYEBROW_STYLE = dict(wght=600, opsz=32)
NAME_STYLE = dict(wght=800, opsz=32)
TEXT_STYLE = dict(wght=500, opsz=22)
def num(v):
"""Formats a coordinate with one decimal and without needless zeros, e.g. 0.5 -> .5, 2.0 -> 2."""
s = f"{v:.1f}"
if s.endswith(".0"):
s = s[:-2]
if s.startswith("0."):
s = s[1:]
elif s.startswith("-0."):
s = "-" + s[2:]
return "0" if s in ("-0", "") else s
class RecordingPen(BasePen):
"""Records the outline of glyphs as absolute drawing operations."""
def __init__(self, glyph_set):
super().__init__(glyph_set)
self.ops = []
def _moveTo(self, p):
self.ops.append(("M", p))
def _lineTo(self, p):
self.ops.append(("L", p))
def _qCurveToOne(self, c, p):
self.ops.append(("Q", c, p))
def _curveToOne(self, c1, c2, p):
self.ops.append(("C", c1, c2, p))
def _closePath(self):
self.ops.append(("Z",))
def _endPath(self):
pass
def compact_path(ops):
"""
Turns recorded operations into short path data: relative commands, coordinates rounded to
tenths, and T wherever a quadratic control point mirrors the previous one, which is the case
for every implied on-curve point of TrueType outlines.
"""
# Rounding the absolute points first keeps the relative steps from drifting:
to_tenths = lambda p: (round(p[0] * 10), round(p[1] * 10))
out, last_cmd = [], None
cur = start = (0, 0)
prev_ctrl = None
def emit(cmd, values):
nonlocal last_cmd
body = ""
for value in values:
text = num(value / 10)
if body and not text.startswith("-"):
body += " "
body += text
# A repeated command may omit its letter:
if cmd == last_cmd and cmd not in ("m", "z"):
out.append((" " if not body.startswith("-") else "") + body)
else:
out.append(cmd + body)
# Further pairs after a moveto are linetos:
last_cmd = "l" if cmd == "m" else cmd
for op in ops:
kind = op[0]
if kind == "M":
p = to_tenths(op[1])
emit("m", (p[0] - cur[0], p[1] - cur[1]))
cur = start = p
prev_ctrl = None
elif kind == "L":
p = to_tenths(op[1])
dx, dy = p[0] - cur[0], p[1] - cur[1]
if dx == 0 and dy == 0:
pass
elif dy == 0:
emit("h", (dx,))
elif dx == 0:
emit("v", (dy,))
else:
emit("l", (dx, dy))
cur = p
prev_ctrl = None
elif kind == "Q":
c, p = to_tenths(op[1]), to_tenths(op[2])
mirrored = prev_ctrl is not None and abs(2 * cur[0] - prev_ctrl[0] - c[0]) <= 1 and abs(2 * cur[1] - prev_ctrl[1] - c[1]) <= 1
if mirrored:
emit("t", (p[0] - cur[0], p[1] - cur[1]))
c = (2 * cur[0] - prev_ctrl[0], 2 * cur[1] - prev_ctrl[1])
else:
emit("q", (c[0] - cur[0], c[1] - cur[1], p[0] - cur[0], p[1] - cur[1]))
prev_ctrl = c
cur = p
elif kind == "C":
c1, c2, p = to_tenths(op[1]), to_tenths(op[2]), to_tenths(op[3])
emit("c", (c1[0] - cur[0], c1[1] - cur[1], c2[0] - cur[0], c2[1] - cur[1], p[0] - cur[0], p[1] - cur[1]))
cur = p
prev_ctrl = None
elif kind == "Z":
emit("z", ())
cur = start
prev_ctrl = None
return "".join(out)
class Font:
"""Shapes text with HarfBuzz and draws its glyphs with fontTools."""
def __init__(self, path):
self.tt = TTFont(path)
self.upem = self.tt["head"].unitsPerEm
self.face = hb.Face(hb.Blob.from_file_path(str(path)))
self.glyph_order = self.tt.getGlyphOrder()
# Glyphs of the chip labels, each defined once and placed with