whiskers.git / third-party / jevcrates / jev-ui / assets / fonts / build-pixel-emoji.py
1#!/usr/bin/env -S uv run --quiet --with pillow --with nanoemoji --with fonttools --with brotli python
2"""Builds pixel-emoji.woff2: a few of SerenityOS's emoji as a small colour font.
3
4    build-pixel-emoji.py <serenity checkout>/Base/res/emoji pixel-emoji.woff2
5
6Each glyph is SerenityOS's own PNG, one SVG path per colour made of a rectangle for each run of equal pixels, put through
7nanoemoji exactly as linusg/serenityos-emoji-font does (glyf_colr_1). The set is GLYPHS below;
8a new codepoint is added here, to `PixelEmoji` in src/components.rs and to the `unicode-range`
9of the `Pixel Emoji` face in ui.css, and a test holds the last two together.
10"""
11import subprocess, sys, tempfile
12from pathlib import Path
13from PIL import Image
14from fontTools.ttLib import TTFont
15
16GLYPHS = ["25B6", "23F8", "23F3", "231B"]
17
18
19def svg(png: Path) -> str:
20    """One path per colour, a rectangle for each run of equal pixels in a row: the runs are one
21    shape, so no hairline shows where two of them meet."""
22    im = Image.open(png).convert("RGBA")
23    paths: dict[str, list[str]] = {}
24    for y in range(im.height):
25        x = 0
26        while x < im.width:
27            p = im.getpixel((x, y))
28            if p[3] == 0:
29                x += 1
30                continue
31            x2 = x
32            while x2 + 1 < im.width and im.getpixel((x2 + 1, y)) == p:
33                x2 += 1
34            paths.setdefault(f"#{p[0]:02x}{p[1]:02x}{p[2]:02x}", []).append(f"M{x} {y}h{x2 - x + 1}v1h-{x2 - x + 1}z")
35            x = x2 + 1
36    body = "".join(f'<path fill="{color}" d="{"".join(runs)}"/>' for color, runs in paths.items())
37    return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {im.width} {im.height}" width="{im.width}" height="{im.height}">{body}</svg>'
38
39
40def main(emoji_dir: str, dest: str) -> None:
41    with tempfile.TemporaryDirectory() as tmp:
42        tmp = Path(tmp)
43        svgs = []
44        for cp in GLYPHS:
45            path = tmp / f"{cp}.svg"
46            path.write_text(svg(Path(emoji_dir) / f"U+{cp}.png"))
47            svgs.append(str(path))
48        subprocess.check_call(
49            ["nanoemoji", "--family", "Pixel Emoji", "--output_file", "PixelEmoji.ttf", "--color_format", "glyf_colr_1", *svgs],
50            cwd=tmp,
51        )
52        font = TTFont(tmp / "build" / "PixelEmoji.ttf")
53        font.flavor = "woff2"
54        font.save(dest)
55
56
57if __name__ == "__main__":
58    main(sys.argv[1], sys.argv[2])