#!/usr/bin/env python '''Checks to be imported by ttfchecks.py Some checks based on examples from Font Bakery, copyright 2017 The Font Bakery Authors, licensed under the Apache 2.0 license''' __url__ = 'http://github.com/silnrsi/pysilfont' __copyright__ = 'Copyright (c) 2022 SIL International (http://www.sil.org)' __license__ = 'Released under the MIT License (http://opensource.org/licenses/MIT)' __author__ = 'David Raymond' from fontbakery.checkrunner import Section, PASS, FAIL, WARN, ERROR, INFO, SKIP from fontbakery.callable import condition, check, disable from fontbakery.message import Message from fontbakery.constants import NameID, PlatformID, WindowsEncodingID @check( id = 'org.sil/check/name/version_format', rationale = """ Based on com.google.fonts/check/name/version_format but: - Checks for two valid formats: - Production: exactly 3 digits after decimal point - Allows major version to be 0 - Allows extra info after numbers, eg for beta or dev versions """ ) def org_sil_version_format(ttFont): "Version format is correct in 'name' table?" from fontbakery.utils import get_name_entry_strings import re failed = False version_entries = get_name_entry_strings(ttFont, NameID.VERSION_STRING) if len(version_entries) == 0: failed = True yield FAIL,\ Message("no-version-string", f"Font lacks a NameID.VERSION_STRING" f" (nameID={NameID.VERSION_STRING}) entry") for ventry in version_entries: if not re.match(r'Version [0-9]+\.\d{3}( .+)*$', ventry): failed = True yield FAIL,\ Message("bad-version-strings", f'The NameID.VERSION_STRING' f' (nameID={NameID.VERSION_STRING}) value must' f' follow the pattern "Version X.nnn devstring" with X.nnn' f' greater than or equal to 0.000.' f' Current version string is: "{ventry}"') if not failed: yield PASS, "Version format in NAME table entries is correct." @check( id = 'org.sil/check/whitespace_widths' ) def org_sil_whitespace_widths(ttFont): """Checks with widths of space characters in the font against best practice""" from fontbakery.utils import get_glyph_name allok = True space_data = { 0x0020: ['Space'], 0x00A0: ['No-break space'], 0x2008: ['Punctuation space'], 0x2003: ['Em space'], 0x2002: ['En space'], 0x2000: ['En quad'], 0x2001: ['Em quad'], 0x2004: ['Three-per-em space'], 0x2005: ['Four-per-em space'], 0x2006: ['Six-per-em space'], 0x2009: ['Thin space'], 0x200A: ['Hair space'], 0x202F: ['Narrow no-break space'], 0x002E: ['Full stop'], # Non-space character where the width is needed for comparison } for sp in space_data: spname = get_glyph_name(ttFont, sp) if spname is None: spwidth = None else: spwidth = ttFont['hmtx'][spname][0] space_data[sp].append(spname) space_data[sp].append(spwidth) # Other width info needed from the font upm = ttFont['head'].unitsPerEm fullstopw = space_data[46][2] # Widths used for comparisons spw = space_data[32][2] if spw is None: allok = False yield WARN, "No space in the font so No-break space (if present) can't be checked" emw = space_data[0x2003][2] if emw is None: allok = False yield WARN, f'No em space in the font. Will be assumed to be units per em ({upm}) for other checking' emw = upm enw = space_data[0x2002][2] if enw is None: allok = False yield WARN, f'No en space in the font. Will be assumed to be 1/2 em space width ({emw/2}) for checking en quad (if present)' enw = emw/2 # Now check all the specific space widths. Only check if the space exists in the font def checkspace(spacechar, minwidth, maxwidth=None): sdata = space_data[spacechar] if sdata[1]: # Name is set to None if not in font # Allow for width(s) not being integer (eg em/6) so test against rounding up or down minw = int(minwidth) if maxwidth: maxw = int(maxwidth) if maxwidth > maxw: maxw += 1 # Had been rounded down, so round up else: maxw = minw if minw == minwidth else minw +1 # Had been rounded down, so allow rounded up as well charw = sdata[2] if not(minw <= charw <= maxw): return (f'Width of {sdata[0]} ({spacechar:#04x}) is {str(charw)}: ', minw, maxw) return (None,0,0) # No-break space (message, minw, maxw) = checkspace(0x00A0, spw) if message: allok = False; yield FAIL, message + f"Should match width of space ({spw})" # Punctuation space (message, minw, maxw) = checkspace(0x2008, fullstopw) if message: allok = False; yield FAIL, message + f"Should match width of full stop ({fullstopw})" # Em space (message, minw, maxw) = checkspace(0x2003, upm) if message: allok = False; yield WARN, message + f"Should match units per em ({upm})" # En space (message, minw, maxw) = checkspace(0x2002, emw/2) if message: allok = False widths = f'{minw}' if minw == maxw else f'{minw} or {maxw}' yield WARN, message + f"Should be half the width of em ({widths})" # En quad (message, minw, maxw) = checkspace(0x2000, enw) if message: allok = False; yield WARN, message + f"Should be the same width as en ({enw})" # Em quad (message, minw, maxw) = checkspace(0x2001, emw) if message: allok = False; yield WARN, message + f"Should be the same width as em ({emw})" # Three-per-em space (message, minw, maxw) = checkspace(0x2004, emw/3) if message: allok = False widths = f'{minw}' if minw == maxw else f'{minw} or {maxw}' yield WARN, message + f"Should be 1/3 the width of em ({widths})" # Four-per-em space (message, minw, maxw) = checkspace(0x2005, emw/4) if message: allok = False widths = f'{minw}' if minw == maxw else f'{minw} or {maxw}' yield WARN, message + f"Should be 1/4 the width of em ({widths})", # Six-per-em space (message, minw, maxw) = checkspace(0x2006, emw/6) if message: allok = False widths = f'{minw}' if minw == maxw else f'{minw} or {maxw}' yield WARN, message + f"Should be 1/6 the width of em ({widths})", # Thin space (message, minw, maxw) = checkspace(0x2009, emw/6, emw/5) if message: allok = False yield WARN, message + f"Should be between 1/6 and 1/5 the width of em ({minw} and {maxw})" # Hair space (message, minw, maxw) = checkspace(0x200A, emw/16, emw/10) if message: allok = False yield WARN, message + f"Should be between 1/16 and 1/10 the width of em ({minw} and {maxw})" # Narrow no-break space (message, minw, maxw) = checkspace(0x202F, emw/6, emw/5) if message: allok = False yield WARN, message + f"Should be between 1/6 and 1/5 the width of em ({minw} and {maxw})" if allok: yield PASS, "Space widths all match expected values" @check( id = 'org.sil/check/number_widths' ) def org_sil_number_widths(ttFont, config): """Check widths of latin digits 0-9 are equal and match that of figure space""" from fontbakery.utils import get_glyph_name num_data = { 0x0030: ['zero'], 0x0031: ['one'], 0x0032: ['two'], 0x0033: ['three'], 0x0034: ['four'], 0x0035: ['five'], 0x0036: ['six'], 0x0037: ['seven'], 0x0038: ['eight'], 0x0039: ['nine'], 0x2007: ['figurespace'] # Figure space should be the same as numerals } fontnames = [] for x in (ttFont['name'].names[1].string, ttFont['name'].names[2].string): txt="" for i in range(1,len(x),2): txt += x.decode()[i] fontnames.append(txt) for num in num_data: name = get_glyph_name(ttFont, num) if name is None: width = -1 # So different from Zero! else: width = ttFont['hmtx'][name][0] num_data[num].append(name) num_data[num].append(width) zerowidth = num_data[48][2] if zerowidth ==-1: yield FAIL, "No zero in font - remainder of check not run" return # Check non-zero digits are present and have same width as zero digitsdiff = "" digitsmissing = "" for i in range(49,58): ndata = num_data[i] width = ndata[2] if width != zerowidth: if width == -1: digitsmissing += ndata[1] + " " else: digitsdiff += ndata[1] + " " # Check figure space figuremess = "" ndata = num_data[0x2007] width = ndata[2] if width != zerowidth: if width == -1: figuremess = "No figure space in font" else: figuremess = f'The width of figure space ({ndata[1]}) does not match the width of zero' if digitsmissing or digitsdiff or figuremess: if digitsmissing: yield FAIL, f"Digits missing: {digitsmissing}" if digitsdiff: yield WARN, f"Digits with different width from Zero: {digitsdiff}" if figuremess: yield WARN, figuremess else: yield PASS, "All number widths are OK"