448 lines
17 KiB
Python
448 lines
17 KiB
Python
#!/usr/bin/env python
|
|
'OpenType analysis for GDL conversion'
|
|
__url__ = 'https://github.com/silnrsi/pysilfont'
|
|
__copyright__ = 'Copyright (c) 2012 SIL International (https://www.sil.org)'
|
|
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
|
|
|
|
import re, traceback, logging
|
|
from fontTools.ttLib.tables import otTables
|
|
|
|
def compress_strings(strings) :
|
|
'''If we replace one column in the string with different lists, can we reduce the number
|
|
of strings? Each string is a tuple of the string and a single value that will be put into
|
|
a class as well when list compression occurs'''
|
|
maxlen = max(map(lambda x: len(x[0]), strings))
|
|
scores = []
|
|
for r in range(maxlen) :
|
|
allchars = {}
|
|
count = 0
|
|
for s in strings :
|
|
if r >= len(s[0]) : continue
|
|
c = tuple(s[0][0:r] + (s[0][r+1:] if r < len(s[0]) - 1 else []))
|
|
if c in allchars :
|
|
allchars[c] += 1
|
|
else :
|
|
allchars[c] = 0
|
|
count += 1
|
|
scores.append((max(allchars.values()), len(allchars), count))
|
|
best = maxlen
|
|
bestr = 0
|
|
for r in range(maxlen) :
|
|
score = maxlen - (scores[r][2] - scores[r][1])
|
|
if score < best :
|
|
best = score
|
|
bestr = r
|
|
numstrings = len(strings)
|
|
i = 0
|
|
allchars = {}
|
|
while i < len(strings) :
|
|
s = strings[i][0]
|
|
if bestr >= len(s) :
|
|
i += 1
|
|
continue
|
|
c = tuple(s[0:bestr] + (s[bestr+1:] if bestr < len(s) - 1 else []))
|
|
if c in allchars :
|
|
allchars[c][1].append(s[bestr])
|
|
allchars[c][2].append(strings[i][1])
|
|
strings.pop(i)
|
|
else :
|
|
allchars[c] = [i, [s[bestr]], [strings[i][1]]]
|
|
i += 1
|
|
for v in allchars.values() :
|
|
if len(set(v[1])) != 1 : # if all values in the list identical, don't output list
|
|
strings[v[0]][0][bestr] = v[1]
|
|
if len(v[2]) > 1 : # don't need a list if length 1
|
|
strings[v[0]][1] = v[2]
|
|
return strings
|
|
|
|
def make_rule(left, right = None, before = None, after = None) :
|
|
res = " ".join(map(lambda x: x or "_", left))
|
|
if right :
|
|
res += " > " + " ".join(map(lambda x: x or "_", right))
|
|
if before or after :
|
|
res += " / "
|
|
if before : res += " ".join(map(lambda x: x or 'ANY', before))
|
|
res += " " + "_ " * len(left) + " "
|
|
if after : res += " ".join(map(lambda x: x or 'ANY', after))
|
|
res += ";"
|
|
return res
|
|
|
|
def add_class_classes(font, name, ctable) :
|
|
vals = {}
|
|
for k, v in ctable.classDefs.items() :
|
|
if v not in vals : vals[v] = []
|
|
vals[v].append(k)
|
|
numk = max(vals.keys())
|
|
res = [None] * (numk + 1)
|
|
for k, v in vals.items() :
|
|
if len(v) > 1 :
|
|
res[k] = font.alias(name+"{}".format(k))
|
|
font.addClass(res[k], map(font.glyph, v))
|
|
else :
|
|
res[k] = font.glyph(v[0]).GDLName()
|
|
return res
|
|
|
|
vrgdlmap = {
|
|
'XPlacement' : 'shift.x',
|
|
'YPlacement' : 'shift.y',
|
|
'XAdvance' : 'advance'
|
|
}
|
|
def valuerectogdl(vr) :
|
|
res = "{"
|
|
for k, v in vrgdlmap.items() :
|
|
if hasattr(vr, k) :
|
|
res += "{}={}; ".format(v, getattr(vr, k))
|
|
res = res[:-1] + "}"
|
|
if len(res) == 1 : return ""
|
|
return res
|
|
|
|
def _add_method(*clazzes):
|
|
"""Returns a decorator function that adds a new method to one or
|
|
more classes."""
|
|
def wrapper(method):
|
|
for c in clazzes:
|
|
assert c.__name__ != 'DefaultTable', \
|
|
'Oops, table class not found.'
|
|
assert not hasattr(c, method.__name__), \
|
|
"Oops, class '%s' has method '%s'." % (c.__name__,
|
|
method.__name__)
|
|
setattr(c, method.__name__, method)
|
|
return None
|
|
return wrapper
|
|
|
|
@_add_method(otTables.Lookup)
|
|
def process(self, font, index) :
|
|
for i, s in enumerate(self.SubTable) :
|
|
if hasattr(s, 'process') :
|
|
s.process(font, index + "_{}".format(i))
|
|
else :
|
|
logging.warning("No processing of {} {}_{}".format(str(s), index, i))
|
|
|
|
@_add_method(otTables.LookupList)
|
|
def process(self, font) :
|
|
for i, s in enumerate(self.Lookup) :
|
|
s.process(font, str(i))
|
|
|
|
@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
|
|
def process(self, font, index) :
|
|
x = self.ExtSubTable
|
|
if hasattr(x, 'process') :
|
|
x.process(font, index)
|
|
else :
|
|
logging.warning("No processing of {} {}".format(str(x), index))
|
|
|
|
@_add_method(otTables.SingleSubst)
|
|
def process(self, font, index) :
|
|
cname = "cot_s{}".format(index)
|
|
if not len(font.alias(cname)) : return
|
|
lists = zip(*self.mapping.items())
|
|
font.addClass(font.alias(cname+"l"), map(font.glyph, lists[0]))
|
|
font.addClass(font.alias(cname+"r"), map(font.glyph, lists[1]))
|
|
|
|
@_add_method(otTables.MultipleSubst)
|
|
def process(self, font, index) :
|
|
cname = "cot_m{}".format(index)
|
|
if not len(font.alias(cname)) : return
|
|
nums = len(self.Coverage.glyphs)
|
|
strings = []
|
|
for i in range(nums) :
|
|
strings.append([self.Sequence[i].Substitute, self.Coverage.glyphs[i]])
|
|
res = compress_strings(strings)
|
|
count = 0
|
|
rules = []
|
|
for r in res :
|
|
if hasattr(r[1], '__iter__') :
|
|
lname = font.alias(cname+"l{}".format(count))
|
|
font.addClass(lname, map(font.glyph, r[1]))
|
|
rule = lname
|
|
else :
|
|
rule = font.glyph(r[1]).GDLName()
|
|
rule += " _" * (len(r[0]) - 1) + " >"
|
|
for c in r[0] :
|
|
if hasattr(c, '__iter__') :
|
|
rname = font.alias(cname+"r{}".format(count))
|
|
font.addClass(rname, map(font.glyph, c))
|
|
rule += " " + rname
|
|
count += 1
|
|
else :
|
|
rule += " " + font.glyph(c).GDLName()
|
|
rule += ';'
|
|
rules.append(rule)
|
|
font.addRules(rules, index)
|
|
|
|
@_add_method(otTables.LigatureSubst)
|
|
def process(self, font, index) :
|
|
cname = "cot_l{}".format(index)
|
|
if not len(font.alias(cname)) : return
|
|
strings = []
|
|
for lg, ls in self.ligatures.items() :
|
|
for l in ls :
|
|
strings.append([[lg] + l.Component, l.LigGlyph])
|
|
res = compress_strings(strings)
|
|
count = 0
|
|
rules = []
|
|
for r in res :
|
|
rule = ""
|
|
besti = 0
|
|
for i, c in enumerate(r[0]) :
|
|
if hasattr(c, '__iter__') :
|
|
lname = font.alias(cname+"l{}".format(count))
|
|
font.addClass(lname, map(font.glyph, c))
|
|
rule += lname + " "
|
|
besti = i
|
|
else :
|
|
rule += font.glyph(c).GDLName() + " "
|
|
rule += "> " + "_ " * besti
|
|
if hasattr(r[1], '__iter__') :
|
|
rname = font.alias(cname+"r{}".format(count))
|
|
font.addClass(rname, map(font.glyph, r[1]))
|
|
rule += rname
|
|
count += 1
|
|
else :
|
|
rule += font.glyph(r[1]).GDLName()
|
|
rule += " _" * (len(r[0]) - 1 - besti) + ";"
|
|
rules.append(rule)
|
|
font.addRules(rules, index)
|
|
|
|
@_add_method(otTables.ChainContextSubst)
|
|
def process(self, font, index) :
|
|
|
|
def procsubst(rule, action) :
|
|
for s in rule.SubstLookupRecord :
|
|
action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
|
|
def procCover(cs, name) :
|
|
res = []
|
|
for i, c in enumerate(cs) :
|
|
if len(c.glyphs) > 1 :
|
|
n = font.alias(name+"{}".format(i))
|
|
font.addClass(n, map(font.glyph, c.glyphs))
|
|
res.append(n)
|
|
else :
|
|
res.append(font.glyph(c.glyphs[0]).GDLName())
|
|
return res
|
|
|
|
cname = "cot_c{}".format(index)
|
|
if not len(font.alias(cname)) : return
|
|
rules = []
|
|
if self.Format == 1 :
|
|
for i in range(len(self.ChainSubRuleSet)) :
|
|
for r in self.ChainSubRuleSet[i].ChainSubRule :
|
|
action = [self.Coverage.glyphs[i]] + r.Input
|
|
procsubst(r, action)
|
|
rules.append(make_rule(action, None, r.Backtrack, r.LookAhead))
|
|
elif self.Format == 2 :
|
|
ilist = add_class_classes(font, cname+"i", self.InputClassDef)
|
|
if self.BacktrackClassDef :
|
|
blist = add_class_classes(font, cname+"b", self.BacktrackClassDef)
|
|
if self.LookAheadClassDef :
|
|
alist = add_class_classes(font, cname+"a", self.LookAheadClassDef)
|
|
for i, s in enumerate(self.ChainSubClassSet) :
|
|
if s is None : continue
|
|
for r in s.ChainSubClassRule :
|
|
action = map(lambda x:ilist[x], [i]+r.Input)
|
|
procsubst(r, action)
|
|
rules.append(make_rule(action, None,
|
|
map(lambda x:blist[x], r.Backtrack or []),
|
|
map(lambda x:alist[x], r.LookAhead or [])))
|
|
elif self.Format == 3 :
|
|
backs = procCover(self.BacktrackCoverage, cname+"b")
|
|
aheads = procCover(self.LookAheadCoverage, cname+"a")
|
|
actions = procCover(self.InputCoverage, cname+"i")
|
|
procsubst(self, actions)
|
|
rules.append(make_rule(actions, None, backs, aheads))
|
|
font.addRules(rules, index)
|
|
|
|
@_add_method(otTables.SinglePos)
|
|
def process(self, font, index) :
|
|
cname = "cot_p{}".format(index)
|
|
if self.Format == 1 :
|
|
font.addClass(font.alias(cname), map(font.glyph, self.Coverage.glyphs))
|
|
rule = cname + " " + valuerectogdl(self.Value)
|
|
font.addPosRules([rule], index)
|
|
elif self.Format == 2 :
|
|
rules = []
|
|
for i, g in enumerage(map(font.glyph, self.Coverage.glyphs)) :
|
|
rule = font.glyph(g).GDLName()
|
|
rule += " " + valuerectogdl(self.Value[i])
|
|
rules.append(rule)
|
|
font.addPosRules(rules, index)
|
|
|
|
@_add_method(otTables.PairPos)
|
|
def process(self, font, index) :
|
|
pass
|
|
|
|
@_add_method(otTables.CursivePos)
|
|
def process(self, font, index) :
|
|
apname = "P{}".format(index)
|
|
if not len(font.alias(apname)) : return
|
|
if self.Format == 1 :
|
|
mark_names = self.Coverage.glyphs
|
|
for i, g in enumerate(map(font.glyph, mark_names)) :
|
|
rec = self.EntryExitRecord[i]
|
|
if rec.EntryAnchor is not None :
|
|
g.setAnchor(font.alias(apname+"_{}M".format(rec.EntryAnchor)),
|
|
rec.EntryAnchor.XCoordinate, rec.EntryAnchor.YCoordinate)
|
|
if rec.ExitAnchor is not None :
|
|
g.setAnchor(font.alias(apname+"_{}S".format(rec.ExitAnchor)),
|
|
rec.ExitAnchor.XCoordinate, rec.ExitAnchor.YCoordinate)
|
|
|
|
@_add_method(otTables.MarkBasePos)
|
|
def process(self, font, index) :
|
|
apname = "P{}".format(index)
|
|
if not len(font.alias(apname)) : return
|
|
if self.Format == 1 :
|
|
mark_names = self.MarkCoverage.glyphs
|
|
for i, g in enumerate(map(font.glyph, mark_names)) :
|
|
rec = self.MarkArray.MarkRecord[i]
|
|
g.setAnchor(font.alias(apname+"_{}M".format(rec.Class)),
|
|
rec.MarkAnchor.XCoordinate, rec.MarkAnchor.YCoordinate)
|
|
base_names = self.BaseCoverage.glyphs
|
|
for i, g in enumerate(map(font.glyph, base_names)) :
|
|
for j,b in enumerate(self.BaseArray.BaseRecord[i].BaseAnchor) :
|
|
if b : g.setAnchor(font.alias(apname+"_{}S".format(j)),
|
|
b.XCoordinate, b.YCoordinate)
|
|
|
|
@_add_method(otTables.MarkMarkPos)
|
|
def process(self, font, index) :
|
|
apname = "P{}".format(index)
|
|
if not len(font.alias(apname)) : return
|
|
if self.Format == 1 :
|
|
mark_names = self.Mark1Coverage.glyphs
|
|
for i, g in enumerate(map(font.glyph, mark_names)) :
|
|
rec = self.Mark1Array.MarkRecord[i]
|
|
g.setAnchor(font.alias(apname+"_{}M".format(rec.Class)),
|
|
rec.MarkAnchor.XCoordinate, rec.MarkAnchor.YCoordinate)
|
|
base_names = self.Mark2Coverage.glyphs
|
|
for i, g in enumerate(map(font.glyph, base_names)) :
|
|
for j,b in enumerate(self.Mark2Array.Mark2Record[i].Mark2Anchor) :
|
|
if b : g.setAnchor(font.alias(apname+"_{}S".format(j)),
|
|
b.XCoordinate, b.YCoordinate)
|
|
|
|
@_add_method(otTables.ContextSubst)
|
|
def process(self, font, index) :
|
|
|
|
def procsubst(rule, action) :
|
|
for s in rule.SubstLookupRecord :
|
|
action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
|
|
def procCover(cs, name) :
|
|
res = []
|
|
for i, c in enumerate(cs) :
|
|
if len(c.glyphs) > 1 :
|
|
n = font.alias(name+"{}".format(i))
|
|
font.addClass(n, map(font.glyph, c.glyphs))
|
|
res.append(n)
|
|
else :
|
|
res.append(font.glyph(c.glyphs[0]).GDLName())
|
|
return res
|
|
|
|
cname = "cot_cs{}".format(index)
|
|
if not len(font.alias(cname)) : return
|
|
rules = []
|
|
if self.Format == 1 :
|
|
for i in range(len(self.SubRuleSet)) :
|
|
for r in self.SubRuleSet[i].SubRule :
|
|
action = [self.Coverage.glyphs[i]] + r.Input
|
|
procsubst(r, action)
|
|
rules.append(make_rule(action, None, None, None))
|
|
elif self.Format == 2 :
|
|
ilist = add_class_classes(font, cname+"i", self.ClassDef)
|
|
for i, s in enumerate(self.SubClassSet) :
|
|
if s is None : continue
|
|
for r in s.SubClassRule :
|
|
action = map(lambda x:ilist[x], [i]+r.Class)
|
|
procsubst(r, action)
|
|
rules.append(make_rule(action, None, None, None))
|
|
elif self.Format == 3 :
|
|
actions = procCover(self.Coverage, cname+"i")
|
|
procsubst(self, actions)
|
|
rules.append(make_rule(actions, None, None, None))
|
|
font.addRules(rules, index)
|
|
|
|
@_add_method(otTables.ContextPos)
|
|
def process(self, font, index) :
|
|
|
|
def procsubst(rule, action) :
|
|
for s in rule.PosLookupRecord :
|
|
action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
|
|
def procCover(cs, name) :
|
|
res = []
|
|
for i, c in enumerate(cs) :
|
|
if len(c.glyphs) > 1 :
|
|
n = font.alias(name+"{}".format(i))
|
|
font.addClass(n, map(font.glyph, c.glyphs))
|
|
res.append(n)
|
|
else :
|
|
res.append(font.glyph(c.glyphs[0]).GDLName())
|
|
return res
|
|
|
|
cname = "cot_cp{}".format(index)
|
|
if not len(font.alias(cname)) : return
|
|
rules = []
|
|
if self.Format == 1 :
|
|
for i in range(len(self.PosRuleSet)) :
|
|
for r in self.PosRuleSet[i] :
|
|
action = [self.Coverage.glyphs[i]] + r.Input
|
|
procsubst(r, action)
|
|
rules.append(make_rule(action, None, None, None))
|
|
elif self.Format == 2 :
|
|
ilist = add_class_classes(font, cname+"i", self.ClassDef)
|
|
for i, s in enumerate(self.PosClassSet) :
|
|
if s is None : continue
|
|
for r in s.PosClassRule :
|
|
action = map(lambda x:ilist[x], [i]+r.Class)
|
|
procsubst(r, action)
|
|
rules.append(make_rule(action, None, None, None))
|
|
elif self.Format == 3 :
|
|
actions = procCover(self.Coverage, cname+"i")
|
|
procsubst(self, actions)
|
|
rules.append(make_rule(actions, None, None, None))
|
|
font.addPosRules(rules, index)
|
|
|
|
@_add_method(otTables.ChainContextPos)
|
|
def process(self, font, index) :
|
|
|
|
def procsubst(rule, action) :
|
|
for s in rule.PosLookupRecord :
|
|
action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
|
|
def procCover(cs, name) :
|
|
res = []
|
|
for i, c in enumerate(cs) :
|
|
if len(c.glyphs) > 1 :
|
|
n = font.alias(name+"{}".format(i))
|
|
font.addClass(n, map(font.glyph, c.glyphs))
|
|
res.append(n)
|
|
else :
|
|
res.append(font.glyph(c.glyphs[0]).GDLName())
|
|
return res
|
|
|
|
cname = "cot_c{}".format(index)
|
|
if not len(font.alias(cname)) : return
|
|
rules = []
|
|
if self.Format == 1 :
|
|
for i in range(len(self.ChainPosRuleSet)) :
|
|
for r in self.ChainPosRuleSet[i].ChainPosRule :
|
|
action = [self.Coverage.glyphs[i]] + r.Input
|
|
procsubst(r, action)
|
|
rules.append(make_rule(action, None, r.Backtrack, r.LookAhead))
|
|
elif self.Format == 2 :
|
|
ilist = add_class_classes(font, cname+"i", self.InputClassDef)
|
|
if self.BacktrackClassDef :
|
|
blist = add_class_classes(font, cname+"b", self.BacktrackClassDef)
|
|
if self.LookAheadClassDef :
|
|
alist = add_class_classes(font, cname+"a", self.LookAheadClassDef)
|
|
for i, s in enumerate(self.ChainPosClassSet) :
|
|
if s is None : continue
|
|
for r in s.ChainPosClassRule :
|
|
action = map(lambda x:ilist[x], [i]+r.Input)
|
|
procsubst(r, action)
|
|
rules.append(make_rule(action, None,
|
|
map(lambda x:blist[x], r.Backtrack or []),
|
|
map(lambda x:alist[x], r.LookAhead or [])))
|
|
elif self.Format == 3 :
|
|
backs = procCover(self.BacktrackCoverage, cname+"b")
|
|
aheads = procCover(self.LookAheadCoverage, cname+"a")
|
|
actions = procCover(self.InputCoverage, cname+"i")
|
|
procsubst(self, actions)
|
|
rules.append(make_rule(actions, None, backs, aheads))
|
|
font.addPosRules(rules, index)
|
|
|