Source code for fontbakery.profiles.fontval

from pathlib import Path
import tempfile

from fontbakery.callable import check
from fontbakery.fonts_profile import profile_factory
from fontbakery.status import ERROR, FAIL, INFO, PASS, WARN
from fontbakery.section import Section
from fontbakery.message import Message
from fontbakery.utils import exit_with_install_instructions

from .shared_conditions import is_cff, is_variable_font

    import lxml.etree
except ImportError:

profile_imports = [".shared_conditions"]
profile = profile_factory(
    default_section=Section("Checks inherited from Microsoft Font Validator")

[docs]@check(id="", proposal="legacy:check/037") def com_google_fonts_check_fontvalidator(font, config): """Checking with Microsoft Font Validator.""" check_config = config.get("", {}) enabled_checks = check_config.get("enabled_checks") disabled_checks = check_config.get("disabled_checks") if enabled_checks is not None and disabled_checks is not None: raise Exception( "The check config must contain either enabled_checks or " "disabled_checks, but not both." ) # In some cases we want to override the severity level of # certain checks in FontValidator: downgrade_to_warn = [ # FIX-ME: Why did we downgrade this one to WARN? "Misoriented contour" ] # Some other checks we want to completely disable: disabled_fval_checks = [ # FontVal E4012 thinks that # "Versions 0x00010000 and 0x0001002 are currently # the only defined versions of the GDEF table." # but the GDEF chapter of the OpenType specification at # # describes GDEF header version 1.3, which is not yet recognized # by FontVal, thus resulting in this spurious false-FAIL: "The version number is neither 0x00010000 nor 0x0001002", # No software is affected by Mac strings nowadays. # More info at: googlei18n/fontmake#414 "The table doesn't contain strings for Mac platform", "The PostScript string is not present for both required platforms", # FontBakery has got a native check for the xAvgCharWidth field # which is: "The xAvgCharWidth field does not equal the calculated value", # The optimal ordering suggested by FVal check W0020 seems to only be # relevant to performance optimizations on old versions of Windows # running on old hardware. Since such performance considerations # are most likely negligible, we're not going to bother users with # this check's table ordering requirements. # More info at: # "Tables are not in optimal order", # FontBakery has its own check for required/optional tables: # "Recommended table is missing", # Check W5300 does not recognise some tags in use, e.g. stylistic sets # tagged `ssXX` (where XX is the number). This warning has been reported # to HinTak here: "The FeatureRecord tag is valid, but unregistered", # Check E5400: field now called featureParamsOffset and can be null. # This error has been reported to HinTak by Khaled Hosny here: # "The FeatureParams field is not null", # Check E5700: Lookup flags more recently used by the pipeline are not # recognized by Font Validator and therefore it flags that they are in a # reserved bit. This error has been reported to HinTak by Khaled Hosny # here: "The LookupFlag reserved bits are not all set to zero.", # Check E4100: We expect this error due to the new way fontmake compiles # anchors. See this bug report on the FontValidator side: # "The AnchorFormat field is invalid", # Check E2101: Complains about the USE_TYPO_METRICS bit. See # "There are undefined bits set in fsSelection field", # Unless there is a Microsoft Symbol subtable in the CMAP table, Font # Validator will check Microsoft Unicode/Apple subtables for the # presence of the euro character. This does not consider the glyph set # of the font, and so will raise a warning in fonts that purposely do # not contain the euro. "Character code U+20AC, the euro character, is not mapped in cmap 3,1", # FontBakery has its own check for this. "The unitsPerEm value is not a power of two", # Actually not a problem, and being produced by ufo2ft for years. "Intersecting components of composite glyph", # OS/2 table version: Yeah, and? "The version number is valid, but less than 5", # W1900: FontVal computes maxp.maxSizeOfInstructions and # maxComponentDepth differently from fontTools "maxSizeOfInstructions computation not via either approved method", # E1900: FontValidator calculates the wrong maxp.maxComponentDepth. This # issue has been reported on the FontValidator side: # "The value doesn't match the calculated value", ] # There are also some checks that do not make # sense when we're dealing with variable fonts: VARFONT_disabled_fval_checks = [ # Variable fonts typically do have lots of self-intersecting # contours because they are used to draw each portion # of variable glyph features. "Intersecting contours", # DeltaFormat = 32768 (same as 0x8000) means VARIATION_INDEX, according to # # The FontVal problem description for this check (E5200) only mentions # the other values as possible valid ones. So apparently this means FontVal # implementation is not up-to-date with more recent versions of the OpenType # spec and that's why these spurious FAILs are being emitted. # That's good enough reason to mute it. # More info at: # "The device table's DeltaFormat value is invalid", ] CFF_disabled_fval_checks = [ # We expect this warning for static OTFs, since they store glyph names in # the CFF table instead of the post table. "Apple recommends against using post table format 3 under most circumstances", ] from fontTools.ttLib import TTFont ttFont = TTFont(font) if is_variable_font(ttFont): disabled_fval_checks.extend(VARFONT_disabled_fval_checks) if is_cff(ttFont): disabled_fval_checks.extend(CFF_disabled_fval_checks) if disabled_checks is not None: disabled_fval_checks = disabled_checks report_dir = tempfile.TemporaryDirectory(prefix="fontval-") import subprocess try: fval_cmd = [ "FontValidator", "-file", font, "-all-tables", "-report-dir",, "-no-raster-tests", ] subprocess.check_output(fval_cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: # Filter uninteresting progress reports. filtered_output = [ msg for msg in e.output.decode().splitlines() if not msg.startswith( ("Table Test:", "Progress: Validating glyph with index") ) ] yield INFO, Message( "fontval-returned-error", ( "Microsoft Font Validator returned an error code." " Output follows :\n\n{}\n" ).format("\n".join(filtered_output)), ) except (OSError, IOError) as error: yield ERROR, Message( "fontval-not-available", "Mono runtime and/or Microsoft Font Validator are not available!", ) raise error def report_message(msg, details): if details: if isinstance(details, list) and len(details) > 1: # We'll print lists with one item per line for # improved readability. if None in details: details.remove(None) # A designer will likely not need the full list # in order to fix a problem. # Showing only the 10 first ones is more than enough # and helps avoid flooding the report. if len(details) > 25: num_similar = len(details) - 10 details = details[:10] details.append( f"NOTE: {num_similar} other similar" " results were hidden!" ) details = "\n\t- " + "\n\t- ".join(details) return f"MS-FonVal: {msg} DETAILS: {details}" else: return f"MS-FonVal: {msg}" report_file = Path( / f"{Path(font).name}.report.xml" grouped_msgs = {} with open(report_file, "rb") as xml_report: doc = lxml.etree.fromstring( for report in doc.iterfind(".//Report"): msg = report.get("Message") details = report.get("Details") disable_it = False if enabled_checks is not None: if not any(substring in msg for substring in enabled_checks): disable_it = True else: if any(substring in msg for substring in disabled_fval_checks): disable_it = True if disable_it: continue if msg not in grouped_msgs: grouped_msgs[msg] = { "errortype": report.get("ErrorType"), "details": [details], } else: if details not in grouped_msgs[msg]["details"]: # avoid cluttering the output with tons of identical reports # yield INFO, 'grouped_msgs[msg]["details"]: {}'.format( # grouped_msgs[msg]["details"]) grouped_msgs[msg]["details"].append(details) # --------------------------- # Here we start emitting the grouped log messages for msg, data in grouped_msgs.items(): # But before printing we try to make the "details" more # readable. Otherwise the user would get the text terminal # flooded with messy data. # No need to print is as a list if wereally only # got one log message of this kind: if len(data["details"]) == 1: data["details"] = data["details"][0] # Simplify the list of glyph indices by only displaying # their numerical values in a list: for glyph_index in ["Glyph index ", "glyph# "]: if ( data["details"] and data["details"][0] and glyph_index in data["details"][0] ): try: data["details"] = { "Glyph index": [ int(x.split(glyph_index)[1]) for x in data["details"] ] } break except ValueError: pass # And, finally, the log messages are emitted: if data["errortype"] == "P": yield PASS, report_message(msg, data["details"]) elif data["errortype"] == "E": status = FAIL for substring in downgrade_to_warn: if substring in msg: status = WARN yield status, Message("fontval-error", report_message(msg, data["details"])) elif data["errortype"] == "W": yield WARN, Message("fontval-warn", report_message(msg, data["details"])) else: yield INFO, Message("fontval-info", report_message(msg, data["details"]))