Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

a bit more refactoring #4915

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ A more detailed list of changes is available in the corresponding milestones for

### On the Google Fonts profile
- **[googlefonts/name/family_name_compliance]** renamed to **googlefonts/family_name_compliance**.
- **[googlefonts/STAT** renamed to **googlefonts/STAT/compulsory_axis_values**


## 0.13.0a5 (2024-Nov-10)
Expand Down
106 changes: 106 additions & 0 deletions Lib/fontbakery/checks/vendorspecific/googlefonts/STAT/axisregistry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from fontbakery.prelude import check, Message, INFO, FAIL
from fontbakery.constants import PlatformID, WindowsEncodingID, WindowsLanguageID
from fontbakery.checks.vendorspecific.googlefonts.utils import GFAxisRegistry


@check(
id="googlefonts/STAT/axisregistry",
rationale="""
Check that particle names and values on STAT table match the fallback names
in each axis entry at the Google Fonts Axis Registry, available at
https://github.com/google/fonts/tree/main/axisregistry
""",
conditions=["is_variable_font"],
proposal="https://github.com/fonttools/fontbakery/issues/3022",
)
def check_STAT_axisregistry_names(ttFont):
"""
Validate STAT particle names and values match the fallback names in GFAxisRegistry.
"""

def normalize_name(name):
return "".join(name.split(" "))

format4_entries = False
if "STAT" not in ttFont:
yield FAIL, "Font is missing STAT table."
return
axis_value_array = ttFont["STAT"].table.AxisValueArray
if not axis_value_array:
yield FAIL, Message(
"missing-axis-values", "STAT table is missing Axis Value Records"
)
return

for axis_value in axis_value_array.AxisValue:
if axis_value.Format == 4:
coords = []
for record in axis_value.AxisValueRecord:
axis = ttFont["STAT"].table.DesignAxisRecord.Axis[record.AxisIndex]
coords.append(f"{axis.AxisTag}:{record.Value}")
coords = ", ".join(coords)

name_entry = ttFont["name"].getName(
axis_value.ValueNameID,
PlatformID.WINDOWS,
WindowsEncodingID.UNICODE_BMP,
WindowsLanguageID.ENGLISH_USA,
)
format4_entries = True
yield INFO, Message("format-4", f"'{name_entry.toUnicode()}' at ({coords})")
continue

axis = ttFont["STAT"].table.DesignAxisRecord.Axis[axis_value.AxisIndex]
# If a family has a MORF axis, we allow users to define their own
# axisValues for this axis.
if axis.AxisTag == "MORF":
continue
if axis.AxisTag in GFAxisRegistry().keys():
fallbacks = GFAxisRegistry()[axis.AxisTag].fallback
fallbacks = {f.name: f.value for f in fallbacks}

# Here we assume that it is enough to check for only the Windows,
# English USA entry corresponding to a given nameID. It is up to other
# checks to ensure all different platform/encoding entries
# with a given nameID are consistent in the name table.
name_entry = ttFont["name"].getName(
axis_value.ValueNameID,
PlatformID.WINDOWS,
WindowsEncodingID.UNICODE_BMP,
WindowsLanguageID.ENGLISH_USA,
)

# Here "name_entry" has the user-friendly name of the current AxisValue
# We want to ensure that this string shows up as a "fallback" name
# on the GF Axis Registry for this specific variation axis tag.
name = normalize_name(name_entry.toUnicode())
expected_names = [normalize_name(n) for n in fallbacks.keys()]
if hasattr(axis_value, "Value"): # Format 1 & 3
is_value = axis_value.Value
elif hasattr(axis_value, "NominalValue"): # Format 2
is_value = axis_value.NominalValue
if name not in expected_names:
expected_names = ", ".join(expected_names)
yield FAIL, Message(
"invalid-name",
f"On the font variation axis '{axis.AxisTag}',"
f" the name '{name_entry.toUnicode()}'"
f" is not among the expected ones ({expected_names}) according"
" to the Google Fonts Axis Registry.",
)
elif is_value != fallbacks[name_entry.toUnicode()]:
yield FAIL, Message(
"bad-coordinate",
f"Axis Value for '{axis.AxisTag}':'{name_entry.toUnicode()}' is"
f" expected to be '{fallbacks[name_entry.toUnicode()]}' but this"
f" font has '{name_entry.toUnicode()}'='{axis_value.Value}'.",
)

if format4_entries:
yield INFO, Message(
"format-4",
"The GF Axis Registry does not currently contain fallback names"
" for the combination of values for more than a single axis,"
" which is what these 'format 4' entries are designed to describe,"
" so this check will ignore them for now.",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from fontbakery.prelude import check, Message, FAIL
from fontbakery.utils import markdown_table


@check(
id="googlefonts/STAT/compulsory_axis_values",
conditions=["is_variable_font", "expected_font_names"],
rationale="""
Check a font's STAT table contains compulsory Axis Values which exist
in the Google Fonts Axis Registry.

We cannot determine what Axis Values the user will set for axes such as
opsz, GRAD since these axes are unique for each font so we'll skip them.
""",
proposal="https://github.com/fonttools/fontbakery/pull/3800",
)
def check_stat(ttFont, expected_font_names):
"""Check a font's STAT table contains compulsory Axis Values."""
if "STAT" not in ttFont:
yield FAIL, "Font is missing STAT table"
return

AXES_TO_CHECK = {
"CASL",
"CRSV",
"FILL",
"FLAR",
"MONO",
"SOFT",
"VOLM",
"wdth",
"wght",
"WONK",
}

def stat_axis_values(ttFont):
name = ttFont["name"]
stat = ttFont["STAT"].table
axes = [a.AxisTag for a in stat.DesignAxisRecord.Axis]
res = {}
if ttFont["STAT"].table.AxisValueCount == 0:
return res
axis_values = stat.AxisValueArray.AxisValue
for ax in axis_values:
# Google Fonts axis registry cannot check format 4 Axis Values
if ax.Format == 4:
continue
axis_tag = axes[ax.AxisIndex]
if axis_tag not in AXES_TO_CHECK:
continue
ax_name = name.getName(ax.ValueNameID, 3, 1, 0x409).toUnicode()
if ax.Format == 2:
value = ax.NominalValue
else:
value = ax.Value
res[(axis_tag, ax_name)] = {
"Axis": axis_tag,
"Name": ax_name,
"Flags": ax.Flags,
"Value": value,
"LinkedValue": None
if not hasattr(ax, "LinkedValue")
else ax.LinkedValue,
}
return res

font_axis_values = stat_axis_values(ttFont)
expected_axis_values = stat_axis_values(expected_font_names)

table = []
for axis, name in set(font_axis_values.keys()) | set(expected_axis_values.keys()):
row = {}
key = (axis, name)
if key in font_axis_values:
row["Name"] = name
row["Axis"] = axis
row["Current Value"] = font_axis_values[key]["Value"]
row["Current Flags"] = font_axis_values[key]["Flags"]
row["Current LinkedValue"] = font_axis_values[key]["LinkedValue"]
else:
row["Name"] = name
row["Axis"] = axis
row["Current Value"] = "N/A"
row["Current Flags"] = "N/A"
row["Current LinkedValue"] = "N/A"
if key in expected_axis_values:
row["Name"] = name
row["Axis"] = axis
row["Expected Value"] = expected_axis_values[key]["Value"]
row["Expected Flags"] = expected_axis_values[key]["Flags"]
row["Expected LinkedValue"] = expected_axis_values[key]["LinkedValue"]
else:
row["Name"] = name
row["Axis"] = axis
row["Expected Value"] = "N/A"
row["Expected Flags"] = "N/A"
row["Expected LinkedValue"] = "N/A"
table.append(row)
table.sort(key=lambda k: (k["Axis"], str(k["Expected Value"])))
md_table = markdown_table(table)

is_italic = any(a.axisTag in ["ital", "slnt"] for a in ttFont["fvar"].axes)
missing_ital_av = any("Italic" in r["Name"] for r in table)
if is_italic and missing_ital_av:
yield FAIL, Message("missing-ital-axis-values", "Italic Axis Value missing.")

if font_axis_values != expected_axis_values:
yield FAIL, Message(
"bad-axis-values",
f"Compulsory STAT Axis Values are incorrect:\n\n{md_table}\n\n",
)
44 changes: 44 additions & 0 deletions Lib/fontbakery/checks/vendorspecific/googlefonts/axes_match.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from fontbakery.prelude import FAIL, PASS, Message, check


@check(
id="googlefonts/axes_match",
conditions=["is_variable_font", "remote_style"],
rationale="""
An updated font family must include the same axes found in the Google "
Fonts version, with the same axis ranges.
""",
)
def check_axes_match(ttFont, remote_style):
"""Check if the axes match between the font and the Google Fonts version."""
remote_axes = {
a.axisTag: (a.minValue, a.maxValue) for a in remote_style["fvar"].axes
}
font_axes = {a.axisTag: (a.minValue, a.maxValue) for a in ttFont["fvar"].axes}

missing_axes = []
for axis, remote_axis_range in remote_axes.items():
if axis not in font_axes:
missing_axes.append(axis)
continue
axis_range = font_axes[axis]
axis_min, axis_max = axis_range
remote_axis_min, remote_axis_max = remote_axis_range
if axis_min > remote_axis_min:
yield FAIL, Message(
"axis-min-out-of-range",
f"Axis '{axis}' min value is out of range."
f" Expected '{remote_axis_min}', got '{axis_min}'.",
)
if axis_max < remote_axis_max:
yield FAIL, Message(
"axis-max-out-of-range",
f"Axis {axis} max value is out of range."
f" Expected {remote_axis_max}, got {axis_max}.",
)

if missing_axes:
missing_axes = ", ".join(missing_axes)
yield FAIL, Message("missing-axes", f"Missing axes: {missing_axes}")
else:
yield PASS, "Axes match Google Fonts version."
Loading