def test_check_italic_angle_agreement(self): """ Check italicangle property zero or negative """ font = Font.get_ttfont(self.operator.path) if font.italicAngle > 0: self.fail('italicAngle must be less or equal zero') if abs(font.italicAngle) > 20: self.fail('italicAngle can\'t be larger than 20 degrees')
def test_check_font_has_dsig_table(self): """ Check that font has DSIG table """ font = Font.get_ttfont(self.operator.path) try: font['DSIG'] except KeyError: self.fail('Font does not have "DSIG" table')
def test_fontname_is_equal_to_macstyle(self): """ Check that fontname is equal to macstyle flags """ font = Font.get_ttfont(self.operator.path) macStyle = font.macStyle try: fontname_style = font.fullname.split('-')[1] except IndexError: fontname_style = '' expected_style = '' if macStyle & 0b01: expected_style += 'Bold' if macStyle & 0b10: expected_style += 'Italic' if not bool(macStyle & 0b11): expected_style = 'Regular' if fontname_style != expected_style: _ = 'macStyle ({0}) supposed style ended with "{1}"' if fontname_style: _ += ' but ends with "{2}"' self.fail( _.format(bin(macStyle)[-2:], expected_style, fontname_style))
def test_metrics_descents_equal_bbox(self): """ Check that descents values are same as min glyph point """ dirname = os.path.dirname(self.operator.path) directory = UpstreamDirectory(dirname) fonts_descents_not_bbox = [] ymin = 0 _cache = {} for filename in directory.get_binaries(): ttfont = Font.get_ttfont(os.path.join(dirname, filename)) ymin_, _ = ttfont.get_bounding() ymin = min(ymin, ymin_) _cache[filename] = { 'os2typo': abs(ttfont.descents.os2typo), 'os2win': abs(ttfont.descents.os2win), 'hhea': abs(ttfont.descents.hhea) } for filename, data in _cache.items(): datas = [data['os2typo'], data['os2win'], data['hhea']] if datas != [abs(ymin)] * 3: fonts_descents_not_bbox.append(filename) if fonts_descents_not_bbox: _ = '[%s] ascents differ to minimum value: %s' self.fail(_ % (', '.join(fonts_descents_not_bbox), ymin))
def test_fontname_is_equal_to_macstyle(self): """ Check that fontname is equal to macstyle flags """ font = Font.get_ttfont(self.path) fontname = font.fullname macStyle = font.macStyle try: fontname_style = fontname.split('-')[1] except IndexError: self.fail(('Fontname is not canonical. Expected it contains ' 'style. eg.: Italic, BoldItalic, Regular')) style = '' if macStyle & 0b01: style += 'Bold' if macStyle & 0b10: style += 'Italic' if not bool(macStyle & 0b11): style = 'Regular' if not fontname_style.endswith(style): _ = 'macStyle (%s) supposed style ended with "%s" but ends with "%s"' self.fail(_ % (bin(macStyle)[-2:], style, fontname_style))
def test_check_italic_angle_agreement(self): """ Check italicangle property zero or negative """ font = Font.get_ttfont(self.path) if font.italicAngle > 0: self.fail('italicAngle must be less or equal zero') if abs(font.italicAngle) > 20: self.fail('italicAngle can\'t be larger than 20 degrees')
def test_fontname_is_equal_to_macstyle(self): """ Check that fontname is equal to macstyle flags """ font = Font.get_ttfont(self.operator.path) macStyle = font.macStyle try: fontname_style = font.fullname.split('-')[1] except IndexError: fontname_style = '' expected_style = '' if macStyle & 0b01: expected_style += 'Bold' if macStyle & 0b10: expected_style += 'Italic' if not bool(macStyle & 0b11): expected_style = 'Regular' if fontname_style != expected_style: _ = 'macStyle ({0}) supposed style ended with "{1}"' if fontname_style: _ += ' but ends with "{2}"' self.fail(_.format(bin(macStyle)[-2:], expected_style, fontname_style))
def test_suggested_subfamily_name(self): """ Family does not contain subfamily in `name` table """ # Currently we just look that family does not contain any spaces # in its name. This prevent us from incorrect suggestions of names font = Font.get_ttfont(self.operator.path) suggestedvalues = getSuggestedFontNameValues(font.ttfont) self.assertEqual(font.familyname, suggestedvalues['family']) self.assertEqual(font.stylename, suggestedvalues['subfamily'])
def test_check_names_are_ascii_only(self): """ NAME and CFF tables must not contain non-ascii characters """ font = Font.get_ttfont(self.operator.path) for name in font.names: string = Font.bin2unistring(name) expected = normalizestr(string) self.assertEqual(string, expected)
def test_no_kern_table_exists(self): """ Check that no "KERN" table exists """ font = Font.get_ttfont(self.operator.path) try: font['KERN'] self.fail('Font does have "KERN" table') except KeyError: pass
def test_check_upm_heigths_less_120(self): """ Check if UPM Heights NOT more than 120% """ ttfont = Font.get_ttfont(self.path) value = ttfont.ascents.get_max() + abs(ttfont.descents.get_min()) value = value * 100 / float(ttfont.get_upm_height()) if value > 120: _ = "UPM:Height is %d%%, consider redesigning to 120%% or less" self.fail(_ % value)
def test_check_names_are_ascii_only(self): """ NAME and CFF tables must not contain non-ascii characters """ font = Font.get_ttfont(self.operator.path) for name in font.names: string = Font.bin2unistring(name) marks = CharacterSymbolsFixer.unicode_marks(string) if marks: self.fail('Contains {}'.format(marks))
def test_check_names_are_ascii_only(self): """ NAME and CFF tables must not contain non-ascii characters """ font = Font.get_ttfont(self.path) for name_record in font.names: string = Font.bin2unistring(name_record) try: string.encode('ascii') except UnicodeEncodeError: self.fail("%s contain non-ascii chars" % name_record.nameID)
def test_prep_magic_code(self): """ Font contains in PREP table magic code """ magiccode = '\xb8\x01\xff\x85\xb0\x04\x8d' font = Font.get_ttfont(self.operator.path) try: bytecode = font.get_program_bytecode() except KeyError: bytecode = '' self.assertTrue(bytecode == magiccode, msg='PREP does not contain magic code')
def test_check_hmtx_hhea_max_advance_width_agreement(self): """ Check if MaxAdvanceWidth agree in the Hmtx and Hhea tables """ font = Font.get_ttfont(self.path) hmtx_advance_width_max = font.get_hmtx_max_advanced_width() hhea_advance_width_max = font.advance_width_max error = ("AdvanceWidthMax mismatch: expected %s (from hmtx);" " got %s (from hhea)") % (hmtx_advance_width_max, hhea_advance_width_max) self.assertEqual(hmtx_advance_width_max, hhea_advance_width_max, error)
def test_check_hmtx_hhea_max_advance_width_agreement(self): """ Check if MaxAdvanceWidth agree in the Hmtx and Hhea tables """ font = Font.get_ttfont(self.operator.path) hmtx_advance_width_max = font.get_hmtx_max_advanced_width() hhea_advance_width_max = font.advance_width_max error = ("AdvanceWidthMax mismatch: expected %s (from hmtx);" " got %s (from hhea)") % (hmtx_advance_width_max, hhea_advance_width_max) self.assertEqual(hmtx_advance_width_max, hhea_advance_width_max, error)
def set(fontpath, value): ttfont = Font.get_ttfont(fontpath) try: ttfont['gasp'] except: print('no table gasp', file=sys.stderr) return ttfont['gasp'].gaspRange[65535] = value ttfont.save(fontpath + '.fix')
def fix(fontpath): ttfont = Font.get_ttfont(fontpath) ot_namerecord = findOrCreateNameRecord(ttfont['name'].names, 16) ot_namerecord.string = ttfont.familyname.encode("utf_16_be") ot_namerecord = findOrCreateNameRecord(ttfont['name'].names, 17) ot_namerecord.string = mapping.get(ttfont.stylename, 'Regular').encode("utf_16_be") ot_namerecord = findOrCreateNameRecord(ttfont['name'].names, 18) ot_namerecord.string = ttfont.fullname.encode("utf_16_be") ttfont.save(fontpath + '.fix')
def show(fontpath): ttfont = Font.get_ttfont(fontpath) try: ttfont['gasp'] except: print('no table gasp', file=sys.stderr) return try: print(ttfont['gasp'].gaspRange[65535]) except IndexError: print('no index 65535', file=sys.stderr)
def test_check_panose_identification(self): font = Font.get_ttfont(self.path) if font['OS/2'].panose['bProportion'] == 9: prev = 0 for g in font.glyphs(): if prev and font.advance_width(g) != prev: link = ('http://www.thomasphinney.com/2013/01' '/obscure-panose-issues-for-font-makers/') self.fail(('Your font does not seem monospaced but PANOSE' ' bProportion set to monospace. It may have ' ' a bug in windows. Details: %s' % link)) prev = font.advance_width(g)
def test_check_nbsp_width_matches_sp_width(self): """ Check non-breaking space's advancewidth is the same as space """ tf = Font.get_ttfont(self.operator.path) space_advance_width = tf.advance_width('space') nbsp_advance_width = tf.advance_width('uni00A0') _ = "Font does not contain a space glyph" self.assertTrue(space_advance_width, _) _ = "Font does not contain a nbsp glyph" self.assertTrue(nbsp_advance_width, _) _ = ("The nbsp advance width does not match " "the space advance width") self.assertEqual(space_advance_width, nbsp_advance_width, _)
def test_ttx_duplicate_glyphs(self): """ Font contains unique glyph names? """ # (Duplicate glyph names prevent font installation on Mac OS X.) font = Font.get_ttfont(self.operator.path) glyphs = [] for _, g in enumerate(font.ttfont.getGlyphOrder()): self.assertFalse(re.search(r'#\w+$', g), msg="Font contains incorrectly named glyph %s" % g) glyphID = re.sub(r'#\w+', '', g) # Each GlyphID has to be unique in TTX self.assertFalse(glyphID in glyphs, msg="GlyphID %s occurs twice in TTX" % g) glyphs.append(glyphs)
def test_check_unused_glyph_data(self): f = Font.get_ttfont(self.path) glyf_length = f.get_glyf_length() loca_num_glyphs = f.get_loca_num_glyphs() last_glyph_offset = f.get_loca_glyph_offset(loca_num_glyphs - 1) last_glyph_length = f.get_loca_glyph_length(loca_num_glyphs - 1) unused_data = glyf_length - (last_glyph_offset + last_glyph_length) error = ("there were %s bytes of unused data at the end" " of the glyf table") % unused_data self.assertEqual(unused_data, 0, error)
def test_source_ttf_font_filename_equals_familystyle(self): """ Source TTF Font filename equals family style """ ttfont = Font.get_ttfont(self.operator.path) style_name = ttfont.stylename if style_name == 'Normal' or style_name == 'Roman': style_name = 'Regular' expectedname = '{0}-{1}'.format(ttfont.familyname.replace(' ', ''), style_name.replace(' ', '')) actualname, extension = os.path.splitext(self.operator.path) self.expectedfilename = '{0}{1}'.format(expectedname, extension) self.assertEqual(os.path.basename(actualname), expectedname)
def test_metrics_linegaps_are_zero(self): """ Check that linegaps in tables are zero """ dirname = os.path.dirname(self.operator.path) directory = UpstreamDirectory(dirname) fonts_gaps_are_not_zero = [] for filename in directory.BIN: ttfont = Font.get_ttfont(os.path.join(dirname, filename)) if bool(ttfont.linegaps.os2typo) or bool(ttfont.linegaps.hhea): fonts_gaps_are_not_zero.append(filename) if fonts_gaps_are_not_zero: _ = '[%s] have not zero linegaps' self.fail(_ % ', '.join(fonts_gaps_are_not_zero))
def test_license_included_in_font_names(self): """ Check font has a correct license url """ font = Font.get_ttfont(self.path) regex = re.compile( r'^(?:http|ftp)s?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... r'localhost|' # localhost... r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) if not regex.match(font.license_url): self.fail("LicenseUrl is required and must be correct url")
def test_license_included_in_font_names(self): """ Check font has a correct license url """ font = Font.get_ttfont(self.operator.path) regex = re.compile( r'^(?:http|ftp)s?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... r'localhost|' # localhost... r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) if not regex.match(font.license_url): self.fail("LicenseUrl is required and must be correct url")
def test_check_names_same_across_platforms(self): """ Font names are same across specific-platforms """ font = Font.get_ttfont(self.operator.path) for name in font.names: for name2 in font.names: if name.nameID != name2.nameID: continue if self.diff_platform(name, name2) or self.diff_platform(name2, name): _name = Font.bin2unistring(name) _name2 = Font.bin2unistring(name2) if _name != _name2: msg = ('Names in "name" table are not the same' ' across specific-platforms') self.fail(msg)
def test_gpos_table_has_kerning_info(self): """ GPOS table has kerning information """ font = Font.get_ttfont(self.operator.path) try: font['GPOS'] except KeyError: self.fail('"GPOS" does not exist in font') flaglookup = False for lookup in font['GPOS'].table.LookupList.Lookup: if lookup.LookupType == 2: # Adjust position of a pair of glyphs flaglookup = lookup break # break for..loop to avoid reading all kerning info self.assertTrue(flaglookup, msg='GPOS doesnt have kerning information') self.assertGreater(flaglookup.SubTableCount, 0) self.assertGreater(flaglookup.SubTable[0].PairSetCount, 0)
def test_source_ttf_font_filename_equals_familystyle(self): """ Source TTF Font filename equals family style """ ttfont = Font.get_ttfont(self.operator.path) suggestedvalues = getSuggestedFontNameValues(ttfont.ttfont) family_name = suggestedvalues['family'] subfamily_name = suggestedvalues['subfamily'] expectedname = '{0}-{1}'.format(family_name.replace(' ', ''), subfamily_name.replace(' ', '')) actualname, extension = os.path.splitext(self.operator.path) expected_filename = '{0}{1}'.format(expectedname, extension) setattr(self, 'expectedfilename', expected_filename) self.assertEqual(os.path.basename(actualname), expectedname)
def fix(fontpath): ttfont = Font.get_ttfont(fontpath) values = getSuggestedFontNameValues(ttfont.ttfont) family_name = values['family'] subfamily_name = values['subfamily'] for pair in [[4, 3, 1], [4, 1, 0]]: name = ttfont.ttfont['name'].getName(*pair) if name: name.string = ' '.join( [family_name.replace(' ', ''), subfamily_name]).encode('utf_16_be') for pair in [[6, 3, 1], [6, 1, 0]]: name = ttfont.ttfont['name'].getName(*pair) if name: name.string = '-'.join([ family_name.replace(' ', ''), subfamily_name.replace(' ', '') ]).encode('utf_16_be') for pair in [[1, 3, 1], [1, 1, 0]]: name = ttfont.ttfont['name'].getName(*pair) if name: name.string = family_name.replace(' ', '').encode('utf_16_be') for pair in [[2, 3, 1], [2, 1, 0]]: name = ttfont.ttfont['name'].getName(*pair) if name: name.string = subfamily_name.encode('utf_16_be') ot_namerecord = findOrCreateNameRecord(ttfont['name'], 16) ot_namerecord.string = family_name.replace(' ', '').encode("utf_16_be") ot_namerecord = findOrCreateNameRecord(ttfont['name'], 17) ot_namerecord.string = mapping.get(subfamily_name, 'Regular').encode("utf_16_be") ot_namerecord = findOrCreateNameRecord(ttfont['name'], 18) ot_namerecord.string = ' '.join( [family_name.replace(' ', ''), mapping.get(subfamily_name, 'Regular')]).encode("utf_16_be") ttfont.save(fontpath + '.fix')
def test_Check_Postscriptname_Glyph_Conventions(self): """ Check glyphs names comply with PostScript conventions """ font = Font.get_ttfont(self.path) for glyph in font.glyphs(): if glyph == '.notdef': continue if not re.match('(^[.0-9])[a-zA-Z_][a-zA-Z_0-9]{,30}', glyph): self.fail( ('Glyph "%s" does not comply conventions.' ' A glyph name may be up to 31 characters in length,' ' must be entirely comprised of characters from' ' the following set:' ' A-Z a-z 0-9 .(period) _(underscore). and must not' ' start with a digit or period. The only exception' ' is the special character ".notdef". "twocents",' ' "a1", and "_" are valid glyph names. "2cents"' ' and ".twocents" are not.'))
def test_glyphname_does_not_contain_disallowed_chars(self): """ GlyphName length < 30 and does contain allowed chars only """ font = Font.get_ttfont(self.operator.path) for _, glyphName in enumerate(font.ttfont.getGlyphOrder()): if glyphName == '.notdef': continue if not re.match(r'(?![.0-9])[a-zA-Z_][a-zA-Z_0-9]{,30}', glyphName): self.fail(('Glyph "%s" does not comply conventions.' ' A glyph name may be up to 31 characters in length,' ' must be entirely comprised of characters from' ' the following set:' ' A-Z a-z 0-9 .(period) _(underscore). and must not' ' start with a digit or period. The only exception' ' is the special character ".notdef". "twocents",' ' "a1", and "_" are valid glyph names. "2cents"' ' and ".twocents" are not.') % glyphName)
def test_check_names_same_across_platforms(self): """ Font names are same across specific-platforms """ font = Font.get_ttfont(self.operator.path) for name in font.names: for name2 in font.names: if name.nameID != name2.nameID: continue if self.diff_platform(name, name2) \ or self.diff_platform(name2, name): _name = Font.bin2unistring(name) _name2 = Font.bin2unistring(name2) if _name != _name2: msg = ('Names in "name" table are not the same' ' across specific-platforms') self.fail(msg)
def test_check_gasp_table_type(self): """ Font table gasp should be 15 """ font = Font.get_ttfont(self.operator.path) try: font['gasp'] except KeyError: self.fail('"GASP" table not found') if not isinstance(font['gasp'].gaspRange, dict): self.fail('GASP.gaspRange method value have wrong type') if 65535 not in font['gasp'].gaspRange: self.fail("GASP does not have 65535 gaspRange") # XXX: Needs review if font['gasp'].gaspRange[65535] != 15: self.fail('gaspRange[65535] value is not 15')
def test_check_nbsp_width_matches_sp_width(self): """ Check non-breaking space's advancewidth is the same as space """ tf = Font.get_ttfont(self.operator.path) space = getGlyph(tf.ttfont, 0x0020) nbsp = getGlyph(tf.ttfont, 0x00A0) _ = "Font does not contain a space glyph" self.assertTrue(space, _) _ = "Font does not contain a nbsp glyph" self.assertTrue(nbsp, _) _ = ("The nbsp advance width does not match " "the space advance width") spaceWidth = getWidth(tf.ttfont, space) nbspWidth = getWidth(tf.ttfont, nbsp) self.assertEqual(spaceWidth, nbspWidth, _)
def test_check_glyf_table_length(self): """ Check if there is unused data at the end of the glyf table """ font = Font.get_ttfont(self.operator.path) expected = font.get_loca_length() actual = font.get_glyf_length() diff = actual - expected # allow up to 3 bytes of padding if diff > 3: _ = ("Glyf table has unreachable data at the end of the table." " Expected glyf table length %s (from loca table), got length" " %s (difference: %s)") % (expected, actual, diff) self.fail(_) elif diff < 0: _ = ("Loca table references data beyond the end of the glyf table." " Expected glyf table length %s (from loca table), got length" " %s (difference: %s)") % (expected, actual, diff) self.fail(_)
def test_check_full_font_name_begins_with_family_name(self): """ Check if full font name begins with the font family name """ font = Font.get_ttfont(self.operator.path) for entry in font.names: if entry.nameID != 1: continue for entry2 in font.names: if entry2.nameID != 4: continue if (entry.platformID == entry2.platformID and entry.platEncID == entry2.platEncID and entry.langID == entry2.langID): entry2value = Font.bin2unistring(entry2) entryvalue = Font.bin2unistring(entry) if not entry2value.startswith(entryvalue): _ = ('Full font name does not begin with family' ' name: FontFamilyName = "%s";' ' FullFontName = "%s"') self.fail(_ % (entryvalue, entry2value))