Adds misc/rmglyph.py for safe and complete removal of glyphs

This commit is contained in:
Rasmus Andersson 2017-09-24 13:07:13 -07:00
parent d9b28168a2
commit 989a5e2e61
2 changed files with 202 additions and 3 deletions

198
misc/rmglyph.py Executable file
View file

@ -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='<ufopath>', type=str, help='Path to UFO font to modify')
argparser.add_argument(
'glyphs', metavar='<glyph>', type=str, nargs='+',
help='Glyph to remove. '+
'Can be a glyphname, '+
'a Unicode code point formatted as "U+<CP>", '+
'or a Unicode code point range formatted as "U+<CP>-<CP>"')
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()