def loadLocalNamesDB(agl, diacriticComps): # { 2126: ['Omega', ...], ...} uc2names = None for fontPath in srcFontPaths: font = OpenFont(fontPath) if uc2names is None: uc2names = font.getCharacterMapping( ) # { 2126: ['Omega', ...], ...} else: for uc, names in font.getCharacterMapping().iteritems(): names2 = uc2names.get(uc, []) for name in names: if name not in names2: names2.append(name) uc2names[uc] = names2 # agl { 2126: 'Omega', ...} -> { 'Omega': [2126, ...], ...} aglName2Ucs = {} for uc, name in agl.iteritems(): aglName2Ucs.setdefault(name, []).append(uc) for glyphName, comp in diacriticComps.iteritems(): for uc in aglName2Ucs.get(glyphName, []): names = uc2names.get(uc, []) if glyphName not in names: names.append(glyphName) uc2names[uc] = names name2ucs = {} for uc, names in uc2names.iteritems(): for name in names: name2ucs.setdefault(name, set()).add(uc) return uc2names, name2ucs
def main(): argparser = ArgumentParser( description='Generate kerning samples by providing the left-hand side glyph') argparser.add_argument( '-u', dest='formatAsUnicode', action='store_const', const=True, default=False, help='Format output as unicode escape sequences instead of glyphnames. ' + 'E.g. "\\u2126" instead of "\\Omega"') argparser.add_argument( '-suffix', dest='suffix', metavar='<text>', type=str, help='Text to append after each pair') argparser.add_argument( '-all-in-groups', dest='includeAllInGroup', action='store_const', const=True, default=False, help='Include all glyphs for groups rather than just the first glyph listed.') argparser.add_argument( 'fontPath', metavar='<ufofile>', type=str, help='UFO font source') argparser.add_argument( 'glyphnames', metavar='<glyphname>', type=str, nargs='+', help='Name of glyphs to generate samples for. '+ 'You can also provide a Unicode code point using the syntax "U+XXXX"') args = argparser.parse_args() font = OpenFont(args.fontPath) groupsFilename = os.path.join(args.fontPath, 'groups.plist') kerningFilename = os.path.join(args.fontPath, 'kerning.plist') groups = plistlib.readPlist(groupsFilename) # { groupName => [glyphName] } kerning = plistlib.readPlist(kerningFilename) # { leftName => {rightName => kernVal} } groupmap = mapGroups(groups) # expand any unicode codepoints glyphnames = [] for glyphname in args.glyphnames: if len(glyphname) > 2 and glyphname[:2] == 'U+': cp = int(glyphname[2:], 16) ucmap = font.getCharacterMapping() # { 2126: ['Omega', ...], ...} for glyphname2 in ucmap[cp]: glyphnames.append(glyphname2) else: glyphnames.append(glyphname) for glyphname in glyphnames: samplesForGlyphname(font, groups, groupmap, kerning, glyphname, args)
def loadUFOGlyphNames(ufoPath): font = OpenFont(ufoPath) libPlist = PList(os.path.join(ufoPath, 'lib.plist')) orderedNames = libPlist['public.glyphOrder'] # [ 'Omega', ...] # append any glyphs that are missing in orderedNames allNames = set(font.keys()) for name in orderedNames: allNames.discard(name) for name in allNames: orderedNames.append(name) ucToNames = font.getCharacterMapping() # { 2126: [ 'Omega', ...], ...} nameToUc = revCharMap(ucToNames) # { 'Omega': 2126, ...} gol = OrderedDict() # OrderedDict{ ('Omega', 2126|None), ...} for name in orderedNames: gol[name] = nameToUc.get(name) # gol.append((name, nameToUc.get(name))) return gol, ucToNames, nameToUc, libPlist
def main(): jsonSchemaDescr = '{[unicode:int]: glyphname:string, ...}' argparser = ArgumentParser( description= 'Rename glyphnames in UFO kerning and remove unused groups and glyphnames.' ) argparser.add_argument( '-dry', dest='dryRun', action='store_const', const=True, default=False, help='Do not modify anything, but instead just print what would happen.' ) argparser.add_argument('-no-stats', dest='noStats', action='store_const', const=True, default=False, help='Do not print statistics at the end.') argparser.add_argument('-save-stats', dest='saveStatsPath', metavar='<file>', type=str, help='Write detailed statistics to JSON file.') argparser.add_argument('-src-json', dest='srcJSONFile', metavar='<file>', type=str, help='JSON file to read glyph names from.' + ' Expected schema: ' + jsonSchemaDescr + ' (e.g. {2126: "Omega"})') argparser.add_argument( '-src-font', dest='srcFontFile', metavar='<file>', type=str, help='TrueType or OpenType font to read glyph names from.') argparser.add_argument('dstFontsPaths', metavar='<ufofile>', type=str, nargs='+', help='UFO fonts to update') args = argparser.parse_args() dryRun = args.dryRun if args.srcJSONFile and args.srcFontFile: argparser.error( 'Both -src-json and -src-font specified -- please provide only one.' ) # Strip trailing slashes from font paths args.dstFontsPaths = [s.rstrip('/ ') for s in args.dstFontsPaths] # Load source char map srcCharMap = None if args.srcJSONFile: try: srcCharMap = loadJSONCharMap(args.srcJSONFile) except Exception as err: argparser.error('Invalid JSON: Expected schema %s (%s)' % (jsonSchemaDescr, err)) elif args.srcFontFile: srcCharMap = getTTCharMap( args.srcFontFile.rstrip('/ ')) # -> { 2126: 'Omegagreek', ...} else: argparser.error('No source provided (-src-* argument missing)') if len(srcCharMap) == 0: print('Empty character map', file=sys.stderr) sys.exit(1) # Find project source dir srcDir = '' for dstFontPath in args.dstFontsPaths: s = os.path.dirname(dstFontPath) if not srcDir: srcDir = s elif srcDir != s: raise Exception( 'All <ufofile>s must be rooted in the same directory') # Load font project config # load fontbuild configuration config = RawConfigParser(dict_type=OrderedDict) configFilename = os.path.join(srcDir, 'fontbuild.cfg') config.read(configFilename) diacriticsFile = configFindResFile(config, srcDir, 'diacriticfile') for dstFontPath in args.dstFontsPaths: dstFont = OpenFont(dstFontPath) dstCharMap = dstFont.getCharacterMapping( ) # -> { 2126: [ 'Omega', ...], ...} dstRevCharMap = revCharMap(dstCharMap) # { 'Omega': 2126, ...} srcToDstMap = getGlyphNameDifferenceMap(srcCharMap, dstCharMap, dstRevCharMap) stats = Stats() groups, glyphToGroups = fixupGroups(dstFontPath, dstRevCharMap, srcToDstMap, dryRun, stats) fixupKerning(dstFontPath, dstRevCharMap, srcToDstMap, groups, glyphToGroups, dryRun, stats) # stats if args.saveStatsPath or not args.noStats: if not args.noStats: print('stats for %s:' % dstFontPath) print(' Deleted %d groups and %d glyphs.' % (len(stats.removedGroups), len(stats.removedGlyphs))) print(' Renamed %d glyphs.' % len(stats.renamedGlyphs)) print(' Simplified %d kerning pairs.' % len(stats.simplifiedKerningPairs)) if args.saveStatsPath: statsObj = { 'deletedGroups': stats.removedGroups, 'deletedGlyphs': stats.removedGlyphs, 'simplifiedKerningPairs': stats.simplifiedKerningPairs, 'renamedGlyphs': stats.renamedGlyphs, } f = sys.stdout try: if args.saveStatsPath != '-': f = open(args.saveStatsPath, 'w') print('Writing stats to', args.saveStatsPath) json.dump(statsObj, sys.stdout, indent=2, separators=(',', ': ')) finally: if f is not sys.stdout: f.close()
def main(argv=None): argparser = ArgumentParser( description='Remove glyphs from all UFOs in src dir') argparser.add_argument( '-dry', dest='dryRun', action='store_const', const=True, default=False, help='Do not modify anything, but instead just print what would happen.') argparser.add_argument( '-decompose', dest='decompose', action='store_const', const=True, default=False, help='When deleting a glyph which is used as a component by another glyph '+ 'which is not being deleted, instead of refusing to delete the glyph, '+ 'decompose the component instances in other glyphs.') argparser.add_argument( '-ignore-git-state', dest='ignoreGitState', action='store_const', const=True, default=False, help='Skip checking with git if there are changes to the target UFO file.') argparser.add_argument( 'glyphs', metavar='<glyph>', type=str, nargs='+', help='Glyph to remove. '+ 'Can be a glyphname, '+ 'a Unicode code point formatted as "U+<CP>", '+ 'or a Unicode code point range formatted as "U+<CP>-<CP>"') args = argparser.parse_args(argv) global dryRun dryRun = args.dryRun srcDir = os.path.join(BASEDIR, 'src') # check if src font has modifications if not args.ignoreGitState: gitStatus = subprocess.check_output( ['git', '-C', BASEDIR, 'status', '-s', '--', os.path.relpath(os.path.abspath(srcDir), BASEDIR) ], shell=False) gitIsDirty = False gitStatusLines = gitStatus.splitlines() for line in gitStatusLines: if len(line) > 3 and line[:2] != '??': gitIsDirty = True break if gitIsDirty: if len(gitStatusLines) > 5: gitStatusLines = gitStatusLines[:5] + [' ...'] print( ("%s has uncommitted changes. It's strongly recommended to run this "+ "script on an unmodified UFO path so to allow \"undoing\" any changes. "+ "Run with -ignore-git-state to ignore this warning.\n%s") % ( srcDir, '\n'.join(gitStatusLines)), file=sys.stderr) sys.exit(1) # Find UFO fonts fontPaths = glob.glob(os.path.join(srcDir, '*.ufo')) if len(fontPaths) == 0: print('No UFOs found in', srcDir, file=sys.stderr) sys.exit(1) # load fontbuild config config = RawConfigParser(dict_type=OrderedDict) configFilename = os.path.join(srcDir, 'fontbuild.cfg') config.read(configFilename) glyphOrderFile = configFindResFile(config, srcDir, 'glyphorder') diacriticsFile = configFindResFile(config, srcDir, 'diacriticfile') featuresFile = os.path.join(srcDir, 'features.fea') # load AGL and diacritics agl = loadAGL(os.path.join(srcDir, 'glyphlist.txt')) # { 2126: 'Omega', ... } comps = loadGlyphCompositions(diacriticsFile) # { glyphName => (baseName, accentNames, offset) } # find glyphnames to remove that are composed (removal happens later) rmnamesUnion = getGlyphNamesComps(comps, agl, args.glyphs) # find glyphnames to remove from UFOs (and remove them) for fontPath in fontPaths: relFontPath = os.path.relpath(fontPath, BASEDIR) print('Loading glyph data for %s...' % relFontPath) font = OpenFont(fontPath) ucmap = font.getCharacterMapping() # { 2126: [ 'Omega', ...], ...} cnmap = font.getReverseComponentMapping() # { 'A' : ['Aacute', 'Aring'], 'acute' : ['Aacute'] ... } glyphnames = getGlyphNamesFont(font, ucmap, args.glyphs) if len(glyphnames) == 0: print('None of the glyphs requested exist in', relFontPath, file=sys.stderr) print('Preparing to remove %d glyphs — resolving component usage...' % len(glyphnames)) # Check component usage cnConflicts = {} for gname in glyphnames: cnUses = cnmap.get(gname) if cnUses: extCnUses = [n for n in cnUses if n not in glyphnames] if len(extCnUses) > 0: cnConflicts[gname] = extCnUses if len(cnConflicts) > 0: if args.decompose: componentsToDecompose = set() for gname in cnConflicts.keys(): componentsToDecompose.add(gname) for gname, dependants in cnConflicts.iteritems(): print('decomposing %s in %s' % (gname, ', '.join(dependants))) for depname in dependants: decomposeComponentInstances(font, font[depname], componentsToDecompose) else: print( '\nComponent conflicts.\n\n'+ 'Some glyphs to-be deleted are used as components in other glyphs.\n'+ 'You need to either decompose the components, also delete glyphs\n'+ 'using them, or not delete the glyphs at all.\n', file=sys.stderr) for gname, dependants in cnConflicts.iteritems(): print('%s used by %s' % (gname, ', '.join(dependants)), file=sys.stderr) sys.exit(1) # find orphaned pure-components for gname in glyphnames: try: g = font[gname] except: print('no glyph %r in %s' % (gname, relFontPath), file=sys.stderr) sys.exit(1) useCount = 0 for cn in g.components: usedBy = cnmap.get(cn.baseGlyph) if usedBy: usedBy = [name for name in usedBy if name not in glyphnames] if len(usedBy) == 0: cng = font[cn.baseGlyph] if len(cng.unicodes) == 0: print('Note: pure-component %s orphaned' % cn.baseGlyph) # remove glyphs from UFO print('Removing %d glyphs' % len(glyphnames)) libPlistFilename = os.path.join(fontPath, 'lib.plist') libPlist = plistlib.readPlist(libPlistFilename) glyphOrder = libPlist.get('public.glyphOrder') if glyphOrder is not None: v = [name for name in glyphOrder if name not in glyphnames] libPlist['public.glyphOrder'] = v roboSort = libPlist.get('com.typemytype.robofont.sort') if roboSort is not None: for entry in roboSort: if isinstance(entry, dict) and entry.get('type') == 'glyphList': asc = entry.get('ascending') if asc is not None: entry['ascending'] = [name for name in asc if name not in glyphnames] desc = entry.get('descending') if desc is not None: entry['descending'] = [name for name in desc if name not in glyphnames] for gname in glyphnames: font.removeGlyph(gname) rmnamesUnion.add(gname) if not dryRun: print('Writing changes to %s' % relFontPath) font.save() plistlib.writePlist(libPlist, libPlistFilename) else: print('Writing changes to %s (dry run)' % relFontPath) print('Cleaning up kerning') if dryRun: cleanup_kerning.main(['-dry', fontPath]) else: cleanup_kerning.main([fontPath]) # end for fontPath in fontPaths # fontbuild config updateDiacriticsFile(diacriticsFile, rmnamesUnion) updateConfigFile(config, configFilename, rmnamesUnion) featuresChanged = updateFeaturesFile(featuresFile, rmnamesUnion) # TMP for testing fuzzy # rmnamesUnion = set() # featuresChanged = False # with open('_local/rmlog') as f: # for line in f: # line = line.strip() # if len(line): # rmnamesUnion.add(line) print('\n————————————————————————————————————————————————————\n'+ 'Removed %d glyphs:\n %s' % ( len(rmnamesUnion), '\n '.join(sorted(rmnamesUnion)))) print('\n————————————————————————————————————————————————————\n') # find possibly-missed instances print('Fuzzy matches:') fuzzyMatchCount = 0 fuzzyMatchCount += grep(diacriticsFile, rmnamesUnion) fuzzyMatchCount += grep(configFilename, rmnamesUnion) fuzzyMatchCount += grep(featuresFile, rmnamesUnion) for fontPath in fontPaths: fuzzyMatchCount += grep(os.path.join(fontPath, 'lib.plist'), rmnamesUnion) if fuzzyMatchCount == 0: print(' (none)\n') else: print('You may want to look into those ^\n') if featuresChanged: print('You need to manually edit features.\n'+ '- git diff src/features.fea\n'+ '- $EDITOR %s/features.fea\n' % '/features.fea\n- $EDITOR '.join(fontPaths)) print(('You need to re-generate %s via\n'+ '`make src/glyphorder.txt` (or misc/gen-glyphorder.py)' ) % glyphOrderFile) print('\nFinally, you should build the Medium weight and make sure it all '+ 'looks good and that no mixglyph failures occur. E.g. `make Medium -j`')