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
|
|
@ -173,7 +173,7 @@ class RefTracker:
|
||||||
return name in self.refs
|
return name in self.refs
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(argv=sys.argv):
|
||||||
argparser = ArgumentParser(description='Remove unused kerning')
|
argparser = ArgumentParser(description='Remove unused kerning')
|
||||||
|
|
||||||
argparser.add_argument(
|
argparser.add_argument(
|
||||||
|
|
@ -183,7 +183,7 @@ def main():
|
||||||
argparser.add_argument(
|
argparser.add_argument(
|
||||||
'fontPaths', metavar='<ufofile>', type=str, nargs='+', help='UFO fonts to update')
|
'fontPaths', metavar='<ufofile>', type=str, nargs='+', help='UFO fonts to update')
|
||||||
|
|
||||||
args = argparser.parse_args()
|
args = argparser.parse_args(argv)
|
||||||
dryRun = args.dryRun
|
dryRun = args.dryRun
|
||||||
|
|
||||||
agl = loadAGL('src/glyphlist.txt') # { 2126: 'Omega', ... }
|
agl = loadAGL('src/glyphlist.txt') # { 2126: 'Omega', ... }
|
||||||
|
|
@ -350,4 +350,5 @@ def main():
|
||||||
# [end] for fontPath in args.fontPaths
|
# [end] for fontPath in args.fontPaths
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
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