Adds misc/rmglyph.py for safe and complete removal of glyphs
This commit is contained in:
parent
d9b28168a2
commit
989a5e2e61
2 changed files with 202 additions and 3 deletions
198
misc/rmglyph.py
Executable file
198
misc/rmglyph.py
Executable 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()
|
||||
Reference in a new issue