summaryrefslogtreecommitdiffstats
path: root/lib/silfont/scripts/psfrenameglyphs.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/silfont/scripts/psfrenameglyphs.py')
-rw-r--r--lib/silfont/scripts/psfrenameglyphs.py587
1 files changed, 587 insertions, 0 deletions
diff --git a/lib/silfont/scripts/psfrenameglyphs.py b/lib/silfont/scripts/psfrenameglyphs.py
new file mode 100644
index 0000000..ff8ef73
--- /dev/null
+++ b/lib/silfont/scripts/psfrenameglyphs.py
@@ -0,0 +1,587 @@
+#!/usr/bin/env python
+__doc__ = '''Assign new working names to glyphs based on csv input file
+- csv format oldname,newname'''
+__url__ = 'http://github.com/silnrsi/pysilfont'
+__copyright__ = 'Copyright (c) 2017 SIL International (http://www.sil.org)'
+__license__ = 'Released under the MIT License (http://opensource.org/licenses/MIT)'
+__author__ = 'Bob Hallissy'
+
+from silfont.core import execute
+from xml.etree import ElementTree as ET
+import re
+import os
+from glob import glob
+
+argspec = [
+ ('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
+ ('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont'}),
+ ('-c', '--classfile', {'help': 'Classes file'}, {}),
+ ('-i','--input',{'help': 'Input csv file'}, {'type': 'incsv', 'def': 'namemap.csv'}),
+ ('--mergecomps',{'help': 'turn on component merge', 'action': 'store_true', 'default': False},{}),
+ ('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': '_renameglyphs.log'})]
+
+csvmap = "" # Variable used globally
+
+def doit(args) :
+ global csvmap, ksetsbymember
+ font = args.ifont
+ incsv = args.input
+ logger = args.logger
+ mergemode = args.mergecomps
+
+ failerrors = 0 # Keep count of errors that should cause the script to fail
+ csvmap = {} # List of all real maps in incsv, so excluding headers, blank lines, comments and identity maps
+ nameMap = {} # remember all glyphs actually renamed
+ kerngroupsrenamed = {} # List of all kern groups actually renamed
+
+ # List of secondary layers (ie layers other than the default)
+ secondarylayers = [x for x in font.layers if x.layername != "public.default"]
+
+ # Obtain lib.plist glyph order(s) and psnames if they exist:
+ publicGlyphOrder = csGlyphOrder = psnames = displayStrings = None
+ if hasattr(font, 'lib'):
+ if 'public.glyphOrder' in font.lib:
+ publicGlyphOrder = font.lib.getval('public.glyphOrder') # This is an array
+ if 'com.schriftgestaltung.glyphOrder' in font.lib:
+ csGlyphOrder = font.lib.getval('com.schriftgestaltung.glyphOrder') # This is an array
+ if 'public.postscriptNames' in font.lib:
+ psnames = font.lib.getval('public.postscriptNames') # This is a dict keyed by glyphnames
+ if 'com.schriftgestaltung.customParameter.GSFont.DisplayStrings' in font.lib:
+ displayStrings = font.lib.getval('com.schriftgestaltung.customParameter.GSFont.DisplayStrings')
+ else:
+ logger.log("no lib.plist found in font", "W")
+
+ # Renaming within the UFO is done in two passes to make sure we can handle circular renames such as:
+ # someglyph.alt = someglyph
+ # someglyph = someglyph.alt
+
+ # Note that the various objects with glyph names are all done independently since
+ # the same glyph names are not necessarily in all structures.
+
+ # First pass: process all records of csv, and for each glyph that is to be renamed:
+ # If the new glyphname is not already present, go ahead and rename it now.
+ # If the new glyph name already exists, rename the glyph to a temporary name
+ # and put relevant details in saveforlater[]
+
+ saveforlaterFont = [] # For the font itself
+ saveforlaterPGO = [] # For public.GlyphOrder
+ saveforlaterCSGO = [] # For GlyphsApp GlyphOrder (com.schriftgestaltung.glyphOrder)
+ saveforlaterPSN = [] # For public.postscriptNames
+ deletelater = [] # Glyphs we'll delete after merging
+
+ for r in incsv:
+ oldname = r[0].strip()
+ newname = r[1].strip()
+ # ignore header row and rows where the newname is blank or a comment marker
+ if oldname == "Name" or oldname.startswith('#') or newname == "" or oldname == newname:
+ continue
+ if len(oldname)==0:
+ logger.log('empty glyph oldname in glyph_data; ignored (newname: %s)' % newname, 'W')
+ continue
+ csvmap[oldname]=newname
+
+ # Handle font first:
+ if oldname not in font.deflayer:
+ logger.log("glyph name not in font: " + oldname , "I")
+ elif newname not in font.deflayer:
+ inseclayers = False
+ for layer in secondarylayers:
+ if newname in layer:
+ logger.log("Glyph %s is already in non-default layers; can't rename %s" % (newname, oldname), "E")
+ failerrors += 1
+ inseclayers = True
+ continue
+ if not inseclayers:
+ # Ok, this case is easy: just rename the glyph in all layers
+ for layer in font.layers:
+ if oldname in layer: layer[oldname].name = newname
+ nameMap[oldname] = newname
+ logger.log("Pass 1 (Font): Renamed %s to %s" % (oldname, newname), "I")
+ elif mergemode:
+ mergeglyphs(font.deflayer[oldname], font.deflayer[newname])
+ for layer in secondarylayers:
+ if oldname in layer:
+ if newname in layer:
+ mergeglyphs(layer[oldname], layer[newname])
+ else:
+ layer[oldname].name = newname
+
+ nameMap[oldname] = newname
+ deletelater.append(oldname)
+ logger.log("Pass 1 (Font): merged %s to %s" % (oldname, newname), "I")
+ else:
+ # newname already in font -- but it might get renamed later in which case this isn't actually a problem.
+ # For now, then, rename glyph to a temporary name and remember it for second pass
+ tempname = gettempname(lambda n : n not in font.deflayer)
+ for layer in font.layers:
+ if oldname in layer:
+ layer[oldname].name = tempname
+ saveforlaterFont.append( (tempname, oldname, newname) )
+
+ # Similar algorithm for public.glyphOrder, if present:
+ if publicGlyphOrder:
+ if oldname not in publicGlyphOrder:
+ logger.log("glyph name not in publicGlyphorder: " + oldname , "I")
+ else:
+ x = publicGlyphOrder.index(oldname)
+ if newname not in publicGlyphOrder:
+ publicGlyphOrder[x] = newname
+ nameMap[oldname] = newname
+ logger.log("Pass 1 (PGO): Renamed %s to %s" % (oldname, newname), "I")
+ elif mergemode:
+ del publicGlyphOrder[x]
+ nameMap[oldname] = newname
+ logger.log("Pass 1 (PGO): Removed %s (now using %s)" % (oldname, newname), "I")
+ else:
+ tempname = gettempname(lambda n : n not in publicGlyphOrder)
+ publicGlyphOrder[x] = tempname
+ saveforlaterPGO.append( (x, oldname, newname) )
+
+ # And for GlyphsApp glyph order, if present:
+ if csGlyphOrder:
+ if oldname not in csGlyphOrder:
+ logger.log("glyph name not in csGlyphorder: " + oldname , "I")
+ else:
+ x = csGlyphOrder.index(oldname)
+ if newname not in csGlyphOrder:
+ csGlyphOrder[x] = newname
+ nameMap[oldname] = newname
+ logger.log("Pass 1 (csGO): Renamed %s to %s" % (oldname, newname), "I")
+ elif mergemode:
+ del csGlyphOrder[x]
+ nameMap[oldname] = newname
+ logger.log("Pass 1 (csGO): Removed %s (now using %s)" % (oldname, newname), "I")
+ else:
+ tempname = gettempname(lambda n : n not in csGlyphOrder)
+ csGlyphOrder[x] = tempname
+ saveforlaterCSGO.append( (x, oldname, newname) )
+
+ # And for psnames
+ if psnames:
+ if oldname not in psnames:
+ logger.log("glyph name not in psnames: " + oldname , "I")
+ elif newname not in psnames:
+ psnames[newname] = psnames.pop(oldname)
+ nameMap[oldname] = newname
+ logger.log("Pass 1 (psn): Renamed %s to %s" % (oldname, newname), "I")
+ elif mergemode:
+ del psnames[oldname]
+ nameMap[oldname] = newname
+ logger.log("Pass 1 (psn): Removed %s (now using %s)" % (oldname, newname), "I")
+ else:
+ tempname = gettempname(lambda n: n not in psnames)
+ psnames[tempname] = psnames.pop(oldname)
+ saveforlaterPSN.append( (tempname, oldname, newname))
+
+ # Second pass: now we can reprocess those things we saved for later:
+ # If the new glyphname is no longer present, we can complete the renaming
+ # Otherwise we've got a fatal error
+
+ for j in saveforlaterFont:
+ tempname, oldname, newname = j
+ if newname in font.deflayer: # Only need to check deflayer, since (if present) it would have been renamed in all
+ # Ok, this really is a problem
+ logger.log("Glyph %s already in font; can't rename %s" % (newname, oldname), "E")
+ failerrors += 1
+ else:
+ for layer in font.layers:
+ if tempname in layer:
+ layer[tempname].name = newname
+ nameMap[oldname] = newname
+ logger.log("Pass 2 (Font): Renamed %s to %s" % (oldname, newname), "I")
+
+ for j in saveforlaterPGO:
+ x, oldname, newname = j
+ if newname in publicGlyphOrder:
+ # Ok, this really is a problem
+ logger.log("Glyph %s already in public.GlyphOrder; can't rename %s" % (newname, oldname), "E")
+ failerrors += 1
+ else:
+ publicGlyphOrder[x] = newname
+ nameMap[oldname] = newname
+ logger.log("Pass 2 (PGO): Renamed %s to %s" % (oldname, newname), "I")
+
+ for j in saveforlaterCSGO:
+ x, oldname, newname = j
+ if newname in csGlyphOrder:
+ # Ok, this really is a problem
+ logger.log("Glyph %s already in com.schriftgestaltung.glyphOrder; can't rename %s" % (newname, oldname), "E")
+ failerrors += 1
+ else:
+ csGlyphOrder[x] = newname
+ nameMap[oldname] = newname
+ logger.log("Pass 2 (csGO): Renamed %s to %s" % (oldname, newname), "I")
+
+ for tempname, oldname, newname in saveforlaterPSN:
+ if newname in psnames:
+ # Ok, this really is a problem
+ logger.log("Glyph %s already in public.postscriptNames; can't rename %s" % (newname, oldname), "E")
+ failerrors += 1
+ else:
+ psnames[newname] = psnames.pop(tempname)
+ nameMap[oldname] = newname
+ logger.log("Pass 2 (psn): Renamed %s to %s" % (oldname, newname), "I")
+
+ # Rebuild font structures from the modified lists we have:
+
+ # Rebuild glyph order elements:
+ if publicGlyphOrder:
+ array = ET.Element("array")
+ for name in publicGlyphOrder:
+ ET.SubElement(array, "string").text = name
+ font.lib.setelem("public.glyphOrder", array)
+
+ if csGlyphOrder:
+ array = ET.Element("array")
+ for name in csGlyphOrder:
+ ET.SubElement(array, "string").text = name
+ font.lib.setelem("com.schriftgestaltung.glyphOrder", array)
+
+ # Rebuild postscriptNames:
+ if psnames:
+ dict = ET.Element("dict")
+ for n in psnames:
+ ET.SubElement(dict, "key").text = n
+ ET.SubElement(dict, "string").text = psnames[n]
+ font.lib.setelem("public.postscriptNames", dict)
+
+ # Iterate over all glyphs, and fix up any components that reference renamed glyphs
+ for layer in font.layers:
+ for name in layer:
+ glyph = layer[name]
+ for component in glyph.etree.findall('./outline/component[@base]'):
+ oldname = component.get('base')
+ if oldname in nameMap:
+ component.set('base', nameMap[oldname])
+ logger.log(f'renamed component base {oldname} to {component.get("base")} in glyph {name} layer {layer.layername}', 'I')
+ lib = glyph['lib']
+ if lib:
+ if 'com.schriftgestaltung.Glyphs.ComponentInfo' in lib:
+ cielem = lib['com.schriftgestaltung.Glyphs.ComponentInfo'][1]
+ for component in cielem:
+ for i in range(0,len(component),2):
+ if component[i].text == 'name':
+ oldname = component[i+1].text
+ if oldname in nameMap:
+ component[i+1].text = nameMap[oldname]
+ logger.log(f'renamed component info {oldname} to {nameMap[oldname]} in glyph {name} layer {layer.layername}', 'I')
+
+ # Delete anything we no longer need:
+ for name in deletelater:
+ for layer in font.layers:
+ if name in layer: layer.delGlyph(name)
+ logger.log("glyph %s removed" % name, "I")
+
+ # Other structures with glyphs in are handled by looping round the structures replacing glyphs rather than
+ # looping round incsv
+
+ # Update Display Strings
+
+ if displayStrings:
+ changed = False
+ glyphRE = re.compile(r'/([a-zA-Z0-9_.-]+)') # regex to match / followed by a glyph name
+ for i, dispstr in enumerate(displayStrings): # Passing the glyphSub function to .sub() causes it to
+ displayStrings[i] = glyphRE.sub(glyphsub, dispstr) # every non-overlapping occurrence of pattern
+ if displayStrings[i] != dispstr:
+ changed = True
+ if changed:
+ array = ET.Element("array")
+ for dispstr in displayStrings:
+ ET.SubElement(array, "string").text = dispstr
+ font.lib.setelem('com.schriftgestaltung.customParameter.GSFont.DisplayStrings', array)
+ logger.log("com.schriftgestaltung.customParameter.GSFont.DisplayStrings updated", "I")
+
+ # Process groups.plist and kerning.plist
+ # group names in the form public.kern[1|2].<glyph name> will automatically be renamed if the glyph name is in the csvmap
+ #
+ groups = kerning = None
+ kgroupprefixes = {"public.kern1.": 1, "public.kern2.": 2}
+
+ if "groups" in font.__dict__: groups = font.groups
+ if "kerning" in font.__dict__: kerning = font.kerning
+
+ if (groups or kerning) and mergemode:
+ logger.log("Note - Kerning and group data not processed when using mergecomps", "P")
+ elif groups or kerning:
+
+ kgroupsmap = ["", {}, {}] # Dicts of kern1/kern2 group renames. Outside the groups if statement, since also used with kerning.plist
+ if groups:
+ # Analyse existing data, building dict from existing data and building some indexes
+ gdict = {}
+ kgroupsbyglyph = ["", {}, {}] # First entry dummy, so index is 1 or 2 for kern1 and kern2
+ kgroupduplicates = ["", [], []] #
+ for gname in groups:
+ group = groups.getval(gname)
+ gdict[gname] = group
+ kprefix = gname[0:13]
+ if kprefix in kgroupprefixes:
+ ktype = kgroupprefixes[kprefix]
+ for glyph in group:
+ if glyph in kgroupsbyglyph[ktype]:
+ kgroupduplicates[ktype].append(glyph)
+ logger.log("In existing kern groups, %s is in more than one kern%s group" % (glyph, str(ktype)), "E")
+ failerrors += 1
+ else:
+ kgroupsbyglyph[ktype][glyph] = gname
+ # Now process the group data
+ glyphsrenamed = []
+ saveforlaterKgroups = []
+ for gname in list(gdict): # Loop round groups renaming glyphs within groups and kern group names
+ group = gdict[gname]
+
+ # Rename group if kern1 or kern2 group
+ kprefix = gname[:13]
+ if kprefix in kgroupprefixes:
+ ktype = kgroupprefixes[kprefix]
+ ksuffix = gname[13:]
+ if ksuffix in csvmap: # This is a kern group that we should rename
+ newgname = kprefix + csvmap[ksuffix]
+ if newgname in gdict: # Will need to be renamed in second pass
+ tempname = gettempname(lambda n : n not in gdict)
+ gdict[tempname] = gdict.pop(gname)
+ saveforlaterKgroups.append((tempname, gname, newgname))
+ else:
+ gdict[newgname] = gdict.pop(gname)
+ kerngroupsrenamed[gname] = newgname
+ logger.log("Pass 1 (Kern groups): Renamed %s to %s" % (gname, newgname), "I")
+ kgroupsmap[ktype][gname] = newgname
+
+ # Now rename glyphs within the group
+ # - This could lead to duplicate names, but that might be valid for arbitrary groups so not checked
+ # - kern group validity will be checked after all renaming is done
+
+ for (i, glyph) in enumerate(group):
+ if glyph in csvmap:
+ group[i] = csvmap[glyph]
+ if glyph not in glyphsrenamed: glyphsrenamed.append(glyph)
+
+ # Need to report glyphs renamed after the loop, since otherwise could report multiple times
+ for oldname in glyphsrenamed:
+ nameMap[oldname] = csvmap[oldname]
+ logger.log("Glyphs in groups: Renamed %s to %s" % (oldname, csvmap[oldname]), "I")
+
+ # Second pass for renaming kern groups. (All glyph renaming is done in first pass)
+
+ for (tempname, oldgname, newgname) in saveforlaterKgroups:
+ if newgname in gdict: # Can't rename
+ logger.log("Kern group %s already in groups.plist; can't rename %s" % (newgname, oldgname), "E")
+ failerrors += 1
+ else:
+ gdict[newgname] = gdict.pop(tempname)
+ kerngroupsrenamed[oldgname] = newgname
+ logger.log("Pass 2 (Kern groups): Renamed %s to %s" % (oldgname, newgname), "I")
+
+ # Finally check kern groups follow the UFO rules!
+ kgroupsbyglyph = ["", {}, {}] # Reset for new analysis
+ for gname in gdict:
+ group = gdict[gname]
+ kprefix = gname[:13]
+ if kprefix in kgroupprefixes:
+ ktype = kgroupprefixes[kprefix]
+ for glyph in group:
+ if glyph in kgroupsbyglyph[ktype]: # Glyph already in a kern group so we have a duplicate
+ if glyph not in kgroupduplicates[ktype]: # This is a newly-created duplicate so report
+ logger.log("After renaming, %s is in more than one kern%s group" % (glyph, str(ktype)), "E")
+ failerrors += 1
+ kgroupduplicates[ktype].append(glyph)
+ else:
+ kgroupsbyglyph[ktype][glyph] = gname
+
+ # Now need to recreate groups.plist from gdict
+
+ for group in list(groups): groups.remove(group) # Empty existing contents
+ for gname in gdict:
+ elem = ET.Element("array")
+ for glyph in gdict[gname]:
+ ET.SubElement(elem, "string").text = glyph
+ groups.setelem(gname, elem)
+
+ # Now process kerning data
+ if kerning:
+ k1map = kgroupsmap[1]
+ k2map = kgroupsmap[2]
+ kdict = {}
+ for setname in kerning: kdict[setname] = kerning.getval(setname) # Create a working dict from plist
+ saveforlaterKsets = []
+ # First pass on set names
+ for setname in list(kdict): # setname could be a glyph in csvmap or a kern1 group name in k1map
+ if setname in csvmap or setname in k1map:
+ newname = csvmap[setname] if setname in csvmap else k1map[setname]
+ if newname in kdict:
+ tempname = gettempname(lambda n : n not in kdict)
+ kdict[tempname] = kdict.pop(setname)
+ saveforlaterKsets.append((tempname, setname, newname))
+ else:
+ kdict[newname] = kdict.pop(setname)
+ if setname in csvmap: nameMap[setname] = newname # Change to kern set name will have been logged previously
+ logger.log("Pass 1 (Kern sets): Renamed %s to %s" % (setname, newname), "I")
+
+ # Now do second pass for set names
+ for (tempname, oldname, newname) in saveforlaterKsets:
+ if newname in kdict: # Can't rename
+ logger.log("Kern set %s already in kerning.plist; can't rename %s" % (newname, oldname), "E")
+ failerrors += 1
+ else:
+ kdict[newname] = kdict.pop(tempname)
+ if oldname in csvmap: nameMap[oldname] = newname
+ logger.log("Pass 1 (Kern sets): Renamed %s to %s" % (oldname, newname), "I")
+
+ # Rename kern set members next.
+
+ # Here, since a member could be in more than one set, take different approach to two passes.
+ # - In first pass, rename to a temp (and invalid) name so duplicates are not possible. Name to include
+ # old name for reporting purposes
+ # - In second pass, set to correct new name after checking for duplicates
+
+ # Do first pass for set names
+ tempnames = []
+ for setname in list(kdict):
+ kset = kdict[setname]
+
+ for mname in list(kset): # mname could be a glyph in csvmap or a kern2 group name in k2map
+ if mname in csvmap or mname in k2map:
+ newname = csvmap[mname] if mname in csvmap else k2map[mname]
+ newname = "^" + newname + "^" + mname
+ if newname not in tempnames: tempnames.append(newname)
+ kset[newname] = kset.pop(mname)
+
+ # Second pass to change temp names to correct final names
+ # We need an index of which sets each member is in
+ ksetsbymember = {}
+ for setname in kdict:
+ kset = kdict[setname]
+ for member in kset:
+ if member not in ksetsbymember:
+ ksetsbymember[member] = [setname]
+ else:
+ ksetsbymember[member].append(setname)
+ # Now do the renaming
+ for tname in tempnames:
+ (newname, oldname) = tname[1:].split("^")
+ if newname in ksetsbymember: # Can't rename
+ logger.log("Kern set %s already in kerning.plist; can't rename %s" % (newname, oldname), "E")
+ failerrors += 1
+ else:
+ for ksetname in ksetsbymember[tname]:
+ kset = kdict[ksetname]
+ kset[newname] = kset.pop(tname)
+ ksetsbymember[newname] = ksetsbymember.pop(tname)
+ if tname in csvmap: nameMap[oldname] = newname
+ logger.log("Kern set members: Renamed %s to %s" % (oldname, newname), "I")
+
+ # Now need to recreate kerning.plist from kdict
+ for kset in list(kerning): kerning.remove(kset) # Empty existing contents
+ for kset in kdict:
+ elem = ET.Element("dict")
+ for member in kdict[kset]:
+ ET.SubElement(elem, "key").text = member
+ ET.SubElement(elem, "integer").text = str(kdict[kset][member])
+ kerning.setelem(kset, elem)
+
+ if failerrors:
+ logger.log(str(failerrors) + " issues detected - see errors reported above", "S")
+
+ logger.log("%d glyphs renamed in UFO" % (len(nameMap)), "P")
+ if kerngroupsrenamed: logger.log("%d kern groups renamed in UFO" % (len(kerngroupsrenamed)), "P")
+
+ # If a classfile was provided, change names within it also
+ #
+ if args.classfile:
+
+ logger.log("Processing classfile {}".format(args.classfile), "P")
+
+ # In order to preserve comments we use our own TreeBuilder
+ class MyTreeBuilder(ET.TreeBuilder):
+ def comment(self, data):
+ self.start(ET.Comment, {})
+ self.data(data)
+ self.end(ET.Comment)
+
+ # RE to match separators between glyph names (whitespace):
+ notGlyphnameRE = re.compile(r'(\s+)')
+
+ # Keep a list of glyphnames that were / were not changed
+ changed = set()
+ notChanged = set()
+
+ # Process one token (might be whitespace separator, glyph name, or embedded classname starting with @):
+ def dochange(gname, logErrors = True):
+ if len(gname) == 0 or gname.isspace() or gname not in csvmap or gname.startswith('@'):
+ # No change
+ return gname
+ try:
+ newgname = csvmap[gname]
+ changed.add(gname)
+ return newgname
+ except KeyError:
+ if logErrors: notChanged.add(gname)
+ return gname
+
+ doc = ET.parse(args.classfile, parser=ET.XMLParser(target=MyTreeBuilder()))
+ for e in doc.iter(None):
+ if e.tag in ('class', 'property'):
+ if 'exts' in e.attrib:
+ logger.log("{} '{}' has 'exts' attribute which may need editing".format(e.tag.title(), e.get('name')), "W")
+ # Rather than just split() the text, we'll use re and thus try to preserve whitespace
+ e.text = ''.join([dochange(x) for x in notGlyphnameRE.split(e.text)])
+ elif e.tag is ET.Comment:
+ # Go ahead and look for glyph names in comment text but don't flag as error
+ e.text = ''.join([dochange(x, False) for x in notGlyphnameRE.split(e.text)])
+ # and process the tail as this might be valid part of class or property
+ e.tail = ''.join([dochange(x) for x in notGlyphnameRE.split(e.tail)])
+
+
+ if len(changed):
+ # Something in classes changed so rewrite it... saving backup
+ (dn,fn) = os.path.split(args.classfile)
+ dn = os.path.join(dn, args.paramsobj.sets['main']['backupdir'])
+ if not os.path.isdir(dn):
+ os.makedirs(dn)
+ # Work out backup name based on existing backups
+ backupname = os.path.join(dn,fn)
+ nums = [int(re.search(r'\.(\d+)~$',n).group(1)) for n in glob(backupname + ".*~")]
+ backupname += ".{}~".format(max(nums) + 1 if nums else 1)
+ logger.log("Backing up input classfile to {}".format(backupname), "P")
+ # Move the original file to backupname
+ os.rename(args.classfile, backupname)
+ # Write the output file
+ doc.write(args.classfile)
+
+ if len(notChanged):
+ logger.log("{} glyphs renamed, {} NOT renamed in {}: {}".format(len(changed), len(notChanged), args.classfile, ' '.join(notChanged)), "W")
+ else:
+ logger.log("All {} glyphs renamed in {}".format(len(changed), args.classfile), "P")
+
+ return font
+
+def mergeglyphs(mergefrom, mergeto): # Merge any "moving" anchors (i.e., those starting with '_') into the glyph we're keeping
+ # Assumption: we are merging one or more component references to just one component; deleting the others
+ for a in mergefrom['anchor']:
+ aname = a.element.get('name')
+ if aname.startswith('_'):
+ # We want to copy this anchor to the glyph being kept:
+ for i, a2 in enumerate(mergeto['anchor']):
+ if a2.element.get('name') == aname:
+ # Overwrite existing anchor of same name
+ mergeto['anchor'][i] = a
+ break
+ else:
+ # Append anchor to glyph
+ mergeto['anchor'].append(a)
+
+def gettempname(f):
+ ''' return a temporary glyph name that, when passed to function f(), returns true'''
+ # Initialize function attribute for use as counter
+ if not hasattr(gettempname, "counter"): gettempname.counter = 0
+ while True:
+ name = "tempglyph%d" % gettempname.counter
+ gettempname.counter += 1
+ if f(name): return name
+
+def glyphsub(m): # Function passed to re.sub() when updating display strings
+ global csvmap
+ gname = m.group(1)
+ return '/' + csvmap[gname] if gname in csvmap else m.group(0)
+
+def cmd() : execute("UFO",doit,argspec)
+if __name__ == "__main__": cmd()