char/iocgn-mono

iosevka variant inspired by Simple Köln-Bonn

git clone https://git.t4t.associates/char/iocgn-mono

Charlotte Somadd some symbols and arrows and stuff925b071

main
8.8 KiB187 linesraw
1"""Render README showcases and the full encoded character set."""
2
3import hashlib
4import json
5from pathlib import Path
6
7import aggdraw
8from fontTools.pens.basePen import BasePen
9from fontTools.pens.boundsPen import BoundsPen
10from fontTools.pens.transformPen import TransformPen
11from fontTools.ttLib import TTFont
12from PIL import Image, ImageDraw, ImageFont
13
14ROOT = Path(__file__).resolve().parent.parent
15CODE = '''use std::collections::BTreeMap;
16
17fn parse_env(input: &str) -> BTreeMap<&str, &str> {
18    input.lines()
19        .map(str::trim)
20        .filter(|line| !line.starts_with('#'))
21        .filter_map(|line| line.split_once('='))
22        .map(|(key, value)| (key.trim(), value.trim()))
23        .collect()
24}
25
26let env = parse_env("LANG=de_DE.UTF-8\\nPORT=8080");
27assert_eq!(env.get("PORT"), Some(&"8080"));'''
28STRESS = 'minimum / maximum / memory / window / www.example.org'
29BACKGROUND, INK, MUTED, RULE, ACCENT = '#ffffff', '#000000', '#6f6f6f', '#d7d7d7', '#595959'
30MARGIN = 64
31
32
33class OutlinePen(BasePen):
34    def __init__(self, glyphs):
35        super().__init__(glyphs)
36        self.path = aggdraw.Path()
37
38    def _moveTo(self, p):
39        self.path.moveto(*p)
40
41    def _lineTo(self, p):
42        self.path.lineto(*p)
43
44    def _curveToOne(self, a, b, c):
45        self.path.curveto(*a, *b, *c)
46
47    def _closePath(self):
48        self.path.close()
49
50
51def code_mask(font, rows, size):
52    """Rasterize (text, em, baseline[, x]) rows without shaping."""
53    supersample = 4
54    glyphs, cmap = font.getGlyphSet(), font.getBestCmap()
55    mask = Image.new('L', tuple(v * supersample for v in size))
56    canvas = aggdraw.Draw(mask)
57    for line, em, baseline, *position in rows:
58        scale = em / font['head'].unitsPerEm
59        x = position[0] if position else 38.0
60        pen = OutlinePen(glyphs)
61        for char in line:
62            name = cmap[ord(char)]
63            glyphs[name].draw(TransformPen(pen, (scale * supersample, 0, 0, -scale * supersample,
64                                                x * supersample, baseline * supersample)))
65            x += font['hmtx'][name][0] * scale
66        assert x < size[0] - 30, line
67        canvas.path(pen.path, aggdraw.Brush('white'))
68    canvas.flush()
69    return mask.resize(size, Image.Resampling.LANCZOS)
70
71
72def showcase(font, style, weight):
73    panel = Image.new('RGB', (1400, 1008), BACKGROUND)
74    draw = ImageDraw.Draw(panel)
75    path = ROOT / 'build/TTF' / f'iocgnMono-{style}.ttf'
76    label = ImageFont.truetype(str(path), size=20)
77    size_label = ImageFont.truetype(str(path), size=16)
78    draw.line((MARGIN, 240, panel.width - MARGIN, 240), fill=RULE, width=2)
79    draw.text((MARGIN, 264), '01: code', font=label, fill=MUTED)
80    draw.text((1080, 264), '02: distinct forms', font=label, fill=MUTED)
81    draw.line((1032, 264, 1032, 852), fill=RULE, width=2)
82    draw.text((1080, 500), '03: extended latin', font=label, fill=MUTED)
83    draw.text((1080, 676), '04: sizes', font=label, fill=MUTED)
84    for em, baseline in [(16, 742), (20, 786), (28, 840)]:
85        draw.text((1080, baseline), f'{em}', font=size_label, fill=MUTED, anchor='ls')
86    draw.line((MARGIN, 876, panel.width - MARGIN, 876), fill=RULE, width=2)
87    draw.text((MARGIN, 898), '05: samples', font=label, fill=MUTED)
88
89    rows = [('iocgn Mono', 112, 144, MARGIN),
90            ('Aa Bb Cc / 0123456789', 42, 206, MARGIN)]
91    rows += [(line, 30, 332 + i * 42, MARGIN) for i, line in enumerate(CODE.splitlines())]
92    rows += [(line, 48, 356 + i * 54, 1080) for i, line in enumerate(('0O 1Il|', '8B 6G 9g', 'aopq mWw'))]
93    rows += [('ÀÉÎÕÜ ß æ œ', 36, 588, 1080), ('āčę ł ñ ø ž', 36, 640, 1080)]
94    rows += [(text, em, baseline, 1128) for text, em, baseline in
95             [('let port = 8080;', 16, 742), ('path.join("src")', 20, 786), ('Some(&"UTF-8")', 28, 840)]]
96    rows += [(STRESS, 24, 966, MARGIN)]
97    panel.paste(INK, (0, 0), code_mask(font, rows, panel.size))
98    return panel
99
100
101def character_sheet(fonts, extended=False):
102    coverage = sorted(fonts[0].getBestCmap())
103    assert all(sorted(font.getBestCmap()) == coverage for font in fonts)
104    coverage = [cp for cp in coverage if (cp >= 0x180) == extended]
105    blocks = [('ascii', 0x20, 0x7F),
106              ('latin-1 supplement', 0xA0, 0x100),
107              ('latin extended-a', 0x100, 0x180),
108              ('combining accents', 0x300, 0x370),
109              ('punctuation', 0x2000, 0x2070),
110              ('arrows', 0x2190, 0x2200),
111              ('mathematical symbols', 0x2200, 0x2300),
112              ('box drawing', 0x2500, 0x2580),
113              ('block elements', 0x2580, 0x25A0),
114              ('geometric indicators', 0x25A0, 0x2600),
115              ('checkboxes and stars', 0x2600, 0x2700),
116              ('checks, crosses and navigation', 0x2700, 0x2800),
117              ('braille patterns', 0x2800, 0x2900),
118              ('replacement character', 0xFFFD, 0xFFFE)]
119    sections = [(title, points) for title, start, end in blocks
120                if (points := [cp for cp in coverage if start <= cp < end])]
121    assert sorted(cp for _, points in sections for cp in points) == coverage
122    columns, cell_width, cell_height = 16, 92, 108
123    height = 320 + sum(72 + ((len(points) + columns - 1) // columns) * cell_height for _, points in sections)
124    panel = Image.new('RGB', (1600, height), BACKGROUND)
125    draw = ImageDraw.Draw(panel)
126    path = ROOT / 'build/TTF/iocgnMono-Regular.ttf'
127    label = ImageFont.truetype(str(path), size=32)
128    small = ImageFont.truetype(str(path), size=24)
129    title = 'other characters' if extended else 'ascii + latin-1 + latin extended-a'
130    draw.text((MARGIN, 48), f'iocgn mono / {title}', font=label, fill=ACCENT, anchor='lt')
131    draw.text((MARGIN, 230), 'Regular + Bold', font=label, fill=MUTED)
132    sample = '→✓□ / ⣿' if extended else 'Aa / Àæ / Āčę'
133    rows = [[(sample, 96, 192, MARGIN)], []]
134    sources = [(font.getGlyphSet(), font.getBestCmap(), 48 / font['head'].unitsPerEm) for font in fonts]
135    y = 284
136    for title, points in sections:
137        draw.line((MARGIN, y, 1536, y), fill=RULE, width=2)
138        draw.text((MARGIN, y + 20), title, font=label, fill=ACCENT)
139        y += 72
140        for i, cp in enumerate(points):
141            left, top = MARGIN + (i % columns) * cell_width, y + (i // columns) * cell_height
142            draw.text((left + cell_width / 2, top + 78), f'{cp:04X}', font=small, fill=MUTED, anchor='mt')
143            for style, (glyphs, cmap, scale) in enumerate(sources):
144                pen = BoundsPen(glyphs)
145                glyphs[cmap[cp]].draw(pen)
146                center = left + (27 if style == 0 else 65)
147                if pen.bounds:
148                    # Center the ink, including zero-advance marks, rather than the advance cell.
149                    x = center - (pen.bounds[0] + pen.bounds[2]) / 2 * scale
150                    rows[style].append((chr(cp), 48, top + 58, x))
151                else:
152                    assert cp in (0x20, 0xA0, 0x2800), f'Unexpected empty glyph: U+{cp:04X}'
153                    draw.text((center, top + 40), {0x20: 'SP', 0xA0: 'NB', 0x2800: 'BL'}[cp], font=small, fill=MUTED, anchor='mm')
154        y += ((len(points) + columns - 1) // columns) * cell_height
155    for font, glyph_rows in zip(fonts, rows):
156        panel.paste(INK, (0, 0), code_mask(font, glyph_rows, panel.size))
157    legend = 'BL = blank braille' if extended else 'SP = space / NB = non-breaking space'
158    draw.text((MARGIN, height - 42), legend, font=small, fill=MUTED)
159    return panel, coverage
160
161
162def main():
163    output = ROOT / 'demos'
164    output.mkdir(exist_ok=True)
165    manifest = {'rendering': 'unhinted vectors, 4x supersampling; fixed advances; no kerning or ligatures',
166                'em_pixels': [112, 48, 42, 36, 30, 28, 24, 20, 16],
167                'renderer_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
168                'fonts': {}}
169    with TTFont(ROOT / 'build/TTF/iocgnMono-Regular.ttf') as regular, \
170         TTFont(ROOT / 'build/TTF/iocgnMono-Bold.ttf') as bold:
171        for font, style, weight in [(regular, 'Regular', 400), (bold, 'Bold', 700)]:
172            path = ROOT / 'build/TTF' / f'iocgnMono-{style}.ttf'
173            manifest['fonts'][path.name] = hashlib.sha256(path.read_bytes()).hexdigest()
174            showcase(font, style, weight).save(output / f'{style.lower()}.png')
175        manifest['character_sheets'] = {}
176        for extended, filename in ((False, 'characters.png'), (True, 'characters-extended.png')):
177            sheet, coverage = character_sheet((regular, bold), extended=extended)
178            sheet.save(output / filename)
179            manifest['character_sheets'][filename] = {
180                'em_pixels': [96, 48], 'codepoints': coverage,
181                'styles': ['Regular', 'Bold'], 'combining_marks': 'isolated outlines'}
182    (ROOT / 'build/demo-manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
183    print('Rendered demos/regular.png, demos/bold.png, demos/characters.png and demos/characters-extended.png.')
184
185
186if __name__ == '__main__':
187    main()