#!/usr/bin/env python # encoding: utf8 from __future__ import print_function import os, sys, plistlib, json, re from collections import OrderedDict from argparse import ArgumentParser from ConfigParser import RawConfigParser from fontTools import ttLib from robofab.objects.objectsRF import OpenFont # Regex matching "default" glyph names, like "uni2043" and "u01C5" uniNameRe = re.compile(r'^u(?:ni)([0-9A-F]{4,8})$') class PList: def __init__(self, filename): self.filename = filename self.plist = None def load(self): self.plist = plistlib.readPlist(self.filename) def save(self): if self.plist is not None: plistlib.writePlist(self.plist, self.filename) def get(self, k, defaultValue=None): if self.plist is None: self.load() return self.plist.get(k, defaultValue) def __getitem__(self, k): if self.plist is None: self.load() return self.plist[k] def __setitem__(self, k, v): if self.plist is None: self.load() self.plist[k] = v def __delitem__(self, k): if self.plist is None: self.load() del self.plist[k] def parseAGL(filename): # -> { 2126: 'Omega', ... } m = {} with open(filename, 'r') as f: for line in f: # Omega;2126 # dalethatafpatah;05D3 05B2 # higher-level combinations; ignored line = line.strip() if len(line) > 0 and line[0] != '#': name, uc = tuple([c.strip() for c in line.split(';')]) if uc.find(' ') == -1: # it's a 1:1 mapping m[int(uc, 16)] = name return m def revCharMap(ucToNames): # {2126:['Omega','Omegagr']} -> {'Omega':2126, 'Omegagr':2126} # {2126:'Omega'} -> {'Omega':2126} m = {} if len(ucToNames) == 0: return m lists = True for v in ucToNames.itervalues(): lists = not isinstance(v, str) break if lists: for uc, names in ucToNames.iteritems(): for name in names: m[name] = uc else: for uc, name in ucToNames.iteritems(): m[name] = uc return m def loadJSONGlyphOrder(jsonFilename): gol = None if jsonFilename == '-': gol = json.load(sys.stdin) else: with open(jsonFilename, 'r') as f: gol = json.load(f) if not isinstance(gol, list): raise Exception('expected [[string, int|null]') if len(gol) > 0: for v in gol: if not isinstance(v, list): raise Exception('expected [[string, int|null]]') break return gol def loadTTGlyphOrder(font): if isinstance(font, str): font = ttLib.TTFont(font) if not 'cmap' in font: raise Exception('missing cmap table') bestCodeSubTable = None bestCodeSubTableFormat = 0 for st in font['cmap'].tables: if st.platformID == 0: # 0=unicode, 1=mac, 2=(reserved), 3=microsoft if st.format > bestCodeSubTableFormat: bestCodeSubTable = st bestCodeSubTableFormat = st.format ucmap = {} if bestCodeSubTable is not None: for cp, glyphname in bestCodeSubTable.cmap.items(): ucmap[glyphname] = cp gol = [] for name in font.getGlyphOrder(): gol.append((name, ucmap.get(name))) return gol def loadSrcGlyphOrder(jsonFilename, fontFilename): # -> [ ('Omegagreek', 2126|None), ...] if jsonFilename: return loadJSONGlyphOrder(jsonFilename) elif fontFilename: return loadTTGlyphOrder(fontFilename.rstrip('/ ')) return None 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 saveUFOGlyphOrder(libPlist, orderedNames, dryRun): libPlist['public.glyphOrder'] = orderedNames roboSort = libPlist.get('com.typemytype.robofont.sort') if roboSort is not None: # lib['com.typemytype.robofont.sort'] has schema # [ { type: "glyphList", ascending: [glyphname, ...] }, ...] for i in range(len(roboSort)): ent = roboSort[i] if isinstance(ent, dict) and ent.get('type') == 'glyphList': roboSort[i] = {'type':'glyphList', 'ascending':orderedNames} break print('Writing', libPlist.filename) if not dryRun: libPlist.save() def getConfigResFile(config, basedir, name): fn = os.path.join(basedir, config.get("res", name)) if not os.path.isfile(fn): basedir = os.path.dirname(basedir) fn = os.path.join(basedir, config.get("res", name)) if not os.path.isfile(fn): fn = None return fn def main(): argparser = ArgumentParser(description='Rewrite glyph order of UFO fonts') 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( '-src-json', dest='srcJSONFile', metavar='', type=str, help='JSON file to read glyph order from.' + ' Should be a list e.g. [["Omega", 2126], [".notdef", null], ...]') argparser.add_argument( '-src-font', dest='srcFontFile', metavar='', type=str, help='TrueType or OpenType font to read glyph order from.') argparser.add_argument( '-out', dest='outFile', metavar='', type=str, help='Write each name per line to ') argparser.add_argument( 'dstFontsPaths', metavar='', 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.') srcGol = loadSrcGlyphOrder(args.srcJSONFile, args.srcFontFile) if srcGol is None: argparser.error('No source provided (-src-* argument missing)') # Load Adobe Glyph List database srcDir = os.path.dirname(args.dstFontsPaths[0]) config = RawConfigParser(dict_type=OrderedDict) config.read(os.path.join(srcDir, 'fontbuild.cfg')) aglUcToName = parseAGL(getConfigResFile(config, srcDir, 'agl_glyphlistfile')) aglNameToUc = revCharMap(aglUcToName) glyphorderUnion = OrderedDict() for dstFontPath in args.dstFontsPaths: glyphOrder, ucToNames, nameToUc, libPlist = loadUFOGlyphNames(dstFontPath) newGol = OrderedDict() for name, uc in srcGol: if uc is None: # if there's no unicode associated, derive from name if possible m = uniNameRe.match(name) if m: try: uc = int(m.group(1), 16) except: pass if uc is None: uc = aglNameToUc.get(name) # has same glyph mapped to same unicode names = ucToNames.get(uc) if names is not None: for name in names: # print('U %s U+%04X' % (name, uc)) newGol[name] = uc continue # has same name in dst? uc2 = glyphOrder.get(name) if uc2 is not None: # print('N %s U+%04X' % (name, uc2)) newGol[name] = uc2 continue # Try AGL[uc] -> name == name if uc is not None: name2 = aglUcToName.get(uc) if name2 is not None: uc2 = glyphOrder.get(name2) if uc2 is not None: # print('A %s U+%04X' % (name2, uc2)) newGol[name2] = uc2 continue # else: ignore glyph name in srcGol not found in target # if uc is None: # print('x %s -' % name) # else: # print('x %s U+%04X' % (name, uc)) # add remaining glyphs from original glyph order for name, uc in glyphOrder.iteritems(): if name not in newGol: # print('E %s U+%04X' % (name, uc)) newGol[name] = uc orderedNames = [] for name in newGol.iterkeys(): orderedNames.append(name) glyphorderUnion[name] = True saveUFOGlyphOrder(libPlist, orderedNames, dryRun) if args.outFile: print('Write', args.outFile) glyphorderUnionNames = glyphorderUnion.keys() if not dryRun: with open(args.outFile, 'w') as f: f.write('\n'.join(glyphorderUnionNames) + '\n') if __name__ == '__main__': main()