| 1 | """Generate the extension's icons from Diffinery's mark. |
| 2 | |
| 3 | python3 -m venv /tmp/v && /tmp/v/bin/pip install fonttools picosvg brotli pillow |
| 4 | /tmp/v/bin/python apps/vscode/scripts/make-icons.py |
| 5 | |
| 6 | Writes apps/vscode/media/icon.png (the extension's 128 px icon, the site's app icon scaled |
| 7 | down) and apps/vscode/media/diffinery-icons.woff (a one-glyph icon font, so the clone picker |
| 8 | can show the mark as `$(diffinery-logo)` next to "Clone from Diffinery", the way GitHub's shows its |
| 9 | logo). The output is committed, so builds need none of these tools. |
| 10 | """ |
| 11 | import base64 |
| 12 | import io |
| 13 | import os |
| 14 | import re |
| 15 | |
| 16 | from fontTools.fontBuilder import FontBuilder |
| 17 | from fontTools.pens.cu2quPen import Cu2QuPen |
| 18 | from fontTools.pens.transformPen import TransformPen |
| 19 | from fontTools.pens.ttGlyphPen import TTGlyphPen |
| 20 | from fontTools.svgLib.path import SVGPath |
| 21 | from PIL import Image |
| 22 | from picosvg.svg import SVG |
| 23 | |
| 24 | HERE = os.path.dirname(os.path.abspath(__file__)) |
| 25 | MEDIA = os.path.join(HERE, "..", "media") |
| 26 | SITE_ICONS = os.path.join(HERE, "..", "..", "diffinery", "src", "web", "icons.generated.ts") |
| 27 | # The mark, as in apps/diffinery/src/web/assets.ts (ICONS.logo), on a 24-unit grid. |
| 28 | MARK = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><path d="M6 8.5v7M8.3 7.2c4 1 7.4 2.2 7.6 3.3M8.3 16.8c4-1 7.4-2.2 7.6-3.3"/></svg>""" |
| 29 | CODEPOINT = 0xE001 |
| 30 | UPM = 1000 |
| 31 | |
| 32 | |
| 33 | def extension_icon(): |
| 34 | src = open(SITE_ICONS).read() |
| 35 | m = re.search(r'"icon-512\.png":\s*"([A-Za-z0-9+/=]+)"', src) |
| 36 | if not m: |
| 37 | raise SystemExit("icon-512.png not found in icons.generated.ts") |
| 38 | img = Image.open(io.BytesIO(base64.b64decode(m.group(1)))).convert("RGBA") |
| 39 | img.resize((128, 128), Image.LANCZOS).save(os.path.join(MEDIA, "icon.png"), optimize=True) |
| 40 | |
| 41 | |
| 42 | def icon_font(): |
| 43 | # Strokes become filled outlines, as a font needs. |
| 44 | flat = SVG.fromstring(MARK).topicosvg() |
| 45 | fb = FontBuilder(UPM, isTTF=True) |
| 46 | fb.setupGlyphOrder([".notdef", "diffinery"]) |
| 47 | fb.setupCharacterMap({CODEPOINT: "diffinery"}) |
| 48 | pen = TTGlyphPen(None) |
| 49 | # 24 units -> 1000, y up, baseline at the bottom of the grid. |
| 50 | scale = UPM / 24 |
| 51 | for el in flat.shapes(): |
| 52 | SVGPath.fromstring(f'<svg xmlns="http://www.w3.org/2000/svg"><path d="{el.d}"/></svg>').draw( |
| 53 | TransformPen(Cu2QuPen(pen, 1.0, reverse_direction=True), (scale, 0, 0, -scale, 0, UPM)) |
| 54 | ) |
| 55 | glyphs = {".notdef": TTGlyphPen(None).glyph(), "diffinery": pen.glyph()} |
| 56 | fb.setupGlyf(glyphs) |
| 57 | fb.setupHorizontalMetrics({".notdef": (UPM, 0), "diffinery": (UPM, 0)}) |
| 58 | fb.setupHorizontalHeader(ascent=UPM, descent=0) |
| 59 | fb.setupNameTable({"familyName": "diffinery-icons", "styleName": "Regular"}) |
| 60 | fb.setupOS2(sTypoAscender=UPM, sTypoDescender=0, usWinAscent=UPM, usWinDescent=0) |
| 61 | fb.setupPost() |
| 62 | fb.font.flavor = "woff" |
| 63 | fb.save(os.path.join(MEDIA, "diffinery-icons.woff")) |
| 64 | |
| 65 | |
| 66 | if __name__ == "__main__": |
| 67 | os.makedirs(MEDIA, exist_ok=True) |
| 68 | extension_icon() |
| 69 | icon_font() |
| 70 | print("wrote media/icon.png and media/diffinery-icons.woff") |