Skip to content

Commit

Permalink
new check: STAT/ital_axis
Browse files Browse the repository at this point in the history
Added to the OpenType profile.

Replaces the old checks (also from OpenType profile):
- opentype/italic_axis_in_stat
- opentype/italic_axis_in_stat_is_boolean
- opentype/italic_axis_last

Large chunk of code back-ported from FontSpector.

(issue fonttools#4865)
  • Loading branch information
felipesanches committed Dec 27, 2024
1 parent 9a33924 commit 5050a74
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 184 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ A more detailed list of changes is available in the corresponding milestones for
- to generate documentation
- to implement a backwards compatibility mechanism

### New checks
#### Added to the OpenType profile
- **[STAT/ital_axis]**: Replaces the old checks (**opentype/italic_axis_in_stat**, **opentype/italic_axis_in_stat_is_boolean** and **opentype/italic_axis_last**) from the same profile (issue #4865)

### Migration of checks
#### Moved from Google Fonts to Universal profile
- **[googlefonts/varfont/duplicate_instance_names]**: Renamed to **varfont/duplicate_instance_names** (PR #4937)
Expand Down
266 changes: 118 additions & 148 deletions Lib/fontbakery/checks/opentype/STAT/ital_axis.py
Original file line number Diff line number Diff line change
@@ -1,185 +1,155 @@
import os

from fontbakery.prelude import check, Message, FAIL, PASS, WARN, SKIP
from fontTools.ttLib import TTFont


@check(
id="opentype/italic_axis_in_STAT",
rationale="""
Check that related Upright and Italic VFs have a
'ital' axis in STAT table.
""",
proposal="https://github.com/fonttools/fontbakery/issues/2934",
)
def check_italic_axis_in_STAT(fonts, config):
"""Ensure VFs have 'ital' STAT axis."""
from fontTools.ttLib import TTFont
def get_STAT_axis(font, tag):
for axis in font["STAT"].table.DesignAxisRecord.Axis:
if axis.AxisTag == tag:
return axis
return None

font_filenames = [f.file for f in fonts]
italics = [f for f in font_filenames if "Italic" in f]
missing_roman = []
italic_to_roman_mapping = {}
for italic in italics:
style_from_filename = os.path.basename(italic).split("-")[-1].split(".")[0]
is_varfont = "[" in style_from_filename

# to remove the axes syntax used on variable-font filenames:
if is_varfont:
style_from_filename = style_from_filename.split("[")[0]

if style_from_filename == "Italic":
if is_varfont:
# "Familyname-Italic[wght,wdth].ttf" => "Familyname[wght,wdth].ttf"
roman_counterpart = italic.replace("-Italic", "")
else:
# "Familyname-Italic.ttf" => "Familyname-Regular.ttf"
roman_counterpart = italic.replace("Italic", "Regular")
else:
# "Familyname-BoldItalic[wght,wdth].ttf" => "Familyname-Bold[wght,wdth].ttf"
roman_counterpart = italic.replace("Italic", "")

if is_varfont:
if roman_counterpart not in font_filenames:
missing_roman.append(italic)
else:
italic_to_roman_mapping[italic] = roman_counterpart
def get_STAT_axis_value(font, tag):
for i, axis in enumerate(font["STAT"].table.DesignAxisRecord.Axis):
if axis.AxisTag == tag:
for axisValue in font["STAT"].table.AxisValueArray.AxisValue:
if axisValue.AxisIndex == i:
linkedValue = None
if hasattr(axisValue, "LinkedValue"):
linkedValue = axisValue.LinkedValue
return axisValue.Value, axisValue.Flags, linkedValue
return None, None, None

if missing_roman:
from fontbakery.utils import pretty_print_list

missing_roman = pretty_print_list(config, missing_roman)
def check_has_ital(ttFont):
font = os.path.basename(ttFont.reader.name)

if "STAT" not in ttFont:
yield FAIL, Message(
"missing-roman",
f"Italics missing a Roman counterpart, so couldn't check"
f" both Roman and Italic for 'ital' axis: {missing_roman}",
"no-stat",
f"Font {font} has no STAT table",
)
return

# Actual check starts here
for italic_filename in italic_to_roman_mapping:
italic = italic_filename
upright = italic_to_roman_mapping[italic_filename]
if "ital" not in [
axis.AxisTag for axis in ttFont["STAT"].table.DesignAxisRecord.Axis
]:
yield FAIL, Message(
"missing-ital-axis",
f"Font {font} lacks an 'ital' axis in the STAT table.",
)

for filepath in (upright, italic):
ttFont = TTFont(filepath)
if "ital" not in [
axis.AxisTag for axis in ttFont["STAT"].table.DesignAxisRecord.Axis
]:
yield FAIL, Message(
"missing-ital-axis",
f"Font {os.path.basename(filepath)}" f" is missing an 'ital' axis.",
)

def check_ital_is_binary_and_last(ttFont, is_italic):
font = os.path.basename(ttFont.reader.name)

@check(
id="opentype/italic_axis_in_STAT_is_boolean",
conditions=["style", "has_STAT_table"],
rationale="""
Check that the value of the 'ital' STAT axis is boolean (either 0 or 1),
and elided for the Upright and not elided for the Italic,
and that the Upright is linked to the Italic.
""",
proposal="https://github.com/fonttools/fontbakery/issues/3668",
)
def check_italic_axis_in_STAT_is_boolean(ttFont, style):
"""Ensure 'ital' STAT axis is boolean value"""

def get_STAT_axis(font, tag):
for axis in font["STAT"].table.DesignAxisRecord.Axis:
if axis.AxisTag == tag:
return axis
return None

def get_STAT_axis_value(font, tag):
for i, axis in enumerate(font["STAT"].table.DesignAxisRecord.Axis):
if axis.AxisTag == tag:
for axisValue in font["STAT"].table.AxisValueArray.AxisValue:
if axisValue.AxisIndex == i:
linkedValue = None
if hasattr(axisValue, "LinkedValue"):
linkedValue = axisValue.LinkedValue
return axisValue.Value, axisValue.Flags, linkedValue
return None, None, None
if "STAT" not in ttFont:
return

if not get_STAT_axis(ttFont, "ital"):
yield SKIP, "Font doesn't have an ital axis"
yield SKIP, "Font {font} doesn't have an ital axis"
return

tags = [axis.AxisTag in ttFont["STAT"].table.DesignAxisRecord.Axis]
ital_pos = tags.index("ital")
if ital_pos != len(tags) - 1:
yield WARN, Message(
"ital-axis-not-last",
f"Font {font} has 'ital' axis in position"
f" {ital_pos + 1} of {len(tags)}.",
)

value, flags, linkedValue = get_STAT_axis_value(ttFont, "ital")
if (value, flags, linkedValue) == (None, None, None):
yield SKIP, "No 'ital' axis in STAT."
return

passed = True
# Font has an 'ital' axis in STAT
if "Italic" in style:
if value != 1:
passed = False
yield WARN, Message(
"wrong-ital-axis-value",
f"STAT table 'ital' axis has wrong value."
f" Expected: 1, got '{value}'.",
)
if flags != 0:
passed = False
yield WARN, Message(
"wrong-ital-axis-flag",
f"STAT table 'ital' axis flag is wrong."
f" Expected: 0 (not elided), got '{flags}'.",
)
if is_italic:
expected_value = 1.0
expected_flags = 0x0000 # AxisValueTableFlags empty
else:
if value != 0:
passed = False
yield WARN, Message(
"wrong-ital-axis-value",
f"STAT table 'ital' axis has wrong value."
f" Expected: 0, got '{value}'.",
)
if flags != 2:
passed = False
yield WARN, Message(
"wrong-ital-axis-flag",
f"STAT table 'ital' axis flag is wrong.\n"
f"Expected: 2 (elided)\n"
f"Got: '{flags}'",
)
if linkedValue != 1:
passed = False
expected_value = 0.0
expected_flags = 0x0002 # ElidableAxisValueName

if value != expected_value:
yield WARN, Message(
"wrong-ital-axis-value",
f"{font} has STAT table 'ital' axis with wrong value."
f" Expected: {expected_value}, got '{value}'",
)

if flags != expected_flags:
yield WARN, Message(
"wrong-ital-axis-flag",
f"{font} has STAT table 'ital' axis with wrong flags."
f" Expected: {expected_flags}, got '{flags}'",
)

# If we are Roman, check for the linked value
if not is_italic:
if linked_value != 1.0:
yield WARN, Message(
"wrong-ital-axis-linkedvalue",
"STAT table 'ital' axis is not linked to Italic.",
f"{font} has STAT table 'ital' axis with wrong linked value."
f" Expected: 1.0, got '{linked_value}'",
)

if passed:
yield PASS, "STAT table ital axis values are good."

def segment_vf_collection(fonts):
roman_italic = []
italics = []
non_italics = []
for font in fonts:
if "-Italic[" in font:
italics.append(font)
else:
non_italics.append(font)

for italic in italics:
# Find a matching roman
suspected_roman = italic.replace("-Italic[", "[")
index = non_italics.index(suspected_roman)
roman = None
if index:
roman = non_italics.pop(index)
roman_italic.append((roman, italic))

# Now add all the remaining non-italic fonts
for roman in non_italics:
roman_italic.append((roman, None))

return roman_italic


@check(
id="opentype/italic_axis_last",
conditions=["style", "has_STAT_table"],
id="opentype/STAT/ital_axis",
rationale="""
Check that the 'ital' STAT axis is last in axis order.
Check that related Upright and Italic VFs have an
'ital' axis in the STAT table; that if there is a stat axis,
it is the last one, and that it is boolean and set correctly.
""",
proposal="https://github.com/fonttools/fontbakery/issues/3669",
proposal=[
"https://github.com/fonttools/fontbakery/issues/2934",
"https://github.com/fonttools/fontbakery/issues/3668",
"https://github.com/fonttools/fontbakery/issues/3669",
],
)
def check_italic_axis_last(ttFont, style):
"""Ensure 'ital' STAT axis is last."""

def get_STAT_axis(font, tag):
for axis in font["STAT"].table.DesignAxisRecord.Axis:
if axis.AxisTag == tag:
return axis
return None

axis = get_STAT_axis(ttFont, "ital")
if not axis:
yield SKIP, "No 'ital' axis in STAT."
return
def check_STAT_ital_axis(fonts, config):
"""Ensure VFs have 'ital' STAT axis."""

if ttFont["STAT"].table.DesignAxisRecord.Axis[-1].AxisTag != "ital":
yield WARN, Message(
"ital-axis-not-last",
"STAT table 'ital' axis is not the last in the axis order.",
)
else:
yield PASS, "STAT table ital axis order is good."
for roman, italic in segment_vf_collection(fonts):
if roman and italic:
# These should definitely both have an ital axis
yield from check_has_ital(roman)
yield from check_has_ital(italic)
yield from check_ital_is_binary_and_last(roman, False)
yield from check_ital_is_binary_and_last(italic, True)
elif italic:
yield FAIL, Message(
"missing-roman",
f"Italic font {italic} has no matching Roman font.",
)
elif roman:
yield from check_ital_is_binary_and_last(roman, False)
7 changes: 3 additions & 4 deletions Lib/fontbakery/legacy_checkids.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,6 @@
"com.google.fonts/check/glyf_non_transformed_duplicate_components": "opentype/glyf_non_transformed_duplicate_components",
"com.google.fonts/check/glyf_unused_data": "opentype/glyf_unused_data",
"com.google.fonts/check/italic_angle": "opentype/italic_angle",
"com.google.fonts/check/italic_axis_in_stat": "opentype/italic_axis_in_STAT",
"com.google.fonts/check/italic_axis_in_stat_is_boolean": "opentype/italic_axis_in_STAT_is_boolean",
"com.google.fonts/check/italic_axis_last": "opentype/italic_axis_last",
"com.google.fonts/check/kern_table": "opentype/kern_table",
"com.google.fonts/check/layout_valid_feature_tags": "opentype/layout_valid_feature_tags",
"com.google.fonts/check/layout_valid_language_tags": "opentype/layout_valid_language_tags",
Expand All @@ -252,7 +249,9 @@
"com.google.fonts/check/post_table_version": "opentype/post_table_version",
"com.adobe.fonts/check/postscript_name": "opentype/postscript_name",
"com.google.fonts/check/slant_direction": "opentype/slant_direction",
# TODO: "opentype/STAT/ital_axis",
"com.google.fonts/check/italic_axis_in_stat": "opentype/STAT/ital_axis",
"com.google.fonts/check/italic_axis_in_stat_is_boolean": "opentype/STAT/ital_axis",
"com.google.fonts/check/italic_axis_last": "opentype/STAT/ital_axis",
"com.google.fonts/check/unitsperem": "opentype/unitsperem",
"com.adobe.fonts/check/varfont/distinct_instance_records": "opentype/varfont/distinct_instance_records",
"com.google.fonts/check/varfont/family_axis_ranges": "opentype/varfont/family_axis_ranges",
Expand Down
4 changes: 1 addition & 3 deletions Lib/fontbakery/profiles/adobefonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,9 @@
"opentype/gdef_non_mark_chars",
"opentype/gdef_spacing_marks",
"opentype/italic_angle",
"opentype/italic_axis_in_STAT",
"opentype/italic_axis_in_STAT_is_boolean",
"opentype/italic_axis_last",
"opentype/mac_style",
"opentype/slant_direction",
"opentype/STAT/ital_axis",
"opentype/varfont/family_axis_ranges",
"opentype/vendor_id",
#
Expand Down
2 changes: 1 addition & 1 deletion Lib/fontbakery/profiles/googlefonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@
"reason": "For Google Fonts, all messages from this check are considered FAILs",
},
],
"opentype/italic_axis_last": [
"opentype/STAT/ital_axis": [
{
"code": "ital-axis-not-last",
"status": "FAIL",
Expand Down
4 changes: 1 addition & 3 deletions Lib/fontbakery/profiles/opentype.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
"opentype/glyf_non_transformed_duplicate_components",
"opentype/glyf_unused_data",
"opentype/italic_angle",
"opentype/italic_axis_in_STAT",
"opentype/italic_axis_in_STAT_is_boolean",
"opentype/italic_axis_last",
"opentype/kern_table",
"opentype/layout_valid_feature_tags",
"opentype/layout_valid_language_tags",
Expand All @@ -44,6 +41,7 @@
"opentype/postscript_name",
"opentype/post_table_version",
"opentype/slant_direction",
"opentype/STAT/ital_axis",
"opentype/unitsperem",
"opentype/varfont/distinct_instance_records",
"opentype/varfont/family_axis_ranges",
Expand Down
10 changes: 5 additions & 5 deletions tests/test_checks_googlefonts_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ def test_check_italic_angle(check):
)


@check_id("opentype/italic_axis_in_STAT_is_boolean", profile=googlefonts_profile)
def test_check_italic_axis_in_STAT_is_boolean(check):
@check_id("opentype/STAT/ital_axis", profile=googlefonts_profile)
def test_check_STAT_ital_axis__axes_values_and_flags(check):
"""Ensure 'ital' STAT axis is boolean value"""

# PASS
Expand Down Expand Up @@ -101,9 +101,9 @@ def test_check_italic_axis_in_STAT_is_boolean(check):
assert_results_contain(check(ttFont), FAIL, "wrong-ital-axis-linkedvalue")


@check_id("opentype/italic_axis_last", profile=googlefonts_profile)
def test_check_italic_axis_last(check):
"""Ensure 'ital' STAT axis is boolean value"""
@check_id("opentype/STAT/ital_axis", profile=googlefonts_profile)
def test_check_STAT_ital_axis(check):
"""Ensure VFs have 'ital' STAT axis."""

font = TEST_FILE("shantell/ShantellSans-Italic[BNCE,INFM,SPAC,wght].ttf")
ttFont = TTFont(font)
Expand Down
Loading

0 comments on commit 5050a74

Please sign in to comment.