char/iocgn-mono

iosevka variant inspired by Simple Köln-Bonn

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

Charlotte Somsimplify pipeline even more (less python!! :D)2ba32a9

main
7.7 KiB147 linesraw
1"""Combine the upright and ten-degree oblique into one weight/italic variable font."""
2
3import hashlib
4import json
5from pathlib import Path
6
7from fontTools.designspaceLib import AxisDescriptor, DesignSpaceDocument, InstanceDescriptor, SourceDescriptor
8from fontTools.otlLib.builder import buildStatTable
9from fontTools.pens.recordingPen import DecomposingRecordingPen
10from fontTools.pens.ttGlyphPen import TTGlyphPen
11from fontTools.ttLib import TTFont
12from fontTools.varLib import build
13from fontTools.varLib.instancer import instantiateVariableFont
14
15from masters import MASTERS, STATIC_STYLES
16from oblique import name_oblique
17
18ROOT = Path(__file__).resolve().parent.parent
19VERSION = 0.517
20
21
22def main():
23    output = ROOT / 'build/combined'
24    output.mkdir(parents=True, exist_ok=True)
25    designspace = DesignSpaceDocument()
26    for name, tag, minimum, default, maximum in (('Weight', 'wght', 200, 400, 800),
27                                                ('Italic', 'ital', 0, 0, 1)):
28        axis = AxisDescriptor()
29        axis.name, axis.tag = name, tag
30        axis.minimum, axis.default, axis.maximum = minimum, default, maximum
31        designspace.addAxis(axis)
32    signatures = None
33    order = None
34    sources = {}
35    for italic, directory in ((0, 'variable'), (1, 'oblique')):
36        for style, weight in MASTERS:
37            path = ROOT / 'build' / directory / f'{style}.ttf'
38            sources[str(path.relative_to(ROOT))] = hashlib.sha256(path.read_bytes()).hexdigest()
39            with TTFont(path, recalcTimestamp=False) as font:
40                if order is None:
41                    order = font.getGlyphOrder()
42                if font.getGlyphOrder() != order:
43                    raise ValueError(f'{path}: incompatible glyph order')
44                glyphs = font.getGlyphSet()
45                replacements = {}
46                # gvar can vary component offsets, but not their affine transforms.
47                for name in order:
48                    if not font['glyf'][name].isComposite():
49                        continue
50                    recording = DecomposingRecordingPen(glyphs)
51                    glyphs[name].draw(recording)
52                    pen = TTGlyphPen(None)
53                    recording.replay(pen)
54                    glyph = pen.glyph()
55                    if glyph.numberOfContours:
56                        glyph.flags[0] |= 0x40  # Preserve overlapping accent joins after decomposition.
57                    replacements[name] = glyph
58                for name, glyph in replacements.items():
59                    font['glyf'][name] = glyph
60                    glyph.recalcBounds(font['glyf'])
61                    font['hmtx'][name] = (font['hmtx'][name][0], getattr(glyph, 'xMin', 0))
62                # Like varLib, which otherwise skips the glyph with only a warning, compare every flag.
63                current = {name: (tuple(g.endPtsOfContours), tuple(g.flags))
64                           for name in order if (g := font['glyf'][name]).numberOfContours}
65                if signatures is None:
66                    signatures = current
67                elif signatures != current:
68                    different = [name for name in order if signatures.get(name) != current.get(name)]
69                    raise ValueError(f'{path}: incompatible point structures: {different}')
70                master = output / f'{style}-{italic}.ttf'
71                font['hhea'].caretSlopeRise = 1000
72                font.save(master)
73            source = SourceDescriptor()
74            source.name, source.path = f'{style}-{italic}', str(master)
75            source.familyName = 'iocgn Mono'
76            source.styleName = ('Italic' if style == 'Regular' else f'{style} Italic') if italic else style
77            source.location = {'Weight': weight, 'Italic': italic}
78            source.copyInfo = source.copyLib = source.copyFeatures = weight == 400 and italic == 0
79            designspace.addSource(source)
80    weights = (*MASTERS[:2], *STATIC_STYLES, *MASTERS[2:])
81    for italic in (0, 1):
82        for style, weight in weights:
83            instance = InstanceDescriptor()
84            instance.familyName = 'iocgn Mono'
85            instance.styleName = ('Italic' if style == 'Regular' else f'{style} Italic') if italic else style
86            instance.postScriptFontName = f'iocgnMono-{instance.styleName.replace(" ", "")}'
87            instance.location = {'Weight': weight, 'Italic': italic}
88            designspace.addInstance(instance)
89    designspace.write(output / 'iocgn.designspace')
90    variable, _, _ = build(designspace, optimize=False)
91    variable['name'].removeNames(platformID=1)
92    # Approximate IUP can make accented letters' base outlines diverge.
93    for name, variations in variable['gvar'].variations.items():
94        coordinates, controls = variable['glyf']._getCoordinatesAndControls(name, variable['hmtx'].metrics)
95        for variation in variations:
96            variation.optimize(coordinates, controls.endPts, tolerance=0)
97    buildStatTable(variable, [
98        {'tag': 'wght', 'name': 'Weight', 'values': [
99            {'name': style, 'value': weight, **({'flags': 2, 'linkedValue': 700} if weight == 400 else {})}
100            for style, weight in weights]},
101        {'tag': 'ital', 'name': 'Italic', 'values': [
102            {'name': 'Roman', 'value': 0, 'flags': 2, 'linkedValue': 1},
103            {'name': 'Italic', 'value': 1},
104        ]},
105    ])
106    variable['head'].fontRevision = version = VERSION
107    variable['OS/2'].recalcUnicodeRanges(variable)
108    variable['OS/2'].recalcCodePageRanges(variable)
109    values = {3: f'iocgnMono-Variable-{version:.3f}', 4: 'iocgn Mono',
110              5: f'Version {version:.3f}; variable weight and italic', 6: 'iocgnMono-Variable', 25: 'iocgnMono'}
111    for record in variable['name'].names:
112        if record.nameID in values:
113            variable['name'].setName(values[record.nameID], record.nameID,
114                                     record.platformID, record.platEncID, record.langID)
115    for name_id, value in values.items():
116        variable['name'].setName(value, name_id, 3, 1, 0x409)
117    variable.recalcTimestamp = False
118    variable['head'].created = variable['head'].modified = 2082844800
119    destination = ROOT / 'build/TTF/iocgnMono-Variable.ttf'
120    destination.parent.mkdir(parents=True, exist_ok=True)
121    variable.save(destination)
122    static_paths = []
123    # Reload compiled tables: varLib leaves optional GDEF fields unset.
124    with TTFont(destination, recalcTimestamp=False) as compiled:
125        for style, weight in (('Regular', 400), *STATIC_STYLES, ('Bold', 700)):
126            for italic in (0, 1):
127                font = instantiateVariableFont(compiled, {'wght': weight, 'ital': italic}, updateFontNames=True)
128                label = style
129                if italic:
130                    name_oblique(font, style, weight)
131                    label = 'Oblique' if style == 'Regular' else f'{style}Oblique'
132                path = destination.with_name(f'iocgnMono-{label}.ttf')
133                font.save(path)
134                font.close()
135                static_paths.append(path)
136                print(f'Built {path.name}: static wght {weight}, ital {italic}.')
137    report = {'axes': {'wght': [200, 400, 800], 'ital': [0, 0, 1]}, 'sources': sources,
138              'builder_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
139              'static_fonts': {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in static_paths},
140              'font': {'path': str(destination.relative_to(ROOT)),
141                       'sha256': hashlib.sha256(destination.read_bytes()).hexdigest()}}
142    (output / 'manifest.json').write_text(json.dumps(report, indent=2) + '\n')
143    print(f'Built {destination.name}: wght 200–800 and ital 0 upright / 1 ten-degree oblique.')
144
145
146if __name__ == '__main__':
147    main()