This repository has been archived by the owner on Jan 25, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 202
/
Copy pathnoto_fonts_for_android_test.py
187 lines (142 loc) · 5.86 KB
/
noto_fonts_for_android_test.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
import collections
from fontbakery.utils import get_name_entry_strings
from fontTools import ttLib
from lxml import etree
from pathlib import Path
import pytest
from typing import Tuple
_KNOWN_PATHLESS = {
"NotoSansSymbols-Regular-Subsetted.ttf",
"NotoColorEmoji.ttf",
"NotoColorEmojiFlags.ttf",
"NotoSansSymbols-Regular-Subsetted2.ttf",
}
_POSTSCRIPT_NAME = 6
def _repo_root() -> Path:
root = (Path(__file__).parent / "..").absolute()
if not (root / "LICENSE").is_file():
raise IOError(f"{root} does not contain LICENSE")
return root
def _noto_4_android_file() -> Path:
xml_file = _repo_root() / "android-connection" / "noto-fonts-4-android.xml"
if not xml_file.is_file():
raise IOError(f"No file {xml_file}")
return xml_file
def _font_file(font_el) -> str:
return ("".join(font_el.itertext())).strip()
def _font_path(font_el) -> Path:
name = _font_file(font_el)
path = font_el.attrib["path"]
return _repo_root() / path / name
def _is_collection(font_el) -> bool:
return _font_file(font_el).lower().endswith(".ttc")
def _open_font(font_el) -> ttLib.TTFont:
path = _font_path(font_el)
if not path.is_file():
raise IOError(f"No such file: {path}")
if _is_collection(font_el):
return ttLib.TTFont(str(path), fontNumber=int(font_el.attrib["index"]))
return ttLib.TTFont(str(path))
def _open_font_path(path, fontNumber) -> ttLib.TTFont:
if not path.is_file():
raise IOError(f"No such file: {path}")
if str(path).lower().endswith(".ttc"):
return ttLib.TTFont(str(path), fontNumber=int(fontNumber))
return ttLib.TTFont(str(path))
def _axis(font, tag):
if "fvar" not in font:
return None
axes = tuple(a for a in font["fvar"].axes if a.axisTag == tag)
if not axes:
return None
assert len(axes) < 2, f"only 0 or 1 fvar entries supported; {tag} has more"
return axes[0]
def _weight(font: ttLib.TTFont) -> Tuple[int, int, int]:
maybe_wght = _axis(font, "wght")
if maybe_wght:
return (maybe_wght.minValue, maybe_wght.defaultValue, maybe_wght.maxValue)
os2_weight = font["OS/2"].usWeightClass
return (os2_weight, os2_weight, os2_weight)
def _psname(font_el) -> str:
# Not every font element will have postScriptName tag. If it's not present then it is assumeed
# that it's the filename less extension
psn = font_el.attrib.get("postScriptName", None)
if psn:
return psn
path = _font_path(font_el)
assert path.is_file(), f"{path} missing"
return path.stem
def test_fonts_have_path():
root = etree.parse(str(_noto_4_android_file()))
bad = []
for font in root.iter("font"):
font_file = _font_file(font)
if font_file in _KNOWN_PATHLESS:
assert "path" not in font.attrib, f"{font_file} not expected to have path. Correct _KNOWN_PATHLESS if you just added path"
continue
if not font.attrib.get("path", ""):
bad.append(font_file)
assert not bad, "Missing path attribute: " + ", ".join(bad)
def test_ttcs_have_index():
root = etree.parse(str(_noto_4_android_file()))
bad = []
for font in root.iter("font"):
if not _is_collection(font):
continue
if "index" not in font.attrib:
bad.append(_font_file(font))
assert not bad, "Missing index attribute: " + ", ".join(bad)
def test_font_paths_are_valid():
root = etree.parse(str(_noto_4_android_file()))
bad = []
for font in root.xpath("//font[@path]"):
path = _font_path(font)
if not path.is_file():
bad.append(str(path))
assert not bad, "No such file: " + ", ".join(bad)
def test_font_weights():
root = etree.parse(str(_noto_4_android_file()))
errors = []
for font_el in root.xpath("//font[@path]"):
xml_weight = int(font_el.attrib["weight"])
path = _font_path(font_el)
font = _open_font(font_el)
min_wght, default_wght, max_weight = _weight(font)
if xml_weight < min_wght or xml_weight > max_weight:
error_str = f"{_font_file(font_el)} weight {xml_weight} outside font capability {min_wght}..{max_weight}"
errors.append(error_str)
assert not errors, ", ".join(errors)
def test_font_full_weight_coverage():
root = etree.parse(str(_noto_4_android_file()))
errors = []
for family in root.iter("family"):
font_to_xml_weights = collections.defaultdict(set)
for font in family.xpath("//font[@path]"):
font_to_xml_weights[(_font_path(font), font.attrib.get("index", -1))].add(int(font.attrib["weight"]))
# now you have a map of font path => set of weights in xml
for (font_path, font_number), xml_weights in font_to_xml_weights.items():
# open the font, compute the 100 weights between it's min/max weight
# if xml_weights != computed weights add this to the error list
font = _open_font_path(font_path, font_number)
min_wght, default_wght, max_weight = _weight(font)
if min(xml_weights) > min_wght or max(xml_weights) < max_weight:
errors.append(f"{font_path} weight range {min(xml_weights)}..{max(xml_weights)} could be expanded to {min_wght}..{max_weight}")
assert not errors, ", ".join(errors)
def test_font_psnames():
root = etree.parse(str(_noto_4_android_file()))
errors = []
font_to_xml_psnames = collections.defaultdict(set)
for font_el in root.xpath("//font[@path]"):
path = _font_path(font_el)
psname = _psname(font_el)
font_to_xml_psnames[(path, font_el.attrib.get("index", -1))].add(str(psname))
for (font_path, font_number), xml_psnames in font_to_xml_psnames.items():
font = _open_font_path(font_path, font_number)
postscript_names = set(get_name_entry_strings(font, _POSTSCRIPT_NAME))
if len(postscript_names) != 1:
errors.append(f"font file {font_path} should have a single postScriptName and not {postscript_names}")
continue
for xml_psname in xml_psnames:
if not (xml_psname in postscript_names):
errors.append(f"postScriptName=\"{postscript_names[0]}\" in font file {font_path} doesn't match the entry in XML: {xml_psname}")
assert not errors, ", ".join(errors)