blob: ca6694288c5e7a54a5f2a6f4fb7cb26232e5fd71 [file] [log] [blame]
# Copyright 2013 Google, Inc. All Rights Reserved.
#
# Google Author(s): Behdad Esfahbod
"""Python OpenType Layout Subsetter.
Later grown into full OpenType subsetter, supporting all standard tables.
"""
import sys
import struct
import time
import array
from fontTools import ttLib
from fontTools.ttLib.tables import otTables
from fontTools.misc import psCharStrings
from fontTools.pens import basePen
def _add_method(*clazzes):
"""Returns a decorator function that adds a new method to one or
more classes."""
def wrapper(method):
for clazz in clazzes:
assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.'
assert not hasattr(clazz, method.func_name), \
"Oops, class '%s' has method '%s'." % (clazz.__name__,
method.func_name)
setattr(clazz, method.func_name, method)
return None
return wrapper
def _uniq_sort(l):
return sorted(set(l))
def _set_update(s, *others):
# Jython's set.update only takes one other argument.
# Emulate real set.update...
for other in others:
s.update(other)
@_add_method(otTables.Coverage)
def intersect(self, glyphs):
"Returns ascending list of matching coverage values."
return [i for i,g in enumerate(self.glyphs) if g in glyphs]
@_add_method(otTables.Coverage)
def intersect_glyphs(self, glyphs):
"Returns set of intersecting glyphs."
return set(g for g in self.glyphs if g in glyphs)
@_add_method(otTables.Coverage)
def subset(self, glyphs):
"Returns ascending list of remaining coverage values."
indices = self.intersect(glyphs)
self.glyphs = [g for g in self.glyphs if g in glyphs]
return indices
@_add_method(otTables.Coverage)
def remap(self, coverage_map):
"Remaps coverage."
self.glyphs = [self.glyphs[i] for i in coverage_map]
@_add_method(otTables.ClassDef)
def intersect(self, glyphs):
"Returns ascending list of matching class values."
return _uniq_sort(
([0] if any(g not in self.classDefs for g in glyphs) else []) +
[v for g,v in self.classDefs.iteritems() if g in glyphs])
@_add_method(otTables.ClassDef)
def intersect_class(self, glyphs, klass):
"Returns set of glyphs matching class."
if klass == 0:
return set(g for g in glyphs if g not in self.classDefs)
return set(g for g,v in self.classDefs.iteritems()
if v == klass and g in glyphs)
@_add_method(otTables.ClassDef)
def subset(self, glyphs, remap=False):
"Returns ascending list of remaining classes."
self.classDefs = dict((g,v) for g,v in self.classDefs.iteritems() if g in glyphs)
# Note: while class 0 has the special meaning of "not matched",
# if no glyph will ever /not match/, we can optimize class 0 out too.
indices = _uniq_sort(
([0] if any(g not in self.classDefs for g in glyphs) else []) +
self.classDefs.values())
if remap:
self.remap(indices)
return indices
@_add_method(otTables.ClassDef)
def remap(self, class_map):
"Remaps classes."
self.classDefs = dict((g,class_map.index(v))
for g,v in self.classDefs.iteritems())
@_add_method(otTables.SingleSubst)
def closure_glyphs(self, s, cur_glyphs=None):
if cur_glyphs == None: cur_glyphs = s.glyphs
if self.Format in [1, 2]:
s.glyphs.update(v for g,v in self.mapping.iteritems() if g in cur_glyphs)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.SingleSubst)
def subset_glyphs(self, s):
if self.Format in [1, 2]:
self.mapping = dict((g,v) for g,v in self.mapping.iteritems()
if g in s.glyphs and v in s.glyphs)
return bool(self.mapping)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.MultipleSubst)
def closure_glyphs(self, s, cur_glyphs=None):
if cur_glyphs == None: cur_glyphs = s.glyphs
if self.Format == 1:
indices = self.Coverage.intersect(cur_glyphs)
_set_update(s.glyphs, *(self.Sequence[i].Substitute for i in indices))
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.MultipleSubst)
def subset_glyphs(self, s):
if self.Format == 1:
indices = self.Coverage.subset(s.glyphs)
self.Sequence = [self.Sequence[i] for i in indices]
# Now drop rules generating glyphs we don't want
indices = [i for i,seq in enumerate(self.Sequence)
if all(sub in s.glyphs for sub in seq.Substitute)]
self.Sequence = [self.Sequence[i] for i in indices]
self.Coverage.remap(indices)
self.SequenceCount = len(self.Sequence)
return bool(self.SequenceCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.AlternateSubst)
def closure_glyphs(self, s, cur_glyphs=None):
if cur_glyphs == None: cur_glyphs = s.glyphs
if self.Format == 1:
_set_update(s.glyphs, *(vlist for g,vlist in self.alternates.iteritems()
if g in cur_glyphs))
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.AlternateSubst)
def subset_glyphs(self, s):
if self.Format == 1:
self.alternates = dict((g,vlist)
for g,vlist in self.alternates.iteritems()
if g in s.glyphs and
all(v in s.glyphs for v in vlist))
return bool(self.alternates)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.LigatureSubst)
def closure_glyphs(self, s, cur_glyphs=None):
if cur_glyphs == None: cur_glyphs = s.glyphs
if self.Format == 1:
_set_update(s.glyphs, *([seq.LigGlyph for seq in seqs
if all(c in s.glyphs for c in seq.Component)]
for g,seqs in self.ligatures.iteritems()
if g in cur_glyphs))
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.LigatureSubst)
def subset_glyphs(self, s):
if self.Format == 1:
self.ligatures = dict((g,v) for g,v in self.ligatures.iteritems()
if g in s.glyphs)
self.ligatures = dict((g,[seq for seq in seqs
if seq.LigGlyph in s.glyphs and
all(c in s.glyphs for c in seq.Component)])
for g,seqs in self.ligatures.iteritems())
self.ligatures = dict((g,v) for g,v in self.ligatures.iteritems() if v)
return bool(self.ligatures)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ReverseChainSingleSubst)
def closure_glyphs(self, s, cur_glyphs=None):
if cur_glyphs == None: cur_glyphs = s.glyphs
if self.Format == 1:
indices = self.Coverage.intersect(cur_glyphs)
if(not indices or
not all(c.intersect(s.glyphs)
for c in self.LookAheadCoverage + self.BacktrackCoverage)):
return
s.glyphs.update(self.Substitute[i] for i in indices)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ReverseChainSingleSubst)
def subset_glyphs(self, s):
if self.Format == 1:
indices = self.Coverage.subset(s.glyphs)
self.Substitute = [self.Substitute[i] for i in indices]
# Now drop rules generating glyphs we don't want
indices = [i for i,sub in enumerate(self.Substitute)
if sub in s.glyphs]
self.Substitute = [self.Substitute[i] for i in indices]
self.Coverage.remap(indices)
self.GlyphCount = len(self.Substitute)
return bool(self.GlyphCount and
all(c.subset(s.glyphs)
for c in self.LookAheadCoverage+self.BacktrackCoverage))
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.SinglePos)
def subset_glyphs(self, s):
if self.Format == 1:
return len(self.Coverage.subset(s.glyphs))
elif self.Format == 2:
indices = self.Coverage.subset(s.glyphs)
self.Value = [self.Value[i] for i in indices]
self.ValueCount = len(self.Value)
return bool(self.ValueCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.SinglePos)
def prune_post_subset(self, options):
if not options.hinting:
# Drop device tables
self.ValueFormat &= ~0x00F0
return True
@_add_method(otTables.PairPos)
def subset_glyphs(self, s):
if self.Format == 1:
indices = self.Coverage.subset(s.glyphs)
self.PairSet = [self.PairSet[i] for i in indices]
for p in self.PairSet:
p.PairValueRecord = [r for r in p.PairValueRecord
if r.SecondGlyph in s.glyphs]
p.PairValueCount = len(p.PairValueRecord)
self.PairSet = [p for p in self.PairSet if p.PairValueCount]
self.PairSetCount = len(self.PairSet)
return bool(self.PairSetCount)
elif self.Format == 2:
class1_map = self.ClassDef1.subset(s.glyphs, remap=True)
class2_map = self.ClassDef2.subset(s.glyphs, remap=True)
self.Class1Record = [self.Class1Record[i] for i in class1_map]
for c in self.Class1Record:
c.Class2Record = [c.Class2Record[i] for i in class2_map]
self.Class1Count = len(class1_map)
self.Class2Count = len(class2_map)
return bool(self.Class1Count and
self.Class2Count and
self.Coverage.subset(s.glyphs))
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.PairPos)
def prune_post_subset(self, options):
if not options.hinting:
# Drop device tables
self.ValueFormat1 &= ~0x00F0
self.ValueFormat2 &= ~0x00F0
return True
@_add_method(otTables.CursivePos)
def subset_glyphs(self, s):
if self.Format == 1:
indices = self.Coverage.subset(s.glyphs)
self.EntryExitRecord = [self.EntryExitRecord[i] for i in indices]
self.EntryExitCount = len(self.EntryExitRecord)
return bool(self.EntryExitCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.Anchor)
def prune_hints(self):
# Drop device tables / contour anchor point
self.Format = 1
@_add_method(otTables.CursivePos)
def prune_post_subset(self, options):
if not options.hinting:
for rec in self.EntryExitRecord:
if rec.EntryAnchor: rec.EntryAnchor.prune_hints()
if rec.ExitAnchor: rec.ExitAnchor.prune_hints()
return True
@_add_method(otTables.MarkBasePos)
def subset_glyphs(self, s):
if self.Format == 1:
mark_indices = self.MarkCoverage.subset(s.glyphs)
self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i]
for i in mark_indices]
self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
base_indices = self.BaseCoverage.subset(s.glyphs)
self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i]
for i in base_indices]
self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord)
# Prune empty classes
class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
self.ClassCount = len(class_indices)
for m in self.MarkArray.MarkRecord:
m.Class = class_indices.index(m.Class)
for b in self.BaseArray.BaseRecord:
b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices]
return bool(self.ClassCount and
self.MarkArray.MarkCount and
self.BaseArray.BaseCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.MarkBasePos)
def prune_post_subset(self, options):
if not options.hinting:
for m in self.MarkArray.MarkRecord:
m.MarkAnchor.prune_hints()
for b in self.BaseArray.BaseRecord:
for a in b.BaseAnchor:
a.prune_hints()
return True
@_add_method(otTables.MarkLigPos)
def subset_glyphs(self, s):
if self.Format == 1:
mark_indices = self.MarkCoverage.subset(s.glyphs)
self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i]
for i in mark_indices]
self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
ligature_indices = self.LigatureCoverage.subset(s.glyphs)
self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i]
for i in ligature_indices]
self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach)
# Prune empty classes
class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
self.ClassCount = len(class_indices)
for m in self.MarkArray.MarkRecord:
m.Class = class_indices.index(m.Class)
for l in self.LigatureArray.LigatureAttach:
for c in l.ComponentRecord:
c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices]
return bool(self.ClassCount and
self.MarkArray.MarkCount and
self.LigatureArray.LigatureCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.MarkLigPos)
def prune_post_subset(self, options):
if not options.hinting:
for m in self.MarkArray.MarkRecord:
m.MarkAnchor.prune_hints()
for l in self.LigatureArray.LigatureAttach:
for c in l.ComponentRecord:
for a in c.LigatureAnchor:
a.prune_hints()
return True
@_add_method(otTables.MarkMarkPos)
def subset_glyphs(self, s):
if self.Format == 1:
mark1_indices = self.Mark1Coverage.subset(s.glyphs)
self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i]
for i in mark1_indices]
self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord)
mark2_indices = self.Mark2Coverage.subset(s.glyphs)
self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i]
for i in mark2_indices]
self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record)
# Prune empty classes
class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord)
self.ClassCount = len(class_indices)
for m in self.Mark1Array.MarkRecord:
m.Class = class_indices.index(m.Class)
for b in self.Mark2Array.Mark2Record:
b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices]
return bool(self.ClassCount and
self.Mark1Array.MarkCount and
self.Mark2Array.MarkCount)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.MarkMarkPos)
def prune_post_subset(self, options):
if not options.hinting:
# Drop device tables or contour anchor point
for m in self.Mark1Array.MarkRecord:
m.MarkAnchor.prune_hints()
for b in self.Mark2Array.Mark2Record:
for m in b.Mark2Anchor:
m.prune_hints()
return True
@_add_method(otTables.SingleSubst,
otTables.MultipleSubst,
otTables.AlternateSubst,
otTables.LigatureSubst,
otTables.ReverseChainSingleSubst,
otTables.SinglePos,
otTables.PairPos,
otTables.CursivePos,
otTables.MarkBasePos,
otTables.MarkLigPos,
otTables.MarkMarkPos)
def subset_lookups(self, lookup_indices):
pass
@_add_method(otTables.SingleSubst,
otTables.MultipleSubst,
otTables.AlternateSubst,
otTables.LigatureSubst,
otTables.ReverseChainSingleSubst,
otTables.SinglePos,
otTables.PairPos,
otTables.CursivePos,
otTables.MarkBasePos,
otTables.MarkLigPos,
otTables.MarkMarkPos)
def collect_lookups(self):
return []
@_add_method(otTables.SingleSubst,
otTables.MultipleSubst,
otTables.AlternateSubst,
otTables.LigatureSubst,
otTables.ContextSubst,
otTables.ChainContextSubst,
otTables.ReverseChainSingleSubst,
otTables.SinglePos,
otTables.PairPos,
otTables.CursivePos,
otTables.MarkBasePos,
otTables.MarkLigPos,
otTables.MarkMarkPos,
otTables.ContextPos,
otTables.ChainContextPos)
def prune_pre_subset(self, options):
return True
@_add_method(otTables.SingleSubst,
otTables.MultipleSubst,
otTables.AlternateSubst,
otTables.LigatureSubst,
otTables.ReverseChainSingleSubst,
otTables.ContextSubst,
otTables.ChainContextSubst,
otTables.ContextPos,
otTables.ChainContextPos)
def prune_post_subset(self, options):
return True
@_add_method(otTables.SingleSubst,
otTables.AlternateSubst,
otTables.ReverseChainSingleSubst)
def may_have_non_1to1(self):
return False
@_add_method(otTables.MultipleSubst,
otTables.LigatureSubst,
otTables.ContextSubst,
otTables.ChainContextSubst)
def may_have_non_1to1(self):
return True
@_add_method(otTables.ContextSubst,
otTables.ChainContextSubst,
otTables.ContextPos,
otTables.ChainContextPos)
def __classify_context(self):
class ContextHelper(object):
def __init__(self, klass, Format):
if klass.__name__.endswith('Subst'):
Typ = 'Sub'
Type = 'Subst'
else:
Typ = 'Pos'
Type = 'Pos'
if klass.__name__.startswith('Chain'):
Chain = 'Chain'
else:
Chain = ''
ChainTyp = Chain+Typ
self.Typ = Typ
self.Type = Type
self.Chain = Chain
self.ChainTyp = ChainTyp
self.LookupRecord = Type+'LookupRecord'
if Format == 1:
Coverage = lambda r: r.Coverage
ChainCoverage = lambda r: r.Coverage
ContextData = lambda r:(None,)
ChainContextData = lambda r:(None, None, None)
RuleData = lambda r:(r.Input,)
ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
SetRuleData = None
ChainSetRuleData = None
elif Format == 2:
Coverage = lambda r: r.Coverage
ChainCoverage = lambda r: r.Coverage
ContextData = lambda r:(r.ClassDef,)
ChainContextData = lambda r:(r.LookAheadClassDef,
r.InputClassDef,
r.BacktrackClassDef)
RuleData = lambda r:(r.Class,)
ChainRuleData = lambda r:(r.LookAhead, r.Input, r.Backtrack)
def SetRuleData(r, d):(r.Class,) = d
def ChainSetRuleData(r, d):(r.LookAhead, r.Input, r.Backtrack) = d
elif Format == 3:
Coverage = lambda r: r.Coverage[0]
ChainCoverage = lambda r: r.InputCoverage[0]
ContextData = None
ChainContextData = None
RuleData = lambda r: r.Coverage
ChainRuleData = lambda r:(r.LookAheadCoverage +
r.InputCoverage +
r.BacktrackCoverage)
SetRuleData = None
ChainSetRuleData = None
else:
assert 0, "unknown format: %s" % Format
if Chain:
self.Coverage = ChainCoverage
self.ContextData = ChainContextData
self.RuleData = ChainRuleData
self.SetRuleData = ChainSetRuleData
else:
self.Coverage = Coverage
self.ContextData = ContextData
self.RuleData = RuleData
self.SetRuleData = SetRuleData
if Format == 1:
self.Rule = ChainTyp+'Rule'
self.RuleCount = ChainTyp+'RuleCount'
self.RuleSet = ChainTyp+'RuleSet'
self.RuleSetCount = ChainTyp+'RuleSetCount'
self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
elif Format == 2:
self.Rule = ChainTyp+'ClassRule'
self.RuleCount = ChainTyp+'ClassRuleCount'
self.RuleSet = ChainTyp+'ClassSet'
self.RuleSetCount = ChainTyp+'ClassSetCount'
self.Intersect = lambda glyphs, c, r: c.intersect_class(glyphs, r)
self.ClassDef = 'InputClassDef' if Chain else 'ClassDef'
self.Input = 'Input' if Chain else 'Class'
if self.Format not in [1, 2, 3]:
return None # Don't shoot the messenger; let it go
if not hasattr(self.__class__, "__ContextHelpers"):
self.__class__.__ContextHelpers = {}
if self.Format not in self.__class__.__ContextHelpers:
helper = ContextHelper(self.__class__, self.Format)
self.__class__.__ContextHelpers[self.Format] = helper
return self.__class__.__ContextHelpers[self.Format]
@_add_method(otTables.ContextSubst,
otTables.ChainContextSubst)
def closure_glyphs(self, s, cur_glyphs=None):
if cur_glyphs == None: cur_glyphs = s.glyphs
c = self.__classify_context()
indices = c.Coverage(self).intersect(s.glyphs)
if not indices:
return []
cur_glyphs = c.Coverage(self).intersect_glyphs(s.glyphs);
if self.Format == 1:
ContextData = c.ContextData(self)
rss = getattr(self, c.RuleSet)
for i in indices:
if not rss[i]: continue
for r in getattr(rss[i], c.Rule):
if not r: continue
if all(all(c.Intersect(s.glyphs, cd, k) for k in klist)
for cd,klist in zip(ContextData, c.RuleData(r))):
chaos = False
for ll in getattr(r, c.LookupRecord):
if not ll: continue
seqi = ll.SequenceIndex
if chaos:
pos_glyphs = s.glyphs
else:
if seqi == 0:
pos_glyphs = set([c.Coverage(self).glyphs[i]])
else:
pos_glyphs = set([r.Input[seqi - 1]])
lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
chaos = chaos or lookup.may_have_non_1to1()
lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
elif self.Format == 2:
ClassDef = getattr(self, c.ClassDef)
indices = ClassDef.intersect(cur_glyphs)
ContextData = c.ContextData(self)
rss = getattr(self, c.RuleSet)
for i in indices:
if not rss[i]: continue
for r in getattr(rss[i], c.Rule):
if not r: continue
if all(all(c.Intersect(s.glyphs, cd, k) for k in klist)
for cd,klist in zip(ContextData, c.RuleData(r))):
chaos = False
for ll in getattr(r, c.LookupRecord):
if not ll: continue
seqi = ll.SequenceIndex
if chaos:
pos_glyphs = s.glyphs
else:
if seqi == 0:
pos_glyphs = ClassDef.intersect_class(cur_glyphs, i)
else:
pos_glyphs = ClassDef.intersect_class(s.glyphs,
getattr(r, c.Input)[seqi - 1])
lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
chaos = chaos or lookup.may_have_non_1to1()
lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
elif self.Format == 3:
if not all(x.intersect(s.glyphs) for x in c.RuleData(self)):
return []
r = self
chaos = False
for ll in getattr(r, c.LookupRecord):
if not ll: continue
seqi = ll.SequenceIndex
if chaos:
pos_glyphs = s.glyphs
else:
if seqi == 0:
pos_glyphs = cur_glyphs
else:
pos_glyphs = r.InputCoverage[seqi].intersect_glyphs(s.glyphs)
lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
chaos = chaos or lookup.may_have_non_1to1()
lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ContextSubst,
otTables.ContextPos,
otTables.ChainContextSubst,
otTables.ChainContextPos)
def subset_glyphs(self, s):
c = self.__classify_context()
if self.Format == 1:
indices = self.Coverage.subset(s.glyphs)
rss = getattr(self, c.RuleSet)
rss = [rss[i] for i in indices]
for rs in rss:
if not rs: continue
ss = getattr(rs, c.Rule)
ss = [r for r in ss
if r and all(all(g in s.glyphs for g in glist)
for glist in c.RuleData(r))]
setattr(rs, c.Rule, ss)
setattr(rs, c.RuleCount, len(ss))
# Prune empty subrulesets
rss = [rs for rs in rss if rs and getattr(rs, c.Rule)]
setattr(self, c.RuleSet, rss)
setattr(self, c.RuleSetCount, len(rss))
return bool(rss)
elif self.Format == 2:
if not self.Coverage.subset(s.glyphs):
return False
indices = getattr(self, c.ClassDef).subset(self.Coverage.glyphs,
remap=False)
rss = getattr(self, c.RuleSet)
rss = [rss[i] for i in indices]
ContextData = c.ContextData(self)
klass_maps = [x.subset(s.glyphs, remap=True) for x in ContextData]
for rs in rss:
if not rs: continue
ss = getattr(rs, c.Rule)
ss = [r for r in ss
if r and all(all(k in klass_map for k in klist)
for klass_map,klist in zip(klass_maps, c.RuleData(r)))]
setattr(rs, c.Rule, ss)
setattr(rs, c.RuleCount, len(ss))
# Remap rule classes
for r in ss:
c.SetRuleData(r, [[klass_map.index(k) for k in klist]
for klass_map,klist in zip(klass_maps, c.RuleData(r))])
# Prune empty subrulesets
rss = [rs for rs in rss if rs and getattr(rs, c.Rule)]
setattr(self, c.RuleSet, rss)
setattr(self, c.RuleSetCount, len(rss))
return bool(rss)
elif self.Format == 3:
return all(x.subset(s.glyphs) for x in c.RuleData(self))
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ContextSubst,
otTables.ChainContextSubst,
otTables.ContextPos,
otTables.ChainContextPos)
def subset_lookups(self, lookup_indices):
c = self.__classify_context()
if self.Format in [1, 2]:
for rs in getattr(self, c.RuleSet):
if not rs: continue
for r in getattr(rs, c.Rule):
if not r: continue
setattr(r, c.LookupRecord,
[ll for ll in getattr(r, c.LookupRecord)
if ll and ll.LookupListIndex in lookup_indices])
for ll in getattr(r, c.LookupRecord):
if not ll: continue
ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
elif self.Format == 3:
setattr(self, c.LookupRecord,
[ll for ll in getattr(self, c.LookupRecord)
if ll and ll.LookupListIndex in lookup_indices])
for ll in getattr(self, c.LookupRecord):
if not ll: continue
ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ContextSubst,
otTables.ChainContextSubst,
otTables.ContextPos,
otTables.ChainContextPos)
def collect_lookups(self):
c = self.__classify_context()
if self.Format in [1, 2]:
return [ll.LookupListIndex
for rs in getattr(self, c.RuleSet) if rs
for r in getattr(rs, c.Rule) if r
for ll in getattr(r, c.LookupRecord) if ll]
elif self.Format == 3:
return [ll.LookupListIndex
for ll in getattr(self, c.LookupRecord) if ll]
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ExtensionSubst)
def closure_glyphs(self, s, cur_glyphs=None):
if self.Format == 1:
self.ExtSubTable.closure_glyphs(s, cur_glyphs)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ExtensionSubst)
def may_have_non_1to1(self):
if self.Format == 1:
return self.ExtSubTable.may_have_non_1to1()
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ExtensionSubst,
otTables.ExtensionPos)
def prune_pre_subset(self, options):
if self.Format == 1:
return self.ExtSubTable.prune_pre_subset(options)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ExtensionSubst,
otTables.ExtensionPos)
def subset_glyphs(self, s):
if self.Format == 1:
return self.ExtSubTable.subset_glyphs(s)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ExtensionSubst,
otTables.ExtensionPos)
def prune_post_subset(self, options):
if self.Format == 1:
return self.ExtSubTable.prune_post_subset(options)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ExtensionSubst,
otTables.ExtensionPos)
def subset_lookups(self, lookup_indices):
if self.Format == 1:
return self.ExtSubTable.subset_lookups(lookup_indices)
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.ExtensionSubst,
otTables.ExtensionPos)
def collect_lookups(self):
if self.Format == 1:
return self.ExtSubTable.collect_lookups()
else:
assert 0, "unknown format: %s" % self.Format
@_add_method(otTables.Lookup)
def closure_glyphs(self, s, cur_glyphs=None):
for st in self.SubTable:
if not st: continue
st.closure_glyphs(s, cur_glyphs)
@_add_method(otTables.Lookup)
def prune_pre_subset(self, options):
ret = False
for st in self.SubTable:
if not st: continue
if st.prune_pre_subset(options): ret = True
return ret
@_add_method(otTables.Lookup)
def subset_glyphs(self, s):
self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)]
self.SubTableCount = len(self.SubTable)
return bool(self.SubTableCount)
@_add_method(otTables.Lookup)
def prune_post_subset(self, options):
ret = False
for st in self.SubTable:
if not st: continue
if st.prune_post_subset(options): ret = True
return ret
@_add_method(otTables.Lookup)
def subset_lookups(self, lookup_indices):
for s in self.SubTable:
s.subset_lookups(lookup_indices)
@_add_method(otTables.Lookup)
def collect_lookups(self):
return _uniq_sort(sum((st.collect_lookups() for st in self.SubTable
if st), []))
@_add_method(otTables.Lookup)
def may_have_non_1to1(self):
return any(st.may_have_non_1to1() for st in self.SubTable if st)
@_add_method(otTables.LookupList)
def prune_pre_subset(self, options):
ret = False
for l in self.Lookup:
if not l: continue
if l.prune_pre_subset(options): ret = True
return ret
@_add_method(otTables.LookupList)
def subset_glyphs(self, s):
"Returns the indices of nonempty lookups."
return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)]
@_add_method(otTables.LookupList)
def prune_post_subset(self, options):
ret = False
for l in self.Lookup:
if not l: continue
if l.prune_post_subset(options): ret = True
return ret
@_add_method(otTables.LookupList)
def subset_lookups(self, lookup_indices):
self.Lookup = [self.Lookup[i] for i in lookup_indices
if i < self.LookupCount]
self.LookupCount = len(self.Lookup)
for l in self.Lookup:
l.subset_lookups(lookup_indices)
@_add_method(otTables.LookupList)
def closure_lookups(self, lookup_indices):
lookup_indices = _uniq_sort(lookup_indices)
recurse = lookup_indices
while True:
recurse_lookups = sum((self.Lookup[i].collect_lookups()
for i in recurse if i < self.LookupCount), [])
recurse_lookups = [l for l in recurse_lookups
if l not in lookup_indices and l < self.LookupCount]
if not recurse_lookups:
return _uniq_sort(lookup_indices)
recurse_lookups = _uniq_sort(recurse_lookups)
lookup_indices.extend(recurse_lookups)
recurse = recurse_lookups
@_add_method(otTables.Feature)
def subset_lookups(self, lookup_indices):
self.LookupListIndex = [l for l in self.LookupListIndex
if l in lookup_indices]
# Now map them.
self.LookupListIndex = [lookup_indices.index(l)
for l in self.LookupListIndex]
self.LookupCount = len(self.LookupListIndex)
return self.LookupCount
@_add_method(otTables.Feature)
def collect_lookups(self):
return self.LookupListIndex[:]
@_add_method(otTables.FeatureList)
def subset_lookups(self, lookup_indices):
"Returns the indices of nonempty features."
feature_indices = [i for i,f in enumerate(self.FeatureRecord)
if f.Feature.subset_lookups(lookup_indices)]
self.subset_features(feature_indices)
return feature_indices
@_add_method(otTables.FeatureList)
def collect_lookups(self, feature_indices):
return _uniq_sort(sum((self.FeatureRecord[i].Feature.collect_lookups()
for i in feature_indices
if i < self.FeatureCount), []))
@_add_method(otTables.FeatureList)
def subset_features(self, feature_indices):
self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices]
self.FeatureCount = len(self.FeatureRecord)
return bool(self.FeatureCount)
@_add_method(otTables.DefaultLangSys,
otTables.LangSys)
def subset_features(self, feature_indices):
if self.ReqFeatureIndex in feature_indices:
self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex)
else:
self.ReqFeatureIndex = 65535
self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices]
# Now map them.
self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex
if f in feature_indices]
self.FeatureCount = len(self.FeatureIndex)
return bool(self.FeatureCount or self.ReqFeatureIndex != 65535)
@_add_method(otTables.DefaultLangSys,
otTables.LangSys)
def collect_features(self):
feature_indices = self.FeatureIndex[:]
if self.ReqFeatureIndex != 65535:
feature_indices.append(self.ReqFeatureIndex)
return _uniq_sort(feature_indices)
@_add_method(otTables.Script)
def subset_features(self, feature_indices):
if(self.DefaultLangSys and
not self.DefaultLangSys.subset_features(feature_indices)):
self.DefaultLangSys = None
self.LangSysRecord = [l for l in self.LangSysRecord
if l.LangSys.subset_features(feature_indices)]
self.LangSysCount = len(self.LangSysRecord)
return bool(self.LangSysCount or self.DefaultLangSys)
@_add_method(otTables.Script)
def collect_features(self):
feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord]
if self.DefaultLangSys:
feature_indices.append(self.DefaultLangSys.collect_features())
return _uniq_sort(sum(feature_indices, []))
@_add_method(otTables.ScriptList)
def subset_features(self, feature_indices):
self.ScriptRecord = [s for s in self.ScriptRecord
if s.Script.subset_features(feature_indices)]
self.ScriptCount = len(self.ScriptRecord)
return bool(self.ScriptCount)
@_add_method(otTables.ScriptList)
def collect_features(self):
return _uniq_sort(sum((s.Script.collect_features()
for s in self.ScriptRecord), []))
@_add_method(ttLib.getTableClass('GSUB'))
def closure_glyphs(self, s):
s.table = self.table
feature_indices = self.table.ScriptList.collect_features()
lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
while True:
orig_glyphs = s.glyphs.copy()
for i in lookup_indices:
if i >= self.table.LookupList.LookupCount: continue
if not self.table.LookupList.Lookup[i]: continue
self.table.LookupList.Lookup[i].closure_glyphs(s)
if orig_glyphs == s.glyphs:
break
del s.table
@_add_method(ttLib.getTableClass('GSUB'),
ttLib.getTableClass('GPOS'))
def subset_glyphs(self, s):
s.glyphs = s.glyphs_gsubed
lookup_indices = self.table.LookupList.subset_glyphs(s)
self.subset_lookups(lookup_indices)
self.prune_lookups()
return True
@_add_method(ttLib.getTableClass('GSUB'),
ttLib.getTableClass('GPOS'))
def subset_lookups(self, lookup_indices):
"""Retrains specified lookups, then removes empty features, language
systems, and scripts."""
self.table.LookupList.subset_lookups(lookup_indices)
feature_indices = self.table.FeatureList.subset_lookups(lookup_indices)
self.table.ScriptList.subset_features(feature_indices)
@_add_method(ttLib.getTableClass('GSUB'),
ttLib.getTableClass('GPOS'))
def prune_lookups(self):
"Remove unreferenced lookups"
feature_indices = self.table.ScriptList.collect_features()
lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
lookup_indices = self.table.LookupList.closure_lookups(lookup_indices)
self.subset_lookups(lookup_indices)
@_add_method(ttLib.getTableClass('GSUB'),
ttLib.getTableClass('GPOS'))
def subset_feature_tags(self, feature_tags):
feature_indices = [i for i,f in
enumerate(self.table.FeatureList.FeatureRecord)
if f.FeatureTag in feature_tags]
self.table.FeatureList.subset_features(feature_indices)
self.table.ScriptList.subset_features(feature_indices)
@_add_method(ttLib.getTableClass('GSUB'),
ttLib.getTableClass('GPOS'))
def prune_pre_subset(self, options):
if '*' not in options.layout_features:
self.subset_feature_tags(options.layout_features)
self.prune_lookups()
self.table.LookupList.prune_pre_subset(options);
return True
@_add_method(ttLib.getTableClass('GSUB'),
ttLib.getTableClass('GPOS'))
def prune_post_subset(self, options):
self.table.LookupList.prune_post_subset(options);
return True
@_add_method(ttLib.getTableClass('GDEF'))
def subset_glyphs(self, s):
glyphs = s.glyphs_gsubed
table = self.table
if table.LigCaretList:
indices = table.LigCaretList.Coverage.subset(glyphs)
table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i]
for i in indices]
table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph)
if not table.LigCaretList.LigGlyphCount:
table.LigCaretList = None
if table.MarkAttachClassDef:
table.MarkAttachClassDef.classDefs = dict((g,v) for g,v in
table.MarkAttachClassDef.
classDefs.iteritems()
if g in glyphs)
if not table.MarkAttachClassDef.classDefs:
table.MarkAttachClassDef = None
if table.GlyphClassDef:
table.GlyphClassDef.classDefs = dict((g,v) for g,v in
table.GlyphClassDef.
classDefs.iteritems()
if g in glyphs)
if not table.GlyphClassDef.classDefs:
table.GlyphClassDef = None
if table.AttachList:
indices = table.AttachList.Coverage.subset(glyphs)
table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i]
for i in indices]
table.AttachList.GlyphCount = len(table.AttachList.AttachPoint)
if not table.AttachList.GlyphCount:
table.AttachList = None
return bool(table.LigCaretList or
table.MarkAttachClassDef or
table.GlyphClassDef or
table.AttachList)
@_add_method(ttLib.getTableClass('kern'))
def prune_pre_subset(self, options):
# Prune unknown kern table types
self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')]
return bool(self.kernTables)
@_add_method(ttLib.getTableClass('kern'))
def subset_glyphs(self, s):
glyphs = s.glyphs_gsubed
for t in self.kernTables:
t.kernTable = dict(((a,b),v) for (a,b),v in t.kernTable.iteritems()
if a in glyphs and b in glyphs)
self.kernTables = [t for t in self.kernTables if t.kernTable]
return bool(self.kernTables)
@_add_method(ttLib.getTableClass('vmtx'),
ttLib.getTableClass('hmtx'))
def subset_glyphs(self, s):
self.metrics = dict((g,v) for g,v in self.metrics.iteritems() if g in s.glyphs)
return bool(self.metrics)
@_add_method(ttLib.getTableClass('hdmx'))
def subset_glyphs(self, s):
self.hdmx = dict((sz,_dict((g,v) for g,v in l.iteritems() if g in s.glyphs))
for sz,l in self.hdmx.iteritems())
return bool(self.hdmx)
@_add_method(ttLib.getTableClass('VORG'))
def subset_glyphs(self, s):
self.VOriginRecords = dict((g,v) for g,v in self.VOriginRecords.iteritems()
if g in s.glyphs)
self.numVertOriginYMetrics = len(self.VOriginRecords)
return True # Never drop; has default metrics
@_add_method(ttLib.getTableClass('post'))
def prune_pre_subset(self, options):
if not options.glyph_names:
self.formatType = 3.0
return True
@_add_method(ttLib.getTableClass('post'))
def subset_glyphs(self, s):
self.extraNames = [] # This seems to do it
return True
@_add_method(ttLib.getTableModule('glyf').Glyph)
def remapComponentsFast(self, indices):
if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
return # Not composite
data = array.array("B", self.data)
i = 10
more = 1
while more:
flags =(data[i] << 8) | data[i+1]
glyphID =(data[i+2] << 8) | data[i+3]
# Remap
glyphID = indices.index(glyphID)
data[i+2] = glyphID >> 8
data[i+3] = glyphID & 0xFF
i += 4
flags = int(flags)
if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS
else: i += 2
if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE
elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE
elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO
more = flags & 0x0020 # MORE_COMPONENTS
self.data = data.tostring()
@_add_method(ttLib.getTableClass('glyf'))
def closure_glyphs(self, s):
decompose = s.glyphs
while True:
components = set()
for g in decompose:
if g not in self.glyphs:
continue
gl = self.glyphs[g]
for c in gl.getComponentNames(self):
if c not in s.glyphs:
components.add(c)
components = set(c for c in components if c not in s.glyphs)
if not components:
break
decompose = components
s.glyphs.update(components)
@_add_method(ttLib.getTableClass('glyf'))
def prune_pre_subset(self, options):
if options.notdef_glyph and not options.notdef_outline:
g = self[self.glyphOrder[0]]
# Yay, easy!
g.__dict__.clear()
g.data = ""
return True
@_add_method(ttLib.getTableClass('glyf'))
def subset_glyphs(self, s):
self.glyphs = dict((g,v) for g,v in self.glyphs.iteritems() if g in s.glyphs)
indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs]
for v in self.glyphs.itervalues():
if hasattr(v, "data"):
v.remapComponentsFast(indices)
else:
pass # No need
self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs]
# Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset.
return True
@_add_method(ttLib.getTableClass('glyf'))
def prune_post_subset(self, options):
if not options.hinting:
for v in self.glyphs.itervalues():
v.removeHinting()
return True
@_add_method(ttLib.getTableClass('CFF '))
def prune_pre_subset(self, options):
cff = self.cff
# CFF table must have one font only
cff.fontNames = cff.fontNames[:1]
if options.notdef_glyph and not options.notdef_outline:
for fontname in cff.keys():
font = cff[fontname]
c,_ = font.CharStrings.getItemAndSelector('.notdef')
# XXX we should preserve the glyph width
c.bytecode = '\x0e' # endchar
c.program = None
return True # bool(cff.fontNames)
@_add_method(ttLib.getTableClass('CFF '))
def subset_glyphs(self, s):
cff = self.cff
for fontname in cff.keys():
font = cff[fontname]
cs = font.CharStrings
# Load all glyphs
for g in font.charset:
if g not in s.glyphs: continue
c,sel = cs.getItemAndSelector(g)
if cs.charStringsAreIndexed:
indices = [i for i,g in enumerate(font.charset) if g in s.glyphs]
csi = cs.charStringsIndex
csi.items = [csi.items[i] for i in indices]
csi.count = len(csi.items)
del csi.file, csi.offsets
if hasattr(font, "FDSelect"):
sel = font.FDSelect
sel.format = None
sel.gidArray = [sel.gidArray[i] for i in indices]
cs.charStrings = dict((g,indices.index(v))
for g,v in cs.charStrings.iteritems()
if g in s.glyphs)
else:
cs.charStrings = dict((g,v)
for g,v in cs.charStrings.iteritems()
if g in s.glyphs)
font.charset = [g for g in font.charset if g in s.glyphs]
font.numGlyphs = len(font.charset)
return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
@_add_method(psCharStrings.T2CharString)
def subset_subroutines(self, subrs, gsubrs):
p = self.program
assert len(p)
for i in xrange(1, len(p)):
if p[i] == 'callsubr':
assert type(p[i-1]) is int
p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias
elif p[i] == 'callgsubr':
assert type(p[i-1]) is int
p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias
@_add_method(psCharStrings.T2CharString)
def drop_hints(self):
hints = self._hints
if hints.has_hint:
self.program = self.program[hints.last_hint:]
if hasattr(self, 'width'):
# Insert width back if needed
if self.width != self.private.defaultWidthX:
self.program.insert(0, self.width - self.private.nominalWidthX)
if hints.has_hintmask:
i = 0
p = self.program
while i < len(p):
if p[i] in ['hintmask', 'cntrmask']:
assert i + 1 <= len(p)
del p[i:i+2]
continue
i += 1
assert len(self.program)
del self._hints
class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler):
def __init__(self, localSubrs, globalSubrs):
psCharStrings.SimpleT2Decompiler.__init__(self,
localSubrs,
globalSubrs)
for subrs in [localSubrs, globalSubrs]:
if subrs and not hasattr(subrs, "_used"):
subrs._used = set()
def op_callsubr(self, index):
self.localSubrs._used.add(self.operandStack[-1]+self.localBias)
psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
def op_callgsubr(self, index):
self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias)
psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
class _DehintingT2Decompiler(psCharStrings.SimpleT2Decompiler):
class Hints:
def __init__(self):
# Whether calling this charstring produces any hint stems
self.has_hint = False
# Index to start at to drop all hints
self.last_hint = 0
# Index up to which we know more hints are possible. Only
# relevant if status is 0 or 1.
self.last_checked = 0
# The status means:
# 0: after dropping hints, this charstring is empty
# 1: after dropping hints, there may be more hints continuing after this
# 2: no more hints possible after this charstring
self.status = 0
# Has hintmask instructions; not recursive
self.has_hintmask = False
pass
def __init__(self, css, localSubrs, globalSubrs):
self._css = css
psCharStrings.SimpleT2Decompiler.__init__(self,
localSubrs,
globalSubrs)
def execute(self, charString):
old_hints = charString._hints if hasattr(charString, '_hints') else None
charString._hints = self.Hints()
psCharStrings.SimpleT2Decompiler.execute(self, charString)
hints = charString._hints
if hints.has_hint or hints.has_hintmask:
self._css.add(charString)
if hints.status != 2:
# Check from last_check, make sure we didn't have any operators.
for i in xrange(hints.last_checked, len(charString.program) - 1):
if type(charString.program[i]) == str:
hints.status = 2
break;
else:
hints.status = 1 # There's *something* here
hints.last_checked = len(charString.program)
if old_hints:
assert hints.__dict__ == old_hints.__dict__
def op_callsubr(self, index):
subr = self.localSubrs[self.operandStack[-1]+self.localBias]
psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
self.processSubr(index, subr)
def op_callgsubr(self, index):
subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
self.processSubr(index, subr)
def op_hstem(self, index):
psCharStrings.SimpleT2Decompiler.op_hstem(self, index)
self.processHint(index)
def op_vstem(self, index):
psCharStrings.SimpleT2Decompiler.op_vstem(self, index)
self.processHint(index)
def op_hstemhm(self, index):
psCharStrings.SimpleT2Decompiler.op_hstemhm(self, index)
self.processHint(index)
def op_vstemhm(self, index):
psCharStrings.SimpleT2Decompiler.op_vstemhm(self, index)
self.processHint(index)
def op_hintmask(self, index):
psCharStrings.SimpleT2Decompiler.op_hintmask(self, index)
self.processHintmask(index)
def op_cntrmask(self, index):
psCharStrings.SimpleT2Decompiler.op_cntrmask(self, index)
self.processHintmask(index)
def processHintmask(self, index):
cs = self.callingStack[-1]
hints = cs._hints
hints.has_hintmask = True
if hints.status != 2 and hints.has_hint:
# Check from last_check, see if we may be an implicit vstem
for i in xrange(hints.last_checked, index - 1):
if type(cs.program[i]) == str:
hints.status = 2
break;
if hints.status != 2:
# We are an implicit vstem
hints.last_hint = index + 1
hints.status = 0
hints.last_checked = index + 1
def processHint(self, index):
cs = self.callingStack[-1]
hints = cs._hints
hints.has_hint = True
hints.last_hint = index
hints.last_checked = index
def processSubr(self, index, subr):
cs = self.callingStack[-1]
hints = cs._hints
subr_hints = subr._hints
if subr_hints.has_hint:
if hints.status != 2:
hints.has_hint = True
hints.last_checked = index
hints.status = subr_hints.status
# Decide where to chop off from
if subr_hints.status == 0:
hints.last_hint = index
else:
hints.last_hint = index - 2 # Leave the subr call in
else:
# In my understanding, this is a font bug. Ie. it has hint stems
# *after* path construction. I've seen this in widespread fonts.
# Best to ignore the hints I suppose...
pass
#assert 0
else:
hints.status = max(hints.status, subr_hints.status)
if hints.status != 2:
# Check from last_check, make sure we didn't have
# any operators.
for i in xrange(hints.last_checked, index - 1):
if type(cs.program[i]) == str:
hints.status = 2
break;
hints.last_checked = index
@_add_method(ttLib.getTableClass('CFF '))
def prune_post_subset(self, options):
cff = self.cff
for fontname in cff.keys():
font = cff[fontname]
cs = font.CharStrings
#
# Drop unused FontDictionaries
#
if hasattr(font, "FDSelect"):
sel = font.FDSelect
indices = _uniq_sort(sel.gidArray)
sel.gidArray = [indices.index (ss) for ss in sel.gidArray]
arr = font.FDArray
arr.items = [arr[i] for i in indices]
arr.count = len(arr.items)
del arr.file, arr.offsets
#
# Drop hints if not needed
#
if not options.hinting:
#
# This can be tricky, but doesn't have to. What we do is:
#
# - Run all used glyph charstrings and recurse into subroutines,
# - For each charstring (including subroutines), if it has any
# of the hint stem operators, we mark it as such. Upon returning,
# for each charstring we note all the subroutine calls it makes
# that (recursively) contain a stem,
# - Dropping hinting then consists of the following two ops:
# * Drop the piece of the program in each charstring before the
# last call to a stem op or a stem-calling subroutine,
# * Drop all hintmask operations.
# - It's trickier... A hintmask right after hints and a few numbers
# will act as an implicit vstemhm. As such, we track whether
# we have seen any non-hint operators so far and do the right
# thing, recursively... Good luck understanding that :(
#
css = set()
for g in font.charset:
c,sel = cs.getItemAndSelector(g)
# Make sure it's decompiled. We want our "decompiler" to walk
# the program, not the bytecode.
c.draw(basePen.NullPen())
subrs = getattr(c.private, "Subrs", [])
decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs)
decompiler.execute(c)
for charstring in css:
charstring.drop_hints()
# Drop font-wide hinting values
all_privs = []
if hasattr(font, 'FDSelect'):
all_privs.extend(fd.Private for fd in font.FDArray)
else:
all_privs.append(font.Private)
for priv in all_privs:
priv.BlueValues = []
for k in ['OtherBlues', 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW', \
'FamilyBlues', 'FamilyOtherBlues']:
if hasattr(priv, k):
setattr(priv, k, None)
#
# Renumber subroutines to remove unused ones
#
# Mark all used subroutines
for g in font.charset:
c,sel = cs.getItemAndSelector(g)
subrs = getattr(c.private, "Subrs", [])
decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs)
decompiler.execute(c)
all_subrs = [font.GlobalSubrs]
if hasattr(font, 'FDSelect'):
all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs)
elif hasattr(font.Private, 'Subrs') and font.Private.Subrs:
all_subrs.append(font.Private.Subrs)
subrs = set(subrs) # Remove duplicates
# Prepare
for subrs in all_subrs:
if not hasattr(subrs, '_used'):
subrs._used = set()
subrs._used = _uniq_sort(subrs._used)
subrs._old_bias = psCharStrings.calcSubrBias(subrs)
subrs._new_bias = psCharStrings.calcSubrBias(subrs._used)
# Renumber glyph charstrings
for g in font.charset:
c,sel = cs.getItemAndSelector(g)
subrs = getattr(c.private, "Subrs", [])
c.subset_subroutines (subrs, font.GlobalSubrs)
# Renumber subroutines themselves
for subrs in all_subrs:
if subrs == font.GlobalSubrs:
if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'):
local_subrs = font.Private.Subrs
else:
local_subrs = []
else:
local_subrs = subrs
subrs.items = [subrs.items[i] for i in subrs._used]
subrs.count = len(subrs.items)
del subrs.file
if hasattr(subrs, 'offsets'):
del subrs.offsets
for i in xrange (subrs.count):
subrs[i].subset_subroutines (local_subrs, font.GlobalSubrs)
# Cleanup
for subrs in all_subrs:
del subrs._used, subrs._old_bias, subrs._new_bias
return True
@_add_method(ttLib.getTableClass('cmap'))
def closure_glyphs(self, s):
tables = [t for t in self.tables
if t.platformID == 3 and t.platEncID in [1, 10]]
for u in s.unicodes_requested:
found = False
for table in tables:
if u in table.cmap:
s.glyphs.add(table.cmap[u])
found = True
break
if not found:
s.log("No glyph for Unicode value %s; skipping." % u)
@_add_method(ttLib.getTableClass('cmap'))
def prune_pre_subset(self, options):
if not options.legacy_cmap:
# Drop non-Unicode / non-Symbol cmaps
self.tables = [t for t in self.tables
if t.platformID == 3 and t.platEncID in [0, 1, 10]]
if not options.symbol_cmap:
self.tables = [t for t in self.tables
if t.platformID == 3 and t.platEncID in [1, 10]]
# TODO(behdad) Only keep one subtable?
# For now, drop format=0 which can't be subset_glyphs easily?
self.tables = [t for t in self.tables if t.format != 0]
self.numSubTables = len(self.tables)
return bool(self.tables)
@_add_method(ttLib.getTableClass('cmap'))
def subset_glyphs(self, s):
s.glyphs = s.glyphs_cmaped
for t in self.tables:
# For reasons I don't understand I need this here
# to force decompilation of the cmap format 14.
try:
getattr(t, "asdf")
except AttributeError:
pass
if t.format == 14:
# TODO(behdad) XXX We drop all the default-UVS mappings(g==None).
t.uvsDict = dict((v,[(u,g) for u,g in l if g in s.glyphs])
for v,l in t.uvsDict.iteritems())
t.uvsDict = dict((v,l) for v,l in t.uvsDict.iteritems() if l)
else:
t.cmap = dict((u,g) for u,g in t.cmap.iteritems()
if g in s.glyphs_requested or u in s.unicodes_requested)
self.tables = [t for t in self.tables
if (t.cmap if t.format != 14 else t.uvsDict)]
self.numSubTables = len(self.tables)
# TODO(behdad) Convert formats when needed.
# In particular, if we have a format=12 without non-BMP
# characters, either drop format=12 one or convert it
# to format=4 if there's not one.
return bool(self.tables)
@_add_method(ttLib.getTableClass('name'))
def prune_pre_subset(self, options):
if '*' not in options.name_IDs:
self.names = [n for n in self.names if n.nameID in options.name_IDs]
if not options.name_legacy:
self.names = [n for n in self.names
if n.platformID == 3 and n.platEncID == 1]
if '*' not in options.name_languages:
self.names = [n for n in self.names if n.langID in options.name_languages]
return True # Retain even if empty
# TODO(behdad) OS/2 ulUnicodeRange / ulCodePageRange?
# TODO(behdad) Drop unneeded GSUB/GPOS Script/LangSys entries.
# TODO(behdad) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left
# TODO(behdad) Drop GDEF subitems if unused by lookups
# TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF)
# TODO(behdad) Text direction considerations.
# TODO(behdad) Text script / language considerations.
class Options(object):
class UnknownOptionError(Exception):
pass
_drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'SVG ',
'PCLT', 'LTSH']
_drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite
_drop_tables_default += ['CBLC', 'CBDT', 'sbix', 'COLR', 'CPAL'] # Color
_no_subset_tables_default = ['gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2',
'loca', 'name', 'cvt ', 'fpgm', 'prep']
_hinting_tables_default = ['cvt ', 'fpgm', 'prep', 'hdmx', 'VDMX']
# Based on HarfBuzz shapers
_layout_features_groups = {
# Default shaper
'common': ['ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'],
'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'],
'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'],
'ltr': ['ltra', 'ltrm'],
'rtl': ['rtla', 'rtlm'],
# Complex shapers
'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3',
'cswh', 'mset'],
'hangul': ['ljmo', 'vjmo', 'tjmo'],
'tibetal': ['abvs', 'blws', 'abvm', 'blwm'],
'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half',
'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres',
'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'],
}
_layout_features_default = _uniq_sort(sum(
_layout_features_groups.itervalues(), []))
drop_tables = _drop_tables_default
no_subset_tables = _no_subset_tables_default
hinting_tables = _hinting_tables_default
layout_features = _layout_features_default
hinting = False
glyph_names = False
legacy_cmap = False
symbol_cmap = False
name_IDs = [1, 2] # Family and Style
name_legacy = False
name_languages = [0x0409] # English
notdef_glyph = True # gid0 for TrueType / .notdef for CFF
notdef_outline = False # No need for notdef to have an outline really
recommended_glyphs = False # gid1, gid2, gid3 for TrueType
recalc_bounds = False # Recalculate font bounding boxes
canonical_order = False # Order tables as recommended
flavor = None # May be 'woff'
def __init__(self, **kwargs):
self.set(**kwargs)
def set(self, **kwargs):
for k,v in kwargs.iteritems():
if not hasattr(self, k):
raise self.UnknownOptionError("Unknown option '%s'" % k)
setattr(self, k, v)
def parse_opts(self, argv, ignore_unknown=False):
ret = []
opts = {}
for a in argv:
orig_a = a
if not a.startswith('--'):
ret.append(a)
continue
a = a[2:]
i = a.find('=')
op = '='
if i == -1:
if a.startswith("no-"):
k = a[3:]
v = False
else:
k = a
v = True
else:
k = a[:i]
if k[-1] in "-+":
op = k[-1]+'=' # Ops is '-=' or '+=' now.
k = k[:-1]
v = a[i+1:]
k = k.replace('-', '_')
if not hasattr(self, k):
if ignore_unknown == True or k in ignore_unknown:
ret.append(orig_a)
continue
else:
raise self.UnknownOptionError("Unknown option '%s'" % a)
ov = getattr(self, k)
if isinstance(ov, bool):
v = bool(v)
elif isinstance(ov, int):
v = int(v)
elif isinstance(ov, list):
vv = v.split(',')
if vv == ['']:
vv = []
vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
if op == '=':
v = vv
elif op == '+=':
v = ov
v.extend(vv)
elif op == '-=':
v = ov
for x in vv:
if x in v:
v.remove(x)
else:
assert 0
opts[k] = v
self.set(**opts)
return ret
class Subsetter(object):
def __init__(self, options=None, log=None):
if not log:
log = Logger()
if not options:
options = Options()
self.options = options
self.log = log
self.unicodes_requested = set()
self.glyphs_requested = set()
self.glyphs = set()
def populate(self, glyphs=[], unicodes=[], text=""):
self.unicodes_requested.update(unicodes)
if isinstance(text, str):
text = text.decode("utf8")
for u in text:
self.unicodes_requested.add(ord(u))
self.glyphs_requested.update(glyphs)
self.glyphs.update(glyphs)
def _prune_pre_subset(self, font):
for tag in font.keys():
if tag == 'GlyphOrder': continue
if(tag in self.options.drop_tables or
(tag in self.options.hinting_tables and not self.options.hinting)):
self.log(tag, "dropped")
del font[tag]
continue
clazz = ttLib.getTableClass(tag)
if hasattr(clazz, 'prune_pre_subset'):
table = font[tag]
self.log.lapse("load '%s'" % tag)
retain = table.prune_pre_subset(self.options)
self.log.lapse("prune '%s'" % tag)
if not retain:
self.log(tag, "pruned to empty; dropped")
del font[tag]
continue
else:
self.log(tag, "pruned")
def _closure_glyphs(self, font):
self.glyphs = self.glyphs_requested.copy()
if 'cmap' in font:
font['cmap'].closure_glyphs(self)
self.glyphs_cmaped = self.glyphs
if self.options.notdef_glyph:
if 'glyf' in font:
self.glyphs.add(font.getGlyphName(0))
self.log("Added gid0 to subset")
else:
self.glyphs.add('.notdef')
self.log("Added .notdef to subset")
if self.options.recommended_glyphs:
if 'glyf' in font:
for i in range(4):
self.glyphs.add(font.getGlyphName(i))
self.log("Added first four glyphs to subset")
if 'GSUB' in font:
self.log("Closing glyph list over 'GSUB': %d glyphs before" %
len(self.glyphs))
self.log.glyphs(self.glyphs, font=font)
font['GSUB'].closure_glyphs(self)
self.log("Closed glyph list over 'GSUB': %d glyphs after" %
len(self.glyphs))
self.log.glyphs(self.glyphs, font=font)
self.log.lapse("close glyph list over 'GSUB'")
self.glyphs_gsubed = self.glyphs.copy()
if 'glyf' in font:
self.log("Closing glyph list over 'glyf': %d glyphs before" %
len(self.glyphs))
self.log.glyphs(self.glyphs, font=font)
font['glyf'].closure_glyphs(self)
self.log("Closed glyph list over 'glyf': %d glyphs after" %
len(self.glyphs))
self.log.glyphs(self.glyphs, font=font)
self.log.lapse("close glyph list over 'glyf'")
self.glyphs_glyfed = self.glyphs.copy()
self.glyphs_all = self.glyphs.copy()
self.log("Retaining %d glyphs: " % len(self.glyphs_all))
def _subset_glyphs(self, font):
for tag in font.keys():
if tag == 'GlyphOrder': continue
clazz = ttLib.getTableClass(tag)
if tag in self.options.no_subset_tables:
self.log(tag, "subsetting not needed")
elif hasattr(clazz, 'subset_glyphs'):
table = font[tag]
self.glyphs = self.glyphs_all
retain = table.subset_glyphs(self)
self.glyphs = self.glyphs_all
self.log.lapse("subset '%s'" % tag)
if not retain:
self.log(tag, "subsetted to empty; dropped")
del font[tag]
else:
self.log(tag, "subsetted")
else:
self.log(tag, "NOT subset; don't know how to subset; dropped")
del font[tag]
glyphOrder = font.getGlyphOrder()
glyphOrder = [g for g in glyphOrder if g in self.glyphs_all]
font.setGlyphOrder(glyphOrder)
font._buildReverseGlyphOrderDict()
self.log.lapse("subset GlyphOrder")
def _prune_post_subset(self, font):
for tag in font.keys():
if tag == 'GlyphOrder': continue
clazz = ttLib.getTableClass(tag)
if hasattr(clazz, 'prune_post_subset'):
table = font[tag]
retain = table.prune_post_subset(self.options)
self.log.lapse("prune '%s'" % tag)
if not retain:
self.log(tag, "pruned to empty; dropped")
del font[tag]
else:
self.log(tag, "pruned")
def subset(self, font):
self._prune_pre_subset(font)
self._closure_glyphs(font)
self._subset_glyphs(font)
self._prune_post_subset(font)
class Logger(object):
def __init__(self, verbose=False, xml=False, timing=False):
self.verbose = verbose
self.xml = xml
self.timing = timing
self.last_time = self.start_time = time.time()
def parse_opts(self, argv):
argv = argv[:]
for v in ['verbose', 'xml', 'timing']:
if "--"+v in argv:
setattr(self, v, True)
argv.remove("--"+v)
return argv
def __call__(self, *things):
if not self.verbose:
return
print ' '.join(str(x) for x in things)
def lapse(self, *things):
if not self.timing:
return
new_time = time.time()
print "Took %0.3fs to %s" %(new_time - self.last_time,
' '.join(str(x) for x in things))
self.last_time = new_time
def glyphs(self, glyphs, font=None):
self("Names: ", sorted(glyphs))
if font:
reverseGlyphMap = font.getReverseGlyphMap()
self("Gids : ", sorted(reverseGlyphMap[g] for g in glyphs))
def font(self, font, file=sys.stdout):
if not self.xml:
return
from fontTools.misc import xmlWriter
writer = xmlWriter.XMLWriter(file)
font.disassembleInstructions = False # Work around ttLib bug
for tag in font.keys():
writer.begintag(tag)
writer.newline()
font[tag].toXML(writer, font)
writer.endtag(tag)
writer.newline()
def load_font(fontFile,
options,
checkChecksums=False,
dontLoadGlyphNames=False):
font = ttLib.TTFont(fontFile,
checkChecksums=checkChecksums,
recalcBBoxes=options.recalc_bounds)
# Hack:
#
# If we don't need glyph names, change 'post' class to not try to
# load them. It avoid lots of headache with broken fonts as well
# as loading time.
#
# Ideally ttLib should provide a way to ask it to skip loading
# glyph names. But it currently doesn't provide such a thing.
#
if dontLoadGlyphNames:
post = ttLib.getTableClass('post')
saved = post.decode_format_2_0
post.decode_format_2_0 = post.decode_format_3_0
f = font['post']
if f.formatType == 2.0:
f.formatType = 3.0
post.decode_format_2_0 = saved
return font
def save_font(font, outfile, options):
if options.flavor and not hasattr(font, 'flavor'):
raise Exception("fonttools version does not support flavors.")
font.flavor = options.flavor
font.save(outfile, reorderTables=options.canonical_order)
def main(args):
log = Logger()
args = log.parse_opts(args)
options = Options()
args = options.parse_opts(args, ignore_unknown=['text'])
if len(args) < 2:
print >>sys.stderr, "usage: pyftsubset font-file glyph... [--text=ABC]... [--option=value]..."
sys.exit(1)
fontfile = args[0]
args = args[1:]
dontLoadGlyphNames =(not options.glyph_names and
all(any(g.startswith(p)
for p in ['gid', 'glyph', 'uni', 'U+'])
for g in args))
font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames)
log.lapse("load font")
subsetter = Subsetter(options=options, log=log)
names = font.getGlyphNames()
log.lapse("loading glyph names")
glyphs = []
unicodes = []
text = ""
for g in args:
if g == '*':
glyphs.extend(font.getGlyphOrder())
continue
if g in names:
glyphs.append(g)
continue
if g.startswith('--text='):
text += g[7:]
continue
if g.startswith('uni') or g.startswith('U+'):
if g.startswith('uni') and len(g) > 3:
g = g[3:]
elif g.startswith('U+') and len(g) > 2:
g = g[2:]
u = int(g, 16)
unicodes.append(u)
continue
if g.startswith('gid') or g.startswith('glyph'):
if g.startswith('gid') and len(g) > 3:
g = g[3:]
elif g.startswith('glyph') and len(g) > 5:
g = g[5:]
try:
glyphs.append(font.getGlyphName(int(g), requireReal=1))
except ValueError:
raise Exception("Invalid glyph identifier: %s" % g)
continue
raise Exception("Invalid glyph identifier: %s" % g)
log.lapse("compile glyph list")
log("Unicodes:", unicodes)
log("Glyphs:", glyphs)
subsetter.populate(glyphs=glyphs, unicodes=unicodes, text=text)
subsetter.subset(font)
outfile = fontfile + '.subset'
save_font (font, outfile, options)
log.lapse("compile and save font")
log.last_time = log.start_time
log.lapse("make one with everything(TOTAL TIME)")
if log.verbose:
import os
log("Input font: %d bytes" % os.path.getsize(fontfile))
log("Subset font: %d bytes" % os.path.getsize(outfile))
log.font(font)
font.close()
__all__ = [
'Options',
'Subsetter',
'Logger',
'load_font',
'save_font',
'main'
]
if __name__ == '__main__':
main(sys.argv[1:])