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`')
def main(): argparser = argparse.ArgumentParser(description='Enrich UFO 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( '-list-missing', dest='listMissing', action='store_const', const=True, default=False, help= 'List glyphs with unicodes found in source files but missing in any of the target UFOs.' ) argparser.add_argument( '-list-unnamed', dest='listUnnamed', action='store_const', const=True, default=False, help= "List glyphs with unicodes in target UFOs that don't have symbolic names." ) argparser.add_argument( '-backfill-agl', dest='backfillWithAgl', action='store_const', const=True, default=False, help= "Use glyphnames from Adobe Glyph List for any glyphs that no names in any of" + " the input font files") argparser.add_argument( '-src', dest='srcFonts', metavar='<fontfile>', type=str, nargs='*', help='TrueType, OpenType or UFO fonts to gather glyph info from. ' + 'Names found in earlier-listed fonts are prioritized over later listings.' ) argparser.add_argument('dstFonts', metavar='<ufofile>', type=str, nargs='+', help='UFO fonts to update') args = argparser.parse_args() # Load UFO fonts dstFonts = [] dstFontPaths = {} # keyed by RFont object srcDir = None for fn in args.dstFonts: fn = fn.rstrip('/') font = OpenFont(fn) dstFonts.append(font) dstFontPaths[font] = fn srcDir2 = os.path.dirname(fn) if srcDir is None: srcDir = srcDir2 elif srcDir != srcDir2: raise Exception('All <ufofile>s must be rooted in same directory') # load fontbuild configuration 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') glyphOrder = readGlyphOrderFile(glyphOrderFile) fallbackGlyphNames = {} # { 2126: 'Omega', ... } if args.backfillWithAgl: fallbackGlyphNames = parseAGL( configFindResFile(config, srcDir, 'agl_glyphlistfile')) # find glyph names uc2names, extraUc2names, name2ucsv = buildGlyphNames( dstFonts, args.srcFonts, glyphOrder, fallbackGlyphNames) # Note: name2ucsv has same order as parameters to buildGlyphNames if args.listMissing: print('# Missing glyphs: (found in -src but not in any <ufofile>)') for uc, names in extraUc2names.iteritems(): print('U+%04X\t%s' % (uc, ', '.join(names))) return elif args.listUnnamed: print('# Unnamed glyphs:') unnamed = set() for name in glyphOrder: if len(name) > 7 and name.startswith('uni'): unnamed.add(name) for gl in name2ucsv[:len(dstFonts)]: for name, ucs in gl.iteritems(): for uc in ucs: if isDefaultGlyphNameForUnicode(name, uc): unnamed.add(name) break for name in unnamed: print(name) return printDry = lambda *args: print(*args) if args.dryRun: printDry = lambda *args: print('[dry-run]', *args) newNames = {} renameGlyphsQueue = {} # keyed by RFont object for font in dstFonts: renameGlyphsQueue[font] = {} for uc, names in uc2names.iteritems(): if len(names) < 2: continue dstGlyphName = names[0] if isDefaultGlyphNameForUnicode(dstGlyphName, uc): newGlyphName = getFirstNonDefaultGlyphName(uc, names[1:]) # if newGlyphName is None: # # if we found no symbolic name, check in fallback list # newGlyphName = fallbackGlyphNames.get(uc) # if newGlyphName is not None: # printDry('Using fallback %s' % newGlyphName) if newGlyphName is not None: printDry('Rename %s -> %s' % (dstGlyphName, newGlyphName)) for font in dstFonts: if dstGlyphName in font: renameGlyphsQueue[font][dstGlyphName] = newGlyphName newNames[dstGlyphName] = newGlyphName if len(newNames) == 0: printDry('No changes') return # rename component instances for font in dstFonts: componentMap = font.getReverseComponentMapping() for currName, newName in renameGlyphsQueue[font].iteritems(): for depName in componentMap.get(currName, []): depG = font[depName] for c in depG.components: if c.baseGlyph == currName: c.baseGlyph = newName c.setChanged() # rename glyphs for font in dstFonts: for currName, newName in renameGlyphsQueue[font].iteritems(): font[currName].name = newName # save fonts and update font data for font in dstFonts: fontPath = dstFontPaths[font] printDry('Saving %d glyphs in %s' % (len(newNames), fontPath)) if not args.dryRun: font.save() renameUFODetails(font, fontPath, newNames, dryRun=args.dryRun, print=printDry) # update resource files renameGlyphOrderFile(glyphOrderFile, newNames, dryRun=args.dryRun, print=printDry) renameDiacriticsFile(diacriticsFile, newNames, dryRun=args.dryRun, print=printDry) renameConfigFile(config, configFilename, newNames, dryRun=args.dryRun, print=printDry)