blob: 0e3736860464b8b01cc59c8fb486fb75c4e5c205 [file] [log] [blame]
#!/usr/bin/python
# Copyright 2013 Google, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0(the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# 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
import fontTools.ttLib
import fontTools.ttLib.tables
import fontTools.ttLib.tables.otTables
import fontTools.cffLib
import fontTools.misc.psCharStrings
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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.Coverage)
def remap(self, coverage_map):
"Remaps coverage."
self.glyphs = [self.glyphs[i] for i in coverage_map]
@_add_method(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.SinglePos)
def prune_post_subset(self, options):
if not options.hinting:
# Drop device tables
self.ValueFormat &= ~0x00F0
return True
@_add_method(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.Anchor)
def prune_hints(self):
# Drop device tables / contour anchor point
self.Format = 1
@_add_method(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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 rec.Mark2Anchor:
m.prune_hints()
return True
@_add_method(fontTools.ttLib.tables.otTables.SingleSubst,
fontTools.ttLib.tables.otTables.MultipleSubst,
fontTools.ttLib.tables.otTables.AlternateSubst,
fontTools.ttLib.tables.otTables.LigatureSubst,
fontTools.ttLib.tables.otTables.ReverseChainSingleSubst,
fontTools.ttLib.tables.otTables.SinglePos,
fontTools.ttLib.tables.otTables.PairPos,
fontTools.ttLib.tables.otTables.CursivePos,
fontTools.ttLib.tables.otTables.MarkBasePos,
fontTools.ttLib.tables.otTables.MarkLigPos,
fontTools.ttLib.tables.otTables.MarkMarkPos)
def subset_lookups(self, lookup_indices):
pass
@_add_method(fontTools.ttLib.tables.otTables.SingleSubst,
fontTools.ttLib.tables.otTables.MultipleSubst,
fontTools.ttLib.tables.otTables.AlternateSubst,
fontTools.ttLib.tables.otTables.LigatureSubst,
fontTools.ttLib.tables.otTables.ReverseChainSingleSubst,
fontTools.ttLib.tables.otTables.SinglePos,
fontTools.ttLib.tables.otTables.PairPos,
fontTools.ttLib.tables.otTables.CursivePos,
fontTools.ttLib.tables.otTables.MarkBasePos,
fontTools.ttLib.tables.otTables.MarkLigPos,
fontTools.ttLib.tables.otTables.MarkMarkPos)
def collect_lookups(self):
return []
@_add_method(fontTools.ttLib.tables.otTables.SingleSubst,
fontTools.ttLib.tables.otTables.MultipleSubst,
fontTools.ttLib.tables.otTables.AlternateSubst,
fontTools.ttLib.tables.otTables.LigatureSubst,
fontTools.ttLib.tables.otTables.ContextSubst,
fontTools.ttLib.tables.otTables.ChainContextSubst,
fontTools.ttLib.tables.otTables.ReverseChainSingleSubst,
fontTools.ttLib.tables.otTables.SinglePos,
fontTools.ttLib.tables.otTables.PairPos,
fontTools.ttLib.tables.otTables.CursivePos,
fontTools.ttLib.tables.otTables.MarkBasePos,
fontTools.ttLib.tables.otTables.MarkLigPos,
fontTools.ttLib.tables.otTables.MarkMarkPos,
fontTools.ttLib.tables.otTables.ContextPos,
fontTools.ttLib.tables.otTables.ChainContextPos)
def prune_pre_subset(self, options):
return True
@_add_method(fontTools.ttLib.tables.otTables.SingleSubst,
fontTools.ttLib.tables.otTables.MultipleSubst,
fontTools.ttLib.tables.otTables.AlternateSubst,
fontTools.ttLib.tables.otTables.LigatureSubst,
fontTools.ttLib.tables.otTables.ReverseChainSingleSubst,
fontTools.ttLib.tables.otTables.ContextSubst,
fontTools.ttLib.tables.otTables.ChainContextSubst,
fontTools.ttLib.tables.otTables.ContextPos,
fontTools.ttLib.tables.otTables.ChainContextPos)
def prune_post_subset(self, options):
return True
@_add_method(fontTools.ttLib.tables.otTables.SingleSubst,
fontTools.ttLib.tables.otTables.AlternateSubst,
fontTools.ttLib.tables.otTables.ReverseChainSingleSubst)
def may_have_non_1to1(self):
return False
@_add_method(fontTools.ttLib.tables.otTables.MultipleSubst,
fontTools.ttLib.tables.otTables.LigatureSubst,
fontTools.ttLib.tables.otTables.ContextSubst,
fontTools.ttLib.tables.otTables.ChainContextSubst)
def may_have_non_1to1(self):
return True
@_add_method(fontTools.ttLib.tables.otTables.ContextSubst,
fontTools.ttLib.tables.otTables.ChainContextSubst,
fontTools.ttLib.tables.otTables.ContextPos,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ContextSubst,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ContextSubst,
fontTools.ttLib.tables.otTables.ContextPos,
fontTools.ttLib.tables.otTables.ChainContextSubst,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ContextSubst,
fontTools.ttLib.tables.otTables.ChainContextSubst,
fontTools.ttLib.tables.otTables.ContextPos,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ContextSubst,
fontTools.ttLib.tables.otTables.ChainContextSubst,
fontTools.ttLib.tables.otTables.ContextPos,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ExtensionSubst,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ExtensionSubst,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ExtensionSubst,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ExtensionSubst,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ExtensionSubst,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.Lookup)
def subset_lookups(self, lookup_indices):
for s in self.SubTable:
s.subset_lookups(lookup_indices)
@_add_method(fontTools.ttLib.tables.otTables.Lookup)
def collect_lookups(self):
return _uniq_sort(sum((st.collect_lookups() for st in self.SubTable
if st), []))
@_add_method(fontTools.ttLib.tables.otTables.Lookup)
def may_have_non_1to1(self):
return any(st.may_have_non_1to1() for st in self.SubTable if st)
@_add_method(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.Feature)
def collect_lookups(self):
return self.LookupListIndex[:]
@_add_method(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.DefaultLangSys,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.DefaultLangSys,
fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.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(fontTools.ttLib.tables.otTables.ScriptList)
def collect_features(self):
return _uniq_sort(sum((s.Script.collect_features()
for s in self.ScriptRecord), []))
@_add_method(fontTools.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(fontTools.ttLib.getTableClass('GSUB'),
fontTools.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(fontTools.ttLib.getTableClass('GSUB'),
fontTools.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(fontTools.ttLib.getTableClass('GSUB'),
fontTools.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(fontTools.ttLib.getTableClass('GSUB'),
fontTools.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(fontTools.ttLib.getTableClass('GSUB'),
fontTools.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(fontTools.ttLib.getTableClass('GSUB'),
fontTools.ttLib.getTableClass('GPOS'))
def prune_post_subset(self, options):
self.table.LookupList.prune_post_subset(options);
return True
@_add_method(fontTools.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(fontTools.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(fontTools.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(fontTools.ttLib.getTableClass('vmtx'),
fontTools.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(fontTools.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(fontTools.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(fontTools.ttLib.getTableClass('post'))
def prune_pre_subset(self, options):
if not options.glyph_names:
self.formatType = 3.0
return True
@_add_method(fontTools.ttLib.getTableClass('post'))
def subset_glyphs(self, s):
self.extraNames = [] # This seems to do it
return True
@_add_method(fontTools.ttLib.getTableModule('glyf').Glyph)
def getComponentNamesFast(self, glyfTable):
if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
return [] # Not composite
data = self.data
i = 10
components = []
more = 1
while more:
flags, glyphID = struct.unpack(">HH", data[i:i+4])
i += 4
flags = int(flags)
components.append(glyfTable.getGlyphName(int(glyphID)))
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
return components
@_add_method(fontTools.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(fontTools.ttLib.getTableModule('glyf').Glyph)
def dropInstructionsFast(self):
if not self.data:
return
numContours = struct.unpack(">h", self.data[:2])[0]
data = array.array("B", self.data)
i = 10
if numContours >= 0:
i += 2 * numContours # endPtsOfContours
instructionLen =(data[i] << 8) | data[i+1]
# Zero it
data[i] = data [i+1] = 0
i += 2
if instructionLen:
# Splice it out
data = data[:i] + data[i+instructionLen:]
else:
more = 1
while more:
flags =(data[i] << 8) | data[i+1]
# Turn instruction flag off
flags &= ~0x0100 # WE_HAVE_INSTRUCTIONS
data[i+0] = flags >> 8
data[i+1] = flags & 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
# Cut off
data = data[:i]
if len(data) % 4:
# add pad bytes
nPadBytes = 4 -(len(data) % 4)
for i in range(nPadBytes):
data.append(0)
self.data = data.tostring()
@_add_method(fontTools.ttLib.getTableClass('glyf'))
def closure_glyphs(self, s):
decompose = s.glyphs
# I don't know if component glyphs can be composite themselves.
# We handle them anyway.
while True:
components = set()
for g in decompose:
if g not in self.glyphs:
continue
gl = self.glyphs[g]
if hasattr(gl, "data"):
for c in gl.getComponentNamesFast(self):
if c not in s.glyphs:
components.add(c)
else:
# TTX seems to expand gid0..3 always
if gl.isComposite():
for c in gl.components:
if c.glyphName not in s.glyphs:
components.add(c.glyphName)
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(fontTools.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(fontTools.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]
return bool(self.glyphs)
@_add_method(fontTools.ttLib.getTableClass('glyf'))
def prune_post_subset(self, options):
if not options.hinting:
for v in self.glyphs.itervalues():
if hasattr(v, "data"):
v.dropInstructionsFast()
else:
v.program = fontTools.ttLib.tables.ttProgram.Program()
v.program.fromBytecode([])
return True
@_add_method(fontTools.ttLib.getTableClass('CFF '))
def prune_pre_subset(self, options):
cff = self.cff
# CFF table should 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')
c.bytecode = '\x0e' # endchar
c.program = None
return bool(cff.fontNames)
@_add_method(fontTools.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 any(cff[fontname].numGlyphs for fontname in cff.keys())
@_add_method(fontTools.misc.psCharStrings.T2CharString)
def subset_subroutines(self, subrs, gsubrs):
p = self.program
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(fontTools.ttLib.getTableClass('CFF '))
def prune_post_subset(self, options):
cff = self.cff
class _MarkingT2Decompiler(fontTools.misc.psCharStrings.SimpleT2Decompiler):
def __init__(self, localSubrs, globalSubrs):
fontTools.misc.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)
fontTools.misc.psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
def op_callgsubr(self, index):
self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias)
fontTools.misc.psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
class _NonrecursingT2Decompiler(fontTools.misc.psCharStrings.SimpleT2Decompiler):
def __init__(self, localSubrs, globalSubrs):
fontTools.misc.psCharStrings.SimpleT2Decompiler.__init__(self,
localSubrs,
globalSubrs)
def op_callsubr(self, index):
self.pop()
def op_callgsubr(self, index):
self.pop()
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
# 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)
# Renumber subroutines to remove unused ones
all_subrs = [font.GlobalSubrs]
all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs'))
# Prepare
for subrs in all_subrs:
if not subrs: continue
if not hasattr(subrs, '_used'):
subrs._used = set()
subrs._used = _uniq_sort(subrs._used)
subrs._old_bias = fontTools.misc.psCharStrings.calcSubrBias(subrs)
subrs._new_bias = fontTools.misc.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 not subrs: continue
decompiler = _NonrecursingT2Decompiler(subrs, font.GlobalSubrs)
for i in xrange (subrs.count):
if i not in subrs._used: continue
decompiler.reset()
decompiler.execute(subrs[i])
subrs[i].subset_subroutines (subrs, font.GlobalSubrs)
# Cleanup
for subrs in all_subrs:
if not subrs: continue
subrs.items = [subrs.items[i] for i in subrs._used]
del subrs.file, subrs.offsets
del subrs._used, subrs._old_bias, subrs._new_bias
if not options.hinting:
pass # TODO(behdad) Drop hints
return True
@_add_method(fontTools.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(fontTools.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]
return bool(self.tables)
@_add_method(fontTools.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)]
# 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(fontTools.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) 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'" % a)
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 = fontTools.ttLib.getTableClass(tag)
if hasattr(clazz, 'prune_pre_subset'):
table = font[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 = fontTools.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 = fontTools.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
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 = fontTools.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 = fontTools.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=None):
if args == None: args = sys.argv
arg0, args = args[0], args[1:]
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: %s font-file glyph... [--text=ABC]... [--option=value]..." % arg0
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)
subsetter = Subsetter(options=options, log=log)
log.lapse("load font")
names = font.getGlyphNames()
log.lapse("loading glyph names")
glyphs = []
unicodes = []
text = ""
for g in args:
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()