├── .gitignore ├── Fixes ├── Fix_selected_composite_metrics.py ├── Set_selected_glyphs_all_layers_sidebearings_60.py ├── __init__.py ├── auto_designer_metrics.py ├── auto_metrics.py ├── build-font-from-fonts.py ├── copy_font_info_from_background_font.py ├── copy_master_metrics_to_other_masters.py ├── copy_masters_vmetrics_2_instances.py ├── fix_gf_spec.py ├── fix_vmetrics_win_clipping.py └── interpolate_vert_metrics_with_2nd_font.py ├── Git ├── latest_commit.py └── previous_commit.py ├── Glyph-Builders └── lowercase_from_upper.py ├── LICENSE.txt ├── QA ├── README.md ├── __init__.pyc ├── checkfamily.py ├── compare_2_fonts_glyph_points.py ├── config_file.png ├── fontinfo.py ├── glyphs.py ├── mark-glyphs-in-other-fonts.py ├── metrics.py ├── metrics_in_diff_upms.py ├── otfeatures.py ├── qa.json ├── test_missing_glyphs_across_fonts.py └── ui.png ├── README.rst ├── Reports ├── gposreport.py └── kernreport.py ├── requirements.txt └── wrappers.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | 4 | -------------------------------------------------------------------------------- /Fixes/Fix_selected_composite_metrics.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Fix selected composite metrics 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Set selected glyphs metrics to parent glyph. 5 | 6 | e.g: 7 | Aacute -> A 8 | Agrave, Acute -> A, A 9 | 10 | This works by glyph names only. Can also do small caps. 11 | 12 | Note: This is extremely ghetto. 13 | ''' 14 | def main(): 15 | align_dict = {} 16 | font = Glyphs.font 17 | glyphs = Glyphs.font.selection 18 | 19 | for glyph in glyphs: 20 | if '.sc' not in glyph.name: 21 | if str(glyph.name[0]) not in align_dict: 22 | align_dict[str(glyph.name[0])] = [glyph.name] 23 | else: 24 | align_dict[str(glyph.name[0])].append(glyph.name) 25 | else: 26 | if str(glyph.name[0] + '.sc') not in align_dict: 27 | align_dict[str(glyph.name[0] + '.sc')] = [glyph.name] 28 | else: 29 | align_dict[str(glyph.name[0] + '.sc')].append(glyph.name) 30 | 31 | # Set metrics to key 32 | masters = font.masters 33 | for parent in align_dict: 34 | for i in range(len(masters)): 35 | for glyph in align_dict[parent]: 36 | font.glyphs[glyph].layers[i].RSB = font.glyphs[parent].layers[i].RSB 37 | font.glyphs[glyph].layers[i].LSB = font.glyphs[parent].layers[i].LSB 38 | print('metrics updated') 39 | 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /Fixes/Set_selected_glyphs_all_layers_sidebearings_60.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Set all selected glyphs sidebearings to +60, all layers 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Set sidebearings of selected glyphs to +60, every layer! 5 | ''' 6 | 7 | R_BEARING = 60 8 | L_BEARING = 60 9 | 10 | 11 | def main(): 12 | # Set metrics of each layer to generic amount: 13 | glyphs = Glyphs.font.selection 14 | masters = Glyphs.font.masters 15 | for glyph in glyphs: 16 | for i in range(len(masters)): 17 | glyph.layers[i].RSB = R_BEARING 18 | glyph.layers[i].LSB = L_BEARING 19 | print('metrics updated') 20 | 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /Fixes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4rc1e/mf-glyphs-scripts/c5ed026e5b72a886f1e574f85659cdcae041e66a/Fixes/__init__.py -------------------------------------------------------------------------------- /Fixes/auto_designer_metrics.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Auto designer vert metrics 2 | ''' 3 | Replace metrics with values calculated from key glyphs. 4 | ''' 5 | 6 | font = Glyphs.font 7 | 8 | ASC_G = 'l' 9 | CAP_G = 'H' 10 | XHEIGHT_G = 'x' 11 | DESC_G = 'p' 12 | 13 | masters = font.masters 14 | 15 | 16 | for i,master in enumerate(masters): 17 | n_asc = font.glyphs[ASC_G].layers[master.id].bounds[-1][-1] - abs(font.glyphs[ASC_G].layers[master.id].bounds[0][-1]) 18 | n_cap = font.glyphs[CAP_G].layers[master.id].bounds[-1][-1] 19 | n_xhe = font.glyphs[XHEIGHT_G].layers[master.id].bounds[-1][-1] 20 | n_desc = font.glyphs[DESC_G].layers[master.id].bounds[0][-1] 21 | 22 | master.ascender = n_asc 23 | master.capHeight = n_cap 24 | master.xHeight = n_xhe 25 | master.descender = n_desc 26 | 27 | print 'Designer vertical metrics updated' 28 | -------------------------------------------------------------------------------- /Fixes/auto_metrics.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Auto Vert Metrics 2 | """Warning: Do not use this for updating fonts 3 | 4 | Apply auto vertical metrics based on legacy 125% rule and Khaled. 5 | 6 | Glyphsapp by default, exports fonts vertical metrics as 120% of the upm. This 7 | script will export the vertical metrics at 125% and feature no clipping on 8 | MS applications. 9 | 10 | The overall aim is to avoid clipping and make the appearance consistent across 11 | all platforms. 12 | """ 13 | from fix_vmetrics_win_clipping import shortest_tallest_glyphs 14 | 15 | 16 | def main(): 17 | font = Glyphs.font 18 | Glyphs.showMacroWindow() 19 | new_win_desc, new_win_asc = shortest_tallest_glyphs(font) 20 | upm_125 = font.upm * 1.25 21 | 22 | for master in font.masters: 23 | vert = master.customParameters 24 | 25 | print 'Updating %s vertical metrics' % master.name 26 | asc = int(upm_125 * 0.75) 27 | desc = -int(upm_125 * 0.25) 28 | 29 | vert['typoAscender'] = asc 30 | vert['typoDescender'] = desc 31 | vert['typoLineGap'] = 0 32 | 33 | vert['winAscent'] = int(new_win_asc) 34 | vert['winDescent'] = int(abs(new_win_desc)) 35 | 36 | vert['hheaAscender'] = asc 37 | vert['hheaDescender'] = desc 38 | vert['hheaLineGap'] = 0 39 | 40 | print 'Enabling Use Typo Metrics' 41 | font.customParameters['Use Typo Metrics'] = True 42 | 43 | print 'Done. Updated all masters vertical metrics' 44 | 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /Fixes/build-font-from-fonts.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Combine all open fonts into selected font 2 | ''' 3 | Combine all open single weigth fonts into the selected font. 4 | 5 | ''' 6 | def main(): 7 | fonts = Glyphs.fonts 8 | new_font = fonts[0] 9 | source_fonts = list(fonts)[1:] 10 | 11 | # Prep source instance first 12 | new_font.instances[0].weightValue = new_font.masters[0].weightValue 13 | # Add master custom parameters to instance 14 | for key in new_font.masters[0].customParameters: 15 | new_font.instances[0].customParameters[key.name] = new_font.masters[0].customParameters[key.name] 16 | 17 | if new_font.customParameters['panose']: 18 | new_font.instances[0].customParameters['panose'] = new_font.customParameters['panose'] 19 | 20 | for i, font in enumerate(source_fonts): 21 | # append master to font 22 | new_font.masters[font.masters[0].id] = font.masters[0] 23 | layer_id = new_font.masters[font.masters[0].id].id 24 | 25 | # Add glyphs to master 26 | for glyph in font.glyphs: 27 | glyf = font.glyphs[glyph.name].layers[0] 28 | if new_font.glyphs[glyph.name]: 29 | new_font.glyphs[glyph.name].layers[layer_id] = glyf 30 | 31 | # Add instances and set values correctly 32 | if font.instances[0] not in new_font.instances: 33 | new_font.instances.append(font.instances[0]) 34 | 35 | # Set weight of instance to masters weight 36 | new_font.instances[-1].weightValue = font.masters[0].weightValue 37 | if font.customParameters['panose']: 38 | new_font.instances[-1].customParameters['panose'] = font.customParameters['panose'] 39 | for key in font.masters[0].customParameters: 40 | new_font.instances[-1].customParameters[key.name] = font.masters[0].customParameters[key.name] 41 | 42 | print 'done' 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /Fixes/copy_font_info_from_background_font.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Copy font info from other font 2 | ''' 3 | With two fonts open, copy font info from font in background 4 | to selected font. 5 | ''' 6 | 7 | CUSTOM_PARAMS = { 8 | "glyphOrdee", 9 | "Family Alignment Zones", 10 | "panose", 11 | "fsType", 12 | "unicodeRanges", 13 | "codePageRanges", 14 | "vendorID", 15 | "blueScale", 16 | "blueShift", 17 | "isFixedPitch", 18 | "trademark", 19 | "description", 20 | "sampleText", 21 | "license", 22 | "licenseURL", 23 | "versionString", 24 | "uniqueID", 25 | "ROS", 26 | "Make morx table", 27 | "EditView Line Height", 28 | "Compatible Name Table", 29 | "Name Table Entry", 30 | "GASP Table", 31 | "localizedFamilyName", 32 | "localizedDesigner", 33 | "TrueType Curve Error", 34 | "Use Typo Metrics", 35 | "Has WWS Names", 36 | "Use Extension Kerning", 37 | "Don't use Production Names", 38 | "makeOTF Argument", 39 | "note", 40 | "Disable Subroutines", 41 | "Disable Last Change", 42 | "Use Line Breaks", 43 | } 44 | 45 | def main(): 46 | to_font = Glyphs.fonts[0] 47 | from_font = Glyphs.fonts[1] 48 | 49 | to_font.copyright = from_font.copyright 50 | to_font.designer = from_font.designer 51 | to_font.designerURL = from_font.designerURL 52 | to_font.manufacturer = from_font.manufacturer 53 | to_font.manufacturerURL = from_font.manufacturerURL 54 | to_font.versionMajor = from_font.versionMajor 55 | to_font.versionMinor = from_font.versionMinor 56 | to_font.date = from_font.date 57 | to_font.familyName = from_font.familyName 58 | 59 | for key in CUSTOM_PARAMS: 60 | if key in from_font.customParameters: 61 | to_font.customParameters[key] = from_font.customParameters[key] 62 | 63 | print 'family info copied' 64 | 65 | if __name__ == '__main__': 66 | if len(Glyphs.fonts) != 2: 67 | print 'Please have two fonts open only!' 68 | else: 69 | main() 70 | -------------------------------------------------------------------------------- /Fixes/copy_master_metrics_to_other_masters.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Copy Selected Master's Vert Metrics To Other Masters 2 | 3 | METRICS_KEYS = [ 4 | 'typoAscender', 5 | 'typoDescender', 6 | 'typoLineGap', 7 | 'hheaAscender', 8 | 'hheaDescender', 9 | 'hheaLineGap', 10 | 'winAscent', 11 | 'winDescent', 12 | ] 13 | 14 | 15 | def main(): 16 | font = Glyphs.font 17 | 18 | selected_master = font.selectedFontMaster 19 | masters = font.masters 20 | 21 | updates = False 22 | for master in masters: 23 | for key in METRICS_KEYS: 24 | if master.customParameters[key] != selected_master.customParameters[key]: 25 | print 'Swapping: %s %s, %s for %s %s, %s' % ( 26 | master.name, 27 | key, 28 | master.customParameters[key], 29 | selected_master.name, 30 | key, 31 | selected_master.customParameters[key] 32 | ) 33 | updates = True 34 | master.customParameters[key] = selected_master.customParameters[key] 35 | if updates: 36 | print 'Metrics have been updated' 37 | else: 38 | print 'Metric keys are the same' 39 | 40 | if __name__ == '__main__': 41 | main() 42 | -------------------------------------------------------------------------------- /Fixes/copy_masters_vmetrics_2_instances.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Copy Masters Vert Metrics to Instances 2 | ''' 3 | Copy Masters Vert Metrics to Instances 4 | ''' 5 | 6 | VERT_KEYS = [ 7 | 'typoDescender', 8 | 'typoLineGap', 9 | 'hheaLineGap', 10 | 'hheaAscender', 11 | 'typoAscender', 12 | 'hheaDescender', 13 | 'winDescent', 14 | 'winAscent', 15 | ] 16 | 17 | def check(masters): 18 | '''Check if masters share same vertical metrics''' 19 | bad_masters = [] 20 | for master1 in masters: 21 | for master2 in masters: 22 | for key in VERT_KEYS: 23 | if key not in master1.customParameters: 24 | print '%s is missing in %s' %(key, master1.name) 25 | return False 26 | if master1.customParameters[key] != master2.customParameters[key]: 27 | bad_masters.append((master1.name, master2.name, key)) 28 | 29 | if bad_masters: 30 | for master1, master2, key in bad_masters: 31 | print "ERROR: Master's %s %s %s not even. Fix first!" % (master1, master2, key) 32 | return False 33 | else: 34 | print 'PASS: Masters share same metrics' 35 | return True 36 | 37 | 38 | def copy_master_verts_2_instances(master, instances): 39 | for instance in instances: 40 | for key in VERT_KEYS: 41 | instance.customParameters[key] = master.customParameters[key] 42 | print "Vertical metrics copied to instances" 43 | 44 | 45 | if __name__ == '__main__': 46 | font = Glyphs.font 47 | masters = font.masters 48 | instances = font.instances 49 | if check(masters): 50 | copy_master_verts_2_instances(masters[0], instances) 51 | else: 52 | print 'Cannot copy vertical metrics to instances. Masters must have same vert metrics and correct keys' -------------------------------------------------------------------------------- /Fixes/fix_gf_spec.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Fix fonts for GF spec 2 | ''' 3 | Fix/add requirements from ProjectChecklist.md 4 | ''' 5 | 6 | from datetime import datetime 7 | import re 8 | 9 | BAD_PARAMETERS = [ 10 | 'openTypeNameLicense', 11 | 'openTypeNameLicenseURL', 12 | 'panose', 13 | 'unicodeRanges', 14 | 'codePageRanges', 15 | 'openTypeNameDescription', 16 | 'Family Alignment Zones', 17 | ] 18 | 19 | 20 | def _convert_camelcase(name, seperator=' '): 21 | """ExtraCondensed -> Extra Condensed""" 22 | return re.sub('(?!^)([A-Z]|[0-9]+)', r'%s\1' % seperator, name) 23 | 24 | 25 | def main(): 26 | # Add README file if it does not exist 27 | 28 | font = Glyphs.font 29 | font.customParameters['license'] = 'This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFL' 30 | font.customParameters['licenseURL'] = 'http://scripts.sil.org/OFL' 31 | font.customParameters['fsType'] = [] 32 | # font.customParameters['Use Typo Metrics'] = True 33 | font.customParameters['Disable Last Change'] = True 34 | font.customParameters['Use Line Breaks'] = True 35 | 36 | # Delete unnecessary customParamters 37 | for key in BAD_PARAMETERS: 38 | del font.customParameters[key] 39 | 40 | # Add http:// to manufacturerURL and designerURL if they don't exist 41 | if font.manufacturerURL: 42 | if not font.manufacturerURL.startswith(('http://', 'https://')): 43 | font.manufacturerURL = 'http://' + font.manufacturerURL 44 | else: 45 | print 'WARNING: manufacturerURL is missing' 46 | 47 | if font.designerURL: 48 | if not font.designerURL.startswith(('http://', 'https://')): 49 | font.designerURL = 'http://' + font.designerURL 50 | else: 51 | print 'WARNING: designerURL is missing' 52 | 53 | # Remove glyph order 54 | if 'glyphOrder' in font.customParameters: 55 | del font.customParameters['glyphOrder'] 56 | 57 | masters = font.masters 58 | instances = font.instances 59 | 60 | # If nbspace does not exist, create it 61 | if not font.glyphs['nbspace'] or font.glyphs['uni00A0']: 62 | nbspace = GSGlyph() 63 | nbspace.name = 'nbspace' 64 | nbspace.unicode = unicode('00A0') 65 | font.glyphs.append(nbspace) 66 | 67 | # if uni000D rename it 68 | if font.glyphs['uni000D']: 69 | font.glyphs['uni000D'].name = 'CR' 70 | 71 | # If CR does not exist, create it 72 | if not font.glyphs['CR']: 73 | cr = GSGlyph() 74 | cr.name = 'CR' 75 | font.glyphs.append(cr) 76 | 77 | # if .null rename it 78 | if font.glyphs['.null']: 79 | font.glyphs['.null'].name = 'NULL' 80 | 81 | # If NULL does not exist, create it 82 | if not font.glyphs['NULL']: 83 | null = GSGlyph() 84 | null.name = 'NULL' 85 | font.glyphs.append(null) 86 | 87 | font.glyphs['CR'].unicode = unicode('000D') 88 | font.glyphs['NULL'].unicode = unicode('0000') 89 | 90 | # fix width glyphs 91 | for i, master in enumerate(masters): 92 | # Set nbspace width so it matches space 93 | font.glyphs['nbspace'].layers[i].width = font.glyphs['space'].layers[i].width 94 | # Set NULL width so it is 0 95 | font.glyphs['NULL'].layers[i].width = 0 96 | # Set CR so width matches space 97 | font.glyphs['CR'].layers[i].width = font.glyphs['space'].layers[i].width 98 | 99 | # fix instance names to pass gf spec 100 | for i, instance in enumerate(instances): 101 | if 'Italic' in instance.name: 102 | instance.isItalic = True 103 | if instance.weight != 'Bold' and instance.weight != 'Regular': 104 | instance.linkStyle = instance.weight 105 | else: 106 | instance.linkStyle = '' 107 | 108 | # Seperate non Reg/Medium weights into their own family 109 | if instance.width != 'Medium (normal)': 110 | if instance.width == 'Semi Expanded': 111 | family_suffix = instance.width 112 | else: 113 | family_suffix = _convert_camelcase(instance.width) 114 | sub_family_name = '%s %s' % (font.familyName, family_suffix) 115 | instance.customParameters['familyName'] = sub_family_name 116 | 117 | if instance.weight == 'Bold': 118 | instance.isBold = True 119 | else: 120 | instance.isBold = False 121 | 122 | # Change ExtraLight weight class from 250 to 275 123 | if instance.weight == 'ExtraLight': 124 | instance.customParameters['weightClass'] = 275 125 | 126 | # If Heavy exists, create a new font family for it 127 | if 'Heavy' in instance.name: 128 | instance.customParameters['familyName'] = '%s Heavy' % (font.familyName) 129 | instance.name = instance.name.replace('Heavy', 'Regular') 130 | instance.weight = 'Regular' 131 | 132 | if instance.name == 'Regular Italic': 133 | instance.name = 'Italic' 134 | 135 | 136 | if __name__ == '__main__': 137 | main() 138 | -------------------------------------------------------------------------------- /Fixes/fix_vmetrics_win_clipping.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Change metrics to stop win clipping 2 | """Update vertical metrics so glyphs do not appear 3 | clipped in MS applications. 4 | 5 | - Set the Typo values to the old Win Metrics 6 | - Set the Win Metrics to the tallest / shortest glyph 7 | - Retain the same hhea values but divide the line gap 8 | between the hhea Ascender and hhea Descender""" 9 | import math 10 | 11 | def shortest_tallest_glyphs(font, *args): 12 | '''find the tallest and shortest glyphs in all masters.''' 13 | lowest = 0.0 14 | highest = 0.0 15 | highest_name = '' 16 | lowest_name = '' 17 | 18 | masters_count = len(font.masters) 19 | 20 | if args: 21 | glyphs = [font.glyphs[i] for i in args] 22 | else: 23 | glyphs = font.glyphs 24 | 25 | for glyph in glyphs: 26 | for i in range(masters_count): 27 | glyph_ymin = glyph.layers[i].bounds[0][-1] 28 | glyph_ymax = glyph.layers[i].bounds[-1][-1] + glyph.layers[i].bounds[0][-1] 29 | if glyph_ymin < lowest: 30 | lowest = glyph_ymin 31 | if glyph_ymax > highest: 32 | highest = glyph_ymax 33 | 34 | return math.ceil(lowest), math.ceil(highest) 35 | 36 | 37 | def main(): 38 | font = Glyphs.font 39 | Glyphs.showMacroWindow() 40 | new_win_desc, new_win_asc = shortest_tallest_glyphs(font) 41 | 42 | if font.customParameters['Use Typo Metrics']: 43 | print 'ERROR: Use typo metrics flag already enabled' 44 | print 'Cannot do metrics acrobatics' 45 | return 46 | 47 | for master in font.masters: 48 | vert = master.customParameters 49 | 50 | print 'Updating %s Typo Metrics' % master.name 51 | vert['typoAscender'] = vert['winAscent'] 52 | vert['typoDescender'] = -vert['winDescent'] 53 | vert['typoLineGap'] = 0 54 | 55 | print 'Updating %s Win Metrics' % master.name 56 | vert['winAscent'] = int(new_win_asc) 57 | vert['winDescent'] = int(abs(new_win_desc)) 58 | 59 | print 'Updating %s hhea metrics' % master.name 60 | hhea_add = vert['hheaLineGap'] / 2 61 | vert['hheaAscender'] = vert['hheaAscender'] + hhea_add 62 | vert['hheaDescender'] = vert['hheaDescender'] + hhea_add 63 | vert['hheaLineGap'] = 0 64 | 65 | print 'Enabling Use Typo Metrics' 66 | font.customParameters['Use Typo Metrics'] = True 67 | print 'Done: Metrics updated' 68 | 69 | 70 | if __name__ == '__main__': 71 | main() 72 | -------------------------------------------------------------------------------- /Fixes/interpolate_vert_metrics_with_2nd_font.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Interpolate Current Font Master's Vert Metrics between Secondary Font Master's Vert Metrics 2 | from os.path import basename 3 | import vanilla 4 | """ 5 | Interpolate the current font master's vertical metrics values against 6 | background font. 7 | """ 8 | 9 | INTERPOLATE_KEYS = [ 10 | "typoAscender", 11 | "typoDescender", 12 | "typoLineGap", 13 | "winAscent", 14 | "winDescent", 15 | "hheaAscender", 16 | "hheaDescender", 17 | "hheaLineGap", 18 | "smallCapHeight", 19 | "shoulderHeight", 20 | "vheaVertAscender", 21 | "vheaVertDescender", 22 | "vheaVertLineGap", 23 | "Preview Ascender", 24 | "Preview Descender" 25 | ] 26 | 27 | 28 | class GlyphsUI(object): 29 | '''Dialog for entering interpolation value''' 30 | def __init__(self): 31 | self.w = vanilla.FloatingWindow((330, 110), "Interpolate Master's Vert Metrics") 32 | self.w.textBox = vanilla.TextBox((10, 10, -10, 17), "Enter value between 1.0 > 0.0") 33 | self.w.interpolation_value = vanilla.TextEditor((10, 30, -10, 17)) 34 | self.w.ignore_win_metrics = vanilla.CheckBox((10, 50, 180, 20), 'Ignore Win Metrics',value=True) 35 | # Check button 36 | self.w.button = vanilla.Button((10, 80, -10, 17), "Interpolate", callback=self.buttonCallback) 37 | 38 | self.w.open() 39 | 40 | def buttonCallback(self, sender): 41 | main(**self.w.__dict__) 42 | self.w.close() 43 | 44 | 45 | def main(**kwargs): 46 | fonts = Glyphs.fonts 47 | Glyphs.showMacroWindow() 48 | INTERPOLATION_VALUE = float(kwargs['interpolation_value'].get()) 49 | if kwargs['ignore_win_metrics'].get() == 1: 50 | if 'winAscent' in INTERPOLATE_KEYS: 51 | INTERPOLATE_KEYS.remove('winAscent') 52 | INTERPOLATE_KEYS.remove('winDescent') 53 | 54 | selected_font = fonts[0] 55 | previous_font = fonts[1] 56 | 57 | selected_layer_metrics = {i.name: i.value for i in 58 | selected_font.selectedFontMaster.customParameters if i.name in INTERPOLATE_KEYS} 59 | 60 | previous_layer_metrics = {i.name: i.value for i in 61 | previous_font.selectedFontMaster.customParameters if i.name in INTERPOLATE_KEYS} 62 | 63 | diff_keys = set(selected_layer_metrics) - set(previous_layer_metrics) 64 | 65 | if diff_keys: 66 | print 'ERROR: Keys not equal, missing %s' % ', '.join(diff_keys) 67 | else: 68 | new_metrics = {} 69 | for key in selected_layer_metrics: 70 | if key in INTERPOLATE_KEYS: 71 | s_val = selected_layer_metrics[key] 72 | p_val = previous_layer_metrics[key] 73 | if s_val < p_val: 74 | smallest = s_val 75 | else: 76 | smallest = p_val 77 | 78 | new_metrics[key] = int(abs(s_val - p_val) * INTERPOLATION_VALUE + smallest) 79 | 80 | for key in new_metrics: 81 | selected_font.selectedFontMaster.customParameters[key] = new_metrics[key] 82 | 83 | print 'Metrics are now %s between %s %s and %s %s' % ( 84 | INTERPOLATION_VALUE, 85 | basename(selected_font.filepath), 86 | selected_font.selectedFontMaster.name, 87 | basename(previous_font.filepath), 88 | previous_font.selectedFontMaster.name, 89 | ) 90 | 91 | 92 | if __name__ == '__main__': 93 | if len(Glyphs.fonts) != 2: 94 | print 'ERROR: please have 2 fonts open only' 95 | else: 96 | GlyphsUI() 97 | -------------------------------------------------------------------------------- /Git/latest_commit.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Git latest commit 2 | #!/usr/bin/env python 3 | # coding: utf-8 4 | ''' 5 | WARNING: This is ghetto as hell. 6 | 7 | Reload the glyph file to the previous commit. 8 | ''' 9 | import os 10 | import git 11 | import re 12 | 13 | font = Glyphs.font 14 | font_path = font.filepath 15 | Glyphs.showMacroWindow() 16 | 17 | repo = git.Git(os.path.dirname(font_path)) 18 | branch = repo.branch() 19 | 20 | if '\n' in branch: 21 | branch = branch.split('\n ')[-1] 22 | font.close() 23 | repo.checkout(branch) 24 | font = Glyphs.open(font_path) 25 | print 'On latest commit, branch %s' % (branch) 26 | else: 27 | branch = repo.branch().split('* ')[-1] 28 | print 'Already on latest commit, branch %s' % (branch) 29 | -------------------------------------------------------------------------------- /Git/previous_commit.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Git previous commit 2 | #!/usr/bin/env python 3 | # coding: utf-8 4 | ''' 5 | WARNING: This is ghetto as hell. 6 | 7 | Reload the glyph file to the previous commit. 8 | ''' 9 | import os 10 | import git 11 | import re 12 | 13 | font = Glyphs.font 14 | font_path = font.filepath 15 | 16 | repo = git.Git(os.path.dirname(font_path)) 17 | git_commits = re.findall(r'(?<=commit ).*', repo.log()) 18 | 19 | font.close() 20 | commit = git_commits.pop(1) 21 | branch = repo.branch().split('\n ')[-1] 22 | repo.checkout(commit) 23 | font = Glyphs.open(font_path) 24 | Glyphs.showMacroWindow() 25 | print 'On commit %s, branch %s' % (commit, branch) 26 | -------------------------------------------------------------------------------- /Glyph-Builders/lowercase_from_upper.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Generate lowercase from uppercase 2 | """ 3 | Generate lowercase a-z from uppercase A-Z 4 | 5 | TODO (M Foley) Generate all lowercase glyphs, not just a-z 6 | """ 7 | 8 | font = Glyphs.font 9 | glyphs = list('abcdefghijklmnopqrstuvwxyz') 10 | masters = font.masters 11 | 12 | for glyph_name in glyphs: 13 | glyph = GSGlyph(glyph_name) 14 | glyph.updateGlyphInfo() 15 | font.glyphs.append(glyph) 16 | 17 | for idx,layer in enumerate(masters): 18 | comp_name = glyph_name.upper() 19 | component = GSComponent(comp_name, (0,0)) 20 | glyph.layers[idx].components.append(component) 21 | 22 | Glyphs.redraw() 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2018 Marc Foley 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /QA/README.md: -------------------------------------------------------------------------------- 1 | # Glyphsapp Font QA 2 | 3 | Test your .glyphs file's metadata against a custom configuration file and for common errors. 4 | 5 | This tool is useful Foundries and individuals who have font metadata which should remain consistent across releases. 6 | 7 | ![alt tag](ui.png) 8 | 9 | ## Configuration file 10 | 11 | Place your custom metadata into *qa.json* 12 | 13 | ![alt tag](config_file.png) 14 | 15 | The following fields are accepted: 16 | - copyright 17 | - designer 18 | - designerURL 19 | - manufacturer 20 | - manufacturerURL 21 | - versionMajor 22 | - versionMinor 23 | - date 24 | - familyName 25 | - upm 26 | - license 27 | - licenseUrl 28 | - glyphOrder 29 | - Family Alignment Zones 30 | - panose 31 | - fsType 32 | - unicodeRanges 33 | - codePageRanges 34 | - vendorID 35 | - blueScale 36 | - blueShift 37 | - isFixedPitch 38 | - trademark 39 | - description 40 | - sampleText 41 | - license 42 | - licenseURL 43 | - versionString 44 | - uniqueID 45 | - ROS 46 | - Make morx table 47 | - EditView Line Height 48 | - Compatible Name Table 49 | - Name Table Entry 50 | - GASP Table 51 | - localizedFamilyName 52 | - localizedDesigner 53 | - TrueType Curve Error 54 | - Use Typo Metrics 55 | - Has WWS Names 56 | - Use Extension Kerning 57 | - Don't use Production Names 58 | - makeOTF Argument 59 | - note 60 | - Disable Subroutines 61 | - Disable Last Change 62 | - Use Line Breaks 63 | 64 | Spelling must be exact. 65 | 66 | ## Further QA tests 67 | Apart from testing metadata, the tool also checks: 68 | 69 | ### Misc: 70 | - Font names don't contain non ASCII characters 71 | 72 | ### Glyphs: 73 | - Report duplicate glyphs 74 | - Space and nbspace share same width 75 | - Glyphs which shouldn't be empty have either components or paths 76 | 77 | ### Vertical Metrics: 78 | - Master/instances share the same vertical metrics 79 | 80 | ## How did this project start? 81 | The Google Fonts collection is currently over 800 fonts. We needed a way to quickly assess if certain families are not meeting our new specification. 82 | 83 | ## Want to contribute? 84 | By all means submit an issue or pull request. 85 | -------------------------------------------------------------------------------- /QA/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4rc1e/mf-glyphs-scripts/c5ed026e5b72a886f1e574f85659cdcae041e66a/QA/__init__.pyc -------------------------------------------------------------------------------- /QA/checkfamily.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Check Font 2 | ''' 3 | 4 | Check family for GlyphsApp 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Check selected family passes qa.json spec and common font errors. 8 | 9 | Refer to README for further info. 10 | ''' 11 | import vanilla 12 | import os 13 | import sys 14 | import glob 15 | import sys 16 | import json 17 | import re 18 | 19 | script_path = os.path.abspath('..') 20 | if script_path not in sys.path: 21 | sys.path.append(script_path) 22 | from QA import ( 23 | glyphs, 24 | fontinfo, 25 | metrics, 26 | otfeatures, 27 | ) 28 | 29 | 30 | __version__ = 0.1 31 | __author__ = ['Marc Foley', 'Lasse Fister'] 32 | 33 | 34 | class GlyphsUI(object): 35 | '''Dialog for enabling/disabling checks''' 36 | def __init__(self, config_file): 37 | self.w = vanilla.FloatingWindow((330, 500), "QA Selected Font", minSize=(300,500), maxSize=(1000,700)) 38 | self.leading = 14 39 | self.head_count = 0 40 | 41 | self._heading('Meta Data') 42 | # iterate over config file and add each entry 43 | for key in config_file: 44 | self._checkbox(key, '%s' % key) 45 | self._checkbox('check_family_name', "Check font name has ASCII chars only") 46 | self._checkbox('check_names_length', "Check names are under 31 chars") 47 | self._checkbox('check_absolute_panose', "Check Panose is not assigned for all weights") 48 | 49 | # Vertical Metrics 50 | self._heading('Vertical Metrics:') 51 | self._checkbox('metrics_fam_vals', "Instances/Masters have same values") 52 | 53 | # Check Glyphs 54 | self._heading('Glyphs:') 55 | self._checkbox('glyph_no_dups', "No duplicate glyphs") 56 | self._checkbox('glyph_nbspace_space', "nbspace and space are same width") 57 | self._checkbox('glyphs_missing_conts_or_comps', "Glyphs missing contours") 58 | self._checkbox('glyphs_duplicate_components', "Glyphs with duplicate components") 59 | self._checkbox('glyphs_00_glyphs', "Glyphs with .00x suffix") 60 | self._checkbox('glyphs_compatibility', "Instance Compatibility") 61 | self._checkbox('glyphs_missing_components', "Glyphs missing GlyphData.xml components", value=False) 62 | 63 | # Check OT Features 64 | self._heading('OT Features:') 65 | self._checkbox('ot_dynamic_frac', "Has Dynamic fraction?") 66 | self._checkbox('vietnamese_correct_schema', "Vietnamese GF Schema", value=False) 67 | 68 | # Check button 69 | self.w.button = vanilla.Button((14, self.leading+40, 300, 20), "Check", callback=self.buttonCallback) 70 | # Resize window to fit all tests 71 | self.w.setPosSize((100.0, 100.0, 350.0, self.leading + 75)) 72 | self.w.open() 73 | 74 | def _heading(self, title): 75 | self.leading += 20 76 | setattr(self.w, 'text%s' % self.head_count, vanilla.TextBox((14, self.leading, 300, 14), title, sizeStyle='small')) 77 | self.leading += 12 78 | self.head_count += 1 79 | setattr(self.w, 'rule%s' % self.head_count, vanilla.HorizontalLine((14, self.leading, 300, 14))) 80 | self.leading += 12 81 | 82 | def _checkbox(self, attr, title, value=True): 83 | setattr(self.w, attr, vanilla.CheckBox((14, self.leading, 300, 20), title, value=value)) 84 | self.leading += 20 85 | 86 | def buttonCallback(self, sender): 87 | main(**self.w.__dict__) 88 | 89 | 90 | def main_glyphs(): 91 | qa_spec = json.load(open(script_path + '/QA/qa.json', 'r')) 92 | ui = GlyphsUI(qa_spec) 93 | 94 | 95 | def main(**kwargs): 96 | font = Glyphs.font 97 | Glyphs.showMacroWindow() 98 | 99 | qa_spec = json.load(open(script_path + '/QA/qa.json', 'r')) 100 | 101 | print '***Check Meta Data***' 102 | for key in qa_spec: 103 | font_attrib = fontinfo.font_field(font, key) 104 | if font_attrib: 105 | fontinfo.check_field(key, qa_spec[key], font_attrib) 106 | else: 107 | print ('ERROR YML DOC: Attribute %s does not exist for font\n' % key) 108 | 109 | if 'check_family_name' in kwargs and kwargs['check_family_name'].get() == 1: 110 | fontinfo.check_family_name(font.familyName) 111 | 112 | if 'check_names_length' in kwargs and kwargs['check_names_length'].get() == 1: 113 | fontinfo.check_names_length(font) 114 | 115 | if 'check_absolute_panose' in kwargs and kwargs['check_absolute_panose'].get() == 1: 116 | fontinfo.panose(font) 117 | 118 | print "***Check Glyph's Data***" 119 | if 'glyphs_missing_conts_or_comps' in kwargs and kwargs['glyphs_missing_conts_or_comps'].get() == 1: 120 | glyphs.outlines_missing(font) 121 | 122 | if 'glyph_nbspace_space' in kwargs and kwargs['glyph_nbspace_space'].get() == 1: 123 | metrics.uni00a0_width(font, font.masters) 124 | 125 | if 'glyph_no_dups' in kwargs and kwargs['glyph_no_dups'].get() == 1: 126 | glyphs.find_duplicates([g.name for g in font.glyphs]) 127 | 128 | if 'glyphs_duplicate_components' in kwargs and kwargs['glyphs_duplicate_components'].get() == 1: 129 | glyphs.find_duplicate_components(font.glyphs) 130 | 131 | if 'glyphs_00_glyphs' in kwargs and kwargs['glyphs_00_glyphs'].get() ==1: 132 | glyphs.find_00_glyphs(font.glyphs) 133 | 134 | if 'glyphs_compatibility' in kwargs and kwargs['glyphs_compatibility'].get() == 1: 135 | glyphs.instance_compatibility(font) 136 | 137 | if 'glyphs_missing_components' in kwargs and kwargs['glyphs_missing_components'].get() == 1: 138 | glyphs.find_missing_components(font.glyphs, [i.id for i in font.masters]) 139 | 140 | print "***Check OT Features***" 141 | if 'ot_dynamic_frac' in kwargs and kwargs['ot_dynamic_frac'].get() == 1: 142 | otfeatures.dynamic_fraction(font) 143 | 144 | if 'vietnamese_correct_schema' in kwargs and kwargs['vietnamese_correct_schema'].get() == 1: 145 | otfeatures.vietnamese_locl(font.features['locl'].code) 146 | 147 | print "***Check Vertical Metrics***" 148 | if 'metrics_fam_vals' in kwargs and kwargs['metrics_fam_vals'].get() == 1: 149 | metrics.synced('master', font.masters) 150 | 151 | 152 | if __name__ == '__main__': 153 | main_glyphs() 154 | -------------------------------------------------------------------------------- /QA/compare_2_fonts_glyph_points.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Report inconsistent glyph nodes between two fonts 2 | 3 | fonts = Glyphs.fonts 4 | 5 | 6 | def glyphs_coords(glyph, master): 7 | points = [] 8 | m = master.name 9 | for path in glyph.layers[m].paths: 10 | for node in path.nodes: 11 | points.append(( 12 | node.position[0], node.position[1] 13 | )) 14 | return points 15 | 16 | 17 | def main(): 18 | Glyphs.showMacroWindow() 19 | inconsistent_glyphs = [] 20 | font1 = fonts[0] 21 | font2 = fonts[1] 22 | 23 | masters = font1.masters 24 | if len(font1.masters) != len(font2.masters): 25 | print 'Fonts do not share same amount of masters, aborting!' 26 | return 27 | 28 | glyphset1 = set(font1.glyphs.keys()) 29 | glyphset2 = set(font2.glyphs.keys()) 30 | shared_glyphs = glyphset1 & glyphset2 31 | print 'Checking %s glyphs' % (len(shared_glyphs)) 32 | if len(glyphset1) != len(glyphset2): 33 | print 'WARNING: [%s] is in either font1 or font2 but not both\n' % ( 34 | ' ,'.join(glyphset1 ^ glyphset2) 35 | ) 36 | for glyph in shared_glyphs: 37 | for m, master in enumerate(masters): 38 | glyph1 = glyphs_coords(font1.glyphs[glyph], master) 39 | glyph2 = glyphs_coords(font2.glyphs[glyph], master) 40 | 41 | if glyph1 != glyph2: 42 | inconsistent_glyphs.append((master.name, glyph)) 43 | 44 | if inconsistent_glyphs: 45 | for master, glyph in inconsistent_glyphs: 46 | print 'ERROR: %s %s is not consistent with other font' % ( 47 | master, glyph 48 | ) 49 | else: 50 | print 'PASS: Shared glyphs have same point coordinates' 51 | 52 | if __name__ == '__main__': 53 | if len(fonts) != 2: 54 | print 'ERROR: Open two files only!' 55 | else: 56 | main() 57 | -------------------------------------------------------------------------------- /QA/config_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4rc1e/mf-glyphs-scripts/c5ed026e5b72a886f1e574f85659cdcae041e66a/QA/config_file.png -------------------------------------------------------------------------------- /QA/fontinfo.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def check_field(key, yml, font, fix=False): 5 | '''Check if a font's attribute matches the yml document''' 6 | if 'any' in str(yml): 7 | print 'PASS: font %s has attribute\n' % key 8 | elif yml != font: 9 | print 'ERROR: font %s is not equal to yml %s\n' % (key, key) 10 | else: 11 | print 'PASS: font %s is equal to yml %s\n' % (key, key) 12 | if fix: 13 | font = yml 14 | 15 | 16 | def font_field(font, key): 17 | '''Check font has key''' 18 | if hasattr(font, key): 19 | return getattr(font, key) 20 | if key in font.customParameters: 21 | return font.customParameters[key] 22 | return None 23 | 24 | 25 | def check_family_name(fontname): 26 | '''Check if family name has non ascii characters as well as dashes, number 27 | and diacritics as well.''' 28 | print('**Check family name has only ASCII characters**') 29 | try: 30 | fontname.decode('ascii') 31 | illegal_char_check = re.search(r'[\-\\/0-9]+', fontname) 32 | if illegal_char_check: 33 | print('ERROR: Font family "%s", contains numbers, slashes or dashes.' % fontname) 34 | return False 35 | except UnicodeDecodeError: 36 | print('ERROR: Font family name %s, has non ascii characters' % fontname) 37 | return False 38 | print('PASS: Family name is correct\n') 39 | return True 40 | 41 | 42 | def panose(font): 43 | '''Panose number should not be set to an absolute if the font has 44 | instances/weights''' 45 | print '**Check Panose Assignment**' 46 | if font.masters > 1 and 'panose' in font.customParameters: 47 | print 'ERROR: Panose should be unique for each weight instance\n' 48 | else: 49 | print 'PASS: Panose is not set as an absolute for family\n' 50 | 51 | 52 | def check_names_length(font): 53 | '''Check font fields will not be longer than 32 characters''' 54 | bad_names = [] 55 | instances = font.instances 56 | 57 | print '**Check Font Name Length**' 58 | for instance in instances: 59 | if instance.customParameters['familyName']: 60 | fontname = instance.customParameters['familyName'] 61 | else: 62 | fontname = font.familyName 63 | 64 | if len(fontname) > 31: 65 | bad_names.append(fontname) 66 | if len(instance.name) > 31: 67 | bad_names.append(instance.name) 68 | 69 | if bad_names: 70 | for name in bad_names: 71 | print 'ERROR: %s is longer than 31 characters, length is %s' % (name, len(name)) 72 | else: 73 | print 'PASS: Font names are under 31 characters\n' 74 | -------------------------------------------------------------------------------- /QA/glyphs.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | import unicodedata as uni 3 | from copy import copy 4 | 5 | IGNORE_GLYPHS_OUTLINE = [ 6 | 'uni0000', 7 | 'uni0002', 8 | 'uni0009', 9 | 'uni000A', 10 | 'NULL', 11 | 'null', 12 | '.null', 13 | 'CR', 14 | 'nbspace', 15 | ] 16 | 17 | 18 | def find_duplicates(font_glyphs): 19 | '''Check if there are duplicate glyphs''' 20 | print '**Find Duplicate glyphs in selected font**' 21 | glyphs_count = Counter(font_glyphs) 22 | if len(set(glyphs_count.values())) >= 2: 23 | for glyph in glyphs_count: 24 | if glyphs_count[glyph] >= 2: 25 | print 'ERROR: %s duplicated\n' % glyph 26 | else: 27 | print 'PASS: No duplicate glyphs\n' 28 | 29 | 30 | def find_00_glyphs(font_glyphs): 31 | print '**Find glyphs with suffix .00**' 32 | bad_glyphs = [] 33 | for glyph in font_glyphs: 34 | if '.0' in glyph.name: 35 | bad_glyphs.append(glyph.name) 36 | 37 | if bad_glyphs: 38 | print 'ERROR: font contains %s\n' % ', '.join(bad_glyphs) 39 | else: 40 | print 'PASS: font contains no .00x glyphs\n' 41 | 42 | 43 | def outlines_missing(font): 44 | '''Check if glyphs are missing outlines or composites. 45 | Only works on glyphs which have unicodes''' 46 | print '**Check Glyphs have outlines or components**' 47 | masters = font.masters 48 | 49 | for i, master in enumerate(masters): 50 | bad_glyphs = [] 51 | for glyph in font.glyphs: 52 | 53 | if str(glyph.category) != 'Separator' and glyph.name not in IGNORE_GLYPHS_OUTLINE: 54 | if len(glyph.layers[i].paths) == 0: 55 | if len(glyph.layers[i].components) == 0: 56 | bad_glyphs.append(glyph.name) 57 | 58 | if bad_glyphs: 59 | for glyph in bad_glyphs: 60 | print "ERROR: %s master's %s should have outlines or components\n" % (master.name, glyph) 61 | else: 62 | print "PASS: %s master's glyphs have components or outlines\n" % master.name 63 | 64 | def find_duplicate_components(glyphs): 65 | '''Find duplicate components in the same glyph and share 66 | the same affine transformation. 67 | This happens when Glyphs generates a glyph like quotedblright.''' 68 | print '**Find duplicate components that share the same position/transformation.**' 69 | no_error = True 70 | for glyph in glyphs: 71 | for layer in glyph.layers: 72 | all_transformations = {} 73 | all_components = {} 74 | for component in layer.components: 75 | name = component.componentName 76 | if name not in all_components: 77 | all_components[name] = [] 78 | all_transformations[name] = set() 79 | all_components[name].append(component) 80 | all_transformations[name].add(tuple(component.transform)) 81 | for name, components in all_components.iteritems(): 82 | transformations = all_transformations[name]; 83 | if len(transformations) != len(components): 84 | no_error = False 85 | print ('ERROR: glyph {glyph} layer {layer}: {count_c} ' 86 | + 'components of {component} share {count_t} ' 87 | + 'transformations.\n ' 88 | + 'All components of the same type must be positioned ' 89 | + 'differently.\n').format( 90 | glyph=glyph.name, 91 | layer=layer.name, 92 | count_c=len(components), 93 | component=name, 94 | count_t=len(transformations)) 95 | if no_error: 96 | print 'PASS: no duplicate components share the same spot.\n' 97 | 98 | 99 | def find_missing_components(glyphs, layers): 100 | """Check composites match GlyphsApp's GlyphData.xml 101 | 102 | .case suffixes are stripped during the check eg: 103 | acute.comb -> acute""" 104 | print '**Find glyphs which should have components**' 105 | component_map = {} 106 | bad_glyphs = [] 107 | 108 | for glyph in glyphs: 109 | component_map[glyph.name] = [] 110 | try: 111 | for component in glyph.glyphInfo.components: 112 | component_map[glyph.name].append(component.name.split('.')[0]) 113 | except: 114 | all 115 | 116 | for glyph in glyphs: 117 | if glyph.category == 'Letter': 118 | for i, master in enumerate(layers): 119 | no_case_comp_names = set(g.componentName.split('.')[0] for 120 | g in glyph.layers[i].components) 121 | if set(component_map[glyph.name]) - no_case_comp_names: 122 | bad_glyphs.append( 123 | (glyph.name, 124 | glyph.layers[i].name, 125 | set(component_map[glyph.name]) - no_case_comp_names) 126 | ) 127 | 128 | if bad_glyphs: 129 | for glyph, layer, comps in bad_glyphs: 130 | print "WARNING: %s %s missing '%s'" % (glyph, layer, ', '.join(comps)) 131 | print '\n' 132 | else: 133 | print "PASS: Fonts have the same components as GlyphData.xml\n" 134 | 135 | 136 | def _remove_overlaps(glyph): 137 | '''remove path overlaps for all layers''' 138 | for layer in glyph.layers: 139 | layer.removeOverlap() 140 | return glyph 141 | 142 | 143 | def font_glyphs_contours(font): 144 | '''Return nested dictionary: 145 | layer[glyph] = contour_count 146 | Reg: 147 | A: 2 148 | B: 3 149 | Bold: 150 | A: 2 151 | B: 3 152 | ''' 153 | glyphs = {} 154 | 155 | masters = font.masters 156 | for master in masters: 157 | if master.name not in glyphs: 158 | glyphs[master.name] = {} 159 | for glyph in font.glyphs: 160 | glyph_cp = copy(glyph) 161 | glyph_no_overlap = _remove_overlaps(glyph_cp) 162 | glyph_contours = len(glyph_no_overlap.layers[master.id].paths) 163 | glyphs[master.name][glyph.name] = glyph_contours 164 | return glyphs 165 | 166 | 167 | def font_glyphs_compatible(glyph_paths_count): 168 | '''Preflight master compatbility check''' 169 | good_glyphs = {} 170 | bad_glyphs = set() 171 | for master1 in glyph_paths_count: 172 | for master2 in glyph_paths_count: 173 | glyphs1 = glyph_paths_count[master1] 174 | glyphs2 = glyph_paths_count[master2] 175 | shared_glyphs = set(glyphs1) & set(glyphs2) 176 | for glyph in shared_glyphs: 177 | if glyphs1[glyph] == glyphs2[glyph]: 178 | good_glyphs[glyph] = glyphs1[glyph] 179 | else: 180 | bad_glyphs.add(glyph) 181 | if bad_glyphs: 182 | for glyph in bad_glyphs: 183 | print 'WARNING: %s not consistent, check masters' % glyph 184 | print '\n' 185 | return good_glyphs 186 | 187 | 188 | def instance_compatibility(font): 189 | '''Check if instances share the same path count as their masters. 190 | This is useful to check if the deiresis have merged into a single 191 | dot.''' 192 | print '**Check glyph instances have same amount of paths**' 193 | glyph_paths_count = font_glyphs_contours(font) 194 | compatible_glyphs = font_glyphs_compatible(glyph_paths_count) 195 | 196 | instances = font.instances 197 | 198 | bad_glyphs = [] 199 | for instance in instances: 200 | faux_font = instance.interpolatedFont 201 | for glyph in compatible_glyphs: 202 | try: 203 | faux_glyph = faux_font.glyphs[glyph].layers[0] 204 | faux_glyph.removeOverlap() 205 | if len(faux_glyph.paths) != compatible_glyphs[glyph]: 206 | print 'WARNING: %s, %s %s Instance has %s, whilst masters have %s' % ( 207 | glyph, 208 | instance.width, 209 | instance.name, 210 | len(faux_glyph.paths), 211 | compatible_glyphs[glyph] 212 | ) 213 | bad_glyphs.append(glyph) 214 | except: 215 | all 216 | print '\n' 217 | if not bad_glyphs: 218 | print 'PASS: Instances and Masters share same contour count\n' 219 | -------------------------------------------------------------------------------- /QA/mark-glyphs-in-other-fonts.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Mark glyphs which exist in open fonts 2 | # -*- coding: utf-8 -*- 3 | 4 | __doc__ = '''Mark glyphs in the selected font which exist in all opened fonts''' 5 | from copy import copy 6 | 7 | all_glyphs = set() 8 | for i, font in enumerate(Glyphs.fonts): 9 | for glyph in font.glyphs: 10 | all_glyphs.add(glyph.name) 11 | 12 | shared_glyphs = copy(all_glyphs) 13 | for font in Glyphs.fonts: 14 | glyphs = set(g.name for g in font.glyphs) 15 | shared_glyphs.intersection_update(glyphs) 16 | 17 | current_font = Glyphs.fonts[0] 18 | for name in shared_glyphs: 19 | current_font.glyphs[name].color = 4 20 | -------------------------------------------------------------------------------- /QA/metrics.py: -------------------------------------------------------------------------------- 1 | VERT_KEYS = [ 2 | 'typoDescender', 3 | 'typoLineGap', 4 | 'hheaLineGap', 5 | 'hheaAscender', 6 | 'typoAscender', 7 | 'hheaDescender', 8 | 'winDescent', 9 | 'winAscent', 10 | ] 11 | 12 | 13 | def synced(layer, masters): 14 | '''Check if masters share same vertical metrics''' 15 | print '**Checking %s share same vert metrics**' % layer 16 | bad_masters = [] 17 | for master1 in masters: 18 | for master2 in masters: 19 | for key in VERT_KEYS: 20 | if key not in master1.customParameters: 21 | print 'ERROR: %s %s is missing in %s\n' % (layer, key, master1.name) 22 | print 'Add all Vertical metrics parameters first!' 23 | return False 24 | if master1.customParameters[key] != master2.customParameters[key]: 25 | bad_masters.append((layer, master1.name, master2.name, key)) 26 | 27 | if bad_masters: 28 | for layer, master1, master2, key in bad_masters: 29 | print "ERROR: %s's %s %s %s not even. Fix first!\n" % (layer, master1, master2, key) 30 | return False 31 | else: 32 | print 'PASS: %s share same metrics\n' % layer 33 | return True 34 | 35 | 36 | def uni00a0_width(font, masters, fix=False): 37 | '''Set nbspace to same width as space''' 38 | print '**Checking space and nbspace have same width**' 39 | for id, master in enumerate(masters): 40 | if font.glyphs['nbspace']: 41 | if font.glyphs['nbspace'].layers[id].width != font.glyphs['space'].layers[id].width: 42 | print "ERROR: %s master's nbspace and space are not same width\n" % (master.name) 43 | else: 44 | print "PASS: %s master's nbspace and space are same width\n" % (master.name) 45 | else: 46 | print "ERROR: nbspace does not exist. It may be named uni00A0" 47 | if fix: 48 | font.glyphs['nbspace'].layers[id].width = font.glyphs['space'].layers[id].width 49 | print 'Now equal widths! space=%s, 00A0=%s' % ( 50 | font.glyphs['space'].layers[id].width, 51 | font.glyphs['nbspace'].layers[id].width 52 | ) 53 | -------------------------------------------------------------------------------- /QA/metrics_in_diff_upms.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Show Metric keys at different upms 2 | ''' 3 | Display the value of each metric key for 1000, 1024 and 2048 upm values. 4 | ''' 5 | 6 | font = Glyphs.font 7 | Glyphs.showMacroWindow() 8 | 9 | UPMS = [ 10 | 1000, 11 | 1024, 12 | 2048, 13 | ] 14 | 15 | 16 | def main(): 17 | print 'Showing metrics keys at different upms' 18 | for master in font.masters: 19 | for upm in UPMS: 20 | print '\n**Master: %s upm: %s**' % (master.name, upm) 21 | for field in master.customParameters: 22 | if int(field.value): 23 | print field.name, int((float(field.value) / font.upm) * upm) 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /QA/otfeatures.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | VIET_GLYPHS = [ 5 | 'idotaccent', 6 | 'idotaccent', 7 | 'idotaccent', 8 | 'idotaccent', 9 | 'periodcentered.loclCAT', 10 | 'periodcentered.loclCAT', 11 | 'idotaccent', 12 | 'Scommaaccent', 13 | 'scommaaccent', 14 | 'Tcommaaccent', 15 | 'tcommaaccent', 16 | 'Scommaaccent', 17 | 'scommaaccent', 18 | 'Tcommaaccent', 19 | 'tcommaaccent', 20 | 'Aacute.loclVIT', 21 | 'Agrave.loclVIT', 22 | 'Eacute.loclVIT', 23 | 'Egrave.loclVIT', 24 | 'Iacute.loclVIT', 25 | 'Igrave.loclVIT', 26 | 'Oacute.loclVIT', 27 | 'Ograve.loclVIT', 28 | 'Uacute.loclVIT', 29 | 'Ugrave.loclVIT', 30 | 'Yacute.loclVIT', 31 | 'Ygrave.loclVIT', 32 | 'aacute.loclVIT', 33 | 'agrave.loclVIT', 34 | 'eacute.loclVIT', 35 | 'egrave.loclVIT', 36 | 'iacute.loclVIT', 37 | 'igrave.loclVIT', 38 | 'oacute.loclVIT', 39 | 'ograve.loclVIT', 40 | 'uacute.loclVIT', 41 | 'ugrave.loclVIT', 42 | 'yacute.loclVIT', 43 | 'ygrave.loclVIT', 44 | 'gravecomb.loclVIT', 45 | 'acutecomb.loclVIT', 46 | 'circumflexcomb.loclVIT', 47 | 'brevecomb.loclVIT', 48 | 'tildecomb.loclVIT' 49 | ] 50 | 51 | 52 | def dynamic_fraction(font): 53 | '''If font has fivesuperior, there should be a dynamic fraction feature''' 54 | print '**Checking frac feature**' 55 | if 'fivesuperior' in font.glyphs: 56 | if font.features['frac']: 57 | if "'" in str(font.features['frac'].code): 58 | print 'PASS: Font has dynamic frac feature\n' 59 | else: 60 | print 'POSSIBLE ERROR: frac feature may not be dynamic\n' 61 | else: 62 | print 'ERROR: no frac OT feature\n' 63 | else: 64 | print 'PASS: font does not have 4-9 numerators glyphs, no'\ 65 | 'dynamic frac needed\n' 66 | 67 | 68 | def vietnamese_locl(feature): 69 | '''Check if localised vietnamese glyphs are implemented''' 70 | print '**Checking for locl Vietnamese**' 71 | 72 | viet_subs = re.findall(r'(?<=by ).*(?`_ and then change user to your username in the below and run 15 | 16 | ``cd /Users/user/Library/Application Support/Glyphs/Scripts; pip install -r requirements.txt`` 17 | -------------------------------------------------------------------------------- /Reports/gposreport.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Generate Gpos Strings 2 | 3 | '''Exports all single gpos combos e.g a+acutcomb 4 | 5 | To do: 6 | Support for non-Latin scripts. 7 | 8 | Release history: 9 | 2015/08/29: 10 | V0.001: This is the first release''' 11 | 12 | import GlyphsApp 13 | import re 14 | 15 | class AnchorData(object): 16 | '''Easier access for specific anchors. Heirachy for each 17 | dictionary as follows: 18 | 19 | {weight: 20 | glyph: 21 | anchor.name = (x, y)}''' 22 | def __init__(self, font, truncate=False): 23 | self.font = font 24 | 25 | self.base_anchors = {} 26 | self.mark_anchors = {} 27 | self.lig_anchors = {} 28 | 29 | for glyph in self.font.glyphs: 30 | for layer in glyph.layers: 31 | 32 | if not layer.name in self.base_anchors: 33 | self.base_anchors[layer.name] = {} 34 | self.mark_anchors[layer.name] = {} 35 | self.lig_anchors[layer.name] = {} 36 | 37 | if layer.anchors: 38 | for anchor in layer.anchors: 39 | anc_name = anchor.name 40 | g_name = glyph.name 41 | 42 | a_x = anchor.position.x 43 | a_y = anchor.position.y 44 | 45 | if truncate: 46 | g_name = glyph.name.split('.')[0] 47 | anc_name = anchor.name.split('.')[0] 48 | 49 | if anchor.name.startswith('_'): 50 | if not glyph.name in self.mark_anchors[layer.name]: 51 | self.mark_anchors[layer.name][g_name] = {} 52 | self.mark_anchors[layer.name][g_name][anc_name] = (a_x, a_y) 53 | 54 | elif '_' in anchor.name[1:]: 55 | if not glyph.name in self.lig_anchors[layer.name]: 56 | self.lig_anchors[layer.name][g_name] = {} 57 | self.lig_anchors[layer.name][g_name][anc_name] = (a_x, a_y) 58 | 59 | else: 60 | if not glyph.name in self.base_anchors[layer.name]: 61 | self.base_anchors[layer.name][g_name] = {} 62 | self.base_anchors[layer.name][g_name][anc_name] = (a_x, a_y) 63 | 64 | @property 65 | def basemarks(self): 66 | 'return basemarks for font e.g A' 67 | return self.base_anchors 68 | 69 | @property 70 | def accentmarks(self): 71 | 'returns accent mark for font e.g acutecomb' 72 | return self.mark_anchors 73 | 74 | @property 75 | def ligmarks(self): 76 | 'returns ligature marks e.g aLamAlif' 77 | return self.lig_anchors 78 | 79 | 80 | def designer_gpos_strings(base, accent, font): 81 | '''Output strings based on Base glyph + Accent glyph. Unfortuantely, non-Latin 82 | support is flakey at best. Devanagari and complex GSUB scripts which rely on 83 | pres or liga OT features are not supported at the moment.''' 84 | text = [] 85 | #loop through both base glyphs and accent glyphs 86 | for weight in base: 87 | text.append("\n" + weight + "\n") 88 | for glyph in base[weight]: 89 | for mark in accent[weight]: 90 | for b_anc in base[weight][glyph]: 91 | for a_anc in accent[weight][mark]: 92 | if b_anc == a_anc[1:]: #chops _top to top on mark glyphs 93 | if hasattr(font.glyphs[glyph], "unicode"): 94 | if hasattr(font.glyphs[mark], "unicode"): 95 | text.append('%s%s ' %(font.glyphs[glyph].string, 96 | font.glyphs[mark].string)) 97 | return ''.join(text).encode('utf-8') 98 | 99 | def main(): 100 | font = Glyphs.font 101 | anchor = AnchorData(font, truncate=True) 102 | 103 | loc = re.split(r'\.ttf|\.otf', font.filepath)[0] 104 | 105 | gpos = open(loc + '_gpos_strings.txt', 'w') 106 | gpos.write(designer_gpos_strings(anchor.basemarks, anchor.accentmarks, font)) 107 | gpos.close() 108 | 109 | Message('Done', 'Strings saved to %s' %loc) 110 | 111 | if __name__ == '__main__': 112 | 113 | if len(Glyphs.fonts) != 1: 114 | print 'Please only have one font open' 115 | else: 116 | main() 117 | -------------------------------------------------------------------------------- /Reports/kernreport.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Generate Kern Report & Strings 2 | 3 | '''Script outputs kerning table and generates proofable strings 4 | 5 | To do: 6 | Support for non-Latin scripts. 7 | 8 | Release history: 9 | 2015/08/29: 10 | V0.001: This is the first release 11 | ''' 12 | 13 | import GlyphsApp 14 | import unicodedata as uni 15 | import re 16 | 17 | __ver__ = 0.1 18 | __author__ = 'Marc Foley' 19 | 20 | 21 | class KernData(object): 22 | '''Wrapper for Glyphsapp Kern table. The Glyph.font.kerningDict() dictionary is 23 | very difficult to work with.''' 24 | def __init__(self, font): 25 | 26 | self.font = font 27 | 28 | self.kern_table = self.font.kerningDict() 29 | 30 | #Unnesting font.kerningDict() it is a multi levelled beast! 31 | self.all_kerns = {} 32 | for step, glyph in enumerate(self.kern_table): 33 | c_layer = str(Glyphs.font.masters[step]).split('"')[1] 34 | self.all_kerns[c_layer] = [] 35 | for left in self.kern_table[glyph]: 36 | for right in self.kern_table[glyph][left]: 37 | self.all_kerns[c_layer].append( 38 | (left, right, self.kern_table[glyph][left][right])) 39 | 40 | @property 41 | def table(self): 42 | '''Output unnested kern table''' 43 | return self.all_kerns 44 | 45 | @property 46 | def classes(self): 47 | '''return list of tuples (name, l_kern class, r_kern class)''' 48 | return [(glyph.name, 49 | glyph.leftKerningGroup, 50 | glyph.rightKerningGroup) for glyph in self.font.glyphs] 51 | 52 | def round_kerning(self): 53 | '''Round floats to integers for kerning pairs''' 54 | pass 55 | 56 | def kern_report(kern): 57 | '''Output kerning table.''' 58 | 59 | text = [] 60 | for layer in kern: 61 | pair = kern[layer] 62 | text.append(layer +' \n') 63 | for left, right, value in pair: 64 | text.append('%s,%s,%s\n' %(left, right, value)) 65 | return ''.join(text) 66 | 67 | 68 | def designer_kern_strings(kern, font): 69 | '''Builds strings which are parsed according to their sub category. 70 | If the unicode name features "UPPER", it will be set with OO, HH. 71 | If the name has "Lower", oo, nn are used instead. Unfortuantely, 72 | this technique can only parse Latin, Greek and Cyrillic. I would 73 | like to make my own parser so we can support non-Latins''' 74 | text = [] 75 | for layer in kern: 76 | pair = kern[layer] 77 | text.append('\n' + layer +'\n') 78 | 79 | for left, right, value in pair: 80 | #Truncate class to main class key e.g @mmk_l_r = r 81 | if '@' in left: 82 | l_kern = font.glyphs[left[7:].split('_')[0]] 83 | else: 84 | l_kern = font.glyphs[left] 85 | 86 | if '@' in right: 87 | r_kern = font.glyphs[right[7:].split('_')[0]] 88 | else: 89 | r_kern = font.glyphs[right] 90 | 91 | if l_kern != None and l_kern.unicode: 92 | if r_kern != None and r_kern.unicode: 93 | 94 | try: 95 | if 'Lowercase' in l_kern.subCategory: 96 | l_pair = 'oo%s' % l_kern.string 97 | l_pair2 = 'nn%s' % l_kern.string 98 | else: 99 | l_pair = 'OO%s' % l_kern.string 100 | l_pair2 = 'HH%s' % l_kern.string 101 | 102 | if 'Lowercase' in r_kern.subCategory: 103 | r_pair = '%soo' % r_kern.string 104 | r_pair2 = '%snn' % r_kern.string 105 | else: 106 | r_pair = '%sOO' % r_kern.string 107 | r_pair2 = '%sHH' % r_kern.string 108 | except: 109 | AttributeError 110 | string = '%s%s%s%s\n' %(l_pair, r_pair, l_pair2, r_pair2) 111 | text.append(string) 112 | 113 | return ''.join(text).encode('utf-8') 114 | 115 | 116 | def main(): 117 | 118 | font = Glyphs.font 119 | kern = KernData(font) 120 | 121 | loc = re.split(r'\.ttf|\.otf', font.filepath)[0] 122 | 123 | report = open(loc + '_kern_report.txt', 'w') 124 | report.write(kern_report(kern.table)) 125 | report.close() 126 | 127 | string = open(loc + '_kern_strings.txt', 'w') 128 | string.write(designer_kern_strings(kern.table, font)) 129 | string.close() 130 | 131 | Message('Done', 'Reports & Strings saved to %s' %loc) 132 | 133 | 134 | if __name__ == '__main__': 135 | 136 | if len(Glyphs.fonts) != 1: 137 | print 'Please only have one font open' 138 | else: 139 | main() 140 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | GitPython==0.3.2rc1 -------------------------------------------------------------------------------- /wrappers.py: -------------------------------------------------------------------------------- 1 | import vanilla 2 | 3 | class GlyphsUI(object): 4 | '''Dialog for enabling/disabling checks''' 5 | def __init__(self, title): 6 | self.w = vanilla.FloatingWindow((330, 500), title, minSize=(300,500), maxSize=(1000,700)) 7 | self.leading = 14 8 | self.head_count = 0 9 | 10 | self.w.open() 11 | 12 | def _heading(self, title): 13 | self.leading += 20 14 | setattr(self.w, 'text%s' % self.head_count, vanilla.TextBox((14, self.leading, 300, 14), title, sizeStyle='small')) 15 | self.leading += 12 16 | self.head_count += 1 17 | setattr(self.w, 'rule%s' % self.head_count, vanilla.HorizontalLine((14, self.leading, 300, 14))) 18 | self.leading += 12 19 | 20 | def _checkbox(self, attr, title, value=True): 21 | setattr(self.w, attr, vanilla.CheckBox((14, self.leading, 300, 20), title, value=value)) 22 | self.leading += 20 23 | 24 | def _combobox(self, attr, vals): 25 | setattr(self.w, attr, vanilla.PopUpButton((14, self.leading, 300, 20), vals)) 26 | self.leading += 20 27 | 28 | def _from_config(): 29 | if config_file != None: 30 | for key in config_file: 31 | self._checkbox(key, '%s' % key) 32 | --------------------------------------------------------------------------------