summaryrefslogtreecommitdiffstats
path: root/lib/silfont/fbtests/silttfchecks.py
blob: d38ab77498e0f6378746d8422c5411ad4dff0b78 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#!/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"