From da875fcb62c801b8d19b3d4d984ad963574fb356 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 2 Mar 2023 21:01:10 +0100 Subject: Adding upstream version 1.6.0. Signed-off-by: Daniel Baumann --- lib/silfont/scripts/psfcopyglyphs.py | 243 +++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 lib/silfont/scripts/psfcopyglyphs.py (limited to 'lib/silfont/scripts/psfcopyglyphs.py') diff --git a/lib/silfont/scripts/psfcopyglyphs.py b/lib/silfont/scripts/psfcopyglyphs.py new file mode 100644 index 0000000..fe37bf7 --- /dev/null +++ b/lib/silfont/scripts/psfcopyglyphs.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python +__doc__ = """Copy glyphs from one UFO to another""" +__url__ = 'http://github.com/silnrsi/pysilfont' +__copyright__ = 'Copyright (c) 2018 SIL International (http://www.sil.org)' +__license__ = 'Released under the MIT License (http://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 exsting 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() -- cgit v1.2.3