Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
137e9ac
Change static font check to check NameID 1, rather than 4
arrowtype May 7, 2025
e76068e
Update variable test to check length of family name plus STAT names
arrowtype May 8, 2025
75926ec
Improve specificity of FAIL messages
arrowtype May 8, 2025
e6dc9a8
Correct problems in STAT check; add back fvar check if STAT missing
arrowtype May 8, 2025
e5b1a4b
improve output formatting for static check
arrowtype May 8, 2025
38fcd0d
Run Black formatter
arrowtype May 8, 2025
20f2977
Note uncertainty about NameID 21
arrowtype May 8, 2025
2b2d9d5
Update changelog
arrowtype May 8, 2025
a27ad8a
formatting for lint test
arrowtype May 8, 2025
9f951d4
add detail on length limit
arrowtype May 8, 2025
0280c01
Formatting for linter
arrowtype May 8, 2025
275aab9
Test update: use static font, check nameID 1 rather than 4
arrowtype May 13, 2025
ff5b777
adjust fvar check to test for >31 characters
arrowtype May 13, 2025
3f95f11
add test for long STAT table entry
arrowtype May 13, 2025
20e7d1f
formatting
arrowtype May 13, 2025
e9549ed
formatting
arrowtype May 13, 2025
a15141d
fix index error, break out of loop after first run
arrowtype May 13, 2025
26d95f0
fix failing test
arrowtype May 14, 2025
e051253
slight refactor: eliminate magic numbers and unneeded try/except block
arrowtype Jul 8, 2025
0dae195
Merge branch 'main' into update-name-length-check
arrowtype Jul 8, 2025
ecc73a5
formatting
arrowtype Jul 24, 2025
09b69ae
Skip STAT names "Regular" and "Italic" which do not count towards limit
arrowtype Aug 26, 2025
9f70f4b
remove extreme legacy check for long PostScript name (NameID 6)
arrowtype Aug 26, 2025
586756b
Remove note about nameID 21, after testing
arrowtype Aug 26, 2025
0af3f66
Merge branch 'main' into update-name-length-check
arrowtype Aug 27, 2025
9bf48e4
Merge branch 'main' into update-name-length-check
felipesanches Sep 29, 2025
ee2fbce
Merge branch 'main' into update-name-length-check
felipesanches Oct 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ A more detailed list of changes is available in the corresponding milestones for
- Replace deprecated `pkg_resources` by `importlib.resources` (issue #5028)
- ...

### Changes to existing checks
### On the Universal profile
- **[name/family_and_style_max_length]:** Update to account for STAT table, along with recent testing and observations contributed to issue #2179

## 1.0.1 (2025-Jul-04)
### Bugfixes
Expand Down
126 changes: 97 additions & 29 deletions Lib/fontbakery/checks/name/family_and_style_max_length.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import re
from collections import defaultdict
from itertools import product

from fontbakery.constants import (
RIBBI_STYLE_NAMES,
NameID,
)
from fontbakery.prelude import check, Message, FAIL, WARN
from fontbakery.constants import RIBBI_STYLE_NAMES, NameID
from fontbakery.prelude import FAIL, WARN, Message, check
from fontbakery.utils import get_name_entry_strings


Expand All @@ -13,38 +12,51 @@
rationale="""
This check ensures that the length of name table entries is not
too long, as this causes problems in some environments.

Background on length limit (credit to Aaron Bell at google/fonts/issues/9185):

The font dropdown in Microsoft Office on PC is driven by the older
GDI font enumeration and rendering technology. GDI uses LOGFONTA
(https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logfonta)
to define the attributes of a font, and CHAR lfFaceName[LF_FACESIZE];
to define the font name. The problem, though, is that the string lfFaceName
is restricted to 32 characters, including the terminating NULL.

In a variable font where the master location is at a location with a
significant number of characters, say, “ExtraLight”, name ID 1 can become
quite long (eg: “Chiron Hei HK ExtraLight”). However, in determining the
font name, Microsoft will prioritize name ID 16 over name ID 1. So this
lets one reduce the character count back to the standard font name
(eg: “Chiron Hei HK”).
""",
proposal=[
"https://github.com/fonttools/fontbakery/issues/1488",
"https://github.com/fonttools/fontbakery/issues/2179",
],
)
def check_name_family_and_style_max_length(ttFont):
"""Combined length of family and style must not exceed 32 characters."""
"""Combined length of family and style must not exceed 31 characters."""

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

# constants for name length limits
NAME_LENGTH_LIMIT = 31
PSNAME_LENGTH_LIMIT = 27
ELIDABLE_FLAG = 2

checks = [
[
FAIL,
NameID.FULL_FONT_NAME,
32,
(
"with the dropdown menu in old versions of Microsoft Word"
" as well as shaping issues for some accented letters in"
" Microsoft Word on Windows 10 and 11"
),
NameID.FONT_FAMILY_NAME,
NAME_LENGTH_LIMIT,
"cause a fallback font to appear for some accented letters, as well"
" as in some scripts such as Thai, in"
" Microsoft Word on Windows 10 and 11. It can also lead to names"
" which are truncated in the Microsoft Word font menu.\n\n",
strip_ribbi,
],
[
WARN,
NameID.POSTSCRIPT_NAME,
27,
"with PostScript printers, especially on Mac platforms",
lambda x: x,
],
]
for loglevel, nameid, maxlen, reason, transform in checks:
for the_name in get_name_entry_strings(ttFont, nameid):
Expand All @@ -57,36 +69,92 @@ def strip_ribbi(x):
f" cause problems {reason}.",
)

# name ID 1/16 + fvar instance name > 32 : FAIL : problems with Windows
if "fvar" in ttFont:
# check variable font name lengths
if "fvar" in ttFont and "STAT" in ttFont:
# Get the family name from the name table (prefer ID 16)
if ttFont["name"].getName(NameID.TYPOGRAPHIC_FAMILY_NAME, 3, 1, 0x409):
family_name = (
ttFont["name"]
.getName(NameID.TYPOGRAPHIC_FAMILY_NAME, 3, 1, 0x409)
.toUnicode()
)
family_name_id = NameID.TYPOGRAPHIC_FAMILY_NAME
else:
family_name = (
ttFont["name"].getName(NameID.FONT_FAMILY_NAME, 3, 1, 0x409).toUnicode()
)
family_name_id = NameID.FONT_FAMILY_NAME

styles_per_axis = defaultdict(list)
for value in ttFont["STAT"].table.AxisValueArray.AxisValue:
# if the value is marked as elidable, don’t count it
if value.Flags & ELIDABLE_FLAG:
continue
# skip "Regular" and "Italic" style names, which do not count towards MS Word limit
if ttFont["name"].getName(value.ValueNameID, 3, 1, 0x409).toUnicode() in [
"Regular",
"Italic",
]:
continue
# otherwise, get the STAT style particle name and add it to the list
styles_per_axis[value.AxisIndex].append(
ttFont["name"].getName(value.ValueNameID, 3, 1, 0x409).toUnicode()
)

# make list of combined family & STAT style names
names = [
f'{family_name} {" ".join(combination)}'
for combination in product(*styles_per_axis.values())
]

for name in names:
if len(name) > NAME_LENGTH_LIMIT:
stat_style_combination = name.replace(f"{family_name} ", "")
yield FAIL, Message(
"familyname-plus-stat-entries-too-long",
f"Name ID {family_name_id} '{family_name}' plus"
f" STAT table style combination '{stat_style_combination}'"
f" exceeds 31 characters (the combination is {len(name)} characters).\n\n"
f" This has been found to"
f" cause a fallback font to appear for some accented letters, as well"
f" as in some scripts such as Thai, in"
f" Microsoft Word on Windows 10 and 11. It can also lead to names"
f" which are truncated in the Microsoft Word font menu.\n\n",
)

# if STAT not in font, assume that "fvar" instance names are used
if "fvar" in ttFont and "STAT" not in ttFont:
for instance in ttFont["fvar"].instances:
for instance_name in get_name_entry_strings(
ttFont, instance.subfamilyNameID
):
typo_family_names = {
(r.platformID, r.platEncID, r.langID): r
for r in ttFont["name"].names
if r.nameID == 16
if r.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME
}
family_names = {
(r.platformID, r.platEncID, r.langID): r
for r in ttFont["name"].names
if r.nameID == 1
if r.nameID == NameID.FONT_FAMILY_NAME
}
for platform in family_names:
if platform in typo_family_names:
family_name = typo_family_names[platform].toUnicode()
else:
family_name = family_names[platform].toUnicode()
full_instance_name = family_name + " " + instance_name
if len(full_instance_name) > 32:
if len(full_instance_name) > NAME_LENGTH_LIMIT:
yield FAIL, Message(
"instance-too-long",
"fvar-instance-too-long",
f"Variable font instance name '{full_instance_name}'"
f" formed by space-separated concatenation of"
f" font family name (nameID {NameID.FONT_FAMILY_NAME})"
f" and instance subfamily nameID {instance.subfamilyNameID}"
f" exceeds 32 characters.\n\n"
f"This has been found to cause shaping issues for some"
f" accented letters in Microsoft Word on Windows 10 and 11.",
f" exceeds {NAME_LENGTH_LIMIT} characters.\n\n"
f" This has been found to"
f" cause a fallback font to appear for some accented letters, as well"
f" as in some scripts such as Thai, in"
f" Microsoft Word on Windows 10 and 11. It can also lead to names"
f" which are truncated in the Microsoft Word font menu.\n\n",
)
49 changes: 42 additions & 7 deletions tests/test_checks_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,8 @@ def test_check_name_char_restrictions(check):
def test_check_name_family_and_style_max_length(check):
"""Name table entries should not be too long."""

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

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

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

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

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

# set long STAT style name, then check for a FAIL
for index, name in enumerate(ttFont["name"].names):
# verify that "Cabin" length, plus 27 chars, exceeds limit of 31
if name.nameID == NameID.TYPOGRAPHIC_FAMILY_NAME:
assert len(ttFont["name"].names[index].string) + 27 > 31

# find the first instance nameID from the STAT table, then make it long
# and check for a FAIL
for value in ttFont["STAT"].table.AxisValueArray.AxisValue:
# if the value is marked as elidable, don’t count it
if value.Flags & 2:
continue
# otherwise, get the STAT style name entry and make it long
bad = "Absurdly Long Name Particle"
assert len(bad) == 27

# edit the name table entry for the STAT style name
for index, name in enumerate(ttFont["name"].names):
if name.nameID == value.ValueNameID:
ttFont["name"].names[index].string = bad.encode(name.getEncoding())

# stop after the first applicable STAT value
break

results = check(ttFont)
assert_results_contain(
results, FAIL, "familyname-plus-stat-entries-too-long", "with a bad font..."
)

# remove STAT table, then check for a FAIL if the STAT table is not present
ttFont = TTFont(TEST_FILE("cabinvf/Cabin[wdth,wght].ttf"))
del ttFont["STAT"]

# ...and break the check again with a bad fvar instance name:
nameid_to_break = ttFont["fvar"].instances[0].subfamilyNameID
for index, name in enumerate(ttFont["name"].names):
Expand All @@ -254,8 +287,10 @@ def test_check_name_family_and_style_max_length(check):
assert len(bad) == 28
ttFont["name"].names[index].string = bad.encode(name.getEncoding())
break

results = check(ttFont)
assert_results_contain(
check(ttFont), FAIL, "instance-too-long", "with a bad font..."
results, FAIL, "fvar-instance-too-long", "with a bad font..."
)


Expand Down
Loading