forked from microsoft/cascadia-code
-
Notifications
You must be signed in to change notification settings - Fork 0
/
build.py
427 lines (362 loc) · 14 KB
/
build.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
import argparse
import multiprocessing
import multiprocessing.pool
import os
import subprocess
from pathlib import Path
from typing import cast
import cffsubr.__main__
import fontmake.instantiator
import fontTools.designspaceLib
import fontTools.ttLib
import fontTools.ttLib.tables._g_l_y_f as _g_l_y_f
import psautohint.__main__
import statmake.classes
import statmake.lib
import ufo2ft
import ufoLib2
import vttLib
import vttLib.transfer
VERSION_YEAR_MONTH = 2009
VERSION_DAY = 22
OUTPUT_DIR = Path("build")
OUTPUT_OTF_DIR = OUTPUT_DIR / "otf"
OUTPUT_TTF_DIR = OUTPUT_DIR / "ttf"
OUTPUT_WOFF2_DIR = OUTPUT_DIR / "woff2"
OUTPUT_STATIC_OTF_DIR = OUTPUT_OTF_DIR / "static"
OUTPUT_STATIC_TTF_DIR = OUTPUT_TTF_DIR / "static"
OUTPUT_STATIC_WOFF2_DIR = OUTPUT_WOFF2_DIR / "static"
INPUT_DIR = Path("sources")
VTT_DATA_FILE = INPUT_DIR / "vtt_data" / "CascadiaCode.ttx"
FEATURES_DIR = INPUT_DIR / "features"
NERDFONTS_DIR = INPUT_DIR / "nerdfonts"
# Font modifications
# ****************************************************************
def step_set_font_name(name: str, instance: ufoLib2.Font) -> None:
instance.info.familyName = name
# We have to change the style map family name because that's what
# Windows uses to map Bold/Regular/Medium/etc. fonts
instance.info.styleMapFamilyName = name
def step_merge_glyphs_from_ufo(path: Path, instance: ufoLib2.Font) -> None:
ufo = ufoLib2.Font.open(path)
for glyph in ufo.glyphOrder:
if glyph not in instance.glyphOrder:
instance.addGlyph(ufo[glyph])
def step_set_feature_file(path: Path, instance: ufoLib2.Font) -> None:
instance.features.text = path.read_text()
def set_font_metaData(font: ufoLib2.Font) -> None:
font.info.versionMajor = VERSION_YEAR_MONTH
font.info.versionMinor = VERSION_DAY
font.info.openTypeOS2TypoAscender = 1900
font.info.openTypeOS2TypoDescender = -480
font.info.openTypeOS2TypoLineGap = 0
font.info.openTypeHheaAscender = font.info.openTypeOS2TypoAscender
font.info.openTypeHheaDescender = font.info.openTypeOS2TypoDescender
font.info.openTypeHheaLineGap = font.info.openTypeOS2TypoLineGap
font.info.openTypeOS2WinAscent = 2226
font.info.openTypeOS2WinDescent = abs(font.info.openTypeOS2TypoDescender)
font.info.openTypeGaspRangeRecords = [
{"rangeMaxPPEM": 9, "rangeGaspBehavior": [1, 3]},
{"rangeMaxPPEM": 50, "rangeGaspBehavior": [0, 1, 2, 3]},
{"rangeMaxPPEM": 65535, "rangeGaspBehavior": [1, 3]},
]
def set_overlap_flag(varfont: fontTools.ttLib.TTFont) -> fontTools.ttLib.TTFont:
glyf = cast(_g_l_y_f.table__g_l_y_f, varfont["glyf"])
for glyph_name in glyf.keys():
glyph = glyf[glyph_name]
if glyph.isComposite():
# Set OVERLAP_COMPOUND bit for compound glyphs
glyph.components[0].flags |= 0x400
elif glyph.numberOfContours > 0:
# Set OVERLAP_SIMPLE bit for simple glyphs
glyph.flags[0] |= 0x40
def manualHacks(varfont: fontTools.ttLib.TTFont) -> fontTools.ttLib.TTFont:
varfont["head"].flags = 0x000b
varfont.importXML(INPUT_DIR / "cvar.ttx")
def prepare_fonts(
designspace: fontTools.designspaceLib.DesignSpaceDocument, name: str
) -> None:
designspace.loadSourceFonts(ufoLib2.Font.open)
for source in designspace.sources:
if "Mono" in name and "PL" in name:
step_set_feature_file(FEATURES_DIR / "features_mono_PL.fea", source.font)
print(f"[{name} {source.styleName}] Merging PL glyphs")
step_merge_glyphs_from_ufo(
NERDFONTS_DIR / "NerdfontsPL-Regular.ufo", source.font
)
step_set_font_name(name, source.font)
elif "Mono" in name:
step_set_feature_file(FEATURES_DIR / "features_mono.fea", source.font)
step_set_font_name(name, source.font)
elif "PL" in name:
step_set_feature_file(FEATURES_DIR / "features_code_PL.fea", source.font)
print(f"[{name} {source.styleName}] Merging PL glyphs")
step_merge_glyphs_from_ufo(
NERDFONTS_DIR / "NerdfontsPL-Regular.ufo", source.font
)
step_set_font_name(name, source.font)
elif name == "Cascadia Code":
step_set_feature_file(FEATURES_DIR / "features_code.fea", source.font)
else:
print("Variant name not identified. Please check.")
set_font_metaData(source.font)
def to_woff2(source_path: Path, target_path: Path) -> None:
print(f"[WOFF2] Compressing {source_path} to {target_path}")
font = fontTools.ttLib.TTFont(source_path)
font.flavor = "woff2"
target_path.parent.mkdir(exist_ok=True, parents=True)
font.save(target_path)
# Build fonts
# ****************************************************************
def build_font_variable(
designspace: fontTools.designspaceLib.DesignSpaceDocument,
name: str,
vtt_compile: bool = True,
) -> None:
prepare_fonts(designspace, name)
compile_variable_and_save(designspace, vtt_compile)
def build_font_static(
designspace: fontTools.designspaceLib.DesignSpaceDocument,
instance_descriptor: fontTools.designspaceLib.InstanceDescriptor,
name: str,
) -> None:
prepare_fonts(designspace, name)
generator = fontmake.instantiator.Instantiator.from_designspace(designspace)
instance = generator.generate_instance(instance_descriptor)
step_set_font_name(name, instance)
compile_static_and_save(instance)
# Export fonts
# ****************************************************************
def compile_variable_and_save(
designspace: fontTools.designspaceLib.DesignSpaceDocument,
vtt_compile: bool = True,
) -> None:
familyName = designspace.default.font.info.familyName
file_stem = familyName.replace(" ", "")
file_path: Path = (OUTPUT_TTF_DIR / file_stem).with_suffix(".ttf")
print(f"[{familyName}] Compiling")
varFont = ufo2ft.compileVariableTTF(designspace, inplace=True)
print(f"[{familyName}] Adding STAT table")
styleSpace = statmake.classes.Stylespace.from_file(INPUT_DIR / "STAT.plist")
statmake.lib.apply_stylespace_to_variable_font(styleSpace, varFont, {})
print(f"[{familyName}] Merging VTT")
vttLib.transfer.merge_from_file(varFont, VTT_DATA_FILE)
if vtt_compile:
print(f"[{familyName}] Compiling VTT")
vttLib.compile_instructions(varFont, ship=True)
set_overlap_flag(varFont)
# last minute manual corrections to set things correctly
manualHacks(varFont)
print(f"[{familyName}] Saving")
file_path.parent.mkdir(exist_ok=True, parents=True)
varFont.save(file_path)
print(f"[{familyName}] Done: {file_path}")
def compile_static_and_save(instance: ufoLib2.Font) -> None:
family_name = instance.info.familyName
style_name = instance.info.styleName
print(f"[{family_name}] Building static instance: {style_name}")
# Use pathops backend for overlap removal because it is, at the time of this
# writing, massively faster than booleanOperations and thanks to autohinting,
# there is no need to keep outlines compatible to previous releases.
static_ttf = ufo2ft.compileTTF(
instance, removeOverlaps=True, overlapsBackend="pathops"
)
static_otf = ufo2ft.compileOTF(
instance,
removeOverlaps=True,
overlapsBackend="pathops",
# Can do inplace now because TTF is already done.
inplace=True,
# Don't optimize here, will be optimized after autohinting.
optimizeCFF=ufo2ft.CFFOptimization.NONE,
)
file_name = f"{family_name}-{style_name}".replace(" ", "")
file_path_static = (OUTPUT_STATIC_TTF_DIR / file_name).with_suffix(".ttf")
file_path_static_otf = (OUTPUT_STATIC_OTF_DIR / file_name).with_suffix(".otf")
file_path_static.parent.mkdir(exist_ok=True, parents=True)
static_ttf.save(file_path_static)
file_path_static_otf.parent.mkdir(exist_ok=True, parents=True)
static_otf.save(file_path_static_otf)
print(f"[{family_name}] Done: {file_path_static}, {file_path_static_otf}")
# Font hinting
# ****************************************************************
def autohint(otf_path: Path) -> None:
path = os.fspath(otf_path)
print(f"Autohinting {path}")
psautohint.__main__.main([path])
print(f"Compressing {path}")
cffsubr.__main__.main(["-i", path])
def ttfautohint(path: str) -> None:
print(f"Autohinting {path}")
subprocess.check_call(
[
"ttfautohint",
"--stem-width",
"nsn",
"--reference",
os.fspath(OUTPUT_STATIC_TTF_DIR / "CascadiaCode-Regular.ttf"),
path,
path[:-4] + "-hinted.ttf",
]
)
os.remove(path)
os.rename(path[:-4] + "-hinted.ttf", path)
# Main build script
# ****************************************************************
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="build some fonts")
parser.add_argument("-P", "--no-powerline", action="store_false", dest="powerline")
parser.add_argument("-M", "--no-mono", action="store_false", dest="mono")
parser.add_argument("-S", "--static-fonts", action="store_true")
parser.add_argument(
"-V",
"--no-vtt-compile",
action="store_false",
dest="vtt_compile",
help="Do not compile VTT code but leave in the VTT sources.",
)
parser.add_argument("-W", "--web-fonts", action="store_true")
args = parser.parse_args()
# Load Designspace and filter out instances that are marked as non-exportable.
designspace = fontTools.designspaceLib.DesignSpaceDocument.fromfile(
INPUT_DIR / "CascadiaCode.designspace"
)
designspace.instances = [
s
for s in designspace.instances
if s.lib.get("com.schriftgestaltung.export", True)
]
# Stage 1: Make all the things.
pool = multiprocessing.pool.Pool(processes=multiprocessing.cpu_count())
processes = []
processes.append(
pool.apply_async(
build_font_variable,
(
designspace,
"Cascadia Code",
args.vtt_compile,
),
)
)
if args.mono:
processes.append(
pool.apply_async(
build_font_variable,
(
designspace,
"Cascadia Mono",
args.vtt_compile,
),
)
)
if args.powerline:
processes.append(
pool.apply_async(
build_font_variable,
(
designspace,
"Cascadia Code PL",
args.vtt_compile,
),
)
)
if args.mono:
processes.append(
pool.apply_async(
build_font_variable,
(
designspace,
"Cascadia Mono PL",
args.vtt_compile,
),
)
)
if args.static_fonts:
for instance_descriptor in designspace.instances:
processes.append(
pool.apply_async(
build_font_static,
(
designspace,
instance_descriptor,
"Cascadia Code",
),
)
)
if args.mono:
processes.append(
pool.apply_async(
build_font_static,
(
designspace,
instance_descriptor,
"Cascadia Mono",
),
)
)
if args.powerline:
processes.append(
pool.apply_async(
build_font_static,
(
designspace,
instance_descriptor,
"Cascadia Code PL",
),
)
)
if args.mono:
processes.append(
pool.apply_async(
build_font_static,
(
designspace,
instance_descriptor,
"Cascadia Mono PL",
),
)
)
pool.close()
pool.join()
for process in processes:
process.get()
del processes, pool
# Stage 2: Autohint and maybe compress all the static things.
if args.static_fonts is True:
otfs = list(OUTPUT_STATIC_OTF_DIR.glob("*.otf"))
if otfs:
pool = multiprocessing.Pool(processes=multiprocessing.cpu_count())
processes = [pool.apply_async(autohint, (otf,)) for otf in otfs]
pool.close()
pool.join()
for process in processes:
process.get()
del processes, pool
try:
for ttf_path in OUTPUT_STATIC_TTF_DIR.glob("*.ttf"):
if not ttf_path.stem.endswith("-hinted"):
ttfautohint(os.fspath(ttf_path))
except Exception as e:
print(f"ttfautohint failed. Please reinstall and try again. {str(e)}")
# Stage 3: Have some web fonts.
if args.web_fonts:
pool = multiprocessing.Pool(processes=multiprocessing.cpu_count())
processes = [
pool.apply_async(
to_woff2,
(
path,
# This removes build/ttf from the found files and prepends
# build/woff2 instead, keeping the sub-structure.
OUTPUT_WOFF2_DIR
/ path.relative_to(OUTPUT_TTF_DIR).with_suffix(".woff2"),
),
)
for path in OUTPUT_TTF_DIR.glob("**/*.ttf")
]
pool.close()
pool.join()
for process in processes:
process.get()
print("All done.")