inter/misc/restore-diacritics-kerning.py
2017-09-18 15:58:34 -07:00

431 lines
15 KiB
Python

#!/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()