Skip to content

Commit

Permalink
splitting out a bit more of googlefonts checks
Browse files Browse the repository at this point in the history
  • Loading branch information
felipesanches committed Nov 25, 2024
1 parent 81528d5 commit da87474
Show file tree
Hide file tree
Showing 59 changed files with 2,169 additions and 2,117 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Below are the noteworthy changes from each release.
A more detailed list of changes is available in the corresponding milestones for each release in the Github issue tracker (https://github.com/googlefonts/fontbakery/milestones?state=closed).

## Upcoming release: 0.13.0 (2024-Nov-15)

## Upcoming release: 0.13.0a6 (2024-Nov-??)
### Noteworthy code-changes
- **Users are encouraged to try this pre-release as we're approaching the date when we'll cut a final v0.13.0 release...**
- Full fix for bug where check details with more severe status would be missing from the HTML report if a check with the same ID but less severe status was omitted (issue #4687)

### New checks
Expand All @@ -16,6 +16,8 @@ A more detailed list of changes is available in the corresponding milestones for
- **[family/control_chars]:** renamed to **control_chars** as it is not realy a family-wide check. (issue #4896)
- **[glyf_nested_components]** renamed to **nested_components**.

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


## 0.13.0a5 (2024-Nov-10)
Expand Down
50 changes: 50 additions & 0 deletions Lib/fontbakery/checks/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,53 @@ def cff_analysis(font):
_analyze_cff(analysis, top_dict, private_dict, fd_index)

return analysis


@condition(Font)
def licenses(font):
"""Get a list of paths for every license
file found in a font project."""
from fontbakery.utils import git_rootdir

found = []
family_directory = font.family_directory
search_paths = [family_directory]
gitroot = git_rootdir(family_directory)
if gitroot and gitroot not in search_paths:
search_paths.append(gitroot)

for directory in search_paths:
if directory:
for license_filename in ["OFL.txt", "LICENSE.txt"]:
license_path = os.path.join(directory, license_filename)
if os.path.exists(license_path):
found.append(license_path)
return found


@condition(Font)
def license_contents(font):
if font.license_path:
return open(font.license_path, encoding="utf-8").read().replace(" \n", "\n")


@condition(Font)
def license_path(font):
"""Get license path."""
# This assumes that a repo can have multiple license files
# and they're all the same.
# FIXME: We should have a fontbakery check for that, though!
if font.licenses and len(font.licenses) > 0:
return font.licenses[0]


@condition(Font)
def license_filename(font):
"""Get license filename."""
if font.license_path:
return os.path.basename(font.license_path)


@condition(Font)
def is_ofl(font):
return font.license_filename and "OFL" in font.license_filename
6 changes: 3 additions & 3 deletions Lib/fontbakery/checks/tnum_glyphs_equal_widths.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ def check_tnum_glyphs_equal_widths(ttFont):
check_text = TEST_STR # type: ignore # noqa:F821 pylint:disable=E0602

# Evaluate any unicode escape sequences, e.g. \N{PLUS SIGN}
check_text = "".join([
parse_unicode_escape(line) for line in check_text.splitlines()
])
check_text = "".join(
[parse_unicode_escape(line) for line in check_text.splitlines()]
)

# Check for existence of tnum opentype feature
if "GSUB" not in ttFont:
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import os

from fontbakery.prelude import check, Message, FAIL, WARN
from fontbakery.utils import typo_metrics_enabled


@check(
id="googlefonts/cjk_vertical_metrics",
conditions=["is_cjk_font", "not listed_on_gfonts_api"],
rationale="""
CJK fonts have different vertical metrics when compared to Latin fonts.
We follow the schema developed by dr Ken Lunde for Source Han Sans and
the Noto CJK fonts.
Our documentation includes further information:
https://github.com/googlefonts/gf-docs/tree/main/Spec#cjk-vertical-metrics
""",
proposal="https://github.com/fonttools/fontbakery/pull/2797",
)
def check_cjk_vertical_metrics(ttFont):
"""Check font follows the Google Fonts CJK vertical metric schema"""

filename = os.path.basename(ttFont.reader.file.name)

# Check necessary tables are present.
missing_tables = False
required = ["OS/2", "hhea", "head"]
for key in required:
if key not in ttFont:
missing_tables = True
yield FAIL, Message(f"lacks-{key}", f"{filename} lacks a '{key}' table.")

if missing_tables:
return

font_upm = ttFont["head"].unitsPerEm
font_metrics = {
"OS/2.sTypoAscender": ttFont["OS/2"].sTypoAscender,
"OS/2.sTypoDescender": ttFont["OS/2"].sTypoDescender,
"OS/2.sTypoLineGap": ttFont["OS/2"].sTypoLineGap,
"hhea.ascent": ttFont["hhea"].ascent,
"hhea.descent": ttFont["hhea"].descent,
"hhea.lineGap": ttFont["hhea"].lineGap,
"OS/2.usWinAscent": ttFont["OS/2"].usWinAscent,
"OS/2.usWinDescent": ttFont["OS/2"].usWinDescent,
}
expected_metrics = {
"OS/2.sTypoAscender": round(font_upm * 0.88),
"OS/2.sTypoDescender": round(font_upm * -0.12),
"OS/2.sTypoLineGap": 0,
"hhea.lineGap": 0,
}

# Check fsSelection bit 7 is not enabled
if typo_metrics_enabled(ttFont):
yield FAIL, Message(
"bad-fselection-bit7", "OS/2 fsSelection bit 7 must be disabled"
)

# Check typo metrics and hhea lineGap match our expected values
for k in expected_metrics:
if font_metrics[k] != expected_metrics[k]:
yield FAIL, Message(
f"bad-{k}",
f'{k} is "{font_metrics[k]}" it should be {expected_metrics[k]}',
)

# Check hhea and win values match
if font_metrics["hhea.ascent"] != font_metrics["OS/2.usWinAscent"]:
yield FAIL, Message(
"ascent-mismatch", "hhea.ascent must match OS/2.usWinAscent"
)

if abs(font_metrics["hhea.descent"]) != font_metrics["OS/2.usWinDescent"]:
yield FAIL, Message(
"descent-mismatch",
"hhea.descent must match absolute value of OS/2.usWinDescent",
)

# Check the sum of the hhea metrics is between 1.1-1.5x of the font's upm
hhea_sum = (
font_metrics["hhea.ascent"]
+ abs(font_metrics["hhea.descent"])
+ font_metrics["hhea.lineGap"]
) / font_upm
if not 1.1 < hhea_sum <= 1.5:
yield WARN, Message(
"bad-hhea-range",
f"We recommend the absolute sum of the hhea metrics should be"
f" between 1.1-1.4x of the font's upm. This font has {hhea_sum}x",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from fontbakery.prelude import check, Message, FAIL


@check(
id="googlefonts/cjk_vertical_metrics_regressions",
conditions=["is_cjk_font", "regular_remote_style", "regular_ttFont"],
rationale="""
Check CJK family has the same vertical metrics as the same family
hosted on Google Fonts.
""",
proposal="https://github.com/fonttools/fontbakery/pull/3244",
)
def check_cjk_vertical_metrics_regressions(regular_ttFont, regular_remote_style):
"""Check if the vertical metrics of a CJK family are similar to the same
family hosted on Google Fonts."""
import math

gf_ttFont = regular_remote_style
ttFont = regular_ttFont

if not ttFont:
yield FAIL, Message(
"couldnt-find-local-regular",
"Could not identify a local Regular style font",
)
return
if not gf_ttFont:
yield FAIL, Message(
"couldnt-find-remote-regular",
"Could not identify a Regular style font hosted on Google Fonts",
)
return

upm_scale = ttFont["head"].unitsPerEm / gf_ttFont["head"].unitsPerEm

for tbl, attrib in [
("OS/2", "sTypoAscender"),
("OS/2", "sTypoDescender"),
("OS/2", "sTypoLineGap"),
("OS/2", "usWinAscent"),
("OS/2", "usWinDescent"),
("hhea", "ascent"),
("hhea", "descent"),
("hhea", "lineGap"),
]:
gf_val = math.ceil(getattr(gf_ttFont[tbl], attrib) * upm_scale)
f_val = math.ceil(getattr(ttFont[tbl], attrib))
if gf_val != f_val:
yield FAIL, Message(
"cjk-metric-regression",
f" {tbl} {attrib} is {f_val}" f" when it should be {gf_val}",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import os

from fontbakery.prelude import check, Message, FAIL
from fontbakery.utils import pretty_print_list


@check(
id="googlefonts/family/has_license",
conditions=["gfonts_repo_structure"],
rationale="""
A license file is required for all fonts in the Google Fonts collection.
This checks that the font's directory contains a file named OFL.txt or
LICENSE.txt.
""",
proposal="https://github.com/fonttools/fontbakery/issues/4829", # legacy check
)
def check_family_has_license(licenses, config):
"""Check font has a license."""

if len(licenses) > 1:
filenames = pretty_print_list(
config, [os.path.basename(license) for license in licenses]
)
yield FAIL, Message(
"multiple",
f"More than a single license file found: {filenames}",
)
elif not licenses:
yield FAIL, Message(
"no-license",
"No license file was found."
" Please add an OFL.txt or a LICENSE.txt file."
" If you are running fontbakery on a Google Fonts"
" upstream repo, which is fine, just make sure"
" there is a temporary license file in the same folder.",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from fontbakery.prelude import check, Message, PASS, FAIL
from fontbakery.constants import NameID


@check(
id="googlefonts/family_name_compliance",
rationale="""
Checks the family name for compliance with the Google Fonts Guide.
https://googlefonts.github.io/gf-guide/onboarding.html#new-fonts
If you want to have your family name added to the CamelCase
exceptions list, please submit a pull request to the
camelcased_familyname_exceptions.txt file.
Similarly, abbreviations can be submitted to the
abbreviations_familyname_exceptions.txt file.
These are located in the Lib/fontbakery/data/googlefonts/ directory
of the FontBakery source code currently hosted at
https://github.com/fonttools/fontbakery/
""",
conditions=[],
proposal="https://github.com/fonttools/fontbakery/issues/4049",
)
def check_family_name_compliance(ttFont):
"""Check family name for GF Guide compliance."""
import re
from pkg_resources import resource_filename
from fontbakery.utils import get_name_entries

camelcase_exceptions_txt = "data/googlefonts/camelcased_familyname_exceptions.txt"
abbreviations_exceptions_txt = (
"data/googlefonts/abbreviations_familyname_exceptions.txt"
)

if get_name_entries(ttFont, NameID.TYPOGRAPHIC_FAMILY_NAME):
family_name = get_name_entries(ttFont, NameID.TYPOGRAPHIC_FAMILY_NAME)[
0
].toUnicode()
else:
family_name = get_name_entries(ttFont, NameID.FONT_FAMILY_NAME)[0].toUnicode()

# CamelCase
if bool(re.match(r"([A-Z][a-z]+){2,}", family_name)):
known_exception = False

# Process exceptions
filename = resource_filename("fontbakery", camelcase_exceptions_txt)
for exception in open(filename, "r", encoding="utf-8").readlines():
exception = exception.split("#")[0].strip()
if exception == "":
continue
if exception in family_name:
known_exception = True
yield PASS, Message(
"known-camelcase-exception",
"Family name is a known exception to the CamelCase rule.",
)
break

if not known_exception:
yield FAIL, Message(
"camelcase",
f'"{family_name}" is a CamelCased name.'
f" To solve this, simply use spaces"
f" instead in the font name.",
)

# Abbreviations
if bool(re.match(r"([A-Z]){2,}", family_name)):
known_exception = False

# Process exceptions
filename = resource_filename("fontbakery", abbreviations_exceptions_txt)
for exception in open(filename, "r", encoding="utf-8").readlines():
exception = exception.split("#")[0].strip()
if exception == "":
continue
if exception in family_name:
known_exception = True
yield PASS, Message(
"known-abbreviation-exception",
"Family name is a known exception to the abbreviation rule.",
)
break

if not known_exception:
# Allow SC ending
if not family_name.endswith("SC"):
yield FAIL, Message(
"abbreviation", f'"{family_name}" contains an abbreviation.'
)

# Allowed characters
forbidden_characters = re.findall(r"[^a-zA-Z0-9 ]", family_name)
if forbidden_characters:
forbidden_characters = "".join(sorted(list(set(forbidden_characters))))
yield FAIL, Message(
"forbidden-characters",
f'"{family_name}" contains the following characters'
f' which are not allowed: "{forbidden_characters}".',
)

# Starts with uppercase
if not bool(re.match(r"^[A-Z]", family_name)):
yield FAIL, Message(
"starts-with-not-uppercase",
f'"{family_name}" doesn\'t start with an uppercase letter.',
)
Loading

0 comments on commit da87474

Please sign in to comment.