fonttools/pyotlss.py
Behdad Esfahbod 4cf7a80fa1 Implement Glyph.getComponentsFast() that works without loading glyph
We still end up loading glyphs if pruning hinting.  And even if
we don't do that, rebuilding the maxp table loads all glyphs.
Working on those.
2013-07-24 16:08:35 -04:00

1183 lines
43 KiB
Python
Executable File

#!/usr/bin/python
# Python OpenType Layout Subsetter
#
# 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
#
# Try running on PyPy
try:
import numpypy
except ImportError:
pass
import fontTools.ttx
import struct
def add_method (*clazzes):
def wrapper(method):
for clazz in clazzes:
assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.'
setattr (clazz, method.func_name, method)
return wrapper
def unique_sorted (l):
return sorted ({v:1 for v in l}.keys ())
@add_method(fontTools.ttLib.tables.otTables.Coverage)
def intersect_glyphs (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 subset_glyphs (self, glyphs):
"Returns ascending list of remaining coverage values."
indices = self.intersect_glyphs (glyphs)
self.glyphs = [g for g in self.glyphs if g in glyphs]
return indices
@add_method(fontTools.ttLib.tables.otTables.ClassDef)
def intersect_glyphs (self, glyphs):
"Returns ascending list of matching class values."
return unique_sorted (v for g,v in self.classDefs.items() if g in glyphs)
@add_method(fontTools.ttLib.tables.otTables.ClassDef)
def intersects_glyphs_class (self, glyphs, klass):
"Returns true if any of glyphs has requested class."
return any (g in glyphs for g,v in self.classDefs.items() if v == klass)
@add_method(fontTools.ttLib.tables.otTables.ClassDef)
def subset_glyphs (self, glyphs, remap=False):
"Returns ascending list of remaining classes."
self.classDefs = {g:v for g,v in self.classDefs.items() if g in glyphs}
indices = unique_sorted (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 = {g:class_map.index (v) for g,v in self.classDefs.items()}
@add_method(fontTools.ttLib.tables.otTables.SingleSubst)
def closure_glyphs (self, glyphs, table):
if self.Format in [1, 2]:
return [v for g,v in self.mapping.items() if g in glyphs]
else:
assert 0, "unknown format: %s" % self.Format
@add_method(fontTools.ttLib.tables.otTables.SingleSubst)
def subset_glyphs (self, glyphs):
if self.Format in [1, 2]:
self.mapping = {g:v for g,v in self.mapping.items() if g in glyphs}
return bool (self.mapping)
else:
assert 0, "unknown format: %s" % self.Format
@add_method(fontTools.ttLib.tables.otTables.MultipleSubst)
def closure_glyphs (self, glyphs, table):
if self.Format == 1:
indices = self.Coverage.intersect_glyphs (glyphs)
return sum ((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, glyphs):
if self.Format == 1:
indices = self.Coverage.subset_glyphs (glyphs)
self.Sequence = [self.Sequence[i] for i in 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, glyphs, table):
if self.Format == 1:
return sum ((v for g,v in self.alternates.items() if g in glyphs), [])
else:
assert 0, "unknown format: %s" % self.Format
@add_method(fontTools.ttLib.tables.otTables.AlternateSubst)
def subset_glyphs (self, glyphs):
if self.Format == 1:
self.alternates = {g:v for g,v in self.alternates.items() if g in glyphs}
return bool (self.alternates)
else:
assert 0, "unknown format: %s" % self.Format
@add_method(fontTools.ttLib.tables.otTables.LigatureSubst)
def closure_glyphs (self, glyphs, table):
if self.Format == 1:
return sum (([seq.LigGlyph for seq in seqs if all(c in glyphs for c in seq.Component)]
for g,seqs in self.ligatures.items() if g in glyphs), [])
else:
assert 0, "unknown format: %s" % self.Format
@add_method(fontTools.ttLib.tables.otTables.LigatureSubst)
def subset_glyphs (self, glyphs):
if self.Format == 1:
self.ligatures = {g:v for g,v in self.ligatures.items() if g in glyphs}
self.ligatures = {g:[seq for seq in seqs if all(c in glyphs for c in seq.Component)]
for g,seqs in self.ligatures.items()}
self.ligatures = {g:v for g,v in self.ligatures.items() 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, glyphs, table):
if self.Format == 1:
indices = self.Coverage.intersect_glyphs (glyphs)
if not indices or \
not all (c.intersect_glyphs (glyphs) for c in self.LookAheadCoverage + self.BacktrackCoverage):
return []
return [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, glyphs):
if self.Format == 1:
indices = self.Coverage.subset_glyphs (glyphs)
self.Substitute = [self.Substitute[i] for i in indices]
self.GlyphCount = len (self.Substitute)
return bool (self.GlyphCount and all (c.subset_glyphs (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, glyphs):
if self.Format == 1:
return len (self.Coverage.subset_glyphs (glyphs))
elif self.Format == 2:
indices = self.Coverage.subset_glyphs (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.PairPos)
def subset_glyphs (self, glyphs):
if self.Format == 1:
indices = self.Coverage.subset_glyphs (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 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_glyphs (glyphs, remap=True)
class2_map = self.ClassDef2.subset_glyphs (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_glyphs (glyphs))
else:
assert 0, "unknown format: %s" % self.Format
@add_method(fontTools.ttLib.tables.otTables.CursivePos)
def subset_glyphs (self, glyphs):
if self.Format == 1:
indices = self.Coverage.subset_glyphs (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.MarkBasePos)
def subset_glyphs (self, glyphs):
if self.Format == 1:
mark_indices = self.MarkCoverage.subset_glyphs (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_glyphs (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 = unique_sorted (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.MarkLigPos)
def subset_glyphs (self, glyphs):
if self.Format == 1:
mark_indices = self.MarkCoverage.subset_glyphs (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_glyphs (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 = unique_sorted (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.MarkMarkPos)
def subset_glyphs (self, glyphs):
if self.Format == 1:
mark1_indices = self.Mark1Coverage.subset_glyphs (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_glyphs (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 = unique_sorted (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.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.ContextSubst, fontTools.ttLib.tables.otTables.ChainContextSubst,
fontTools.ttLib.tables.otTables.ContextPos, fontTools.ttLib.tables.otTables.ChainContextPos)
def __classify_context (self):
class ContextHelper:
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:
ContextData = None
ChainContextData = None
RuleData = lambda r: r.Input
ChainRuleData = lambda r: r.Backtrack + r.Input + r.LookAhead
SetRuleData = None
ChainSetRuleData = None
elif Format == 2:
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:
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.ContextData = ChainContextData
self.RuleData = ChainRuleData
self.SetRuleData = ChainSetRuleData
else:
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'
elif Format == 2:
self.Rule = ChainTyp+'ClassRule'
self.RuleCount = ChainTyp+'ClassRuleCount'
self.RuleSet = ChainTyp+'ClassSet'
self.RuleSetCount = ChainTyp+'ClassSetCount'
self.ClassDef = 'InputClassDef' if Chain else 'ClassDef'
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:
self.__class__.__ContextHelpers[self.Format] = ContextHelper (self.__class__, self.Format)
return self.__class__.__ContextHelpers[self.Format]
@add_method(fontTools.ttLib.tables.otTables.ContextSubst, fontTools.ttLib.tables.otTables.ChainContextSubst)
def closure_glyphs (self, glyphs, table):
c = self.__classify_context ()
if self.Format == 1:
indices = self.Coverage.intersect_glyphs (glyphs)
rss = getattr (self, c.RuleSet)
return sum ((table.table.LookupList.Lookup[ll.LookupListIndex].closure_glyphs (glyphs, table) \
for i in indices \
for r in getattr (rss[i], c.Rule) \
if r and all (g in glyphs for g in c.RuleData (r)) \
for ll in getattr (r, c.LookupRecord) if ll \
), [])
elif self.Format == 2:
if not self.Coverage.intersect_glyphs (glyphs):
return []
indices = getattr (self, c.ClassDef).intersect_glyphs (glyphs)
rss = getattr (self, c.RuleSet)
return sum ((table.table.LookupList.Lookup[ll.LookupListIndex].closure_glyphs (glyphs, table) \
for i in indices \
for r in getattr (rss[i], c.Rule) \
if r and all (cd.intersects_glyphs_class (glyphs, k) \
for cd,k in zip (c.ContextData (self), c.RuleData (r))) \
for ll in getattr (r, c.LookupRecord) if ll \
), [])
elif self.Format == 3:
if not all (x.intersect_glyphs (glyphs) for x in c.RuleData (self)):
return []
return sum ((table.table.LookupList.Lookup[ll.LookupListIndex].closure_glyphs (glyphs, table) \
for ll in getattr (self, c.LookupRecord) if ll), [])
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, glyphs):
c = self.__classify_context ()
if self.Format == 1:
indices = self.Coverage.subset_glyphs (glyphs)
rss = getattr (self, c.RuleSet)
rss = [rss[i] for i in indices]
for rs in rss:
if rs:
ss = getattr (rs, c.Rule)
ss = [r for r in ss \
if r and all (g in glyphs for g 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_glyphs (glyphs):
return False
indices = getattr (self, c.ClassDef).intersect_glyphs (glyphs)
rss = getattr (self, c.RuleSet)
rss = [rss[i] for i in indices]
ContextData = c.ContextData (self)
klass_maps = [x.subset_glyphs (glyphs, remap=True) for x in ContextData]
for rs in rss:
if rs:
ss = getattr (rs, c.Rule)
ss = [r for r in ss \
if r and all (k in klass_map \
for klass_map,k 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 klassmap,k 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)
return all (x.subset_glyphs (glyphs) for x in c.ContextData (self))
elif self.Format == 3:
return all (x.subset_glyphs (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 rs:
for r in getattr (rs, c.Rule):
if r:
setattr (r, c.LookupRecord, [ll for ll in getattr (r, c.LookupRecord) if ll \
if ll.LookupListIndex in lookup_indices])
for ll in getattr (r, c.LookupRecord):
if ll:
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 \
if ll.LookupListIndex in lookup_indices])
for ll in getattr (self, c.LookupRecord):
if ll:
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, glyphs, table):
if self.Format == 1:
return self.ExtSubTable.closure_glyphs (glyphs, table)
else:
assert 0, "unknown format: %s" % self.Format
@add_method(fontTools.ttLib.tables.otTables.ExtensionSubst, fontTools.ttLib.tables.otTables.ExtensionPos)
def subset_glyphs (self, glyphs):
if self.Format == 1:
return self.ExtSubTable.subset_glyphs (glyphs)
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, glyphs, table):
return sum ((s.closure_glyphs (glyphs, table) for s in self.SubTable if s), [])
@add_method(fontTools.ttLib.tables.otTables.Lookup)
def subset_glyphs (self, glyphs):
self.SubTable = [s for s in self.SubTable if s and s.subset_glyphs (glyphs)]
self.SubTableCount = len (self.SubTable)
return bool (self.SubTableCount)
@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 unique_sorted (sum ((s.collect_lookups () for s in self.SubTable if s), []))
@add_method(fontTools.ttLib.tables.otTables.LookupList)
def subset_glyphs (self, glyphs):
"Returns the indices of nonempty lookups."
return [i for (i,l) in enumerate (self.Lookup) if l and l.subset_glyphs (glyphs)]
@add_method(fontTools.ttLib.tables.otTables.LookupList)
def subset_lookups (self, lookup_indices):
self.Lookup = [self.Lookup[i] for i in lookup_indices]
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 = unique_sorted (lookup_indices)
recurse = lookup_indices
while True:
recurse_lookups = sum ((self.Lookup[i].collect_lookups () for i in recurse), [])
recurse_lookups = [l for l in recurse_lookups if l not in lookup_indices]
if not recurse_lookups:
return unique_sorted (lookup_indices)
recurse_lookups = unique_sorted (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 unique_sorted (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 unique_sorted (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 unique_sorted (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 unique_sorted (sum ((s.Script.collect_features () for s in self.ScriptRecord), []))
@add_method(fontTools.ttLib.getTableClass('GSUB'))
def closure_glyphs (self, glyphs):
feature_indices = self.table.ScriptList.collect_features ()
lookup_indices = self.table.FeatureList.collect_lookups (feature_indices)
glyphs = unique_sorted (glyphs)
while True:
additions = (sum ((self.table.LookupList.Lookup[i].closure_glyphs (glyphs, self) for i in lookup_indices), []))
additions = unique_sorted (g for g in additions if g not in glyphs)
if not additions:
return glyphs
glyphs.extend (additions)
@add_method(fontTools.ttLib.getTableClass('GSUB'), fontTools.ttLib.getTableClass('GPOS'))
def subset_glyphs (self, glyphs):
lookup_indices = self.table.LookupList.subset_glyphs (glyphs)
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 options['layout-features'] and '*' not in options['layout-features']:
self.subset_feature_tags (options['layout-features'])
self.prune_lookups ()
return True
@add_method(fontTools.ttLib.getTableClass('GDEF'))
def subset_glyphs (self, glyphs):
table = self.table
if table.LigCaretList:
indices = table.LigCaretList.Coverage.subset_glyphs (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 = {g:v for g,v in table.MarkAttachClassDef.classDefs.items() if g in glyphs}
if not table.MarkAttachClassDef.classDefs:
table.MarkAttachClassDef = None
if table.GlyphClassDef:
table.GlyphClassDef.classDefs = {g:v for g,v in table.GlyphClassDef.classDefs.items() if g in glyphs}
if not table.GlyphClassDef.classDefs:
table.GlyphClassDef = None
if table.AttachList:
indices = table.AttachList.Coverage.subset_glyphs (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 subset_glyphs (self, glyphs):
for t in self.kernTables:
t.kernTable = {(a,b):v for ((a,b),v) in t.kernTable.items() 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('hmtx'), fontTools.ttLib.getTableClass('vmtx'))
def subset_glyphs (self, glyphs):
self.metrics = {g:v for g,v in self.metrics.items() if g in glyphs}
return bool (self.metrics)
@add_method(fontTools.ttLib.getTableClass('hdmx'))
def subset_glyphs (self, glyphs):
self.hdmx = {s:{g:v for g,v in l.items() if g in glyphs} for (s,l) in self.hdmx.items()}
return bool (self.hdmx)
@add_method(fontTools.ttLib.getTableClass('VORG'))
def subset_glyphs (self, glyphs):
self.VOriginRecords = {g:v for g,v in self.VOriginRecords.items() if g in 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, glyphs):
self.extraNames = [] # This seems to do it
return True
# Copied from _g_l_y_f.py
ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes
ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points
ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true
WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0
NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!)
MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy
WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11
WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow
USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph
OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts
SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple)
UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS)
@add_method(fontTools.ttLib.getTableModule('glyf').Glyph)
def getComponentNamesFast (self, glyfTable):
if 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 & ARG_1_AND_2_ARE_WORDS: i += 4
else: i += 2
if flags & WE_HAVE_A_SCALE: i += 2
elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4
elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8
more = flags & MORE_COMPONENTS
return components
@add_method(fontTools.ttLib.getTableModule('glyf').Glyph)
def remapComponentsFast (self, indices):
if struct.unpack(">h", self.data[:2])[0] >= 0:
return # Not composite
data = bytearray (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 & ARG_1_AND_2_ARE_WORDS: i += 4
else: i += 2
if flags & WE_HAVE_A_SCALE: i += 2
elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4
elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8
more = flags & MORE_COMPONENTS
self.data = str (data)
@add_method(fontTools.ttLib.getTableClass('glyf'))
def closure_glyphs (self, glyphs):
glyphs = unique_sorted (glyphs)
decompose = glyphs
# I don't know if component glyphs can be composite themselves.
# We handle them anyway.
while True:
components = []
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 glyphs:
components.append (c)
else:
# TTX seems to expand gid0..3 always
if gl.isComposite ():
for c in gl.components:
if c.glyphName not in glyphs:
components.append (c.glyphName)
components = [c for c in components if c not in glyphs]
if not components:
return glyphs
decompose = unique_sorted (components)
glyphs.extend (components)
@add_method(fontTools.ttLib.getTableClass('glyf'))
def subset_glyphs (self, glyphs):
self.glyphs = {g:v for g,v in self.glyphs.items() if g in glyphs}
indices = [i for i,g in enumerate (self.glyphOrder) if g in glyphs]
for v in self.glyphs.values ():
if hasattr (v, "data"):
v.remapComponentsFast (indices)
else:
pass # No need
self.glyphOrder = [g for g in self.glyphOrder if g in glyphs]
return bool (self.glyphs)
@add_method(fontTools.ttLib.getTableClass('glyf'))
def prune_post_subset (self, options):
if not options['hinting']:
for g in self.glyphs.values ():
g.expand (self)
g.program = fontTools.ttLib.tables.ttProgram.Program()
g.program.fromBytecode([])
return True
@add_method(fontTools.ttLib.getTableClass('CFF '))
def subset_glyphs (self, glyphs):
assert 0, "unimplemented"
@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 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, glyphs):
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:
# XXX We drop all the default-UVS mappings (g==None)
t.uvsDict = {v:[(u,g) for (u,g) in l if g in glyphs] for (v,l) in t.uvsDict.items()}
t.uvsDict = {v:l for (v,l) in t.uvsDict.items() if l}
else:
t.cmap = {u:g for (u,g) in t.cmap.items() if g in glyphs}
self.tables = [t for t in self.tables if (t.cmap if t.format != 14 else t.uvsDict)]
# XXX Convert formats when needed
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
drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'PCLT', 'LTSH']
drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite
drop_tables_default += ['CBLC', 'CBDT', 'sbix', 'COLR', 'CPAL'] # Color
no_subset_tables = ['gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', 'name', 'cvt ', 'fpgm', 'prep']
hinting_tables = ['cvt ', 'fpgm', 'prep', 'hdmx', 'VDMX']
# Based on HarfBuzz shapers
layout_features_dict = {
# 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'],
'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_all = unique_sorted (sum (layout_features_dict.values (), []))
options_default = {
'drop-tables': drop_tables_default,
'layout-features': layout_features_all,
'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
'mandatory-glyphs': True, # First four for TrueType, .notdef for CFF
}
# TODO OS/2 ulUnicodeRange / ulCodePageRange?
# TODO Drop unneeded GSUB/GPOS Script/LangSys entries
# TODO Avoid recursing too much
# TODO Text direction considerations
# TODO Text script / language considerations
# TODO Drop unknown tables
def main ():
import sys, time
global last_time
start_time = time.time ()
last_time = start_time
verbose = False
if "--verbose" in sys.argv:
verbose = True
sys.argv.remove ("--verbose")
xml = False
if "--xml" in sys.argv:
xml = True
sys.argv.remove ("--xml")
timing = False
if "--timing" in sys.argv:
timing = True
sys.argv.remove ("--timing")
options = options_default.copy ()
def lapse (what):
if not timing:
return
global last_time
new_time = time.time ()
print "Took %0.3fs to %s" % (new_time - last_time, what)
last_time = new_time
if len (sys.argv) < 3:
print >>sys.stderr, "usage: pyotlss.py font-file glyph..."
sys.exit (1)
fontfile = sys.argv[1]
glyphs = sys.argv[2:]
font = fontTools.ttx.TTFont (fontfile)
font.disassembleInstructions = False
lapse ("load font")
if options["mandatory-glyphs"]:
# Always include .notdef; anything else?
if 'glyf' in font:
glyphs.extend (['gid0', 'gid1', 'gid2', 'gid3'])
if verbose:
print "Added first four glyphs to subset"
else:
glyphs.append ('.notdef')
if verbose:
print "Added .notdef glyph to subset"
names = font.getGlyphNames()
lapse ("loading glyph names")
# Convert to glyph names
glyph_names = []
cmap_tables = None
for g in glyphs:
if g in names:
glyph_names.append (g)
continue
if g.startswith ('uni') and len (g) > 3:
if not cmap_tables:
cmap = font['cmap']
cmap_tables = [t for t in cmap.tables if t.platformID == 3 and t.platEncID in [1, 10]]
del cmap
found = False
u = int (g[3:], 16)
for table in cmap_tables:
if u in table.cmap:
glyph_names.append (table.cmap[u])
found = True
break
if not found:
if verbose:
print ("No glyph for Unicode value %s; skipping." % g)
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:
glyph_names.append (font.getGlyphName (int (g), requireReal=1))
except ValueError:
raise Exception ("Invalid glyph identifier %s" % g)
continue
raise Exception ("Invalid glyph identifier %s" % g)
del cmap_tables
glyphs = unique_sorted (glyph_names)
del glyph_names
lapse ("compile glyph list")
if verbose:
print "Glyphs:", glyphs
for tag in font.keys():
if tag == 'GlyphOrder': continue
if tag in options['drop-tables'] or \
(tag in hinting_tables and not options['hinting']):
if verbose:
print 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 (options)
lapse ("prune '%s'" % tag)
if not retain:
if verbose:
print tag, "pruned to empty; dropped."
del font[tag]
continue
else:
if verbose:
print tag, "pruned."
glyphs_requested = glyphs
if 'GSUB' in font:
if verbose:
print "Closing glyph list over 'GSUB': %d glyphs before" % len (glyphs)
glyphs = font['GSUB'].closure_glyphs (glyphs)
if verbose:
print "Closed glyph list over 'GSUB': %d glyphs after" % len (glyphs)
print "Glyphs:", glyphs
lapse ("close glyph list over 'GSUB'")
glyphs_gsubed = glyphs
# Close over composite glyphs
if 'glyf' in font:
if verbose:
print "Closing glyph list over 'glyf': %d glyphs before" % len (glyphs)
print "Glyphs:", glyphs
glyphs = font['glyf'].closure_glyphs (glyphs)
if verbose:
print "Closed glyph list over 'glyf': %d glyphs after" % len (glyphs)
print "Glyphs:", glyphs
lapse ("close glyph list over 'glyf'")
else:
glyphs = glyphs
glyphs_glyfed = glyphs
glyphs_closed = glyphs
del glyphs
if verbose:
print "Retaining %d glyphs: " % len (glyphs_closed)
for tag in font.keys():
if tag == 'GlyphOrder': continue
clazz = fontTools.ttLib.getTableClass(tag)
if tag in no_subset_tables:
if verbose:
print tag, "subsetting not needed."
elif hasattr (clazz, 'subset_glyphs'):
table = font[tag]
if tag == 'cmap': # What else?
glyphs = glyphs_requested
elif tag in ['GSUB', 'GPOS', 'GDEF', 'cmap', 'kern', 'post']: # What else?
glyphs = glyphs_gsubed
else:
glyphs = glyphs_closed
retain = table.subset_glyphs (glyphs)
lapse ("subset '%s'" % tag)
if not retain:
if verbose:
print tag, "subsetted to empty; dropped."
del font[tag]
continue
else:
if verbose:
print tag, "subsetted."
del glyphs
else:
if verbose:
print tag, "NOT subset; don't know how to subset."
continue
if hasattr (clazz, 'prune_post_subset'):
table = font[tag]
retain = table.prune_post_subset (options)
lapse ("prune '%s'" % tag)
if not retain:
if verbose:
print tag, "pruned to empty; dropped."
del font[tag]
continue
else:
if verbose:
print tag, "pruned."
glyphOrder = font.getGlyphOrder()
glyphOrder = [g for g in glyphOrder if g in glyphs_closed]
font.setGlyphOrder (glyphOrder)
font._buildReverseGlyphOrderDict ()
lapse ("subset GlyphOrder")
font.save (fontfile + '.subset')
lapse ("compile and save font")
last_time = start_time
lapse ("make one with everything (TOTAL TIME)")
if xml:
import xmlWriter
writer = xmlWriter.XMLWriter (sys.stdout)
for tag in font.keys():
writer.begintag (tag)
writer.newline ()
font[tag].toXML(writer, font)
writer.endtag (tag)
writer.newline ()
if __name__ == '__main__':
main ()