From 989a5e2e61b16dfe3bc782cb86ef870168c66250 Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Sun, 24 Sep 2017 13:07:13 -0700 Subject: Adds misc/rmglyph.py for safe and complete removal of glyphs --- misc/rmglyph.py | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100755 misc/rmglyph.py (limited to 'misc/rmglyph.py') diff --git a/misc/rmglyph.py b/misc/rmglyph.py new file mode 100755 index 000000000..729205791 --- /dev/null +++ b/misc/rmglyph.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# encoding: utf8 +from __future__ import print_function +import os, sys, plistlib, re +from collections import OrderedDict +from ConfigParser import RawConfigParser +from argparse import ArgumentParser +from robofab.objects.objectsRF import OpenFont +import cleanup_kerning + + +dryRun = False + + +def decomposeComponentInstances(font, glyph, componentsToDecompose): + """Moves the components of a glyph to its outline.""" + if len(glyph.components): + deepCopyContours(font, glyph, glyph, (0, 0), (1, 1), componentsToDecompose) + glyph.clearComponents() + + +def deepCopyContours(font, parent, component, offset, scale, componentsToDecompose): + """Copy contours to parent from component, including nested components.""" + for nested in component.components: + if componentsToDecompose is None or nested.baseGlyph in componentsToDecompose: + deepCopyContours( + font, parent, font[nested.baseGlyph], + (offset[0] + nested.offset[0], offset[1] + nested.offset[1]), + (scale[0] * nested.scale[0], scale[1] * nested.scale[1]), + None) + component.removeComponent(nested) + if component == parent: + return + for contour in component: + contour = contour.copy() + contour.scale(scale) + contour.move(offset) + parent.appendContour(contour) + + +def addGlyphsForCP(cp, ucmap, glyphnames): + if cp in ucmap: + for name in ucmap[cp]: + glyphnames.append(name) + # else: + # print('no glyph for U+%04X' % cp) + + +def getGlyphNamesFromArgs(font, ucmap, glyphs): + glyphnames = [] + for s in glyphs: + if len(s) > 2 and s[:2] == 'U+': + p = s.find('-') + if p != -1: + # range, e.g. "U+1D0A-1DBC" + cpStart = int(s[2:p], 16) + cpEnd = int(s[p+1:], 16) + for cp in range(cpStart, cpEnd): + addGlyphsForCP(cp, ucmap, glyphnames) + else: + # single code point e.g. "U+1D0A" + cp = int(s[2:], 16) + addGlyphsForCP(cp, ucmap, glyphnames) + else: + glyphnames.append(s) + return glyphnames + + +def main(argv=sys.argv): + argparser = ArgumentParser(description='Remove glyphs from UFOs') + + 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( + 'fontPath', metavar='', type=str, help='Path to UFO font to modify') + + argparser.add_argument( + 'glyphs', metavar='', type=str, nargs='+', + help='Glyph to remove. '+ + 'Can be a glyphname, '+ + 'a Unicode code point formatted as "U+", '+ + 'or a Unicode code point range formatted as "U+-"') + + args = argparser.parse_args() + dryRun = args.dryRun + + print('Loading glyph data...') + font = OpenFont(args.fontPath) + ucmap = font.getCharacterMapping() # { 2126: [ 'Omega', ...], ...} + cnmap = font.getReverseComponentMapping() # { 'A' : ['Aacute', 'Aring'], 'acute' : ['Aacute'] ... } + + glyphnames = set(getGlyphNamesFromArgs(font, ucmap, args.glyphs)) + + if len(glyphnames) == 0: + print('None of the glyphs requested exist in', args.fontPath) + return + + 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') + for gname, dependants in cnConflicts.iteritems(): + print('%s used by %s' % (gname, ', '.join(dependants))) + sys.exit(1) + + # find orphaned pure-components + for gname in glyphnames: + g = font[gname] + 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 becomes orphaned' % cn.baseGlyph) + + # remove glyphs from UFO + print('Removing %d glyphs' % len(glyphnames)) + + libPlistFilename = os.path.join(args.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) + + if not dryRun: + print('Writing changes to %s' % args.fontPath) + font.save() + plistlib.writePlist(libPlist, libPlistFilename) + else: + print('Writing changes to %s (dry run)' % args.fontPath) + + print('Cleaning up kerning') + if dryRun: + cleanup_kerning.main(['-dry', args.fontPath]) + else: + cleanup_kerning.main([args.fontPath]) + + print('\n————————————————————————————————————————————————————\n'+ + 'Removed %d glyphs:\n %s' % ( + len(glyphnames), '\n '.join(sorted(glyphnames)))) + + print('\n————————————————————————————————————————————————————\n\n'+ + 'You now need to manually remove any occurances of these glyphs in\n'+ + 'src/features.fea and\n'+ + '%s/features.fea\n' % args.fontPath) + + +if __name__ == '__main__': + main() -- cgit v1.2.3