| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- #!/usr/bin/env python3
- """Generate assets/languages/*.svg — the README "Language Support" icon grid.
- One tile per supported language: a paper card with the language's vector mark and
- its name centered underneath. Labels are converted to vector outlines (Archivo,
- same pipeline as generate-waitlist.py) so the SVG renders pixel-identical
- everywhere: GitHub loads README SVGs in "secure static mode", which blocks
- @font-face / web-font loading, so outlines are the only reliable way to ship the
- brand typeface. Every tile sits on the brand paper (#f7f6f2), so brand-colored
- marks — including near-black ones like Rust — stay legible on GitHub light *and*
- dark themes with no media-query tricks.
- Glyph sources (fetched at generation time, results checked in):
- - simple-icons via https://cdn.simpleicons.org/<slug> (CC0-1.0)
- - devicon via jsDelivr, pinned @v2.16.0 (MIT) — Java, C#,
- Objective-C, which simple-icons doesn't carry
- - hand-drawn in this file: CFML, COBOL, VB.NET (no usable upstream mark)
- All trademarks/logos belong to their respective owners; they're used here only
- to indicate language support.
- Adding a language: append to LANGS, re-run, reference the new SVG in README.md
- with a fresh `?v=1`. If you change EXISTING tile bytes, bump that tile's `?v=N`
- in README.md in the same commit (GitHub caches README images aggressively).
- Requires: fonttools, brotli (pip install fonttools brotli), network access.
- Font: @fontsource-variable/archivo (latin, wght axis) from the landing-page package.
- Usage: python3 generate-language-tiles.py [--preview /path/to/preview.svg]
- """
- import os
- import re
- import sys
- import urllib.request
- from fontTools.ttLib import TTFont
- from fontTools.varLib.instancer import instantiateVariableFont
- from fontTools.pens.svgPathPen import SVGPathPen
- from fontTools.pens.transformPen import TransformPen
- from fontTools.misc.transform import Transform
- # --- brand palette (mirrors generate-waitlist.py) ---------------------------
- PAPER = "#f7f6f2"
- HAIRLINE = "#d6d3c8"
- INK = "#16150f"
- FONT = os.path.join(
- os.path.dirname(__file__),
- "../../landing-page/node_modules/@fontsource-variable/archivo/files/archivo-latin-wght-normal.woff2",
- )
- # --- tile geometry (px) ------------------------------------------------------
- TILE = 104 # square tile, rx=8 card like the waitlist button
- GLYPH_BOX = 44 # logo box, horizontally centered
- GLYPH_TOP = 18
- LABEL_SIZE = 12.5
- LABEL_WT = 640
- LABEL_GAP = 12 # glyph box bottom -> label cap top
- TRACK = 0.15
- DEVICON = "https://cdn.jsdelivr.net/gh/devicons/devicon@v2.16.0/icons/{0}/{0}-{1}.svg"
- # (file slug, label, source) — README "Supported Languages" table order.
- # source: ("si", slug) simple-icons | ("devicon", name, variant) | ("custom", key)
- LANGS = [
- ("typescript", "TypeScript", ("si", "typescript")),
- ("javascript", "JavaScript", ("si", "javascript")),
- ("arkts", "ArkTS", ("si", "harmonyos")),
- ("python", "Python", ("si", "python")),
- ("go", "Go", ("si", "go")),
- ("rust", "Rust", ("si", "rust")),
- ("java", "Java", ("devicon", "java", "original")),
- ("csharp", "C#", ("devicon", "csharp", "original")),
- ("php", "PHP", ("si", "php")),
- ("ruby", "Ruby", ("si", "ruby")),
- ("c", "C", ("si", "c")),
- ("cpp", "C++", ("si", "cplusplus")),
- ("objective-c", "Objective-C", ("devicon", "objectivec", "plain")),
- ("metal", "Metal", ("si", "apple")),
- ("cuda", "CUDA", ("si", "nvidia")),
- ("swift", "Swift", ("si", "swift")),
- ("kotlin", "Kotlin", ("si", "kotlin")),
- ("scala", "Scala", ("si", "scala")),
- ("dart", "Dart", ("si", "dart")),
- ("svelte", "Svelte", ("si", "svelte")),
- ("vue", "Vue", ("si", "vuedotjs")),
- ("astro", "Astro", ("si", "astro")),
- ("liquid", "Liquid", ("si", "shopify")),
- ("delphi", "Delphi", ("si", "delphi")),
- ("lua", "Lua", ("si", "lua")),
- ("r", "R", ("si", "r")),
- ("luau", "Luau", ("si", "luau")),
- ("cfml", "CFML", ("custom", "cfml")),
- ("cobol", "COBOL", ("custom", "cobol")),
- ("vbnet", "VB.NET", ("custom", "vbnet")),
- ("erlang", "Erlang", ("si", "erlang")),
- ("solidity", "Solidity", ("si", "solidity")),
- ("terraform", "Terraform", ("si", "terraform")),
- ("nix", "Nix", ("si", "nixos")),
- ]
- def fetch(url):
- req = urllib.request.Request(url, headers={"User-Agent": "codegraph-assets"})
- with urllib.request.urlopen(req, timeout=20) as r:
- return r.read().decode("utf-8")
- class Outliner:
- """Archivo text -> SVG path outlines, one font instance per weight."""
- def __init__(self):
- self._by_weight = {}
- def _font(self, weight):
- if weight not in self._by_weight:
- ft = TTFont(FONT)
- instantiateVariableFont(ft, {"wght": weight}, inplace=True)
- self._by_weight[weight] = (ft, ft.getBestCmap(), ft.getGlyphSet(), ft["hmtx"])
- return self._by_weight[weight]
- def measure(self, text, size, weight, track=TRACK):
- ft, cmap, _, hmtx = self._font(weight)
- scale = size / ft["head"].unitsPerEm
- w = sum(hmtx[cmap[ord(ch)]][0] * scale + track for ch in text)
- return w - track
- def cap_height(self, size, weight):
- ft, _, _, _ = self._font(weight)
- return getattr(ft["OS/2"], "sCapHeight", 700) * size / ft["head"].unitsPerEm
- def outline(self, text, size, weight, start_x, baseline_y, track=TRACK):
- ft, cmap, glyphs, hmtx = self._font(weight)
- scale = size / ft["head"].unitsPerEm
- sink = SVGPathPen(glyphs, ntos=lambda v: f"{round(v, 2):g}")
- pen_x = start_x
- for ch in text:
- gname = cmap[ord(ch)]
- if ch != " ":
- t = Transform(scale, 0, 0, -scale, pen_x, baseline_y)
- glyphs[gname].draw(TransformPen(sink, t))
- pen_x += hmtx[gname][0] * scale + track
- return sink.getCommands()
- def centered(self, text, size, weight, center_x, baseline_y, track=TRACK):
- w = self.measure(text, size, weight, track)
- return self.outline(text, size, weight, center_x - w / 2, baseline_y, track)
- def si_glyph(slug):
- """simple-icons: single brand-colored 24x24 path, scaled into the glyph box."""
- svg = fetch(f"https://cdn.simpleicons.org/{slug}")
- fill = re.search(r'fill="(#[0-9A-Fa-f]{3,8})"', svg).group(1)
- d = re.search(r'<path d="([^"]+)"', svg).group(1)
- s = round(GLYPH_BOX / 24, 5)
- x = (TILE - GLYPH_BOX) / 2
- return f'<g transform="translate({x},{GLYPH_TOP}) scale({s})"><path fill="{fill}" d="{d}"/></g>'
- def devicon_glyph(name, variant):
- """devicon: multi-path 128x128 markup, embedded verbatim and scaled."""
- svg = fetch(DEVICON.format(name, variant))
- inner = re.search(r"<svg[^>]*>(.*)</svg>", svg, re.S).group(1).strip()
- s = round(GLYPH_BOX / 128, 5)
- x = (TILE - GLYPH_BOX) / 2
- return f'<g transform="translate({x},{GLYPH_TOP}) scale({s})">{inner}</g>'
- def custom_glyph(key, out):
- x = (TILE - GLYPH_BOX) / 2 # glyph box left edge (30)
- cx = TILE / 2 # 52
- cy = GLYPH_TOP + GLYPH_BOX / 2 # glyph box vertical center (40)
- if key == "cfml":
- # The community CFML mark: a <cf> tag, set in Archivo.
- size, wt = 20.0, 700
- baseline = round(cy + out.cap_height(size, wt) / 2, 2)
- d = out.centered("<cf>", size, wt, cx, baseline, track=0.4)
- return f'<path fill="#1b5ea6" d="{d}"/>'
- if key == "cobol":
- # A punched card: manila stock, clipped corner, punched rows.
- w, h = GLYPH_BOX, 28
- top, cut = round(cy - h / 2, 2), 7
- holes = []
- for row in range(3):
- hy = top + 7.4 + row * 6.4
- for col in range(7):
- if (row * 3 + col * 5) % 4 != 0: # deterministic "data" pattern
- holes.append(
- f'<rect x="{round(x + 4.4 + col * 5.4, 2)}" y="{round(hy, 2)}" width="2.4" height="4" fill="#6b5d33"/>'
- )
- card = (
- f'M{x + cut},{top} H{x + w} V{top + h} H{x} V{top + cut} Z'
- )
- return (
- f'<path d="{card}" fill="#ecdfb1" stroke="#b3a06a" stroke-width="1" stroke-linejoin="miter"/>'
- + "".join(holes)
- )
- if key == "vbnet":
- # The .NET-purple badge with a VB monogram.
- size, wt = 16.5, 720
- baseline = round(cy + out.cap_height(size, wt) / 2, 2)
- d = out.centered("VB", size, wt, cx, baseline, track=0.6)
- return (
- f'<rect x="{x}" y="{GLYPH_TOP}" width="{GLYPH_BOX}" height="{GLYPH_BOX}" rx="6" fill="#512bd4"/>'
- f'<path fill="{PAPER}" d="{d}"/>'
- )
- raise KeyError(key)
- def main():
- preview_path = None
- if "--preview" in sys.argv:
- preview_path = sys.argv[sys.argv.index("--preview") + 1]
- out = Outliner()
- label_baseline = round(GLYPH_TOP + GLYPH_BOX + LABEL_GAP + out.cap_height(LABEL_SIZE, LABEL_WT), 2)
- out_dir = os.path.join(os.path.dirname(__file__), "languages")
- os.makedirs(out_dir, exist_ok=True)
- bodies = {}
- for slug, label, source in LANGS:
- if source[0] == "si":
- glyph = si_glyph(source[1])
- elif source[0] == "devicon":
- glyph = devicon_glyph(source[1], source[2])
- else:
- glyph = custom_glyph(source[1], out)
- label_d = out.centered(label, LABEL_SIZE, LABEL_WT, TILE / 2, label_baseline)
- body = (
- f'<rect x="0.5" y="0.5" width="{TILE - 1}" height="{TILE - 1}" rx="8" fill="{PAPER}" stroke="{HAIRLINE}"/>\n'
- f" {glyph}\n"
- f' <path fill="{INK}" d="{label_d}"/>'
- )
- bodies[slug] = body
- svg = (
- f'<svg xmlns="http://www.w3.org/2000/svg" width="{TILE}" height="{TILE}" '
- f'viewBox="0 0 {TILE} {TILE}" role="img" aria-label="{label}">\n'
- f" <title>{label}</title>\n"
- f" {body}\n"
- f"</svg>\n"
- )
- path = os.path.join(out_dir, f"{slug}.svg")
- with open(path, "w") as fh:
- fh.write(svg)
- print(f"wrote {os.path.relpath(path, os.path.dirname(__file__))} ({len(svg)}b) {label}")
- if preview_path:
- cols, pad = 7, 8
- rows = -(-len(LANGS) // cols)
- w = cols * (TILE + pad) + pad
- h = rows * (TILE + pad) + pad
- cells = []
- for i, (slug, _, _) in enumerate(LANGS):
- tx = pad + (i % cols) * (TILE + pad)
- ty = pad + (i // cols) * (TILE + pad)
- cells.append(f'<g transform="translate({tx},{ty})">{bodies[slug]}</g>')
- light = "".join(cells)
- with open(preview_path, "w") as fh:
- fh.write(
- f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h * 2}" viewBox="0 0 {w} {h * 2}">'
- f'<rect width="{w}" height="{h}" fill="#ffffff"/>{light}'
- f'<g transform="translate(0,{h})"><rect width="{w}" height="{h}" fill="#0d1117"/>{light}</g>'
- f"</svg>\n"
- )
- print(f"wrote preview {preview_path} (top: light bg, bottom: dark bg)")
- if __name__ == "__main__":
- main()
|