def get_module_profile(module, name=None): """ Get or create a profile from a module and return it. If the name `module.profile` is present the value of that is returned. Otherwise, if the name `module.profile_factory` is present, a new profile is created using `module.profile_factory` and then `profile.auto_register` is called with the module namespace. If neither name is defined, the module is not considered a profile-module and None is returned. TODO: describe the `name` argument and better define the signature of `profile_factory`. The `module` argument is expected to behave like a python module. The optional `name` argument is used when `profile_factory` is called to give a name to the default section of the new profile. If name is not present `module.__name__` is the fallback. `profile_factory` is called like this: `profile = module.profile_factory(default_section=default_section)` """ try: # if profile is defined we just use it return module.profile except AttributeError: # > 'module' object has no attribute 'profile' # try to create one on the fly. # e.g. module.__name__ == "fontbakery.profiles.cmap" if "profile_factory" not in module.__dict__: return None default_section = Section(name or module.__name__) profile = module.profile_factory(default_section=default_section, module_spec=module.__spec__) profile.auto_register(module.__dict__) return profile
def _test(profile_imports, expected_tests, expected_conditions=tuple()): profile = profile_factory(default_section=Section("Testing")) profile.auto_register({}, profile_imports=profile_imports) profile.test_expected_checks(expected_tests) if expected_conditions: registered_conditions = profile.conditions.keys() for name in expected_conditions: assert name in registered_conditions, \ f'"{name}" is expected to be registered as a condition.'
def test_in_and_exclude_checks_default(): profile_imports = ("fontbakery.profiles.opentype", ) profile = profile_factory(default_section=Section("OpenType Testing")) profile.auto_register({}, profile_imports=profile_imports) profile.test_dependencies() explicit_checks = None # "All checks aboard" exclude_checks = None # "No checks left behind" iterargs = {"font": 1} check_names = { c[1].id for c in \ profile.execution_order(iterargs, explicit_checks=explicit_checks, exclude_checks=exclude_checks) } check_names_expected = set() for section in profile.sections: for check in section.checks: check_names_expected.add(check.id) assert check_names == check_names_expected
def test_external_profile(): """Test the creation of external profiles.""" profile = profile_factory(default_section=Section("Dalton Maag OpenType")) profile.auto_register(globals(), profile_imports=["fontbakery.profiles.opentype"], filter_func=check_filter) # Probe some tests expected_tests = [ "com.google.fonts/check/family/panose_proportion", "com.google.fonts/check/varfont/regular_opsz_coord" ] profile.test_expected_checks(expected_tests) # Probe tests we don't want assert "com.google.fonts/check/ftxvalidator" not in profile._check_registry.keys( ) assert len(profile.sections) > 1
def test_in_and_exclude_checks(): profile_imports = ("fontbakery.profiles.opentype", ) profile = profile_factory(default_section=Section("OpenType Testing")) profile.auto_register({}, profile_imports=profile_imports) profile.test_dependencies() explicit_checks = ["06", "07"] # "06" or "07" in check ID exclude_checks = ["065", "079"] # "065" or "079" in check ID iterargs = {"font": 1} check_names = { c[1].id for c in \ profile.execution_order(iterargs, explicit_checks=explicit_checks, exclude_checks=exclude_checks) } check_names_expected = set() for section in profile.sections: for check in section.checks: if any(i in check.id for i in explicit_checks) and \ not any(x in check.id for x in exclude_checks): check_names_expected.add(check.id) assert check_names == check_names_expected
import textwrap from pathlib import Path from fontbakery.callable import check, condition from fontbakery.checkrunner import FAIL, PASS, SKIP from fontbakery.section import Section from fontbakery.message import Message from fontbakery.fonts_profile import profile_factory from vharfbuzz import Vharfbuzz from os.path import basename, relpath from stringbrewer import StringBrewer from collidoscope import Collidoscope shaping_basedir = Path("qa", "shaping_tests") profile_imports = () profile = profile_factory(default_section=Section("Shaping Checks")) PROFILE_CHECKS = [ "com.google.fonts/check/shaping/regression", "com.google.fonts/check/shaping/forbidden", "com.google.fonts/check/shaping/collides", ] STYLESHEET = """ <style type="text/css"> @font-face {font-family: "TestFont"; src: url(%s);} .tf { font-family: "TestFont"; } .shaping pre { font-size: 1.2rem; } .shaping li { font-size: 1.2rem; border-top: 1px solid #ddd; padding: 12px; margin-top: 12px; } .shaping-svg { height: 100px; margin:10px; transform: matrix(1, 0, 0, -1, 0, 0); } </style>
"hhea", "dsig", "hmtx", "gdef", "gpos", "kern", "glyf", "fvar", "stat", "layout", "shared_conditions", ), ) profile_imports = (OPENTYPE_PROFILE_IMPORTS, ) profile = profile_factory( default_section=Section("OpenType Specification Checks")) OPENTYPE_PROFILE_CHECKS = [ 'com.google.fonts/check/family/underline_thickness', 'com.google.fonts/check/family/panose_proportion', 'com.google.fonts/check/family/panose_familytype', 'com.google.fonts/check/family/equal_unicode_encodings', 'com.google.fonts/check/family/equal_font_versions', 'com.adobe.fonts/check/family/bold_italic_unique_for_nameid1', 'com.adobe.fonts/check/family/max_4_fonts_per_family_name', 'com.adobe.fonts/check/name/postscript_vs_cff', 'com.adobe.fonts/check/name/postscript_name_consistency', 'com.adobe.fonts/check/name/empty_records', 'com.google.fonts/check/name/no_copyright_on_description', 'com.google.fonts/check/name/match_familyname_fullfont', 'com.google.fonts/check/varfont/regular_wght_coord',
from fontbakery.callable import check from fontbakery.section import Section from fontbakery.status import PASS, FAIL, WARN from fontbakery.fonts_profile import profile_factory from fontbakery.message import Message profile = profile_factory(default_section=Section("Just a Test")) @check( id='com.google.fonts/check_for_testing/configuration', rationale=""" Check that we can inject the configuration object and read it. """, ) def com_google_fonts_check_for_testing_configuration(config): """Check if we can inject a config file""" if config and "a_test_profile" in config and config["a_test_profile"]["OK"] == 123: yield PASS, 'we have injected a config' else: yield FAIL, "config variable didn't look like we expected" profile.auto_register(globals())
for yExpected in [-180, -90, 0, 90, 180]: if close_but_not_on(angle, yExpected, 0.5): warnings.append(f"{glyphname}: {s}") if warnings: formatted_list = " * " + pretty_print_list(sorted(warnings), sep="\n * ") yield WARN,\ Message("found-semi-vertical", f"The following glyphs have semi-vertical/semi-horizontal lines:\n" f"{formatted_list}") else: yield PASS, "No semi-horizontal/semi-vertical lines found." OUTLINE_PROFILE_IMPORTS = ( ".", ("shared_conditions",), ) profile_imports = (OUTLINE_PROFILE_IMPORTS,) profile = profile_factory(default_section=Section("Outline Correctness Checks")) OUTLINE_PROFILE_CHECKS = [ "com.google.fonts/check/outline_alignment_miss", "com.google.fonts/check/outline_short_segments", "com.google.fonts/check/outline_colinear_vectors", "com.google.fonts/check/outline_jaggy_segments", "com.google.fonts/check/outline_semi_vertical", ] profile.auto_register(globals()) profile.test_expected_checks(OUTLINE_PROFILE_CHECKS, exclusive=True)
from fontbakery.profiles.universal import UNIVERSAL_PROFILE_CHECKS from fontbakery.section import Section from fontbakery.status import WARN, PASS #, INFO, ERROR, SKIP, FAIL from fontbakery.callable import check #, disable from fontbakery.message import Message from fontbakery.fonts_profile import profile_factory from fontbakery.constants import (PlatformID, WindowsEncodingID, UnicodeEncodingID, MacintoshEncodingID) from .googlefonts_conditions import * # pylint: disable=wildcard-import,unused-wildcard-import profile_imports = ('fontbakery.profiles.universal', ) # Maybe this should be .googlefonts instead... profile = profile_factory(default_section=Section("Noto Fonts")) CMAP_TABLE_CHECKS = [ 'com.google.fonts/check/cmap/unexpected_subtables', ] OS2_TABLE_CHECKS = [ 'com.google.fonts/check/unicode_range_bits', ] # Maybe this should be GOOGLEFONTS_PROFILE_CHECKS instead... NOTOFONTS_PROFILE_CHECKS = \ UNIVERSAL_PROFILE_CHECKS + \ CMAP_TABLE_CHECKS + \ OS2_TABLE_CHECKS @check(id='com.google.fonts/check/cmap/unexpected_subtables', rationale="""
import os from fontbakery.callable import check from fontbakery.status import ERROR, FAIL, INFO, PASS, WARN from fontbakery.section import Section from fontbakery.message import Message # used to inform get_module_profile whether and how to create a profile from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import from .shared_conditions import is_variable_font profile_imports = ['.shared_conditions'] profile = profile_factory( default_section=Section("Checks inherited from Microsoft Font Validator")) @check(id='com.google.fonts/check/fontvalidator') def com_google_fonts_check_fontvalidator(font): """Checking with Microsoft Font Validator.""" # In some cases we want to override the severity level of # certain checks in FontValidator: downgrade_to_warn = [ # There are reports that this fontval check has an out-of-date # understanding of valid bits in fsSelection. # More info at: # https://github.com/googlei18n/fontmake/issues/414#issuecomment-379408127 "There are undefined bits set in fsSelection field", # FIX-ME: Why did we downgrade this one to WARN? "Misoriented contour" ]
def __init__( self, sections=None, iterargs=None, derived_iterables=None, conditions=None, aliases=None, expected_values=None, default_section=None, check_skip_filter=None, profile_tag=None, module_spec=None, ): """ sections: a list of sections, which are ideally ordered sets of individual checks. It makes no sense to have checks repeatedly, they yield the same results anyway, thus we don't allow this. iterargs: maping 'singular' variable names to the iterable in values e.g.: `{'font': 'fonts'}` in this case fonts must be iterable AND 'font' may not be a value NOR a condition name. derived_iterables: a dictionary {"plural": ("singular", bool simple)} where singular points to a condition, that consumes directly or indirectly iterargs. plural will be a list of all values the condition produces with all combination of it's iterargs. If simple is False, the result returns tuples of: (iterars, value) where iterargs is a tuple of ('iterargname', number index) Especially for cases where only one iterarg is involved, simple can be set to True and the result list will just contain the values. Example: @condition def ttFont(font): return TTFont(font) values={'fonts': ['font_0', 'font_1']} iterargs={'font': 'fonts'} derived_iterables={'ttFonts': ('ttFont', True)} # Then: ttfons = ( <TTFont object from font_0> , <TTFont object from font_1> ) # However derived_iterables={'ttFonts': ('ttFont', False)} ttfons = [ ((('font', 0), ), <TTFont object from font_0>) , ((('font', 1), ), <TTFont object from font_1>) ] We will: a) get all needed values/variable names from here b) add some validation, so that we know the values match our expectations! These values must be treated as user input! """ self._namespace = { "config": "config" # Filled in by checkrunner } self.iterargs = {} if iterargs: self._add_dict_to_namespace("iterargs", iterargs) self.derived_iterables = {} if derived_iterables: self._add_dict_to_namespace("derived_iterables", derived_iterables) self.aliases = {} if aliases: self._add_dict_to_namespace("aliases", aliases) self.conditions = {} if conditions: self._add_dict_to_namespace("conditions", conditions) self.expected_values = {} if expected_values: self._add_dict_to_namespace("expected_values", expected_values) self._check_registry = {} self._sections = OrderedDict() if sections: for section in sections: self.add_section(section) if not default_section: default_section = (sections[0] if sections and len(sections) else Section("Default")) self._default_section = default_section self.add_section(self._default_section) # currently only used for new check ids in self.check_log_override # only a-z everything else is deleted self.profile_tag = re.sub(r"[^a-z]", "", (profile_tag or self._default_section.name).lower()) self._check_skip_filter = check_skip_filter # Used in multiprocessing because pickling the profiles fail on # Mac and Windows. See: googlefonts/fontbakery#2982 # module_locator can actually a module.__spec__ but also just a dict # self.module_locator will always be just a dict if module_spec is None: # This is a bit of a hack, but the idea is to reduce boilerplate # when writing modules that directly define a profile. try: frame = inspect.currentframe().f_back while frame: # Note, if __spec__ is a local variable we shpuld be at a # module top level. It should also be the correct ModuleSpec # according to how we do this "usually" (as documented and # practiced as far as I'm aware of), e.g. the profile module # defines a profile object directly by calling a Profile constructor # (e.g. profies.ufo_sources) or indirectly via a profile_factory # (e.g. profiles.google_fonts). Otherwise, if this fails # or finds a wrong ModuleSpec, there's still the option to # pass module_spec as an argument (module.__spec__), which is # actually demonstrated in get_module_profile. if "__spec__" in frame.f_locals: module_spec = frame.f_locals["__spec__"] if module_spec and isinstance( module_spec, importlib.machinery.ModuleSpec): break module_spec = None # reset frame = frame.f_back finally: del frame # If not module_spec: this is only a problem in multiprocessing, in # that case we'll be failing to access this with an AttributeError. if module_spec is not None: self.module_locator = dict(name=module_spec.name, origin=module_spec.origin)
""" from fontbakery.callable import check, condition from fontbakery.section import Section from fontbakery.status import PASS, FAIL, WARN from fontbakery.fonts_profile import profile_factory from fontbakery.message import Message from fontTools.pens.boundsPen import BoundsPen from beziers.path import BezierPath from beziers.line import Line from beziers.point import Point import beziers import uharfbuzz as hb profile = profile_factory(default_section=Section("Suitability for In-Car Display")) DISCLAIMER = """ (Note that PASSing this check does not guarantee compliance with ISO 15008.) """ CHECKS = [ "com.google.fonts/check/iso15008_proportions", "com.google.fonts/check/iso15008_stem_width", "com.google.fonts/check/iso15008_intercharacter_spacing", "com.google.fonts/check/iso15008_interword_spacing", "com.google.fonts/check/iso15008_interline_spacing", ] def xheight_intersections(ttFont, glyph):
import os from fontbakery.status import PASS, FAIL, WARN, ERROR, INFO, SKIP from fontbakery.section import Section from fontbakery.callable import condition, check, disable from fontbakery.message import Message from fontbakery.fonts_profile import profile_factory from fontbakery.profiles.opentype import OPENTYPE_PROFILE_CHECKS from fontbakery.profiles.outline import OUTLINE_PROFILE_CHECKS from fontbakery.profiles.shaping import PROFILE_CHECKS as SHAPING_PROFILE_CHECKS profile_imports = ('fontbakery.profiles.opentype', 'fontbakery.profiles.outline', 'fontbakery.profiles.shaping', '.shared_conditions') profile = profile_factory(default_section=Section("Universal")) THIRDPARTY_CHECKS = [ 'com.google.fonts/check/ots', 'com.google.fonts/check/ftxvalidator', 'com.google.fonts/check/ftxvalidator_is_available' ] SUPERFAMILY_CHECKS = [ 'com.google.fonts/check/superfamily/list', 'com.google.fonts/check/superfamily/vertical_metrics', ] UNIVERSAL_PROFILE_CHECKS = \ OPENTYPE_PROFILE_CHECKS + \ OUTLINE_PROFILE_CHECKS + \
def test_googlefonts_checks_load(): profile_imports = ("fontbakery.profiles.googlefonts", ) profile = profile_factory(default_section=Section("Google Fonts Testing")) profile.auto_register({}, profile_imports=profile_imports) profile.test_dependencies()
def test_opentype_checks_load(): profile_imports = ("fontbakery.profiles.opentype", ) profile = profile_factory(default_section=Section("OpenType Testing")) profile.auto_register({}, profile_imports=profile_imports) profile.test_dependencies()
""" Checks for Font Bureau. """ from fontbakery.callable import check from fontbakery.section import Section from fontbakery.status import PASS, FAIL, WARN from fontbakery.fonts_profile import profile_factory from fontbakery.message import Message from fontbakery.profiles.universal import UNIVERSAL_PROFILE_CHECKS profile_imports = ('fontbakery.profiles.universal', ) profile = profile_factory(default_section=Section("Type Network")) TYPENETWORK_PROFILE_CHECKS = \ UNIVERSAL_PROFILE_CHECKS + [ 'io.github.abysstypeco/check/ytlc_sanity' ] @check(id='io.github.abysstypeco/check/ytlc_sanity', rationale=""" This check follows the proposed values of the ytlc axis proposed by font bureau at the site url. add more later. """, conditions=["is_variable_font"]) def io_github_abysstypeco_check_ytlc_sanity(ttFont): """Check if ytlc values are sane in vf""" passed = True for axis in ttFont['fvar'].axes: if not axis.axisTag == 'ytlc': continue
""" Checks for Adobe Fonts (formerly known as Typekit). """ import unicodedata from fontbakery.callable import check from fontbakery.status import PASS, FAIL, WARN from fontbakery.section import Section from fontbakery.fonts_profile import profile_factory from fontbakery.profiles.universal import UNIVERSAL_PROFILE_CHECKS from fontbakery.message import Message profile_imports = ('fontbakery.profiles.universal',) profile = profile_factory(default_section=Section("Adobe Fonts")) ADOBEFONTS_PROFILE_CHECKS = \ UNIVERSAL_PROFILE_CHECKS + [ 'com.adobe.fonts/check/family/consistent_upm', 'com.adobe.fonts/check/find_empty_letters' ] OVERRIDDEN_CHECKS = [ 'com.google.fonts/check/dsig', 'com.google.fonts/check/whitespace_glyphs', 'com.google.fonts/check/valid_glyphnames', ] ADOBEFONTS_PROFILE_CHECKS += [f'{cid}:{profile.profile_tag}' for cid in OVERRIDDEN_CHECKS] ADOBEFONTS_PROFILE_CHECKS[:] = [cid for cid in ADOBEFONTS_PROFILE_CHECKS if cid not in OVERRIDDEN_CHECKS]
return ('fonts',) fonts_expected_value = ExpectedValue( 'fonts' , default=[] , description='A list of the ufo file paths to check.' , validator=lambda fonts: (True, None) if len(fonts) \ else (False, 'Value is empty.') ) # ---------------------------------------------------------------------------- # This variable serves as an exportable anchor point, see e.g. the # Lib/fontbakery/commands/check_ufo_sources.py script. profile = UFOProfile( default_section=Section('Default'), iterargs={'font': 'fonts'}, derived_iterables={'ufo_fonts': ('ufo_font', True)}, expected_values={fonts_expected_value.name: fonts_expected_value}) register_check = profile.register_check register_condition = profile.register_condition # ---------------------------------------------------------------------------- basic_checks = Section("Basic UFO checks") @register_condition @condition def ufo_font(font): import defcon