Skip to content

Commit 9202385

Browse files
arrowtypefelipesanches
authored andcommitted
[family_and_style_max_length] Modernize check
Relates to issue #2179, specifically later comments and testing, like #2179 (comment) Changes: - Change main static font check to consider NameID 1 (FONT_FAMILY_NAME) rather than NameID 4 (FULL_FONT_NAME). - Change VF check to test NameID 16 + STAT particles. - Consider removing test of NameID 6 ... I ended up leaving this, but noting in its warning message that the issue is probably only with pre-Mac OS X systems. - Update tests to pass with new check criteria.
1 parent 6506025 commit 9202385

3 files changed

Lines changed: 144 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ A more detailed list of changes is available in the corresponding milestones for
1212
### On microsoft profile
1313
- **[instances_name_length_req]:** Similar to **name_length_req**, but checks the family and subfamily names of the instances, computed from STAT table.
1414

15+
### Changes to existing checks
16+
### On the Universal profile
17+
- **[name/family_and_style_max_length]:** Update to account for STAT table, along with recent testing and observations contributed to issue #2179 (PR #5020)
18+
1519

1620
## 1.0.1 (2025-Jul-04)
1721
### Bugfixes
Lines changed: 97 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import re
2+
from collections import defaultdict
3+
from itertools import product
24

3-
from fontbakery.constants import (
4-
RIBBI_STYLE_NAMES,
5-
NameID,
6-
)
7-
from fontbakery.prelude import check, Message, FAIL, WARN
5+
from fontbakery.constants import RIBBI_STYLE_NAMES, NameID
6+
from fontbakery.prelude import FAIL, Message, check
87
from fontbakery.utils import get_name_entry_strings
98

109

@@ -13,38 +12,51 @@
1312
rationale="""
1413
This check ensures that the length of name table entries is not
1514
too long, as this causes problems in some environments.
15+
16+
Background on length limit (credit to Aaron Bell at google/fonts/issues/9185):
17+
18+
The font dropdown in Microsoft Office on PC is driven by the older
19+
GDI font enumeration and rendering technology. GDI uses LOGFONTA
20+
(https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logfonta)
21+
to define the attributes of a font, and CHAR lfFaceName[LF_FACESIZE];
22+
to define the font name. The problem, though, is that the string lfFaceName
23+
is restricted to 32 characters, including the terminating NULL.
24+
25+
In a variable font where the master location is at a location with a
26+
significant number of characters, say, “ExtraLight”, name ID 1 can become
27+
quite long (eg: “Chiron Hei HK ExtraLight”). However, in determining the
28+
font name, Microsoft will prioritize name ID 16 over name ID 1. So this
29+
lets one reduce the character count back to the standard font name
30+
(eg: “Chiron Hei HK”).
1631
""",
1732
proposal=[
1833
"https://github.com/fonttools/fontbakery/issues/1488",
1934
"https://github.com/fonttools/fontbakery/issues/2179",
2035
],
2136
)
2237
def check_name_family_and_style_max_length(ttFont):
23-
"""Combined length of family and style must not exceed 32 characters."""
38+
"""Combined length of family and style must not exceed 31 characters."""
2439

2540
def strip_ribbi(x):
2641
ribbi_re = " (" + "|".join(RIBBI_STYLE_NAMES) + ")$"
2742
return re.sub(ribbi_re, "", x)
2843

44+
# constants for name length limits
45+
NAME_LENGTH_LIMIT = 31
46+
PSNAME_LENGTH_LIMIT = 27
47+
ELIDABLE_FLAG = 2
48+
2949
checks = [
3050
[
3151
FAIL,
32-
NameID.FULL_FONT_NAME,
33-
32,
34-
(
35-
"with the dropdown menu in old versions of Microsoft Word"
36-
" as well as shaping issues for some accented letters in"
37-
" Microsoft Word on Windows 10 and 11"
38-
),
52+
NameID.FONT_FAMILY_NAME,
53+
NAME_LENGTH_LIMIT,
54+
"cause a fallback font to appear for some accented letters, as well"
55+
" as in some scripts such as Thai, in"
56+
" Microsoft Word on Windows 10 and 11. It can also lead to names"
57+
" which are truncated in the Microsoft Word font menu.\n\n",
3958
strip_ribbi,
4059
],
41-
[
42-
WARN,
43-
NameID.POSTSCRIPT_NAME,
44-
27,
45-
"with PostScript printers, especially on Mac platforms",
46-
lambda x: x,
47-
],
4860
]
4961
for loglevel, nameid, maxlen, reason, transform in checks:
5062
for the_name in get_name_entry_strings(ttFont, nameid):
@@ -57,36 +69,92 @@ def strip_ribbi(x):
5769
f" cause problems {reason}.",
5870
)
5971

60-
# name ID 1/16 + fvar instance name > 32 : FAIL : problems with Windows
61-
if "fvar" in ttFont:
72+
# check variable font name lengths
73+
if "fvar" in ttFont and "STAT" in ttFont:
74+
# Get the family name from the name table (prefer ID 16)
75+
if ttFont["name"].getName(NameID.TYPOGRAPHIC_FAMILY_NAME, 3, 1, 0x409):
76+
family_name = (
77+
ttFont["name"]
78+
.getName(NameID.TYPOGRAPHIC_FAMILY_NAME, 3, 1, 0x409)
79+
.toUnicode()
80+
)
81+
family_name_id = NameID.TYPOGRAPHIC_FAMILY_NAME
82+
else:
83+
family_name = (
84+
ttFont["name"].getName(NameID.FONT_FAMILY_NAME, 3, 1, 0x409).toUnicode()
85+
)
86+
family_name_id = NameID.FONT_FAMILY_NAME
87+
88+
styles_per_axis = defaultdict(list)
89+
for value in ttFont["STAT"].table.AxisValueArray.AxisValue:
90+
# if the value is marked as elidable, don’t count it
91+
if value.Flags & ELIDABLE_FLAG:
92+
continue
93+
# skip "Regular" and "Italic" style names, which do not count towards MS Word limit
94+
if ttFont["name"].getName(value.ValueNameID, 3, 1, 0x409).toUnicode() in [
95+
"Regular",
96+
"Italic",
97+
]:
98+
continue
99+
# otherwise, get the STAT style particle name and add it to the list
100+
styles_per_axis[value.AxisIndex].append(
101+
ttFont["name"].getName(value.ValueNameID, 3, 1, 0x409).toUnicode()
102+
)
103+
104+
# make list of combined family & STAT style names
105+
names = [
106+
f'{family_name} {" ".join(combination)}'
107+
for combination in product(*styles_per_axis.values())
108+
]
109+
110+
for name in names:
111+
if len(name) > NAME_LENGTH_LIMIT:
112+
stat_style_combination = name.replace(f"{family_name} ", "")
113+
yield FAIL, Message(
114+
"familyname-plus-stat-entries-too-long",
115+
f"Name ID {family_name_id} '{family_name}' plus"
116+
f" STAT table style combination '{stat_style_combination}'"
117+
f" exceeds 31 characters (the combination is {len(name)} characters).\n\n"
118+
f" This has been found to"
119+
f" cause a fallback font to appear for some accented letters, as well"
120+
f" as in some scripts such as Thai, in"
121+
f" Microsoft Word on Windows 10 and 11. It can also lead to names"
122+
f" which are truncated in the Microsoft Word font menu.\n\n",
123+
)
124+
125+
# if STAT not in font, assume that "fvar" instance names are used
126+
if "fvar" in ttFont and "STAT" not in ttFont:
62127
for instance in ttFont["fvar"].instances:
63128
for instance_name in get_name_entry_strings(
64129
ttFont, instance.subfamilyNameID
65130
):
66131
typo_family_names = {
67132
(r.platformID, r.platEncID, r.langID): r
68133
for r in ttFont["name"].names
69-
if r.nameID == 16
134+
if r.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME
70135
}
71136
family_names = {
72137
(r.platformID, r.platEncID, r.langID): r
73138
for r in ttFont["name"].names
74-
if r.nameID == 1
139+
if r.nameID == NameID.FONT_FAMILY_NAME
75140
}
76141
for platform in family_names:
77142
if platform in typo_family_names:
78143
family_name = typo_family_names[platform].toUnicode()
79144
else:
80145
family_name = family_names[platform].toUnicode()
81146
full_instance_name = family_name + " " + instance_name
82-
if len(full_instance_name) > 32:
147+
if len(full_instance_name) > NAME_LENGTH_LIMIT:
83148
yield FAIL, Message(
84-
"instance-too-long",
149+
"fvar-instance-too-long",
85150
f"Variable font instance name '{full_instance_name}'"
86151
f" formed by space-separated concatenation of"
87152
f" font family name (nameID {NameID.FONT_FAMILY_NAME})"
88153
f" and instance subfamily nameID {instance.subfamilyNameID}"
89-
f" exceeds 32 characters.\n\n"
90-
f"This has been found to cause shaping issues for some"
91-
f" accented letters in Microsoft Word on Windows 10 and 11.",
154+
f" exceeds {NAME_LENGTH_LIMIT} characters.\n\n"
155+
f" This has been found to"
156+
f" cause a fallback font to appear for some accented letters, as well"
157+
f" as in some scripts such as Thai, in"
158+
f" Microsoft Word on Windows 10 and 11. It can also lead to names"
159+
f" which are truncated in the Microsoft Word font menu.\n\n",
92160
)

tests/test_checks_name.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,8 @@ def test_check_name_char_restrictions(check):
216216
def test_check_name_family_and_style_max_length(check):
217217
"""Name table entries should not be too long."""
218218

219-
# Our reference Cabin Regular is known to be good
220-
ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
219+
# Our static reference Merriweather Regular is known to be good
220+
ttFont = TTFont(TEST_FILE("merriweather/Merriweather-Regular.ttf"))
221221

222222
# So it must PASS the check:
223223
assert_PASS(check(ttFont), "with a good font...")
@@ -227,8 +227,8 @@ def test_check_name_family_and_style_max_length(check):
227227
# a discussion of the requirements
228228

229229
for index, name in enumerate(ttFont["name"].names):
230-
if name.nameID == NameID.FULL_FONT_NAME:
231-
# This has 33 chars, while the max currently allowed is 32
230+
if name.nameID == NameID.FONT_FAMILY_NAME:
231+
# This has 33 chars, while the max currently allowed is 31
232232
bad = "An Absurdly Long Family Name Font"
233233
assert len(bad) == 33
234234
ttFont["name"].names[index].string = bad.encode(name.getEncoding())
@@ -238,12 +238,45 @@ def test_check_name_family_and_style_max_length(check):
238238
ttFont["name"].names[index].string = bad.encode(name.getEncoding())
239239

240240
results = check(ttFont)
241-
assert_results_contain(results, FAIL, "nameid4-too-long", "with a bad font...")
242-
assert_results_contain(results, WARN, "nameid6-too-long", "with a bad font...")
241+
assert_results_contain(results, FAIL, "nameid1-too-long", "with a bad font...")
242+
assert_results_contain(results, FAIL, "nameid6-too-long", "with a bad font...")
243243

244-
# Restore the original VF
244+
# Now get a variable font reference
245245
ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
246246

247+
# set long STAT style name, then check for a FAIL
248+
for index, name in enumerate(ttFont["name"].names):
249+
# verify that "Cabin" length, plus 27 chars, exceeds limit of 31
250+
if name.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME:
251+
assert len(ttFont["name"].names[index].string) + 27 > 31
252+
253+
# find the first instance nameID from the STAT table, then make it long
254+
# and check for a FAIL
255+
for value in ttFont["STAT"].table.AxisValueArray.AxisValue:
256+
# if the value is marked as elidable, don’t count it
257+
if value.Flags & 2:
258+
continue
259+
# otherwise, get the STAT style name entry and make it long
260+
bad = "Absurdly Long Name Particle"
261+
assert len(bad) == 27
262+
263+
# edit the name table entry for the STAT style name
264+
for index, name in enumerate(ttFont["name"].names):
265+
if name.nameID == value.ValueNameID:
266+
ttFont["name"].names[index].string = bad.encode(name.getEncoding())
267+
268+
# stop after the first applicable STAT value
269+
break
270+
271+
results = check(ttFont)
272+
assert_results_contain(
273+
results, FAIL, "familyname-plus-stat-entries-too-long", "with a bad font..."
274+
)
275+
276+
# remove STAT table, then check for a FAIL if the STAT table is not present
277+
ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
278+
del ttFont["STAT"]
279+
247280
# ...and break the check again with a bad fvar instance name:
248281
nameid_to_break = ttFont["fvar"].instances[0].subfamilyNameID
249282
for index, name in enumerate(ttFont["name"].names):
@@ -254,8 +287,10 @@ def test_check_name_family_and_style_max_length(check):
254287
assert len(bad) == 28
255288
ttFont["name"].names[index].string = bad.encode(name.getEncoding())
256289
break
290+
291+
results = check(ttFont)
257292
assert_results_contain(
258-
check(ttFont), FAIL, "instance-too-long", "with a bad font..."
293+
results, FAIL, "fvar-instance-too-long", "with a bad font..."
259294
)
260295

261296

0 commit comments

Comments
 (0)