# 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 import fontTools.pens.basePen def _add_method(*clazzes): """Returns a decorator function that adds a new method to one or more classes.""" def wrapper(method): for clazz in clazzes: assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.' assert not hasattr(clazz, method.func_name), \ "Oops, class '%s' has method '%s'." % (clazz.__name__, method.func_name) setattr(clazz, method.func_name, method) return None return wrapper def _uniq_sort(l): return sorted(set(l)) def _set_update(s, *others): # Jython's set.update only takes one other argument. # Emulate real set.update... for other in others: s.update(other) @_add_method(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 b.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] # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset. return True @_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 must have one font only cff.fontNames = cff.fontNames[:1] if options.notdef_glyph and not options.notdef_outline: for fontname in cff.keys(): font = cff[fontname] c,_ = font.CharStrings.getItemAndSelector('.notdef') # XXX we should preserve the glyph width c.bytecode = '\x0e' # endchar c.program = None return True # bool(cff.fontNames) @_add_method(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 True # any(cff[fontname].numGlyphs for fontname in cff.keys()) @_add_method(fontTools.misc.psCharStrings.T2CharString) def subset_subroutines(self, subrs, gsubrs): p = self.program assert len(p) for i in xrange(1, len(p)): if p[i] == 'callsubr': assert type(p[i-1]) is int p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias elif p[i] == 'callgsubr': assert type(p[i-1]) is int p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias @_add_method(fontTools.misc.psCharStrings.T2CharString) def drop_hints(self): hints = self._hints if hints.has_hint: self.program = self.program[hints.last_hint:] if hasattr(self, 'width'): # Insert width back if needed if self.width != self.private.defaultWidthX: self.program.insert(0, self.width - self.private.nominalWidthX) if hints.has_hintmask: i = 0 p = self.program while i < len(p): if p[i] in ['hintmask', 'cntrmask']: assert i + 1 <= len(p) del p[i:i+2] continue i += 1 assert len(self.program) del self._hints class _MarkingT2Decompiler(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 _DehintingT2Decompiler(fontTools.misc.psCharStrings.SimpleT2Decompiler): class Hints: def __init__(self): # Whether calling this charstring produces any hint stems self.has_hint = False # Index to start at to drop all hints self.last_hint = 0 # Index up to which we know more hints are possible. Only # relevant if status is 0 or 1. self.last_checked = 0 # The status means: # 0: after dropping hints, this charstring is empty # 1: after dropping hints, there may be more hints continuing after this # 2: no more hints possible after this charstring self.status = 0 # Has hintmask instructions; not recursive self.has_hintmask = False pass def __init__(self, css, localSubrs, globalSubrs): self._css = css fontTools.misc.psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs) def execute(self, charString): old_hints = charString._hints if hasattr(charString, '_hints') else None charString._hints = self.Hints() fontTools.misc.psCharStrings.SimpleT2Decompiler.execute(self, charString) hints = charString._hints if hints.has_hint or hints.has_hintmask: self._css.add(charString) if hints.status != 2: # Check from last_check, make sure we didn't have any operators. for i in xrange(hints.last_checked, len(charString.program) - 1): if type(charString.program[i]) == str: hints.status = 2 break; else: hints.status = 1 # There's *something* here hints.last_checked = len(charString.program) if old_hints: assert hints.__dict__ == old_hints.__dict__ def op_callsubr(self, index): subr = self.localSubrs[self.operandStack[-1]+self.localBias] fontTools.misc.psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) self.processSubr(index, subr) def op_callgsubr(self, index): subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] fontTools.misc.psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) self.processSubr(index, subr) def op_hstem(self, index): fontTools.misc.psCharStrings.SimpleT2Decompiler.op_hstem(self, index) self.processHint(index) def op_vstem(self, index): fontTools.misc.psCharStrings.SimpleT2Decompiler.op_vstem(self, index) self.processHint(index) def op_hstemhm(self, index): fontTools.misc.psCharStrings.SimpleT2Decompiler.op_hstemhm(self, index) self.processHint(index) def op_vstemhm(self, index): fontTools.misc.psCharStrings.SimpleT2Decompiler.op_vstemhm(self, index) self.processHint(index) def op_hintmask(self, index): fontTools.misc.psCharStrings.SimpleT2Decompiler.op_hintmask(self, index) self.processHintmask(index) def op_cntrmask(self, index): fontTools.misc.psCharStrings.SimpleT2Decompiler.op_cntrmask(self, index) self.processHintmask(index) def processHintmask(self, index): cs = self.callingStack[-1] hints = cs._hints hints.has_hintmask = True if hints.status != 2 and hints.has_hint: # Check from last_check, see if we may be an implicit vstem for i in xrange(hints.last_checked, index - 1): if type(cs.program[i]) == str: hints.status = 2 break; if hints.status != 2: # We are an implicit vstem hints.last_hint = index + 1 hints.status = 0 hints.last_checked = index + 1 def processHint(self, index): cs = self.callingStack[-1] hints = cs._hints hints.has_hint = True hints.last_hint = index hints.last_checked = index def processSubr(self, index, subr): cs = self.callingStack[-1] hints = cs._hints subr_hints = subr._hints if subr_hints.has_hint: if hints.status != 2: hints.has_hint = True hints.last_checked = index hints.status = subr_hints.status # Decide where to chop off from if subr_hints.status == 0: hints.last_hint = index else: hints.last_hint = index - 2 # Leave the subr call in else: # In my understanding, this is a font bug. Ie. it has hint stems # *after* path construction. I've seen this in widespread fonts. # Best to ignore the hints I suppose... pass #assert 0 else: hints.status = max(hints.status, subr_hints.status) if hints.status != 2: # Check from last_check, make sure we didn't have # any operators. for i in xrange(hints.last_checked, index - 1): if type(cs.program[i]) == str: hints.status = 2 break; hints.last_checked = index @_add_method(fontTools.ttLib.getTableClass('CFF ')) def prune_post_subset(self, options): cff = self.cff for fontname in cff.keys(): font = cff[fontname] cs = font.CharStrings # # Drop unused FontDictionaries # if hasattr(font, "FDSelect"): sel = font.FDSelect indices = _uniq_sort(sel.gidArray) sel.gidArray = [indices.index (ss) for ss in sel.gidArray] arr = font.FDArray arr.items = [arr[i] for i in indices] arr.count = len(arr.items) del arr.file, arr.offsets # # Drop hints if not needed # if not options.hinting: # # This can be tricky, but doesn't have to. What we do is: # # - Run all used glyph charstrings and recurse into subroutines, # - For each charstring (including subroutines), if it has any # of the hint stem operators, we mark it as such. Upon returning, # for each charstring we note all the subroutine calls it makes # that (recursively) contain a stem, # - Dropping hinting then consists of the following two ops: # * Drop the piece of the program in each charstring before the # last call to a stem op or a stem-calling subroutine, # * Drop all hintmask operations. # - It's trickier... A hintmask right after hints and a few numbers # will act as an implicit vstemhm. As such, we track whether # we have seen any non-hint operators so far and do the right # thing, recursively... Good luck understanding that :( # css = set() for g in font.charset: c,sel = cs.getItemAndSelector(g) # Make sure it's decompiled. We want our "decompiler" to walk # the program, not the bytecode. c.draw(fontTools.pens.basePen.NullPen()) subrs = getattr(c.private, "Subrs", []) decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs) decompiler.execute(c) for charstring in css: charstring.drop_hints() # # Renumber subroutines to remove unused ones # # Mark all used subroutines for g in font.charset: c,sel = cs.getItemAndSelector(g) subrs = getattr(c.private, "Subrs", []) decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs) decompiler.execute(c) all_subrs = [font.GlobalSubrs] if hasattr(font, 'FDSelect'): all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: all_subrs.append(font.Private.Subrs) subrs = set(subrs) # Remove duplicates # Prepare for subrs in all_subrs: if not hasattr(subrs, '_used'): subrs._used = set() subrs._used = _uniq_sort(subrs._used) subrs._old_bias = 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 subrs == font.GlobalSubrs: if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'): local_subrs = font.Private.Subrs else: local_subrs = [] else: local_subrs = subrs subrs.items = [subrs.items[i] for i in subrs._used] subrs.count = len(subrs.items) del subrs.file if hasattr(subrs, 'offsets'): del subrs.offsets for i in xrange (subrs.count): subrs[i].subset_subroutines (local_subrs, font.GlobalSubrs) # Cleanup for subrs in all_subrs: del subrs._used, subrs._old_bias, subrs._new_bias return True @_add_method(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) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left # TODO(behdad) Drop GDEF subitems if unused by lookups # TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF) # TODO(behdad) Text direction considerations. # TODO(behdad) Text script / language considerations. # TODO(behdad) Option to drop hmtx for CFF? class Options(object): class UnknownOptionError(Exception): pass _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'SVG ', 'PCLT', 'LTSH'] _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite _drop_tables_default += ['CBLC', 'CBDT', 'sbix', 'COLR', 'CPAL'] # Color _no_subset_tables_default = ['gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', 'name', 'cvt ', 'fpgm', 'prep'] _hinting_tables_default = ['cvt ', 'fpgm', 'prep', 'hdmx', 'VDMX'] # Based on HarfBuzz shapers _layout_features_groups = { # Default shaper 'common': ['ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'], 'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'], 'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'], 'ltr': ['ltra', 'ltrm'], 'rtl': ['rtla', 'rtlm'], # Complex shapers 'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3', 'cswh', 'mset'], 'hangul': ['ljmo', 'vjmo', 'tjmo'], 'tibetal': ['abvs', 'blws', 'abvm', 'blwm'], 'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half', 'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres', 'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'], } _layout_features_default = _uniq_sort(sum( _layout_features_groups.itervalues(), [])) drop_tables = _drop_tables_default no_subset_tables = _no_subset_tables_default hinting_tables = _hinting_tables_default layout_features = _layout_features_default hinting = False glyph_names = False legacy_cmap = False symbol_cmap = False name_IDs = [1, 2] # Family and Style name_legacy = False name_languages = [0x0409] # English notdef_glyph = True # gid0 for TrueType / .notdef for CFF notdef_outline = False # No need for notdef to have an outline really recommended_glyphs = False # gid1, gid2, gid3 for TrueType recalc_bounds = False # Recalculate font bounding boxes canonical_order = False # Order tables as recommended flavor = None # May be 'woff' def __init__(self, **kwargs): self.set(**kwargs) def set(self, **kwargs): for k,v in kwargs.iteritems(): if not hasattr(self, k): raise self.UnknownOptionError("Unknown option '%s'" % k) setattr(self, k, v) def parse_opts(self, argv, ignore_unknown=False): ret = [] opts = {} for a in argv: orig_a = a if not a.startswith('--'): ret.append(a) continue a = a[2:] i = a.find('=') op = '=' if i == -1: if a.startswith("no-"): k = a[3:] v = False else: k = a v = True else: k = a[:i] if k[-1] in "-+": op = k[-1]+'=' # Ops is '-=' or '+=' now. k = k[:-1] v = a[i+1:] k = k.replace('-', '_') if not hasattr(self, k): if ignore_unknown == True or k in ignore_unknown: ret.append(orig_a) continue else: raise self.UnknownOptionError("Unknown option '%s'" % a) ov = getattr(self, k) if isinstance(ov, bool): v = bool(v) elif isinstance(ov, int): v = int(v) elif isinstance(ov, list): vv = v.split(',') if vv == ['']: vv = [] vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] if op == '=': v = vv elif op == '+=': v = ov v.extend(vv) elif op == '-=': v = ov for x in vv: if x in v: v.remove(x) else: assert 0 opts[k] = v self.set(**opts) return ret class Subsetter(object): def __init__(self, options=None, log=None): if not log: log = Logger() if not options: options = Options() self.options = options self.log = log self.unicodes_requested = set() self.glyphs_requested = set() self.glyphs = set() def populate(self, glyphs=[], unicodes=[], text=""): self.unicodes_requested.update(unicodes) if isinstance(text, str): text = text.decode("utf8") for u in text: self.unicodes_requested.add(ord(u)) self.glyphs_requested.update(glyphs) self.glyphs.update(glyphs) def _prune_pre_subset(self, font): for tag in font.keys(): if tag == 'GlyphOrder': continue if(tag in self.options.drop_tables or (tag in self.options.hinting_tables and not self.options.hinting)): self.log(tag, "dropped") del font[tag] continue clazz = fontTools.ttLib.getTableClass(tag) if hasattr(clazz, 'prune_pre_subset'): table = font[tag] self.log.lapse("load '%s'" % tag) retain = table.prune_pre_subset(self.options) self.log.lapse("prune '%s'" % tag) if not retain: self.log(tag, "pruned to empty; dropped") del font[tag] continue else: self.log(tag, "pruned") def _closure_glyphs(self, font): self.glyphs = self.glyphs_requested.copy() if 'cmap' in font: font['cmap'].closure_glyphs(self) self.glyphs_cmaped = self.glyphs if self.options.notdef_glyph: if 'glyf' in font: self.glyphs.add(font.getGlyphName(0)) self.log("Added gid0 to subset") else: self.glyphs.add('.notdef') self.log("Added .notdef to subset") if self.options.recommended_glyphs: if 'glyf' in font: for i in range(4): self.glyphs.add(font.getGlyphName(i)) self.log("Added first four glyphs to subset") if 'GSUB' in font: self.log("Closing glyph list over 'GSUB': %d glyphs before" % len(self.glyphs)) self.log.glyphs(self.glyphs, font=font) font['GSUB'].closure_glyphs(self) self.log("Closed glyph list over 'GSUB': %d glyphs after" % len(self.glyphs)) self.log.glyphs(self.glyphs, font=font) self.log.lapse("close glyph list over 'GSUB'") self.glyphs_gsubed = self.glyphs.copy() if 'glyf' in font: self.log("Closing glyph list over 'glyf': %d glyphs before" % len(self.glyphs)) self.log.glyphs(self.glyphs, font=font) font['glyf'].closure_glyphs(self) self.log("Closed glyph list over 'glyf': %d glyphs after" % len(self.glyphs)) self.log.glyphs(self.glyphs, font=font) self.log.lapse("close glyph list over 'glyf'") self.glyphs_glyfed = self.glyphs.copy() self.glyphs_all = self.glyphs.copy() self.log("Retaining %d glyphs: " % len(self.glyphs_all)) def _subset_glyphs(self, font): for tag in font.keys(): if tag == 'GlyphOrder': continue clazz = 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): log = Logger() args = log.parse_opts(args) options = Options() args = options.parse_opts(args, ignore_unknown=['text']) if len(args) < 2: print >>sys.stderr, "usage: pyftsubset font-file glyph... [--text=ABC]... [--option=value]..." sys.exit(1) fontfile = args[0] args = args[1:] dontLoadGlyphNames =(not options.glyph_names and all(any(g.startswith(p) for p in ['gid', 'glyph', 'uni', 'U+']) for g in args)) font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames) 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 == '*': glyphs.extend(font.getGlyphOrder()) continue if g in names: glyphs.append(g) continue if g.startswith('--text='): text += g[7:] continue if g.startswith('uni') or g.startswith('U+'): if g.startswith('uni') and len(g) > 3: g = g[3:] elif g.startswith('U+') and len(g) > 2: g = g[2:] u = int(g, 16) unicodes.append(u) continue if g.startswith('gid') or g.startswith('glyph'): if g.startswith('gid') and len(g) > 3: g = g[3:] elif g.startswith('glyph') and len(g) > 5: g = g[5:] try: glyphs.append(font.getGlyphName(int(g), requireReal=1)) except ValueError: raise Exception("Invalid glyph identifier: %s" % g) continue raise Exception("Invalid glyph identifier: %s" % g) log.lapse("compile glyph list") log("Unicodes:", unicodes) log("Glyphs:", glyphs) subsetter.populate(glyphs=glyphs, unicodes=unicodes, text=text) subsetter.subset(font) outfile = fontfile + '.subset' save_font (font, outfile, options) log.lapse("compile and save font") log.last_time = log.start_time log.lapse("make one with everything(TOTAL TIME)") if log.verbose: import os log("Input font: %d bytes" % os.path.getsize(fontfile)) log("Subset font: %d bytes" % os.path.getsize(outfile)) log.font(font) font.close() __all__ = [ 'Options', 'Subsetter', 'Logger', 'load_font', 'save_font', 'main' ] if __name__ == '__main__': main(sys.argv[1:])