import pathlib
import re
import sys
from fontTools.misc.transform import Transform
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib import TTFont
FONT = "branding/Rajdhani-Bold.ttf"
VERDIGRIS = "#4db08a"
COPPER = "#bd6b3d"
BONE = "#ece2d8"
GROUND = "#1b1614"
GLYPH_PX = 120.0
BASELINE = 96.0
LEAF = "M8 8 C21 7 29 15 27 40 C13 41 6 29 8 8 Z"
LEAF_AT = (13, 40)
COUNTER = (15, 42, 32, 46, 12) RING = (13, 109, 7, 4) DOTS = ((1, 120, 3.5, 0.75), (-5.5, 128, 2, 0.45))
LOCKUP = dict(w=362, h=213, r=16, mark=(78.4, 45.7), text=(157.5, 120.8), size=48.6)
WORD, SPLIT = "gitwig", 3
def _round(data, places=2):
def shorten(match):
value = round(float(match.group()), places)
return str(int(value)) if value == int(value) else str(value)
return re.sub(r"-?\d+\.?\d*(?:e-?\d+)?", shorten, data)
def _font():
if not pathlib.Path(FONT).exists():
sys.exit(f"{FONT} not found -- run this from the repository root.")
return TTFont(FONT)
def glyph_path(font, char, size, origin_x=0.0, baseline=0.0):
scale = size / font["head"].unitsPerEm
glyph_set = font.getGlyphSet()
pen = SVGPathPen(glyph_set)
transform = Transform(scale, 0, 0, -scale, origin_x, baseline)
glyph_set[font.getBestCmap()[ord(char)]].draw(TransformPen(pen, transform))
return _round(pen.getCommands())
def kern_pairs(font):
pairs = {}
if "GPOS" not in font:
return pairs
for lookup in font["GPOS"].table.LookupList.Lookup:
for sub in lookup.SubTable:
if getattr(sub, "LookupType", lookup.LookupType) != 2:
continue
if sub.Format == 1:
for first, pairset in zip(sub.Coverage.glyphs, sub.PairSet):
for rec in pairset.PairValueRecord:
if value := getattr(rec.Value1, "XAdvance", 0) or 0:
pairs[(first, rec.SecondGlyph)] = value
elif sub.Format == 2:
class1, class2 = sub.ClassDef1.classDefs, sub.ClassDef2.classDefs
for g1 in sub.Coverage.glyphs:
for g2, k2 in class2.items():
try:
rec = sub.Class1Record[class1.get(g1, 0)].Class2Record[k2]
except IndexError:
continue
if value := getattr(rec.Value1, "XAdvance", 0) or 0:
pairs[(g1, g2)] = value
return pairs
def wordmark(font, size):
upm = font["head"].unitsPerEm
cmap, glyph_set, hmtx = font.getBestCmap(), font.getGlyphSet(), font["hmtx"]
kerns = kern_pairs(font)
names = [cmap[ord(ch)] for ch in WORD]
scale = size / upm
drawn, pen_x = [], 0.0
for index, name in enumerate(names):
pen = SVGPathPen(glyph_set)
glyph_set[name].draw(
TransformPen(pen, Transform(scale, 0, 0, -scale, pen_x * scale, 0))
)
drawn.append(pen.getCommands())
pen_x += hmtx[name][0]
if index + 1 < len(names):
pen_x += kerns.get((name, names[index + 1]), 0)
return _round("".join(drawn[:SPLIT])), _round("".join(drawn[SPLIT:])), pen_x * scale
def mark(font, uid, indent=" "):
cx, cy, r, stroke = RING
x, y, w, h, rx = COUNTER
body = f"""<mask id="{uid}" maskUnits="userSpaceOnUse" x="-16" y="24" width="88" height="116">
<rect x="-16" y="24" width="88" height="116" fill="#fff"/>
<!-- Leaf and ring interior are knocked out, so they read as true negative space. -->
<path d="{LEAF}" transform="translate({LEAF_AT[0]} {LEAF_AT[1]})" fill="#000"/>
<circle cx="{cx}" cy="{cy}" r="{r}" fill="#000"/>
</mask>
<g mask="url(#{uid})">
<path d="{glyph_path(font, 'g', GLYPH_PX, 0, BASELINE)}" fill="{VERDIGRIS}"/>
<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{rx}" fill="{VERDIGRIS}"/>
</g>
<circle cx="{cx}" cy="{cy}" r="{r}" fill="none" stroke="{COPPER}" stroke-width="{stroke}"/>"""
for dx, dy, dr, opacity in DOTS:
body += f'\n<circle cx="{dx}" cy="{dy}" r="{dr}" fill="{COPPER}" opacity="{opacity}"/>'
return "\n".join(indent + line for line in body.splitlines())
def main():
font = _font()
git_d, wig_d, advance = wordmark(font, LOCKUP["size"])
lw, lh, lr = LOCKUP["w"], LOCKUP["h"], LOCKUP["r"]
mx, my = LOCKUP["mark"]
tx, ty = LOCKUP["text"]
def wordmark_group(git_fill, x, y):
return (
f' <!-- Wordmark: Rajdhani Bold (700) as outlines; no font needed to render. -->\n'
f' <g transform="translate({x} {y})">\n'
f' <path fill="{git_fill}" d="{git_d}"/>\n'
f' <path fill="{VERDIGRIS}" d="{wig_d}"/>\n'
f' </g>\n'
)
pathlib.Path("branding/logo-mark.svg").write_text(
f'<svg xmlns="http://www.w3.org/2000/svg" width="512" height="674" '
f'viewBox="-16 24 88 116" role="img" aria-label="Gitwig">\n'
f'{mark(font, "gitwig-mark")}\n</svg>\n'
)
pathlib.Path("branding/app-icon.svg").write_text(
f'<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" '
f'viewBox="0 0 64 64" role="img" aria-label="Gitwig">\n'
f' <rect width="64" height="64" rx="14" fill="{GROUND}"/>\n'
f' <g transform="translate(32 32) scale(0.462) translate(-24.09 -82.4)">\n'
f'{mark(font, "gitwig-icon", indent=" ")}\n'
f' </g>\n</svg>\n'
)
pathlib.Path("branding/lockup.svg").write_text(
f'<svg xmlns="http://www.w3.org/2000/svg" width="{lw * 2}" height="{lh * 2}" '
f'viewBox="0 0 {lw} {lh}" role="img" aria-label="Gitwig">\n'
f' <rect width="{lw}" height="{lh}" rx="{lr}" fill="{GROUND}"/>\n'
f' <g transform="translate({mx} {my})">\n'
f'{mark(font, "gitwig-lockup", indent=" ")}\n'
f' </g>\n'
f'{wordmark_group(BONE, tx, ty)}</svg>\n'
)
dx, dy = tx - mx, ty - my pad = 8
ink_x0, ink_x1 = -7.5, dx + advance ink_y0, ink_y1 = 34.8, 130.0 hero_w = round(ink_x1 - ink_x0 + pad * 2)
hero_h = round(ink_y1 - ink_y0 + pad * 2)
hero_mark = (pad - ink_x0, pad - ink_y0)
for name, fill in (("dark", BONE), ("light", GROUND)):
pathlib.Path(f"resources/logo-{name}.svg").write_text(
f'<svg xmlns="http://www.w3.org/2000/svg" width="{hero_w * 2}" '
f'height="{hero_h * 2}" viewBox="0 0 {hero_w} {hero_h}" role="img" '
f'aria-label="Gitwig">\n'
f' <g transform="translate({hero_mark[0]:.1f} {hero_mark[1]:.1f})">\n'
f'{mark(font, f"gitwig-hero-{name}", indent=" ")}\n'
f' </g>\n'
f'{wordmark_group(fill, round(hero_mark[0] + dx, 1), round(hero_mark[1] + dy, 1))}</svg>\n'
)
print(f"wordmark advance {advance:.2f} -> hero canvas {hero_w}x{hero_h}")
print("wrote branding/{logo-mark,app-icon,lockup}.svg, resources/logo-{dark,light}.svg")
if __name__ == "__main__":
main()