Skip to content

Commit f43aa24

Browse files
authored
Merge pull request #5 from IvanUkhov/iu-metrics
Extend the font metadata in accordance with the 2024 targets Added "head" data
2 parents 968dbec + 1768bff commit f43aa24

6 files changed

+1489
-82
lines changed

.github/workflows/requirements.txt

+17-16
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
pylint
2-
pytest
3-
numpy
4-
selenium
5-
psutil
6-
pillow
7-
requests
8-
nested_diff
1+
black
2+
brotli
93
dnspython
10-
monotonic
11-
tornado
124
fonttools
13-
wsaccel
14-
ujson
155
future
16-
brotli
17-
xvfbwrapper
18-
google-cloud-storage
196
google-cloud-pubsub
7+
google-cloud-storage
8+
monotonic
9+
nested_diff
10+
numpy
11+
pillow
12+
psutil
13+
pylint
14+
pytest
2015
pytz
21-
tzlocal
16+
requests
17+
selenium
18+
tornado
19+
tzlocal
20+
ujson
21+
wsaccel
22+
xvfbwrapper

.github/workflows/wptagent_test.yml

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ jobs:
3636
python3 -m pip install --upgrade pip
3737
pip3 install -r .github/workflows/requirements.txt
3838
39+
- name: Analysing the code with Black
40+
run: black --include font --check .
41+
3942
- name: Analysing the code with pylint
4043
run: |
4144
find -name "*.py" -not -path "./ws4py/*" -exec pylint {} --errors-only \;

internal/font_metadata.py

+102-66
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
# found in the LICENSE.md file.
66
"""Extract metadata from OpenType fonts."""
77

8-
from fontTools.ttLib import TTFont
9-
from fontTools.ttLib.tables import otTables
108
import functools
119
import logging
10+
from typing import Optional
11+
12+
from fontTools.ttLib import TTFont
13+
from fontTools.ttLib.tables import otTables
1214

1315

1416
_NAME_ID_VERSION = 5
@@ -27,80 +29,96 @@ def _safe_map(m):
2729
return {k: v for k, v in m.items() if _safe_result_type(v)}
2830

2931

32+
def _read_head(font: TTFont) -> Optional[dict]:
33+
try:
34+
return _safe_map(font["head"].__dict__)
35+
except Exception:
36+
logging.exception("Error reading font head")
37+
38+
3039
def _read_names(ttf, name_ids):
3140
names = {}
3241
try:
3342
# limit # of names we retain
3443
unicode_names = sorted(
35-
(n for n in ttf['name'].names
36-
if n.isUnicode() and n.nameID <= _MAX_NAME_ID),
37-
key=lambda n: n.nameID
44+
(
45+
n
46+
for n in ttf["name"].names
47+
if n.isUnicode() and n.nameID <= _MAX_NAME_ID
48+
),
49+
key=lambda n: n.nameID,
3850
)[:_MAX_NAMES]
3951
# limit length of names we retain
4052
for name in unicode_names:
4153
try:
4254
names[name.nameID] = name.toUnicode()[:_MAX_NAME_LEN]
4355
except Exception:
44-
logging.exception('Error converting name to unicode')
56+
logging.exception("Error converting name to unicode")
4557

4658
except Exception:
47-
logging.exception('Error reading font names')
59+
logging.exception("Error reading font names")
4860
if not names:
4961
return None
5062
return names
5163

5264

5365
def _read_os2(ttf):
5466
try:
55-
os2 = _safe_map(ttf['OS/2'].__dict__)
56-
os2['panose'] = _safe_map(ttf['OS/2'].panose.__dict__)
67+
os2 = _safe_map(ttf["OS/2"].__dict__)
68+
os2["panose"] = _safe_map(ttf["OS/2"].panose.__dict__)
5769
return os2
5870
except Exception:
59-
logging.exception('Error reading font OS/2')
71+
logging.exception("Error reading font OS/2")
6072
return None
6173

6274

6375
def _read_post(ttf):
6476
try:
65-
post = _safe_map(ttf['post'].__dict__)
77+
post = _safe_map(ttf["post"].__dict__)
6678
return post
6779
except Exception:
68-
logging.exception('Error reading font post')
80+
logging.exception("Error reading font post")
6981
return None
7082

7183

7284
def _read_fvar(ttf):
73-
if 'fvar' in ttf:
85+
if "fvar" in ttf:
7486
try:
7587
return {
7688
a.axisTag: {
77-
'min': a.minValue,
78-
'default': a.defaultValue,
79-
'max': a.maxValue
89+
"min": a.minValue,
90+
"default": a.defaultValue,
91+
"max": a.maxValue,
8092
}
81-
for a in ttf['fvar'].axes
93+
for a in ttf["fvar"].axes
8294
}
8395
except Exception:
84-
logging.exception('Error reading axes')
96+
logging.exception("Error reading axes")
8597
return None
8698

8799

88100
def _read_codepoint_glyph_counts(ttf):
89101
try:
90102
glyph_count = len(ttf.getGlyphOrder())
91-
unicode_cmaps = (t.cmap.keys() for t in ttf['cmap'].tables if t.isUnicode())
92-
unique_codepoints = functools.reduce(lambda acc, u: acc | u, unicode_cmaps, set())
103+
unicode_cmaps = (t.cmap.keys() for t in ttf["cmap"].tables if t.isUnicode())
104+
unique_codepoints = functools.reduce(
105+
lambda acc, u: acc | u, unicode_cmaps, set()
106+
)
93107
return {
94-
'num_cmap_codepoints': len(unique_codepoints),
95-
'num_glyphs': glyph_count
108+
"num_cmap_codepoints": len(unique_codepoints),
109+
"num_glyphs": glyph_count,
96110
}
97111
except Exception:
98-
logging.exception('Error reading codepoint and glyph count')
112+
logging.exception("Error reading codepoint and glyph count")
99113
return None
100114

115+
101116
def _read_cmap(ttf):
102117
try:
103-
encodings = [{ 'platform': t.platformID, 'encoding': t.platEncID } for t in ttf['cmap'].tables]
118+
encodings = [
119+
{"platform": t.platformID, "encoding": t.platEncID}
120+
for t in ttf["cmap"].tables
121+
]
104122
codepoints = []
105123

106124
cmap = ttf.getBestCmap()
@@ -109,99 +127,112 @@ def _read_cmap(ttf):
109127
codepoints = [codepoint for codepoint in ttf.getBestCmap()]
110128

111129
return {
112-
'encodings': encodings,
113-
'codepoints': codepoints,
130+
"encodings": encodings,
131+
"codepoints": codepoints,
114132
}
115133
except Exception:
116-
logging.exception('Error reading cmap data')
134+
logging.exception("Error reading cmap data")
117135
return None
118136

137+
119138
def _read_color(ttf):
120139
try:
121140
t = []
122141

123142
# It is possible a single font uses multiple color
124143
# formats for wider OS and browser support.
125-
if 'COLR' in ttf and ttf['COLR'].version == 0:
126-
t.append('COLRv0')
144+
if "COLR" in ttf and ttf["COLR"].version == 0:
145+
t.append("COLRv0")
127146

128-
if 'COLR' in ttf and ttf['COLR'].version == 1:
129-
t.append('COLRv1')
147+
if "COLR" in ttf and ttf["COLR"].version == 1:
148+
t.append("COLRv1")
130149

131-
if 'SVG ' in ttf:
132-
t.append('SVG')
150+
if "SVG " in ttf:
151+
t.append("SVG")
133152

134-
if 'CBDT' in ttf:
135-
t.append('CBDT')
153+
if "CBDT" in ttf:
154+
t.append("CBDT")
136155

137-
if 'sbix' in ttf:
138-
t.append('sbix')
156+
if "sbix" in ttf:
157+
t.append("sbix")
139158

140159
numPalettes = 0
141160
numPaletteEntries = 0
142161

143-
if 'CPAL' in ttf:
144-
numPaletteEntries = ttf['CPAL'].numPaletteEntries
145-
numPalettes = len(ttf['CPAL'].palettes)
162+
if "CPAL" in ttf:
163+
numPaletteEntries = ttf["CPAL"].numPaletteEntries
164+
numPalettes = len(ttf["CPAL"].palettes)
146165

147166
return {
148-
'formats': t,
149-
'numPalettes': numPalettes,
150-
'numPaletteEntries': numPaletteEntries
167+
"formats": t,
168+
"numPalettes": numPalettes,
169+
"numPaletteEntries": numPaletteEntries,
151170
}
152171
except Exception:
153-
logging.exception('Error reading color font data')
172+
logging.exception("Error reading color font data")
154173
return None
155174

175+
156176
def _read_features(ttf):
157177
try:
158178
result = {}
159179

160180
# This is loosely based on: https://github.com/fonttools/fonttools/blob/main/Snippets/layout-features.py
161-
for tag in ('GSUB', 'GPOS'):
162-
if not tag in ttf: continue
181+
for tag in ("GSUB", "GPOS"):
182+
if not tag in ttf:
183+
continue
163184
table = ttf[tag].table
164185

165186
if not tag in result:
166187
result[tag] = {}
167188

168-
if not table.ScriptList or not table.FeatureList: continue
189+
if not table.ScriptList or not table.FeatureList:
190+
continue
169191
featureRecords = table.FeatureList.FeatureRecord
170192
for script in table.ScriptList.ScriptRecord:
171-
if not script.Script: continue
193+
if not script.Script:
194+
continue
172195
if not script.ScriptTag in result[tag]:
173196
result[tag][script.ScriptTag] = {}
174197

175198
languages = list(script.Script.LangSysRecord)
176199

177200
if script.Script.DefaultLangSys:
201+
# pylint: disable=no-member
178202
defaultlangsys = otTables.LangSysRecord()
179203
defaultlangsys.LangSysTag = "default"
180204
defaultlangsys.LangSys = script.Script.DefaultLangSys
181205
languages.insert(0, defaultlangsys)
182206

183207
for langsys in languages:
184-
if not langsys.LangSys: continue
208+
if not langsys.LangSys:
209+
continue
185210

186211
if not langsys.LangSysTag in result[tag][script.ScriptTag]:
187212
result[tag][script.ScriptTag][langsys.LangSysTag] = []
188213

189-
features = [featureRecords[index] for index in langsys.LangSys.FeatureIndex]
214+
features = [
215+
featureRecords[index] for index in langsys.LangSys.FeatureIndex
216+
]
190217

191218
if langsys.LangSys.ReqFeatureIndex != 0xFFFF:
192219
record = featureRecords[langsys.LangSys.ReqFeatureIndex]
220+
# pylint: disable=no-member
193221
requiredfeature = otTables.FeatureRecord()
194-
requiredfeature.FeatureTag = 'required(%s)' % record.FeatureTag
222+
requiredfeature.FeatureTag = "required(%s)" % record.FeatureTag
195223
requiredfeature.Feature = record.Feature
196224
features.insert(0, requiredfeature)
197225
for feature in features:
198-
result[tag][script.ScriptTag][langsys.LangSysTag].append(feature.FeatureTag)
226+
result[tag][script.ScriptTag][langsys.LangSysTag].append(
227+
feature.FeatureTag
228+
)
199229

200230
return result
201231
except Exception:
202-
logging.exception('Error reading OpenType feature data')
232+
logging.exception("Error reading OpenType feature data")
203233
return None
204234

235+
205236
def read_metadata(font):
206237
ttf = TTFont(font, fontNumber=0, lazy=True)
207238
try:
@@ -211,29 +242,34 @@ def read_metadata(font):
211242
reader = ttf.reader
212243

213244
metadata = {
214-
'table_sizes': {tag: reader.tables[tag].length
215-
for tag in sorted(reader.keys())},
216-
'names': _read_names(ttf, (_NAME_ID_VERSION,
217-
_NAME_ID_POSTSCRIPT_NAME, _NAME_ID_LICENSE_URL)),
218-
'OS2': _read_os2(ttf),
219-
'post': _read_post(ttf),
220-
'fvar': _read_fvar(ttf),
221-
'cmap': _read_cmap(ttf),
222-
'color': _read_color(ttf),
223-
'features': _read_features(ttf),
224-
'counts': _read_codepoint_glyph_counts(ttf),
245+
"table_sizes": {
246+
tag: reader.tables[tag].length for tag in sorted(reader.keys())
247+
},
248+
"head": _read_head(ttf),
249+
"names": _read_names(
250+
ttf, (_NAME_ID_VERSION, _NAME_ID_POSTSCRIPT_NAME, _NAME_ID_LICENSE_URL)
251+
),
252+
"OS2": _read_os2(ttf),
253+
"post": _read_post(ttf),
254+
"fvar": _read_fvar(ttf),
255+
"cmap": _read_cmap(ttf),
256+
"color": _read_color(ttf),
257+
"features": _read_features(ttf),
258+
"counts": _read_codepoint_glyph_counts(ttf),
225259
}
226260
ttf.close()
227261

228-
return {k: v for k,v in metadata.items() if v is not None}
262+
return {k: v for k, v in metadata.items() if v is not None}
229263

230264

231265
def main():
232266
import pprint
233267
import sys
268+
234269
for filename in sys.argv[1:]:
235270
pp = pprint.PrettyPrinter()
236271
pp.pprint(read_metadata(filename))
237272

273+
238274
if __name__ == "__main__":
239-
main()
275+
main()

internal/font_metadata_test.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import json
2+
3+
from .font_metadata import read_metadata
4+
5+
6+
def test_read_metadata():
7+
base = "test/data/SourceSerif4-VariableFont_opsz,wght"
8+
9+
actual = read_metadata(f"{base}.ttf")
10+
actual = json.loads(json.dumps(actual, default=str))
11+
12+
with open(f"{base}.json", encoding="utf-8") as file:
13+
expected = json.load(file)
14+
15+
assert actual == expected

0 commit comments

Comments
 (0)