#!/usr/bin/env python3 __doc__ = """Copy glyphs from one UFO to another""" __url__ = 'https://github.com/silnrsi/pysilfont' __copyright__ = 'Copyright (c) 2018 SIL International (https://www.sil.org)' __license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)' __author__ = 'Bob Hallissy' from xml.etree import ElementTree as ET from silfont.core import execute from silfont.ufo import makeFileName, Uglif import re argspec = [ ('ifont',{'help': 'Input font file'}, {'type': 'infont'}), ('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont'}), ('-s','--source',{'help': 'Font to get glyphs from'}, {'type': 'infont'}), ('-i','--input',{'help': 'Input csv file'}, {'type': 'incsv', 'def': 'glyphlist.csv'}), ('-f','--force',{'help' : 'Overwrite existing glyphs in the font', 'action' : 'store_true'}, {}), ('-l','--log',{'help': 'Set log file name'}, {'type': 'outfile', 'def': '_copy.log'}), ('-n', '--name', {'help': 'Include glyph named name', 'action': 'append'}, {}), ('--rename',{'help' : 'Rename glyphs to names in this column'}, {}), ('--unicode', {'help': 'Re-encode glyphs to USVs in this column'}, {}), ('--scale',{'type' : float, 'help' : 'Scale glyphs by this factor'}, {}) ] class Glyph: """details about a glyph we have, or need to, copy; mostly just for syntactic sugar""" # Glyphs that are used *only* as component glyphs may have to be renamed if there already exists a glyph # by the same name in the target font. we compute a new name by appending .copy1, .copy2, etc until we get a # unique name. We keep track of the mapping from source font glyphname to target font glyphname using a dictionary. # For ease of use, glyphs named by the input file (which won't have their names changed, see --force) will also # be added to this dictionary because they can also be used as components. nameMap = dict() def __init__(self, oldname, newname="", psname="", dusv=None): self.oldname = oldname self.newname = newname or oldname self.psname = psname or None self.dusv = dusv or None # Keep track of old-to-new name mapping Glyph.nameMap[oldname] = self.newname # Mapping from decimal USV to glyphname in target font dusv2gname = None # RE for parsing glyph names and peeling off the .copyX if present in order to search for a unique name to use: gcopyRE = re.compile(r'(^.+?)(?:\.copy(\d+))?$') def copyglyph(sfont, tfont, g, args): """copy glyph from source font to target font""" # Generally, 't' variables are target, 's' are source. E.g., tfont is target font. global dusv2gname if not dusv2gname: # Create mappings to find existing glyph name from decimal usv: dusv2gname = {int(unicode.hex, 16): gname for gname in tfont.deflayer for unicode in tfont.deflayer[gname]['unicode']} # NB: Assumes font is well-formed and has at most one glyph with any particular Unicode value. # The layer where we want the copied glyph: tlayer = tfont.deflayer # if new name present in target layer, delete it. if g.newname in tlayer: # New name is already in font: tfont.logger.log("Replacing glyph '{0}' with new glyph".format(g.newname), "V") glyph = tlayer[g.newname] # While here, remove from our mapping any Unicodes from the old glyph: for unicode in glyph["unicode"]: dusv = int(unicode.hex, 16) if dusv in dusv2gname: del dusv2gname[dusv] # Ok, remove old glyph from the layer tlayer.delGlyph(g.newname) else: # New name is not in the font: tfont.logger.log("Adding glyph '{0}'".format(g.newname), "V") # Create new glyph glyph = Uglif(layer = tlayer) # Set etree from source glyph glyph.etree = ET.fromstring(sfont.deflayer[g.oldname].inxmlstr) glyph.process_etree() # Rename the glyph if needed if glyph.name != g.newname: # Use super to bypass normal glyph renaming logic since it isn't yet in the layer super(Uglif, glyph).__setattr__("name", g.newname) # add new glyph to layer: tlayer.addGlyph(glyph) tfont.logger.log("Added glyph '{0}'".format(g.newname), "V") # todo: set psname if requested; adjusting any other glyphs in the font as needed. # Adjust encoding of new glyph if args.unicode: # First remove any encodings the copied glyph had in the source font: for i in range(len(glyph['unicode']) - 1, -1, -1): glyph.remove('unicode', index=i) if g.dusv: # we want this glyph to be encoded. # First remove this Unicode from any other glyph in the target font if g.dusv in dusv2gname: oglyph = tlayer[dusv2gname[g.dusv]] for unicode in oglyph["unicode"]: if int(unicode.hex,16) == g.dusv: oglyph.remove("unicode", object=unicode) tfont.logger.log("Removed USV {0:04X} from existing glyph '{1}'".format(g.dusv,dusv2gname[g.dusv]), "V") break # Now add and record it: glyph.add("unicode", {"hex": '{:04X}'.format(g.dusv)}) dusv2gname[g.dusv] = g.newname tfont.logger.log("Added USV {0:04X} to glyph '{1}'".format(g.dusv, g.newname), "V") # Scale glyph if desired if args.scale: for e in glyph.etree.iter(): for attr in ('width', 'height', 'x', 'y', 'xOffset', 'yOffset'): if attr in e.attrib: e.set(attr, str(int(float(e.get(attr))* args.scale))) # Look through components, adjusting names and finding out if we need to copy some. for component in glyph.etree.findall('./outline/component[@base]'): oldname = component.get('base') # Note: the following will cause recursion: component.set('base', copyComponent(sfont, tfont, oldname ,args)) def copyComponent(sfont, tfont, oldname, args): """copy component glyph if not already copied; make sure name and psname are unique; return its new name""" if oldname in Glyph.nameMap: # already copied return Glyph.nameMap[oldname] # if oldname is already in the target font, make up a new name by adding ".copy1", incrementing as necessary if oldname not in tfont.deflayer: newname = oldname tfont.logger.log("Copying component '{0}' with existing name".format(oldname), "V") else: x = gcopyRE.match(oldname) base = x.group(1) try: i = int(x.group(2)) except: i = 1 while "{0}.copy{1}".format(base,i) in tfont.deflayer: i += 1 newname = "{0}.copy{1}".format(base,i) tfont.logger.log("Copying component '{0}' with new name '{1}'".format(oldname, newname), "V") # todo: something similar to above but for psname # Now copy the glyph, giving it new name if needed. copyglyph(sfont, tfont, Glyph(oldname, newname), args) return newname def doit(args) : sfont = args.source # source UFO tfont = args.ifont # target UFO incsv = args.input logger = args.logger # Get headings from csvfile: fl = incsv.firstline if fl is None: logger.log("Empty input file", "S") numfields = len(fl) incsv.numfields = numfields # defaults for single column csv (no headers): nameCol = 0 renameCol = None psCol = None usvCol = None if numfields > 1 or args.rename or args.unicode: # required columns: try: nameCol = fl.index('glyph_name'); if args.rename: renameCol = fl.index(args.rename); if args.unicode: usvCol = fl.index(args.unicode); except ValueError as e: logger.log('Missing csv input field: ' + e.message, 'S') except Exception as e: logger.log('Error reading csv input field: ' + e.message, 'S') # optional columns psCol = fl.index('ps_name') if 'ps_name' in fl else None if 'glyph_name' in fl: next(incsv.reader, None) # Skip first line with headers in # list of glyphs to copy glist = list() def checkname(oldname, newname = None): if not newname: newname = oldname if oldname in Glyph.nameMap: logger.log("Line {0}: Glyph '{1}' specified more than once; only the first kept".format(incsv.line_num, oldname), 'W') elif oldname not in sfont.deflayer: logger.log("Line {0}: Glyph '{1}' is not in source font; skipping".format(incsv.line_num, oldname),"W") elif newname in tfont.deflayer and not args.force: logger.log("Line {0}: Glyph '{1}' already present; skipping".format(incsv.line_num, newname), "W") else: return True return False # glyphs specified in csv file for r in incsv: oldname = r[nameCol] newname = r[renameCol] if args.rename else oldname psname = r[psCol] if psCol is not None else None if args.unicode and r[usvCol]: # validate USV: try: dusv = int(r[usvCol],16) except ValueError: logger.log("Line {0}: Invalid USV '{1}'; ignored.".format(incsv.line_num, r[usvCol]), "W") dusv = None else: dusv = None if checkname(oldname, newname): glist.append(Glyph(oldname, newname, psname, dusv)) # glyphs specified on the command line if args.name: for gname in args.name: if checkname(gname): glist.append(Glyph(gname)) # Ok, now process them: if len(glist) == 0: logger.log("No glyphs to copy", "S") # copy glyphs by name while len(glist) : g = glist.pop(0) tfont.logger.log("Copying source glyph '{0}' as '{1}'{2}".format(g.oldname, g.newname, " (U+{0:04X})".format(g.dusv) if g.dusv else ""), "I") copyglyph(sfont, tfont, g, args) return tfont def cmd() : execute("UFO",doit,argspec) if __name__ == "__main__": cmd()