summaryrefslogtreecommitdiff
path: root/misc/tools/restore-diacritics-kerning.py
diff options
context:
space:
mode:
Diffstat (limited to 'misc/tools/restore-diacritics-kerning.py')
-rw-r--r--misc/tools/restore-diacritics-kerning.py431
1 files changed, 431 insertions, 0 deletions
diff --git a/misc/tools/restore-diacritics-kerning.py b/misc/tools/restore-diacritics-kerning.py
new file mode 100644
index 000000000..6fd8c1601
--- /dev/null
+++ b/misc/tools/restore-diacritics-kerning.py
@@ -0,0 +1,431 @@
+#!/usr/bin/env python
+# encoding: utf8
+#
+# This script was used specifically to re-introduce a bunch of kerning values
+# that where lost in an old kerning cleanup that failed to account for
+# automatically composed glyphs defined in diacritics.txt.
+#
+# Steps:
+# 1. git diff 10e15297b 10e15297b^ > 10e15297b.diff
+# 2. edit 10e15297b.diff and remove the python script add
+# 3. fetch copies of kerning.plist and groups.plist from before the loss change
+# bold-groups.plist
+# bold-kerning.plist
+# regular-groups.plist
+# regular-kerning.plist
+# 4. run this script
+#
+from __future__ import print_function
+import os, sys, plistlib, json
+from collections import OrderedDict
+from ConfigParser import RawConfigParser
+from argparse import ArgumentParser
+from fontTools import ttLib
+from robofab.objects.objectsRF import OpenFont
+
+
+srcFontPaths = ['src/Inter-UI-Regular.ufo', 'src/Inter-UI-Bold.ufo']
+
+
+def getTTGlyphList(font): # -> { 'Omega': [2126, ...], ... }
+ if isinstance(font, str):
+ font = ttLib.TTFont(font)
+
+ if not 'cmap' in font:
+ raise Exception('missing cmap table')
+
+ gl = {}
+ bestCodeSubTable = None
+ bestCodeSubTableFormat = 0
+
+ for st in font['cmap'].tables:
+ if st.platformID == 0: # 0=unicode, 1=mac, 2=(reserved), 3=microsoft
+ if st.format > bestCodeSubTableFormat:
+ bestCodeSubTable = st
+ bestCodeSubTableFormat = st.format
+
+ if bestCodeSubTable is not None:
+ for cp, glyphname in bestCodeSubTable.cmap.items():
+ if glyphname in gl:
+ gl[glyphname].append(cp)
+ else:
+ gl[glyphname] = [cp]
+
+ return gl, font
+
+
+def parseAGL(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 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):
+ 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 loadNamesFromDiff(diffFilename):
+ with open(diffFilename, 'r') as f:
+ diffLines = [s.strip() for s in f.read().splitlines() if s.startswith('+\t')]
+ diffLines = [s for s in diffLines if not s.startswith('<int')]
+ namesInDiff = set()
+ for s in diffLines:
+ if s.startswith('<int') or s.startswith('<arr') or s.startswith('</'):
+ continue
+ p = s.find('>')
+ if p != -1:
+ p2 = s.find('<', p+1)
+ if p2 != -1:
+ name = s[p+1:p2]
+ try:
+ int(name)
+ except:
+ if not name.startswith('@'):
+ namesInDiff.add(s[p+1:p2])
+ return namesInDiff
+
+
+def loadGroups(filename):
+ groups = plistlib.readPlist(filename)
+ nameMap = {} # { glyphName => set(groupName) }
+ for groupName, glyphNames in groups.iteritems():
+ for glyphName in glyphNames:
+ nameMap.setdefault(glyphName, set()).add(groupName)
+ return groups, nameMap
+
+
+def loadKerning(filename):
+ kerning = plistlib.readPlist(filename)
+ # <dict>
+ # <key>@KERN_LEFT_A</key>
+ # <dict>
+ # <key>@KERN_RIGHT_C</key>
+ # <integer>-96</integer>
+
+ 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 kerning, leftIndex, rightIndex, rightGroupIndex
+
+
+def loadAltNamesDB(agl, fontFilename):
+ uc2names = {} # { 2126: ['Omega', ...], ...}
+ name2ucs = {} # { 'Omega': [2126, ...], ...}
+
+ name2ucs, _ = getTTGlyphList(fontFilename)
+ # -> { 'Omega': [2126, ...], ... }
+ for name, ucs in name2ucs.iteritems():
+ for uc in ucs:
+ uc2names.setdefault(uc, []).append(name)
+
+ for uc, name in agl.iteritems():
+ name2ucs.setdefault(name, []).append(uc)
+ uc2names.setdefault(uc, []).append(name)
+ # -> { 2126: 'Omega', ... }
+
+ return uc2names, name2ucs
+
+
+def loadLocalNamesDB(agl, diacriticComps): # { 2126: ['Omega', ...], ...}
+ uc2names = None
+
+ for fontPath in srcFontPaths:
+ font = OpenFont(fontPath)
+ if uc2names is None:
+ uc2names = font.getCharacterMapping() # { 2126: ['Omega', ...], ...}
+ else:
+ for uc, names in font.getCharacterMapping().iteritems():
+ names2 = uc2names.get(uc, [])
+ for name in names:
+ if name not in names2:
+ names2.append(name)
+ uc2names[uc] = names2
+
+ # agl { 2126: 'Omega', ...} -> { 'Omega': [2126, ...], ...}
+ aglName2Ucs = {}
+ for uc, name in agl.iteritems():
+ aglName2Ucs.setdefault(name, []).append(uc)
+
+ for glyphName, comp in diacriticComps.iteritems():
+ for uc in aglName2Ucs.get(glyphName, []):
+ names = uc2names.get(uc, [])
+ if glyphName not in names:
+ names.append(glyphName)
+ uc2names[uc] = names
+
+ name2ucs = {}
+ for uc, names in uc2names.iteritems():
+ for name in names:
+ name2ucs.setdefault(name, set()).add(uc)
+
+ return uc2names, name2ucs
+
+
+def _canonicalGlyphName(name, localName2ucs, localUc2Names, altName2ucs):
+ ucs = localName2ucs.get(name)
+ if ucs:
+ return name, list(ucs)[0]
+ ucs = altName2ucs.get(name)
+ if ucs:
+ for uc in ucs:
+ localNames = localUc2Names.get(uc)
+ if localNames and len(localNames):
+ return localNames[0], uc
+ return None, None
+
+
+def main():
+ argparser = ArgumentParser(description='Restore lost 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(
+ 'srcFont', metavar='<fontfile>', type=str,
+ help='TrueType, OpenType or UFO fonts to gather glyph info from')
+
+ argparser.add_argument(
+ 'diffFile', metavar='<diffile>', type=str, help='Diff file')
+
+ args = argparser.parse_args()
+
+ dryRun = args.dryRun
+
+ agl = parseAGL('src/glyphlist.txt')
+ diacriticComps = loadGlyphCompositions('src/diacritics.txt')
+
+ altUc2names, altName2ucs = loadAltNamesDB(agl, args.srcFont)
+ localUc2Names, localName2ucs = loadLocalNamesDB(agl, diacriticComps)
+
+ canonicalGlyphName = lambda name: _canonicalGlyphName(
+ name, localName2ucs, localUc2Names, altName2ucs)
+
+ deletedNames = loadNamesFromDiff(args.diffFile) # 10e15297b.diff
+ deletedDiacriticNames = OrderedDict()
+
+ for glyphName, comp in diacriticComps.iteritems():
+ if glyphName in deletedNames:
+ deletedDiacriticNames[glyphName] = comp
+
+
+ for fontPath in srcFontPaths:
+ addedGroupNames = set()
+
+ oldFilenamePrefix = 'regular'
+ if fontPath.find('Bold') != -1:
+ oldFilenamePrefix = 'bold'
+ oldGroups, oldNameToGroups = loadGroups(
+ oldFilenamePrefix + '-groups.plist')
+ oldKerning, oldLIndex, oldRIndex, oldRGroupIndex = loadKerning(
+ oldFilenamePrefix + '-kerning.plist')
+ # lIndex : { name => <ref to plist right-hand side dict> }
+ # rIndex : { name => [(left-hand-side-name, kernVal), ...] }
+
+ currGroupFilename = os.path.join(fontPath, 'groups.plist')
+ currKerningFilename = os.path.join(fontPath, 'kerning.plist')
+ currGroups, currNameToGroups = loadGroups(currGroupFilename)
+ currKerning, currLIndex, currRIndex, currRGroupIndex = loadKerning(currKerningFilename)
+
+ for glyphName, comp in deletedDiacriticNames.iteritems():
+ oldGroupMemberships = oldNameToGroups.get(glyphName)
+ localGlyphName, localUc = canonicalGlyphName(glyphName)
+
+ # if glyphName != 'dcaron':
+ # continue # XXX DEBUG
+
+ if localGlyphName is None:
+ # glyph does no longer exist -- ignore
+ print('[IGNORE]', glyphName)
+ continue
+
+ if oldGroupMemberships:
+ # print('group', localGlyphName,
+ # '=>', localUc,
+ # 'in old group:', oldGroupMemberships, ', curr group:', currGroupMemberships)
+ for oldGroupName in oldGroupMemberships:
+ currGroup = currGroups.get(oldGroupName) # None|[glyphname, ...]
+ # print('GM ', localGlyphName, oldGroupName, len(currGroup) if currGroup else 0)
+ if currGroup is not None:
+ if localGlyphName not in currGroup:
+ # print('[UPDATE group]', oldGroupName, 'append', localGlyphName)
+ currGroup.append(localGlyphName)
+ else:
+ # group does not currently exist
+ if currNameToGroups.get(localGlyphName):
+ raise Exception('TODO: case where glyph is in some current groups, but not the' +
+ 'original-named group')
+ print('[ADD group]', oldGroupName, '=> [', localGlyphName, ']')
+ currGroups[oldGroupName] = [localGlyphName]
+ addedGroupNames.add(oldGroupName)
+ # if oldGroupName in oldKerning:
+ # print('TODO: effects of oldGroupName being in oldKerning:',
+ # oldKerning[oldGroupName])
+ if oldGroupName in oldRGroupIndex:
+ print('TODO: effects of oldGroupName being in oldRGroupIndex:',
+ oldRGroupIndex[oldGroupName])
+
+ else: # if not oldGroupMemberships
+ ucs = localName2ucs.get(glyphName)
+ if not ucs:
+ raise Exception(
+ 'TODO non-group, non-local name ' + glyphName + ' -- lookup in alt names')
+
+ asLeft = oldLIndex.get(glyphName)
+ atRightOf = oldRIndex.get(glyphName)
+
+ # print('individual', glyphName,
+ # '=>', ', '.join([str(uc) for uc in ucs]),
+ # '\n as left:', asLeft is not None,
+ # '\n at right of:', atRightOf is not None)
+
+ if asLeft:
+ currKern = currKerning.get(localGlyphName)
+ if currKern is None:
+ rightValues = {}
+ for rightName, kernValue in asLeft.iteritems():
+ if rightName[0] == '@':
+ currGroup = currGroups.get(rightName)
+ if currGroup and localGlyphName not in currGroup:
+ rightValues[rightName] = kernValue
+ else:
+ localName, localUc = canonicalGlyphName(rightName)
+ if localName:
+ rightValues[localName] = kernValue
+ if len(rightValues) > 0:
+ print('[ADD currKerning]', localGlyphName, '=>', rightValues)
+ currKerning[localGlyphName] = rightValues
+
+ if atRightOf:
+ for parentLeftName, kernVal in atRightOf:
+ # print('atRightOf:', parentLeftName, kernVal)
+ if parentLeftName[0] == '@':
+ if parentLeftName in currGroups:
+ k = currKerning.get(parentLeftName)
+ if k:
+ if localGlyphName not in k:
+ print('[UPDATE currKerning g]',
+ parentLeftName, '+= {', localGlyphName, ':', kernVal, '}')
+ k[localGlyphName] = kernVal
+ else:
+ print('TODO: left-group is NOT in currKerning; left-group', parentLeftName)
+ else:
+ localParentLeftGlyphName, _ = canonicalGlyphName(parentLeftName)
+ if localParentLeftGlyphName:
+ k = currKerning.get(localParentLeftGlyphName)
+ if k:
+ if localGlyphName not in k:
+ print('[UPDATE currKerning i]',
+ localParentLeftGlyphName, '+= {', localGlyphName, ':', kernVal, '}')
+ k[localGlyphName] = kernVal
+ else:
+ print('[ADD currKerning i]',
+ localParentLeftGlyphName, '=> {', localGlyphName, ':', kernVal, '}')
+ currKerning[localParentLeftGlyphName] = {localGlyphName: kernVal}
+
+
+ for groupName in addedGroupNames:
+ print('————————————————————————————————————————————')
+ print('re-introduce group', groupName, 'to kerning')
+
+ oldRKern = oldKerning.get(groupName)
+ if oldRKern is not None:
+ newRKern = {}
+ for oldRightName, kernVal in oldRKern.iteritems():
+ if oldRightName[0] == '@':
+ if oldRightName in currGroups:
+ newRKern[oldRightName] = kernVal
+ else:
+ # Note: (oldRightName in addedGroupNames) should always be False here
+ # as we would have added it to currGroups already.
+ print('[DROP group]', oldRightName, kernVal)
+ if oldRightName in currGroups:
+ del currGroups[oldRightName]
+ else:
+ localGlyphName, _ = canonicalGlyphName(oldRightName)
+ if localGlyphName:
+ newRKern[localGlyphName] = kernVal
+ print('localGlyphName', localGlyphName)
+
+ if len(newRKern):
+ print('[ADD currKerning g]', groupName, newRKern)
+ currKerning[groupName] = newRKern
+
+ # oldRGroupIndex : { group-name => [(left-hand-side-name, kernVal), ...] }
+ oldLKern = oldRGroupIndex.get(groupName)
+ if oldLKern:
+ for oldRightName, kernVal in oldLKern:
+ if oldRightName[0] == '@':
+ if oldRightName in currGroups:
+ k = currKerning.get(oldRightName)
+ if k is not None:
+ print('[UPDATE kerning g]', oldRightName, '+= {', groupName, ':', kernVal, '}')
+ k[groupName] = kernVal
+ else:
+ currKerning[oldRightName] = {groupName: kernVal}
+ print('[ADD kerning g]', oldRightName, '= {', groupName, ':', kernVal, '}')
+ else:
+ localGlyphName, _ = canonicalGlyphName(oldRightName)
+ if localGlyphName:
+ k = currKerning.get(localGlyphName)
+ if k is not None:
+ print('[UPDATE kerning i]', localGlyphName, '+= {', groupName, ':', kernVal, '}')
+ k[groupName] = kernVal
+ else:
+ currKerning[localGlyphName] = {groupName: kernVal}
+ print('[ADD kerning i]', localGlyphName, '= {', groupName, ':', kernVal, '}')
+
+
+ print('Write', currGroupFilename)
+ if not dryRun:
+ plistlib.writePlist(currGroups, currGroupFilename)
+
+ print('Write', currKerningFilename)
+ if not dryRun:
+ plistlib.writePlist(currKerning, currKerningFilename)
+
+ # end: for fontPath
+
+main()