summaryrefslogtreecommitdiff
path: root/misc/tools/cleanup_kerning.py
diff options
context:
space:
mode:
Diffstat (limited to 'misc/tools/cleanup_kerning.py')
-rwxr-xr-xmisc/tools/cleanup_kerning.py354
1 files changed, 354 insertions, 0 deletions
diff --git a/misc/tools/cleanup_kerning.py b/misc/tools/cleanup_kerning.py
new file mode 100755
index 000000000..e9dce5771
--- /dev/null
+++ b/misc/tools/cleanup_kerning.py
@@ -0,0 +1,354 @@
+#!/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 fontTools import ttLib
+from robofab.objects.objectsRF import OpenFont
+
+
+# Regex matching "default" glyph names, like "uni2043" and "u01C5"
+uniNameRe = re.compile(r'^u(?:ni)([0-9A-F]{4,8})$')
+
+
+def unicodeForDefaultGlyphName(glyphName):
+ m = uniNameRe.match(glyphName)
+ if m is not None:
+ try:
+ return int(m.group(1), 16)
+ except:
+ pass
+ return None
+
+
+def canonicalGlyphName(glyphName, uc2names):
+ uc = unicodeForDefaultGlyphName(glyphName)
+ if uc is not None:
+ names = uc2names.get(uc)
+ if names is not None and len(names) > 0:
+ return names[0]
+ return glyphName
+
+
+
+def parseGlyphComposition(composite):
+ c = composite.split("=")
+ d = c[1].split("/")
+ glyphName = d[0]
+ if len(d) == 1:
+ offset = [0, 0]
+ else:
+ offset = [int(i) for i in d[1].split(",")]
+ accentString = c[0]
+ accents = accentString.split("+")
+ baseName = accents.pop(0)
+ accentNames = [i.split(":") for i in accents]
+ return (glyphName, baseName, accentNames, offset)
+
+
+def loadGlyphCompositions(filename): # { glyphName => (baseName, accentNames, offset) }
+ compositions = OrderedDict()
+ with open(filename, 'r') as f:
+ for line in f:
+ line = line.strip()
+ if len(line) > 0 and line[0] != '#':
+ glyphName, baseName, accentNames, offset = parseGlyphComposition(line)
+ compositions[glyphName] = (baseName, accentNames, offset)
+ return compositions
+
+
+def loadAGL(filename): # -> { 2126: 'Omega', ... }
+ m = {}
+ with open(filename, 'r') as f:
+ for line in f:
+ # Omega;2126
+ # dalethatafpatah;05D3 05B2 # higher-level combinations; ignored
+ line = line.strip()
+ if len(line) > 0 and line[0] != '#':
+ name, uc = tuple([c.strip() for c in line.split(';')])
+ if uc.find(' ') == -1:
+ # it's a 1:1 mapping
+ m[int(uc, 16)] = name
+ return m
+
+
+def loadLocalNamesDB(fonts, agl, diacriticComps):
+ uc2names = None # { 2126: ['Omega', ...], ...}
+ allNames = set() # set('Omega', ...)
+
+ for font in fonts:
+ _uc2names = font.getCharacterMapping() # { 2126: ['Omega', ...], ...}
+ if uc2names is None:
+ uc2names = _uc2names
+ else:
+ for uc, _names in _uc2names.iteritems():
+ names = uc2names.setdefault(uc, [])
+ for name in _names:
+ if name not in names:
+ names.append(name)
+ for g in font:
+ allNames.add(g.name)
+
+ # agl { 2126: 'Omega', ...} -> { 'Omega': [2126, ...], ...}
+ aglName2Ucs = {}
+ for uc, name in agl.iteritems():
+ aglName2Ucs.setdefault(name, []).append(uc)
+
+ for glyphName, comp in diacriticComps.iteritems():
+ aglUCs = aglName2Ucs.get(glyphName)
+ if aglUCs is None:
+ uc = unicodeForDefaultGlyphName(glyphName)
+ if uc is not None:
+ glyphName2 = agl.get(uc)
+ if glyphName2 is not None:
+ glyphName = glyphName2
+ names = uc2names.setdefault(uc, [])
+ if glyphName not in names:
+ names.append(glyphName)
+ allNames.add(glyphName)
+ else:
+ allNames.add(glyphName)
+ for uc in aglUCs:
+ names = uc2names.get(uc, [])
+ if glyphName not in names:
+ names.append(glyphName)
+ uc2names[uc] = names
+
+ name2ucs = {} # { 'Omega': [2126, ...], ...}
+ for uc, names in uc2names.iteritems():
+ for name in names:
+ name2ucs.setdefault(name, set()).add(uc)
+
+ return uc2names, name2ucs, allNames
+
+
+# def getNameToGroupsMap(groups): # => { glyphName => set(groupName) }
+# nameMap = {}
+# for groupName, glyphNames in groups.iteritems():
+# for glyphName in glyphNames:
+# nameMap.setdefault(glyphName, set()).add(groupName)
+# return nameMap
+
+
+# def inspectKerning(kerning):
+# leftIndex = {} # { glyph-name => <ref to plist right-hand side dict> }
+# rightIndex = {} # { glyph-name => [(left-hand-side-name, kernVal), ...] }
+# rightGroupIndex = {} # { group-name => [(left-hand-side-name, kernVal), ...] }
+# for leftName, right in kerning.iteritems():
+# if leftName[0] != '@':
+# leftIndex[leftName] = right
+# for rightName, kernVal in right.iteritems():
+# if rightName[0] != '@':
+# rightIndex.setdefault(rightName, []).append((leftName, kernVal))
+# else:
+# rightGroupIndex.setdefault(rightName, []).append((leftName, kernVal))
+# return leftIndex, rightIndex, rightGroupIndex
+
+
+class RefTracker:
+ def __init__(self):
+ self.refs = {}
+
+ def incr(self, name):
+ self.refs[name] = self.refs.get(name, 0) + 1
+
+ def decr(self, name): # => bool hasNoRefs
+ r = self.refs.get(name)
+
+ if r is None:
+ raise Exception('decr untracked ref ' + repr(name))
+
+ if r < 1:
+ raise Exception('decr already zero ref ' + repr(name))
+
+ if r == 1:
+ del self.refs[name]
+ return True
+
+ self.refs[name] = r - 1
+
+ def __contains__(self, name):
+ return name in self.refs
+
+
+def main(argv=None):
+ argparser = ArgumentParser(description='Remove unused kerning')
+
+ 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(
+ 'fontPaths', metavar='<ufofile>', type=str, nargs='+', help='UFO fonts to update')
+
+ args = argparser.parse_args(argv)
+ dryRun = args.dryRun
+
+ agl = loadAGL('src/glyphlist.txt') # { 2126: 'Omega', ... }
+ diacriticComps = loadGlyphCompositions('src/diacritics.txt') # {glyphName => (baseName, a, o)}
+
+ for fontPath in args.fontPaths:
+ print(fontPath)
+
+ groupsFilename = os.path.join(fontPath, 'groups.plist')
+ kerningFilename = os.path.join(fontPath, 'kerning.plist')
+
+ groups = plistlib.readPlist(groupsFilename) # { groupName => [glyphName] }
+ kerning = plistlib.readPlist(kerningFilename) # { leftName => {rightName => kernVal} }
+
+ font = OpenFont(fontPath)
+ uc2names, name2ucs, allNames = loadLocalNamesDB([font], agl, diacriticComps)
+
+ # start with eliminating non-existent glyphs from groups and completely
+ # eliminate groups with all-dead glyphs.
+ eliminatedGroups = set()
+ for groupName, glyphNames in list(groups.items()):
+ glyphNames2 = []
+ for name in glyphNames:
+ if name in allNames:
+ glyphNames2.append(name)
+ else:
+ name2 = canonicalGlyphName(name, uc2names)
+ if name2 != name and name2 in allNames:
+ print('group: rename glyph', name, '->', name2)
+ glyphNames2.append(name2)
+
+ if len(glyphNames2) == 0:
+ print('group: eliminate', groupName)
+ eliminatedGroups.add(groupName)
+ del groups[groupName]
+ elif len(glyphNames2) != len(glyphNames):
+ print('group: shrink', groupName)
+ groups[groupName] = glyphNames2
+
+ # now eliminate kerning
+ groupRefs = RefTracker() # tracks group references, so we can eliminate unreachable ones
+
+ for leftName, right in list(kerning.items()):
+ leftIsGroup = leftName[0] == '@'
+
+ if leftIsGroup:
+ if leftName in eliminatedGroups:
+ print('kerning: eliminate LHS', leftName)
+ del kerning[leftName]
+ continue
+ groupRefs.incr(leftName)
+ else:
+ if leftName not in allNames:
+ print('kerning: eliminate LHS', leftName)
+ del kerning[leftName]
+ continue
+
+ right2 = {}
+ for rightName, kernVal in right.iteritems():
+ rightIsGroup = rightName[0] == '@'
+ if rightIsGroup:
+ if rightIsGroup in eliminatedGroups:
+ print('kerning: eliminate RHS group', rightName)
+ else:
+ groupRefs.incr(rightName)
+ right2[rightName] = kernVal
+ else:
+ if rightName not in allNames:
+ # maybe an unnamed glyph?
+ rightName2 = canonicalGlyphName(rightName, uc2names)
+ if rightName2 != rightName:
+ print('kerning: rename & update RHS glyph', rightName, '->', rightName2)
+ right2[rightName2] = kernVal
+ else:
+ print('kerning: eliminate RHS glyph', rightName)
+ else:
+ right2[rightName] = kernVal
+
+ if len(right2) == 0:
+ print('kerning: eliminate LHS', leftName)
+ del kerning[leftName]
+ if leftIsGroup:
+ groupRefs.decr(leftName)
+ else:
+ kerning[leftName] = right2
+
+ # eliminate any unreferenced groups
+ for groupName, glyphNames in list(groups.items()):
+ if not groupName in groupRefs:
+ print('group: eliminate unreferenced group', groupName)
+ del groups[groupName]
+
+
+ # verify that there are no conflicting kerning pairs
+ pairs = {} # { key => [...] }
+ conflictingPairs = set()
+
+ for leftName, right in kerning.iteritems():
+ # expand LHS group -> names
+ topLeftName = leftName
+ for leftName in groups[leftName] if leftName[0] == '@' else [leftName]:
+ if leftName not in allNames:
+ raise Exception('unknown LHS glyph name ' + repr(leftName))
+ keyPrefix = leftName + '+'
+ for rightName, kernVal in right.iteritems():
+ # expand RHS group -> names
+ topRightName = rightName
+ for rightName in groups[rightName] if rightName[0] == '@' else [rightName]:
+ if rightName not in allNames:
+ raise Exception('unknown RHS glyph name ' + repr(rightName))
+ # print(leftName, '+', rightName, '=>', kernVal)
+ key = keyPrefix + rightName
+ isConflict = key in pairs
+ pairs.setdefault(key, []).append(( topLeftName, topRightName, kernVal ))
+ if isConflict:
+ conflictingPairs.add(key)
+
+ # # resolve pair conflicts by preferring pairs defined via group kerning
+ # for key in conflictingPairs:
+ # pairs = pairs[key]
+ # print('kerning: conflicting pairs %r: %r' % (key, pairs))
+ # bestPair = None
+ # redundantPairs = []
+ # for pair in pairs:
+ # leftName, rightName, kernVal = pair
+ # if bestPair is None:
+ # bestPair = pair
+ # else:
+ # bestLeftName, bestRightName, _ = bestPair
+ # bestScore = 0
+ # score = 0
+ # if bestLeftName[0] == '@': bestScore += 1
+ # if bestRightName[0] == '@': bestScore += 1
+ # if leftName[0] == '@': score += 1
+ # if rightName[0] == '@': score += 1
+ # if bestScore == 2:
+ # # doesn't get better than this
+ # break
+ # elif score > bestScore:
+ # redundantPairs.append(bestPair)
+ # bestPair = pair
+ # else:
+ # redundantPairs.append(pair)
+ # print('- keeping', bestPair)
+ # print('- eliminating', redundantPairs)
+ # for redundantPairs
+
+
+ # # eliminate any unreferenced groups
+ # for groupName, glyphNames in list(groups.items()):
+ # if not groupName in groupRefs:
+ # print('group: eliminate unreferenced group', groupName)
+ # del groups[groupName]
+
+
+ print('Write', groupsFilename)
+ if not dryRun:
+ plistlib.writePlist(groups, groupsFilename)
+
+ print('Write', kerningFilename)
+ if not dryRun:
+ plistlib.writePlist(kerning, kerningFilename)
+
+ # [end] for fontPath in args.fontPaths
+
+
+if __name__ == '__main__':
+ main()