68 lines
3.8 KiB
Python
68 lines
3.8 KiB
Python
"""Offline font subset builder. Sources and fonttools are build-only, not shipped.
|
|
|
|
Run with fonttools on PYTHONPATH (e.g. .font-tools), from this directory.
|
|
Source downloads: https://github.com/google/fonts/tree/main/ofl/{nunito,notosanssc}
|
|
"""
|
|
from pathlib import Path
|
|
from hashlib import sha256
|
|
import json
|
|
from fontTools.ttLib import TTFont
|
|
from fontTools import subset
|
|
from fontTools.varLib.instancer import instantiateVariableFont
|
|
|
|
root = Path(__file__).resolve().parent
|
|
corpus = ''.join(p.read_text(encoding='utf-8') for p in (root / 'src').rglob('*')
|
|
if p.suffix in ('.mjs', '.json'))
|
|
chars = ''.join(sorted(set(corpus + ''.join(chr(i) for i in range(32, 127)) + '✓→…!:·')))
|
|
manifest = {'generator': 'fonttools', 'sources': [], 'outputs': [], 'characters': chars}
|
|
for family, weights in [('notosanssc', [500, 600, 700]), ('nunito', [800])]:
|
|
source = root / '.font-sources' / (family + '.ttf')
|
|
manifest['sources'].append({'family': family, 'sha256': sha256(source.read_bytes()).hexdigest(),
|
|
'url': 'https://github.com/google/fonts/tree/main/ofl/' + family})
|
|
for weight in weights:
|
|
font = TTFont(source)
|
|
options = subset.Options()
|
|
options.name_IDs = ['*']
|
|
sub = subset.Subsetter(options=options)
|
|
text = chars if family == 'notosanssc' else '0123456789'
|
|
sub.populate(text=text)
|
|
sub.subset(font)
|
|
font = instantiateVariableFont(font, {'wght': weight}, inplace=True)
|
|
name = ('NumberSwipeSans' if family == 'notosanssc' else 'NumberSwipeDigits') + str(weight)
|
|
for record in font['name'].names:
|
|
if record.nameID in (1, 2, 3, 4, 6, 16, 17):
|
|
value = 'Regular' if record.nameID in (2, 17) else name
|
|
record.string = value.encode(record.getEncoding(), errors='replace')
|
|
target = root / 'fonts' / (name + '.ttf')
|
|
font.save(target)
|
|
cmap = font.getBestCmap()
|
|
missing = sorted({c for c in text if not c.isspace() and ord(c) not in cmap})
|
|
# Source fonts don't contain code symbols like private-use/non-printing chars;
|
|
# all player-visible Han characters and digits must be present.
|
|
critical = [c for c in missing if '\u4e00' <= c <= '\u9fff' or c.isdigit()]
|
|
if critical:
|
|
raise ValueError('Missing glyphs: ' + ''.join(critical))
|
|
manifest['outputs'].append({'file': target.name, 'weight': weight,
|
|
'sha256': sha256(target.read_bytes()).hexdigest(),
|
|
'bytes': target.stat().st_size, 'glyphs': len(cmap)})
|
|
print(target.name, target.stat().st_size)
|
|
source = root / '.font-sources' / 'zcoolkuaile.ttf'
|
|
font = TTFont(source)
|
|
options = subset.Options()
|
|
options.name_IDs = ['*']
|
|
sub = subset.Subsetter(options=options)
|
|
playful_chars = '重开撤销指路已走步还剩块第关0123456789 ·'
|
|
sub.populate(text=playful_chars)
|
|
sub.subset(font)
|
|
for record in font['name'].names:
|
|
if record.nameID in (1, 2, 3, 4, 6, 16, 17):
|
|
record.string = ('Regular' if record.nameID in (2, 17) else 'NumberSwipePlayful').encode(record.getEncoding(), errors='replace')
|
|
target = root / 'fonts' / 'NumberSwipePlayful.ttf'
|
|
font.save(target)
|
|
manifest['sources'].append({'family': 'zcoolkuaile', 'sha256': sha256(source.read_bytes()).hexdigest(),
|
|
'url': 'https://github.com/google/fonts/tree/main/ofl/zcoolkuaile'})
|
|
manifest['outputs'].append({'file': target.name, 'weight': 400, 'sha256': sha256(target.read_bytes()).hexdigest(),
|
|
'bytes': target.stat().st_size, 'glyphs': len(font.getBestCmap()), 'characters': playful_chars})
|
|
print(target.name, target.stat().st_size)
|
|
(root / 'fonts' / 'manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8')
|