def main(): argparser = ArgumentParser( description='Generate tabular number glyphs from regular number glyphs') 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( 'fontPaths', metavar='<ufofile>', type=str, nargs='+', help='UFO fonts') args = argparser.parse_args() dryRun = args.dryRun # Strip trailing slashes from font paths and iterate for fontPath in [s.rstrip('/ ') for s in args.fontPaths]: fontName = os.path.basename(fontPath) font = OpenFont(fontPath) # Find widest glyph width = 0 for name in numNames: g = font[name] width = max(width, g.width) print('[%s] tnums width:' % fontName, width) # Create tnum glyphs for name in numNames: g = font[name] tnum = font.newGlyph(name + '.tnum') tnum.width = width # calculate component x-offset xoffs = 0 if g.width != width: print('[%s] gen (adjust width)' % fontName, tnum.name) # center shape, ignoring existing margins # xMin, yMin, xMax, yMax = g.box # graphicWidth = xMax - xMin # leftMargin = round((width - graphicWidth) / 2) # xoffs = leftMargin - g.leftMargin # adjust margins widthDelta = width - g.width leftMargin = g.leftMargin + int(floor(widthDelta / 2)) rightMargin = g.rightMargin + int(ceil(widthDelta / 2)) xoffs = leftMargin - g.leftMargin else: print('[%s] gen (same width)' % fontName, tnum.name) tnum.appendComponent(name, (xoffs, 0)) if dryRun: print('[%s] save [dry run]' % fontName) else: print('[%s] save' % fontName) font.save()
def main(): argparser = ArgumentParser( description='Operate on UFO glyf "lib" properties') 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( '-m', dest='renameProps', metavar='<currentName>=<newName>[,...]', type=str, help='Rename properties') argparser.add_argument( 'fontPaths', metavar='<ufofile>', type=str, nargs='+', help='UFO fonts to update') args = argparser.parse_args() dryRun = args.dryRun renames = [] if args.renameProps: renames = [tuple(s.split('=')) for s in args.renameProps.split(',')] # TODO: verify data structure print('renaming properties:') for rename in renames: print(' %r => %r' % rename) # Strip trailing slashes from font paths and iterate for fontPath in [s.rstrip('/ ') for s in args.fontPaths]: font = OpenFont(fontPath) if len(renames): print('Renaming properties in %s' % fontPath) renameProps(font, renames) if dryRun: print('Saving changes to %s (dry run)' % fontPath) if not dryRun: print('Saving changes to %s' % fontPath) font.save()
def main(): argparser = ArgumentParser( description='Set robofont color marks on glyphs based on unicode categories') 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( '-ucd', dest='ucdFile', metavar='<file>', type=str, help='UnicodeData.txt file from http://www.unicode.org/') argparser.add_argument( 'fontPaths', metavar='<ufofile>', type=str, nargs='+', help='UFO fonts to update') args = argparser.parse_args() dryRun = args.dryRun markLibKey = 'com.typemytype.robofont.mark' ucd = {} if args.ucdFile: ucd = parseUnicodeDataFile(args.ucdFile) for fontPath in args.fontPaths: font = OpenFont(fontPath) for g in font: rgba = colorForGlyph(g.name, g.unicodes, ucd) if rgba is None: if markLibKey in g.lib: del g.lib[markLibKey] else: g.lib[markLibKey] = [float(n) for n in rgba] print('Write', fontPath) if not dryRun: font.save()
font = OpenFont(ufopath) effectiveAscender = max(font.info.ascender, font.info.unitsPerEm) svgdir = args.svgdir if len(svgdir) == 0: svgdir = os.path.join( os.path.dirname(ufopath), 'svg', font.info.familyName, font.info.styleName ) print 'svgsync sync %s (%s)' % (font.info.familyName, font.info.styleName) createSVGs = len(args.glyphs) > 0 glyphnames = args.glyphs if len(args.glyphs) else font.keys() modifiedGlifFiles = [] for glyphname in glyphnames: glyphFile, mtime = syncGlyph(glyphname, createSVG=createSVGs) if glyphFile is not None: modifiedGlifFiles.append((glyphFile, mtime)) if len(modifiedGlifFiles) > 0: font.save() for glyphFile, mtime in modifiedGlifFiles: os.utime(glyphFile, (mtime, mtime)) print 'svgsync write', glyphFile
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`')
""" Remove overlap on all glyphs in a .ufo font. This script sis more than a little silly, but it demonstrates how objectsRF and objectsFL can work hand in hand. """ from robofab.objects.objectsRF import OpenFont from robofab.objects.objectsFL import NewFont from robofab.interface.all.dialogs import ProgressBar ufoFont = OpenFont(note="Select a .ufo") if ufoFont: bar = ProgressBar('Removing Overlap...', len(ufoFont)) flFont = NewFont() flGlyph = flFont.newGlyph('OverlapRemover') for ufoGlyph in ufoFont: flPen = flGlyph.getPointPen() ufoGlyph.drawPoints(flPen) flGlyph.removeOverlap() ufoPen = ufoGlyph.getPointPen() ufoGlyph.clear() flGlyph.drawPoints(ufoPen) flGlyph.clear() bar.tick() flFont.close(save=0) bar.close() ufoFont.save(doProgress=True)
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)
class WriteUFOFormatVersion2TestCase(unittest.TestCase): def setUpFont(self): self.dstDir = tempfile.mktemp() os.mkdir(self.dstDir) self.font = OpenFont(ufoPath2) self.font.save(self.dstDir) def tearDownFont(self): shutil.rmtree(self.dstDir) def compareToUFO(self): readerExpected = UFOReader(ufoPath2) readerWritten = UFOReader(self.dstDir) results = {} # info matches = True expectedPath = os.path.join(ufoPath2, "fontinfo.plist") writtenPath = os.path.join(self.dstDir, "fontinfo.plist") if not os.path.exists(writtenPath): matches = False else: expected = readPlist(expectedPath) written = readPlist(writtenPath) for attr, expectedValue in expected.items(): if expectedValue != written[attr]: matches = False break results["info"] = matches # kerning matches = True expectedPath = os.path.join(ufoPath2, "kerning.plist") writtenPath = os.path.join(self.dstDir, "kerning.plist") if not os.path.exists(writtenPath): matches = False else: matches = readPlist(expectedPath) == readPlist(writtenPath) results["kerning"] = matches # groups matches = True expectedPath = os.path.join(ufoPath2, "groups.plist") writtenPath = os.path.join(self.dstDir, "groups.plist") if not os.path.exists(writtenPath): matches = False else: matches = readPlist(expectedPath) == readPlist(writtenPath) results["groups"] = matches # features matches = True expectedPath = os.path.join(ufoPath2, "features.fea") writtenPath = os.path.join(self.dstDir, "features.fea") if not os.path.exists(writtenPath): matches = False else: f = open(expectedPath, "r") expectedText = f.read() f.close() f = open(writtenPath, "r") writtenText = f.read() f.close() # FontLab likes to add lines to the features, so skip blank lines. expectedText = [line for line in expectedText.splitlines() if line] writtenText = [line for line in writtenText.splitlines() if line] matches = "\n".join(expectedText) == "\n".join(writtenText) results["features"] = matches # lib matches = True expectedPath = os.path.join(ufoPath2, "lib.plist") writtenPath = os.path.join(self.dstDir, "lib.plist") if not os.path.exists(writtenPath): matches = False else: writtenLib = readPlist(writtenPath) matches = readPlist(expectedPath) == writtenLib results["lib"] = matches return results def testFull(self): self.setUpFont() otherResults = self.compareToUFO() self.assertEqual(otherResults["info"], True) self.assertEqual(otherResults["kerning"], True) self.assertEqual(otherResults["groups"], True) self.assertEqual(otherResults["features"], True) self.assertEqual(otherResults["lib"], True) self.tearDownFont()
class WriteUFOFormatVersion1TestCase(unittest.TestCase): def setUpFont(self): self.dstDir = tempfile.mktemp() os.mkdir(self.dstDir) self.font = OpenFont(ufoPath2) self.font.save(self.dstDir, formatVersion=1) def tearDownFont(self): shutil.rmtree(self.dstDir) def compareToUFO(self): readerExpected = UFOReader(ufoPath1) readerWritten = UFOReader(self.dstDir) results = {} # info matches = True expectedPath = os.path.join(ufoPath1, "fontinfo.plist") writtenPath = os.path.join(self.dstDir, "fontinfo.plist") if not os.path.exists(writtenPath): matches = False else: expected = readPlist(expectedPath) written = readPlist(writtenPath) for attr, expectedValue in expected.items(): if expectedValue != written.get(attr): matches = False break results["info"] = matches # kerning matches = True expectedPath = os.path.join(ufoPath1, "kerning.plist") writtenPath = os.path.join(self.dstDir, "kerning.plist") if not os.path.exists(writtenPath): matches = False else: matches = readPlist(expectedPath) == readPlist(writtenPath) results["kerning"] = matches # groups matches = True expectedPath = os.path.join(ufoPath1, "groups.plist") writtenPath = os.path.join(self.dstDir, "groups.plist") if not os.path.exists(writtenPath): matches = False else: matches = readPlist(expectedPath) == readPlist(writtenPath) results["groups"] = matches # features matches = True expectedPath = os.path.join(ufoPath1, "features.fea") writtenPath = os.path.join(self.dstDir, "features.fea") if os.path.exists(writtenPath): matches = False results["features"] = matches # lib matches = True expectedPath = os.path.join(ufoPath1, "lib.plist") writtenPath = os.path.join(self.dstDir, "lib.plist") if not os.path.exists(writtenPath): matches = False else: writtenLib = readPlist(writtenPath) matches = readPlist(expectedPath) == writtenLib results["lib"] = matches return results def testFull(self): self.setUpFont() otherResults = self.compareToUFO() self.assertEqual(otherResults["info"], True) self.assertEqual(otherResults["kerning"], True) self.assertEqual(otherResults["groups"], True) self.assertEqual(otherResults["features"], True) self.assertEqual(otherResults["lib"], True) self.tearDownFont()