Home | History | Annotate | Download | only in fontTools
      1 # Copyright 2013 Google, Inc. All Rights Reserved.
      2 #
      3 # Google Author(s): Behdad Esfahbod
      4 
      5 """Python OpenType Layout Subsetter.
      6 
      7 Later grown into full OpenType subsetter, supporting all standard tables.
      8 """
      9 
     10 from __future__ import print_function, division, absolute_import
     11 from fontTools.misc.py23 import *
     12 from fontTools import ttLib
     13 from fontTools.ttLib.tables import otTables
     14 from fontTools.misc import psCharStrings
     15 from fontTools.pens import basePen
     16 import sys
     17 import struct
     18 import time
     19 import array
     20 
     21 
     22 def _add_method(*clazzes):
     23   """Returns a decorator function that adds a new method to one or
     24   more classes."""
     25   def wrapper(method):
     26     for clazz in clazzes:
     27       assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.'
     28       assert not hasattr(clazz, method.__name__), \
     29           "Oops, class '%s' has method '%s'." % (clazz.__name__,
     30                                                  method.__name__)
     31       setattr(clazz, method.__name__, method)
     32     return None
     33   return wrapper
     34 
     35 def _uniq_sort(l):
     36   return sorted(set(l))
     37 
     38 def _set_update(s, *others):
     39   # Jython's set.update only takes one other argument.
     40   # Emulate real set.update...
     41   for other in others:
     42     s.update(other)
     43 
     44 
     45 @_add_method(otTables.Coverage)
     46 def intersect(self, glyphs):
     47   "Returns ascending list of matching coverage values."
     48   return [i for i,g in enumerate(self.glyphs) if g in glyphs]
     49 
     50 @_add_method(otTables.Coverage)
     51 def intersect_glyphs(self, glyphs):
     52   "Returns set of intersecting glyphs."
     53   return set(g for g in self.glyphs if g in glyphs)
     54 
     55 @_add_method(otTables.Coverage)
     56 def subset(self, glyphs):
     57   "Returns ascending list of remaining coverage values."
     58   indices = self.intersect(glyphs)
     59   self.glyphs = [g for g in self.glyphs if g in glyphs]
     60   return indices
     61 
     62 @_add_method(otTables.Coverage)
     63 def remap(self, coverage_map):
     64   "Remaps coverage."
     65   self.glyphs = [self.glyphs[i] for i in coverage_map]
     66 
     67 @_add_method(otTables.ClassDef)
     68 def intersect(self, glyphs):
     69   "Returns ascending list of matching class values."
     70   return _uniq_sort(
     71      ([0] if any(g not in self.classDefs for g in glyphs) else []) +
     72       [v for g,v in self.classDefs.items() if g in glyphs])
     73 
     74 @_add_method(otTables.ClassDef)
     75 def intersect_class(self, glyphs, klass):
     76   "Returns set of glyphs matching class."
     77   if klass == 0:
     78     return set(g for g in glyphs if g not in self.classDefs)
     79   return set(g for g,v in self.classDefs.items()
     80               if v == klass and g in glyphs)
     81 
     82 @_add_method(otTables.ClassDef)
     83 def subset(self, glyphs, remap=False):
     84   "Returns ascending list of remaining classes."
     85   self.classDefs = dict((g,v) for g,v in self.classDefs.items() if g in glyphs)
     86   # Note: while class 0 has the special meaning of "not matched",
     87   # if no glyph will ever /not match/, we can optimize class 0 out too.
     88   indices = _uniq_sort(
     89      ([0] if any(g not in self.classDefs for g in glyphs) else []) +
     90       list(self.classDefs.values()))
     91   if remap:
     92     self.remap(indices)
     93   return indices
     94 
     95 @_add_method(otTables.ClassDef)
     96 def remap(self, class_map):
     97   "Remaps classes."
     98   self.classDefs = dict((g,class_map.index(v))
     99                          for g,v in self.classDefs.items())
    100 
    101 @_add_method(otTables.SingleSubst)
    102 def closure_glyphs(self, s, cur_glyphs=None):
    103   if cur_glyphs is None: cur_glyphs = s.glyphs
    104   s.glyphs.update(v for g,v in self.mapping.items() if g in cur_glyphs)
    105 
    106 @_add_method(otTables.SingleSubst)
    107 def subset_glyphs(self, s):
    108   self.mapping = dict((g,v) for g,v in self.mapping.items()
    109                       if g in s.glyphs and v in s.glyphs)
    110   return bool(self.mapping)
    111 
    112 @_add_method(otTables.MultipleSubst)
    113 def closure_glyphs(self, s, cur_glyphs=None):
    114   if cur_glyphs is None: cur_glyphs = s.glyphs
    115   indices = self.Coverage.intersect(cur_glyphs)
    116   _set_update(s.glyphs, *(self.Sequence[i].Substitute for i in indices))
    117 
    118 @_add_method(otTables.MultipleSubst)
    119 def subset_glyphs(self, s):
    120   indices = self.Coverage.subset(s.glyphs)
    121   self.Sequence = [self.Sequence[i] for i in indices]
    122   # Now drop rules generating glyphs we don't want
    123   indices = [i for i,seq in enumerate(self.Sequence)
    124        if all(sub in s.glyphs for sub in seq.Substitute)]
    125   self.Sequence = [self.Sequence[i] for i in indices]
    126   self.Coverage.remap(indices)
    127   self.SequenceCount = len(self.Sequence)
    128   return bool(self.SequenceCount)
    129 
    130 @_add_method(otTables.AlternateSubst)
    131 def closure_glyphs(self, s, cur_glyphs=None):
    132   if cur_glyphs is None: cur_glyphs = s.glyphs
    133   _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items()
    134                           if g in cur_glyphs))
    135 
    136 @_add_method(otTables.AlternateSubst)
    137 def subset_glyphs(self, s):
    138   self.alternates = dict((g,vlist)
    139                          for g,vlist in self.alternates.items()
    140                          if g in s.glyphs and
    141                             all(v in s.glyphs for v in vlist))
    142   return bool(self.alternates)
    143 
    144 @_add_method(otTables.LigatureSubst)
    145 def closure_glyphs(self, s, cur_glyphs=None):
    146   if cur_glyphs is None: cur_glyphs = s.glyphs
    147   _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs
    148                            if all(c in s.glyphs for c in seq.Component)]
    149                           for g,seqs in self.ligatures.items()
    150                           if g in cur_glyphs))
    151 
    152 @_add_method(otTables.LigatureSubst)
    153 def subset_glyphs(self, s):
    154   self.ligatures = dict((g,v) for g,v in self.ligatures.items()
    155                         if g in s.glyphs)
    156   self.ligatures = dict((g,[seq for seq in seqs
    157                             if seq.LigGlyph in s.glyphs and
    158                                all(c in s.glyphs for c in seq.Component)])
    159                          for g,seqs in self.ligatures.items())
    160   self.ligatures = dict((g,v) for g,v in self.ligatures.items() if v)
    161   return bool(self.ligatures)
    162 
    163 @_add_method(otTables.ReverseChainSingleSubst)
    164 def closure_glyphs(self, s, cur_glyphs=None):
    165   if cur_glyphs is None: cur_glyphs = s.glyphs
    166   if self.Format == 1:
    167     indices = self.Coverage.intersect(cur_glyphs)
    168     if(not indices or
    169         not all(c.intersect(s.glyphs)
    170                  for c in self.LookAheadCoverage + self.BacktrackCoverage)):
    171       return
    172     s.glyphs.update(self.Substitute[i] for i in indices)
    173   else:
    174     assert 0, "unknown format: %s" % self.Format
    175 
    176 @_add_method(otTables.ReverseChainSingleSubst)
    177 def subset_glyphs(self, s):
    178   if self.Format == 1:
    179     indices = self.Coverage.subset(s.glyphs)
    180     self.Substitute = [self.Substitute[i] for i in indices]
    181     # Now drop rules generating glyphs we don't want
    182     indices = [i for i,sub in enumerate(self.Substitute)
    183          if sub in s.glyphs]
    184     self.Substitute = [self.Substitute[i] for i in indices]
    185     self.Coverage.remap(indices)
    186     self.GlyphCount = len(self.Substitute)
    187     return bool(self.GlyphCount and
    188                  all(c.subset(s.glyphs)
    189                       for c in self.LookAheadCoverage+self.BacktrackCoverage))
    190   else:
    191     assert 0, "unknown format: %s" % self.Format
    192 
    193 @_add_method(otTables.SinglePos)
    194 def subset_glyphs(self, s):
    195   if self.Format == 1:
    196     return len(self.Coverage.subset(s.glyphs))
    197   elif self.Format == 2:
    198     indices = self.Coverage.subset(s.glyphs)
    199     self.Value = [self.Value[i] for i in indices]
    200     self.ValueCount = len(self.Value)
    201     return bool(self.ValueCount)
    202   else:
    203     assert 0, "unknown format: %s" % self.Format
    204 
    205 @_add_method(otTables.SinglePos)
    206 def prune_post_subset(self, options):
    207   if not options.hinting:
    208     # Drop device tables
    209     self.ValueFormat &= ~0x00F0
    210   return True
    211 
    212 @_add_method(otTables.PairPos)
    213 def subset_glyphs(self, s):
    214   if self.Format == 1:
    215     indices = self.Coverage.subset(s.glyphs)
    216     self.PairSet = [self.PairSet[i] for i in indices]
    217     for p in self.PairSet:
    218       p.PairValueRecord = [r for r in p.PairValueRecord
    219                            if r.SecondGlyph in s.glyphs]
    220       p.PairValueCount = len(p.PairValueRecord)
    221     # Remove empty pairsets
    222     indices = [i for i,p in enumerate(self.PairSet) if p.PairValueCount]
    223     self.Coverage.remap(indices)
    224     self.PairSet = [self.PairSet[i] for i in indices]
    225     self.PairSetCount = len(self.PairSet)
    226     return bool(self.PairSetCount)
    227   elif self.Format == 2:
    228     class1_map = self.ClassDef1.subset(s.glyphs, remap=True)
    229     class2_map = self.ClassDef2.subset(s.glyphs, remap=True)
    230     self.Class1Record = [self.Class1Record[i] for i in class1_map]
    231     for c in self.Class1Record:
    232       c.Class2Record = [c.Class2Record[i] for i in class2_map]
    233     self.Class1Count = len(class1_map)
    234     self.Class2Count = len(class2_map)
    235     return bool(self.Class1Count and
    236                  self.Class2Count and
    237                  self.Coverage.subset(s.glyphs))
    238   else:
    239     assert 0, "unknown format: %s" % self.Format
    240 
    241 @_add_method(otTables.PairPos)
    242 def prune_post_subset(self, options):
    243   if not options.hinting:
    244     # Drop device tables
    245     self.ValueFormat1 &= ~0x00F0
    246     self.ValueFormat2 &= ~0x00F0
    247   return True
    248 
    249 @_add_method(otTables.CursivePos)
    250 def subset_glyphs(self, s):
    251   if self.Format == 1:
    252     indices = self.Coverage.subset(s.glyphs)
    253     self.EntryExitRecord = [self.EntryExitRecord[i] for i in indices]
    254     self.EntryExitCount = len(self.EntryExitRecord)
    255     return bool(self.EntryExitCount)
    256   else:
    257     assert 0, "unknown format: %s" % self.Format
    258 
    259 @_add_method(otTables.Anchor)
    260 def prune_hints(self):
    261   # Drop device tables / contour anchor point
    262   self.ensureDecompiled()
    263   self.Format = 1
    264 
    265 @_add_method(otTables.CursivePos)
    266 def prune_post_subset(self, options):
    267   if not options.hinting:
    268     for rec in self.EntryExitRecord:
    269       if rec.EntryAnchor: rec.EntryAnchor.prune_hints()
    270       if rec.ExitAnchor: rec.ExitAnchor.prune_hints()
    271   return True
    272 
    273 @_add_method(otTables.MarkBasePos)
    274 def subset_glyphs(self, s):
    275   if self.Format == 1:
    276     mark_indices = self.MarkCoverage.subset(s.glyphs)
    277     self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i]
    278                                  for i in mark_indices]
    279     self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
    280     base_indices = self.BaseCoverage.subset(s.glyphs)
    281     self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i]
    282                                  for i in base_indices]
    283     self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord)
    284     # Prune empty classes
    285     class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
    286     self.ClassCount = len(class_indices)
    287     for m in self.MarkArray.MarkRecord:
    288       m.Class = class_indices.index(m.Class)
    289     for b in self.BaseArray.BaseRecord:
    290       b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices]
    291     return bool(self.ClassCount and
    292                  self.MarkArray.MarkCount and
    293                  self.BaseArray.BaseCount)
    294   else:
    295     assert 0, "unknown format: %s" % self.Format
    296 
    297 @_add_method(otTables.MarkBasePos)
    298 def prune_post_subset(self, options):
    299     if not options.hinting:
    300       for m in self.MarkArray.MarkRecord:
    301         if m.MarkAnchor:
    302           m.MarkAnchor.prune_hints()
    303       for b in self.BaseArray.BaseRecord:
    304         for a in b.BaseAnchor:
    305           if a:
    306             a.prune_hints()
    307     return True
    308 
    309 @_add_method(otTables.MarkLigPos)
    310 def subset_glyphs(self, s):
    311   if self.Format == 1:
    312     mark_indices = self.MarkCoverage.subset(s.glyphs)
    313     self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i]
    314                                  for i in mark_indices]
    315     self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
    316     ligature_indices = self.LigatureCoverage.subset(s.glyphs)
    317     self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i]
    318                                          for i in ligature_indices]
    319     self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach)
    320     # Prune empty classes
    321     class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
    322     self.ClassCount = len(class_indices)
    323     for m in self.MarkArray.MarkRecord:
    324       m.Class = class_indices.index(m.Class)
    325     for l in self.LigatureArray.LigatureAttach:
    326       for c in l.ComponentRecord:
    327         c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices]
    328     return bool(self.ClassCount and
    329                  self.MarkArray.MarkCount and
    330                  self.LigatureArray.LigatureCount)
    331   else:
    332     assert 0, "unknown format: %s" % self.Format
    333 
    334 @_add_method(otTables.MarkLigPos)
    335 def prune_post_subset(self, options):
    336     if not options.hinting:
    337       for m in self.MarkArray.MarkRecord:
    338         if m.MarkAnchor:
    339           m.MarkAnchor.prune_hints()
    340       for l in self.LigatureArray.LigatureAttach:
    341         for c in l.ComponentRecord:
    342           for a in c.LigatureAnchor:
    343             if a:
    344               a.prune_hints()
    345     return True
    346 
    347 @_add_method(otTables.MarkMarkPos)
    348 def subset_glyphs(self, s):
    349   if self.Format == 1:
    350     mark1_indices = self.Mark1Coverage.subset(s.glyphs)
    351     self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i]
    352                                   for i in mark1_indices]
    353     self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord)
    354     mark2_indices = self.Mark2Coverage.subset(s.glyphs)
    355     self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i]
    356                                    for i in mark2_indices]
    357     self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record)
    358     # Prune empty classes
    359     class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord)
    360     self.ClassCount = len(class_indices)
    361     for m in self.Mark1Array.MarkRecord:
    362       m.Class = class_indices.index(m.Class)
    363     for b in self.Mark2Array.Mark2Record:
    364       b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices]
    365     return bool(self.ClassCount and
    366                  self.Mark1Array.MarkCount and
    367                  self.Mark2Array.MarkCount)
    368   else:
    369     assert 0, "unknown format: %s" % self.Format
    370 
    371 @_add_method(otTables.MarkMarkPos)
    372 def prune_post_subset(self, options):
    373     if not options.hinting:
    374       # Drop device tables or contour anchor point
    375       for m in self.Mark1Array.MarkRecord:
    376         if m.MarkAnchor:
    377           m.MarkAnchor.prune_hints()
    378       for b in self.Mark2Array.Mark2Record:
    379         for m in b.Mark2Anchor:
    380           if m:
    381             m.prune_hints()
    382     return True
    383 
    384 @_add_method(otTables.SingleSubst,
    385              otTables.MultipleSubst,
    386              otTables.AlternateSubst,
    387              otTables.LigatureSubst,
    388              otTables.ReverseChainSingleSubst,
    389              otTables.SinglePos,
    390              otTables.PairPos,
    391              otTables.CursivePos,
    392              otTables.MarkBasePos,
    393              otTables.MarkLigPos,
    394              otTables.MarkMarkPos)
    395 def subset_lookups(self, lookup_indices):
    396   pass
    397 
    398 @_add_method(otTables.SingleSubst,
    399              otTables.MultipleSubst,
    400              otTables.AlternateSubst,
    401              otTables.LigatureSubst,
    402              otTables.ReverseChainSingleSubst,
    403              otTables.SinglePos,
    404              otTables.PairPos,
    405              otTables.CursivePos,
    406              otTables.MarkBasePos,
    407              otTables.MarkLigPos,
    408              otTables.MarkMarkPos)
    409 def collect_lookups(self):
    410   return []
    411 
    412 @_add_method(otTables.SingleSubst,
    413              otTables.MultipleSubst,
    414              otTables.AlternateSubst,
    415              otTables.LigatureSubst,
    416              otTables.ContextSubst,
    417              otTables.ChainContextSubst,
    418              otTables.ReverseChainSingleSubst,
    419              otTables.SinglePos,
    420              otTables.PairPos,
    421              otTables.CursivePos,
    422              otTables.MarkBasePos,
    423              otTables.MarkLigPos,
    424              otTables.MarkMarkPos,
    425              otTables.ContextPos,
    426              otTables.ChainContextPos)
    427 def prune_pre_subset(self, options):
    428   return True
    429 
    430 @_add_method(otTables.SingleSubst,
    431              otTables.MultipleSubst,
    432              otTables.AlternateSubst,
    433              otTables.LigatureSubst,
    434              otTables.ReverseChainSingleSubst,
    435              otTables.ContextSubst,
    436              otTables.ChainContextSubst,
    437              otTables.ContextPos,
    438              otTables.ChainContextPos)
    439 def prune_post_subset(self, options):
    440   return True
    441 
    442 @_add_method(otTables.SingleSubst,
    443              otTables.AlternateSubst,
    444              otTables.ReverseChainSingleSubst)
    445 def may_have_non_1to1(self):
    446   return False
    447 
    448 @_add_method(otTables.MultipleSubst,
    449              otTables.LigatureSubst,
    450              otTables.ContextSubst,
    451              otTables.ChainContextSubst)
    452 def may_have_non_1to1(self):
    453   return True
    454 
    455 @_add_method(otTables.ContextSubst,
    456              otTables.ChainContextSubst,
    457              otTables.ContextPos,
    458              otTables.ChainContextPos)
    459 def __classify_context(self):
    460 
    461   class ContextHelper(object):
    462     def __init__(self, klass, Format):
    463       if klass.__name__.endswith('Subst'):
    464         Typ = 'Sub'
    465         Type = 'Subst'
    466       else:
    467         Typ = 'Pos'
    468         Type = 'Pos'
    469       if klass.__name__.startswith('Chain'):
    470         Chain = 'Chain'
    471       else:
    472         Chain = ''
    473       ChainTyp = Chain+Typ
    474 
    475       self.Typ = Typ
    476       self.Type = Type
    477       self.Chain = Chain
    478       self.ChainTyp = ChainTyp
    479 
    480       self.LookupRecord = Type+'LookupRecord'
    481 
    482       if Format == 1:
    483         Coverage = lambda r: r.Coverage
    484         ChainCoverage = lambda r: r.Coverage
    485         ContextData = lambda r:(None,)
    486         ChainContextData = lambda r:(None, None, None)
    487         RuleData = lambda r:(r.Input,)
    488         ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead)
    489         SetRuleData = None
    490         ChainSetRuleData = None
    491       elif Format == 2:
    492         Coverage = lambda r: r.Coverage
    493         ChainCoverage = lambda r: r.Coverage
    494         ContextData = lambda r:(r.ClassDef,)
    495         ChainContextData = lambda r:(r.LookAheadClassDef,
    496                                       r.InputClassDef,
    497                                       r.BacktrackClassDef)
    498         RuleData = lambda r:(r.Class,)
    499         ChainRuleData = lambda r:(r.LookAhead, r.Input, r.Backtrack)
    500         def SetRuleData(r, d):(r.Class,) = d
    501         def ChainSetRuleData(r, d):(r.LookAhead, r.Input, r.Backtrack) = d
    502       elif Format == 3:
    503         Coverage = lambda r: r.Coverage[0]
    504         ChainCoverage = lambda r: r.InputCoverage[0]
    505         ContextData = None
    506         ChainContextData = None
    507         RuleData = lambda r: r.Coverage
    508         ChainRuleData = lambda r:(r.LookAheadCoverage +
    509                                    r.InputCoverage +
    510                                    r.BacktrackCoverage)
    511         SetRuleData = None
    512         ChainSetRuleData = None
    513       else:
    514         assert 0, "unknown format: %s" % Format
    515 
    516       if Chain:
    517         self.Coverage = ChainCoverage
    518         self.ContextData = ChainContextData
    519         self.RuleData = ChainRuleData
    520         self.SetRuleData = ChainSetRuleData
    521       else:
    522         self.Coverage = Coverage
    523         self.ContextData = ContextData
    524         self.RuleData = RuleData
    525         self.SetRuleData = SetRuleData
    526 
    527       if Format == 1:
    528         self.Rule = ChainTyp+'Rule'
    529         self.RuleCount = ChainTyp+'RuleCount'
    530         self.RuleSet = ChainTyp+'RuleSet'
    531         self.RuleSetCount = ChainTyp+'RuleSetCount'
    532         self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
    533       elif Format == 2:
    534         self.Rule = ChainTyp+'ClassRule'
    535         self.RuleCount = ChainTyp+'ClassRuleCount'
    536         self.RuleSet = ChainTyp+'ClassSet'
    537         self.RuleSetCount = ChainTyp+'ClassSetCount'
    538         self.Intersect = lambda glyphs, c, r: c.intersect_class(glyphs, r)
    539 
    540         self.ClassDef = 'InputClassDef' if Chain else 'ClassDef'
    541         self.ClassDefIndex = 1 if Chain else 0
    542         self.Input = 'Input' if Chain else 'Class'
    543 
    544   if self.Format not in [1, 2, 3]:
    545     return None  # Don't shoot the messenger; let it go
    546   if not hasattr(self.__class__, "__ContextHelpers"):
    547     self.__class__.__ContextHelpers = {}
    548   if self.Format not in self.__class__.__ContextHelpers:
    549     helper = ContextHelper(self.__class__, self.Format)
    550     self.__class__.__ContextHelpers[self.Format] = helper
    551   return self.__class__.__ContextHelpers[self.Format]
    552 
    553 @_add_method(otTables.ContextSubst,
    554              otTables.ChainContextSubst)
    555 def closure_glyphs(self, s, cur_glyphs=None):
    556   if cur_glyphs is None: cur_glyphs = s.glyphs
    557   c = self.__classify_context()
    558 
    559   indices = c.Coverage(self).intersect(s.glyphs)
    560   if not indices:
    561     return []
    562   cur_glyphs = c.Coverage(self).intersect_glyphs(s.glyphs);
    563 
    564   if self.Format == 1:
    565     ContextData = c.ContextData(self)
    566     rss = getattr(self, c.RuleSet)
    567     rssCount = getattr(self, c.RuleSetCount)
    568     for i in indices:
    569       if i >= rssCount or not rss[i]: continue
    570       for r in getattr(rss[i], c.Rule):
    571         if not r: continue
    572         if all(all(c.Intersect(s.glyphs, cd, k) for k in klist)
    573           for cd,klist in zip(ContextData, c.RuleData(r))):
    574           chaos = False
    575           for ll in getattr(r, c.LookupRecord):
    576             if not ll: continue
    577             seqi = ll.SequenceIndex
    578             if chaos:
    579               pos_glyphs = s.glyphs
    580             else:
    581               if seqi == 0:
    582                 pos_glyphs = set([c.Coverage(self).glyphs[i]])
    583               else:
    584                 pos_glyphs = set([r.Input[seqi - 1]])
    585             lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
    586             chaos = chaos or lookup.may_have_non_1to1()
    587             lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
    588   elif self.Format == 2:
    589     ClassDef = getattr(self, c.ClassDef)
    590     indices = ClassDef.intersect(cur_glyphs)
    591     ContextData = c.ContextData(self)
    592     rss = getattr(self, c.RuleSet)
    593     rssCount = getattr(self, c.RuleSetCount)
    594     for i in indices:
    595       if i >= rssCount or not rss[i]: continue
    596       for r in getattr(rss[i], c.Rule):
    597         if not r: continue
    598         if all(all(c.Intersect(s.glyphs, cd, k) for k in klist)
    599           for cd,klist in zip(ContextData, c.RuleData(r))):
    600           chaos = False
    601           for ll in getattr(r, c.LookupRecord):
    602             if not ll: continue
    603             seqi = ll.SequenceIndex
    604             if chaos:
    605               pos_glyphs = s.glyphs
    606             else:
    607               if seqi == 0:
    608                 pos_glyphs = ClassDef.intersect_class(cur_glyphs, i)
    609               else:
    610                 pos_glyphs = ClassDef.intersect_class(s.glyphs,
    611                                                       getattr(r, c.Input)[seqi - 1])
    612             lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
    613             chaos = chaos or lookup.may_have_non_1to1()
    614             lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
    615   elif self.Format == 3:
    616     if not all(x.intersect(s.glyphs) for x in c.RuleData(self)):
    617       return []
    618     r = self
    619     chaos = False
    620     for ll in getattr(r, c.LookupRecord):
    621       if not ll: continue
    622       seqi = ll.SequenceIndex
    623       if chaos:
    624         pos_glyphs = s.glyphs
    625       else:
    626         if seqi == 0:
    627           pos_glyphs = cur_glyphs
    628         else:
    629           pos_glyphs = r.InputCoverage[seqi].intersect_glyphs(s.glyphs)
    630       lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
    631       chaos = chaos or lookup.may_have_non_1to1()
    632       lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
    633   else:
    634     assert 0, "unknown format: %s" % self.Format
    635 
    636 @_add_method(otTables.ContextSubst,
    637              otTables.ContextPos,
    638              otTables.ChainContextSubst,
    639              otTables.ChainContextPos)
    640 def subset_glyphs(self, s):
    641   c = self.__classify_context()
    642 
    643   if self.Format == 1:
    644     indices = self.Coverage.subset(s.glyphs)
    645     rss = getattr(self, c.RuleSet)
    646     rss = [rss[i] for i in indices]
    647     for rs in rss:
    648       if not rs: continue
    649       ss = getattr(rs, c.Rule)
    650       ss = [r for r in ss
    651             if r and all(all(g in s.glyphs for g in glist)
    652               for glist in c.RuleData(r))]
    653       setattr(rs, c.Rule, ss)
    654       setattr(rs, c.RuleCount, len(ss))
    655     # Prune empty subrulesets
    656     rss = [rs for rs in rss if rs and getattr(rs, c.Rule)]
    657     setattr(self, c.RuleSet, rss)
    658     setattr(self, c.RuleSetCount, len(rss))
    659     return bool(rss)
    660   elif self.Format == 2:
    661     if not self.Coverage.subset(s.glyphs):
    662       return False
    663     ContextData = c.ContextData(self)
    664     klass_maps = [x.subset(s.glyphs, remap=True) for x in ContextData]
    665 
    666     # Keep rulesets for class numbers that survived.
    667     indices = klass_maps[c.ClassDefIndex]
    668     rss = getattr(self, c.RuleSet)
    669     rssCount = getattr(self, c.RuleSetCount)
    670     rss = [rss[i] for i in indices if i < rssCount]
    671     del rssCount
    672     # Delete, but not renumber, unreachable rulesets.
    673     indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs)
    674     rss = [rss if i in indices else None for i,rss in enumerate(rss)]
    675     while rss and rss[-1] is None:
    676       del rss[-1]
    677 
    678     for rs in rss:
    679       if not rs: continue
    680       ss = getattr(rs, c.Rule)
    681       ss = [r for r in ss
    682             if r and all(all(k in klass_map for k in klist)
    683               for klass_map,klist in zip(klass_maps, c.RuleData(r)))]
    684       setattr(rs, c.Rule, ss)
    685       setattr(rs, c.RuleCount, len(ss))
    686 
    687       # Remap rule classes
    688       for r in ss:
    689         c.SetRuleData(r, [[klass_map.index(k) for k in klist]
    690                for klass_map,klist in zip(klass_maps, c.RuleData(r))])
    691     return bool(rss)
    692   elif self.Format == 3:
    693     return all(x.subset(s.glyphs) for x in c.RuleData(self))
    694   else:
    695     assert 0, "unknown format: %s" % self.Format
    696 
    697 @_add_method(otTables.ContextSubst,
    698              otTables.ChainContextSubst,
    699              otTables.ContextPos,
    700              otTables.ChainContextPos)
    701 def subset_lookups(self, lookup_indices):
    702   c = self.__classify_context()
    703 
    704   if self.Format in [1, 2]:
    705     for rs in getattr(self, c.RuleSet):
    706       if not rs: continue
    707       for r in getattr(rs, c.Rule):
    708         if not r: continue
    709         setattr(r, c.LookupRecord,
    710                  [ll for ll in getattr(r, c.LookupRecord)
    711                   if ll and ll.LookupListIndex in lookup_indices])
    712         for ll in getattr(r, c.LookupRecord):
    713           if not ll: continue
    714           ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
    715   elif self.Format == 3:
    716     setattr(self, c.LookupRecord,
    717              [ll for ll in getattr(self, c.LookupRecord)
    718               if ll and ll.LookupListIndex in lookup_indices])
    719     for ll in getattr(self, c.LookupRecord):
    720       if not ll: continue
    721       ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
    722   else:
    723     assert 0, "unknown format: %s" % self.Format
    724 
    725 @_add_method(otTables.ContextSubst,
    726              otTables.ChainContextSubst,
    727              otTables.ContextPos,
    728              otTables.ChainContextPos)
    729 def collect_lookups(self):
    730   c = self.__classify_context()
    731 
    732   if self.Format in [1, 2]:
    733     return [ll.LookupListIndex
    734       for rs in getattr(self, c.RuleSet) if rs
    735       for r in getattr(rs, c.Rule) if r
    736       for ll in getattr(r, c.LookupRecord) if ll]
    737   elif self.Format == 3:
    738     return [ll.LookupListIndex
    739       for ll in getattr(self, c.LookupRecord) if ll]
    740   else:
    741     assert 0, "unknown format: %s" % self.Format
    742 
    743 @_add_method(otTables.ExtensionSubst)
    744 def closure_glyphs(self, s, cur_glyphs=None):
    745   if self.Format == 1:
    746     self.ExtSubTable.closure_glyphs(s, cur_glyphs)
    747   else:
    748     assert 0, "unknown format: %s" % self.Format
    749 
    750 @_add_method(otTables.ExtensionSubst)
    751 def may_have_non_1to1(self):
    752   if self.Format == 1:
    753     return self.ExtSubTable.may_have_non_1to1()
    754   else:
    755     assert 0, "unknown format: %s" % self.Format
    756 
    757 @_add_method(otTables.ExtensionSubst,
    758              otTables.ExtensionPos)
    759 def prune_pre_subset(self, options):
    760   if self.Format == 1:
    761     return self.ExtSubTable.prune_pre_subset(options)
    762   else:
    763     assert 0, "unknown format: %s" % self.Format
    764 
    765 @_add_method(otTables.ExtensionSubst,
    766              otTables.ExtensionPos)
    767 def subset_glyphs(self, s):
    768   if self.Format == 1:
    769     return self.ExtSubTable.subset_glyphs(s)
    770   else:
    771     assert 0, "unknown format: %s" % self.Format
    772 
    773 @_add_method(otTables.ExtensionSubst,
    774              otTables.ExtensionPos)
    775 def prune_post_subset(self, options):
    776   if self.Format == 1:
    777     return self.ExtSubTable.prune_post_subset(options)
    778   else:
    779     assert 0, "unknown format: %s" % self.Format
    780 
    781 @_add_method(otTables.ExtensionSubst,
    782              otTables.ExtensionPos)
    783 def subset_lookups(self, lookup_indices):
    784   if self.Format == 1:
    785     return self.ExtSubTable.subset_lookups(lookup_indices)
    786   else:
    787     assert 0, "unknown format: %s" % self.Format
    788 
    789 @_add_method(otTables.ExtensionSubst,
    790              otTables.ExtensionPos)
    791 def collect_lookups(self):
    792   if self.Format == 1:
    793     return self.ExtSubTable.collect_lookups()
    794   else:
    795     assert 0, "unknown format: %s" % self.Format
    796 
    797 @_add_method(otTables.Lookup)
    798 def closure_glyphs(self, s, cur_glyphs=None):
    799   for st in self.SubTable:
    800     if not st: continue
    801     st.closure_glyphs(s, cur_glyphs)
    802 
    803 @_add_method(otTables.Lookup)
    804 def prune_pre_subset(self, options):
    805   ret = False
    806   for st in self.SubTable:
    807     if not st: continue
    808     if st.prune_pre_subset(options): ret = True
    809   return ret
    810 
    811 @_add_method(otTables.Lookup)
    812 def subset_glyphs(self, s):
    813   self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)]
    814   self.SubTableCount = len(self.SubTable)
    815   return bool(self.SubTableCount)
    816 
    817 @_add_method(otTables.Lookup)
    818 def prune_post_subset(self, options):
    819   ret = False
    820   for st in self.SubTable:
    821     if not st: continue
    822     if st.prune_post_subset(options): ret = True
    823   return ret
    824 
    825 @_add_method(otTables.Lookup)
    826 def subset_lookups(self, lookup_indices):
    827   for s in self.SubTable:
    828     s.subset_lookups(lookup_indices)
    829 
    830 @_add_method(otTables.Lookup)
    831 def collect_lookups(self):
    832   return _uniq_sort(sum((st.collect_lookups() for st in self.SubTable
    833                          if st), []))
    834 
    835 @_add_method(otTables.Lookup)
    836 def may_have_non_1to1(self):
    837   return any(st.may_have_non_1to1() for st in self.SubTable if st)
    838 
    839 @_add_method(otTables.LookupList)
    840 def prune_pre_subset(self, options):
    841   ret = False
    842   for l in self.Lookup:
    843     if not l: continue
    844     if l.prune_pre_subset(options): ret = True
    845   return ret
    846 
    847 @_add_method(otTables.LookupList)
    848 def subset_glyphs(self, s):
    849   "Returns the indices of nonempty lookups."
    850   return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)]
    851 
    852 @_add_method(otTables.LookupList)
    853 def prune_post_subset(self, options):
    854   ret = False
    855   for l in self.Lookup:
    856     if not l: continue
    857     if l.prune_post_subset(options): ret = True
    858   return ret
    859 
    860 @_add_method(otTables.LookupList)
    861 def subset_lookups(self, lookup_indices):
    862   self.ensureDecompiled()
    863   self.Lookup = [self.Lookup[i] for i in lookup_indices
    864                  if i < self.LookupCount]
    865   self.LookupCount = len(self.Lookup)
    866   for l in self.Lookup:
    867     l.subset_lookups(lookup_indices)
    868 
    869 @_add_method(otTables.LookupList)
    870 def closure_lookups(self, lookup_indices):
    871   lookup_indices = _uniq_sort(lookup_indices)
    872   recurse = lookup_indices
    873   while True:
    874     recurse_lookups = sum((self.Lookup[i].collect_lookups()
    875                             for i in recurse if i < self.LookupCount), [])
    876     recurse_lookups = [l for l in recurse_lookups
    877                        if l not in lookup_indices and l < self.LookupCount]
    878     if not recurse_lookups:
    879       return _uniq_sort(lookup_indices)
    880     recurse_lookups = _uniq_sort(recurse_lookups)
    881     lookup_indices.extend(recurse_lookups)
    882     recurse = recurse_lookups
    883 
    884 @_add_method(otTables.Feature)
    885 def subset_lookups(self, lookup_indices):
    886   self.LookupListIndex = [l for l in self.LookupListIndex
    887                           if l in lookup_indices]
    888   # Now map them.
    889   self.LookupListIndex = [lookup_indices.index(l)
    890                           for l in self.LookupListIndex]
    891   self.LookupCount = len(self.LookupListIndex)
    892   return self.LookupCount or self.FeatureParams
    893 
    894 @_add_method(otTables.Feature)
    895 def collect_lookups(self):
    896   return self.LookupListIndex[:]
    897 
    898 @_add_method(otTables.FeatureList)
    899 def subset_lookups(self, lookup_indices):
    900   "Returns the indices of nonempty features."
    901   # Note: Never ever drop feature 'pref', even if it's empty.
    902   # HarfBuzz chooses shaper for Khmer based on presence of this
    903   # feature.  See thread at:
    904   # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html
    905   feature_indices = [i for i,f in enumerate(self.FeatureRecord)
    906                      if (f.Feature.subset_lookups(lookup_indices) or
    907                          f.FeatureTag == 'pref')]
    908   self.subset_features(feature_indices)
    909   return feature_indices
    910 
    911 @_add_method(otTables.FeatureList)
    912 def collect_lookups(self, feature_indices):
    913   return _uniq_sort(sum((self.FeatureRecord[i].Feature.collect_lookups()
    914                          for i in feature_indices
    915                           if i < self.FeatureCount), []))
    916 
    917 @_add_method(otTables.FeatureList)
    918 def subset_features(self, feature_indices):
    919   self.ensureDecompiled()
    920   self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices]
    921   self.FeatureCount = len(self.FeatureRecord)
    922   return bool(self.FeatureCount)
    923 
    924 @_add_method(otTables.DefaultLangSys,
    925              otTables.LangSys)
    926 def subset_features(self, feature_indices):
    927   if self.ReqFeatureIndex in feature_indices:
    928     self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex)
    929   else:
    930     self.ReqFeatureIndex = 65535
    931   self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices]
    932   # Now map them.
    933   self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex
    934                        if f in feature_indices]
    935   self.FeatureCount = len(self.FeatureIndex)
    936   return bool(self.FeatureCount or self.ReqFeatureIndex != 65535)
    937 
    938 @_add_method(otTables.DefaultLangSys,
    939              otTables.LangSys)
    940 def collect_features(self):
    941   feature_indices = self.FeatureIndex[:]
    942   if self.ReqFeatureIndex != 65535:
    943     feature_indices.append(self.ReqFeatureIndex)
    944   return _uniq_sort(feature_indices)
    945 
    946 @_add_method(otTables.Script)
    947 def subset_features(self, feature_indices):
    948   if(self.DefaultLangSys and
    949       not self.DefaultLangSys.subset_features(feature_indices)):
    950     self.DefaultLangSys = None
    951   self.LangSysRecord = [l for l in self.LangSysRecord
    952                         if l.LangSys.subset_features(feature_indices)]
    953   self.LangSysCount = len(self.LangSysRecord)
    954   return bool(self.LangSysCount or self.DefaultLangSys)
    955 
    956 @_add_method(otTables.Script)
    957 def collect_features(self):
    958   feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord]
    959   if self.DefaultLangSys:
    960     feature_indices.append(self.DefaultLangSys.collect_features())
    961   return _uniq_sort(sum(feature_indices, []))
    962 
    963 @_add_method(otTables.ScriptList)
    964 def subset_features(self, feature_indices):
    965   self.ScriptRecord = [s for s in self.ScriptRecord
    966                        if s.Script.subset_features(feature_indices)]
    967   self.ScriptCount = len(self.ScriptRecord)
    968   return bool(self.ScriptCount)
    969 
    970 @_add_method(otTables.ScriptList)
    971 def collect_features(self):
    972   return _uniq_sort(sum((s.Script.collect_features()
    973                          for s in self.ScriptRecord), []))
    974 
    975 @_add_method(ttLib.getTableClass('GSUB'))
    976 def closure_glyphs(self, s):
    977   s.table = self.table
    978   if self.table.ScriptList:
    979     feature_indices = self.table.ScriptList.collect_features()
    980   else:
    981     feature_indices = []
    982   if self.table.FeatureList:
    983     lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
    984   else:
    985     lookup_indices = []
    986   if self.table.LookupList:
    987     while True:
    988       orig_glyphs = s.glyphs.copy()
    989       for i in lookup_indices:
    990         if i >= self.table.LookupList.LookupCount: continue
    991         if not self.table.LookupList.Lookup[i]: continue
    992         self.table.LookupList.Lookup[i].closure_glyphs(s)
    993       if orig_glyphs == s.glyphs:
    994         break
    995   del s.table
    996 
    997 @_add_method(ttLib.getTableClass('GSUB'),
    998              ttLib.getTableClass('GPOS'))
    999 def subset_glyphs(self, s):
   1000   s.glyphs = s.glyphs_gsubed
   1001   if self.table.LookupList:
   1002     lookup_indices = self.table.LookupList.subset_glyphs(s)
   1003   else:
   1004     lookup_indices = []
   1005   self.subset_lookups(lookup_indices)
   1006   self.prune_lookups()
   1007   return True
   1008 
   1009 @_add_method(ttLib.getTableClass('GSUB'),
   1010              ttLib.getTableClass('GPOS'))
   1011 def subset_lookups(self, lookup_indices):
   1012   """Retains specified lookups, then removes empty features, language
   1013      systems, and scripts."""
   1014   if self.table.LookupList:
   1015     self.table.LookupList.subset_lookups(lookup_indices)
   1016   if self.table.FeatureList:
   1017     feature_indices = self.table.FeatureList.subset_lookups(lookup_indices)
   1018   else:
   1019     feature_indices = []
   1020   if self.table.ScriptList:
   1021     self.table.ScriptList.subset_features(feature_indices)
   1022 
   1023 @_add_method(ttLib.getTableClass('GSUB'),
   1024              ttLib.getTableClass('GPOS'))
   1025 def prune_lookups(self):
   1026   "Remove unreferenced lookups"
   1027   if self.table.ScriptList:
   1028     feature_indices = self.table.ScriptList.collect_features()
   1029   else:
   1030     feature_indices = []
   1031   if self.table.FeatureList:
   1032     lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
   1033   else:
   1034     lookup_indices = []
   1035   if self.table.LookupList:
   1036     lookup_indices = self.table.LookupList.closure_lookups(lookup_indices)
   1037   else:
   1038     lookup_indices = []
   1039   self.subset_lookups(lookup_indices)
   1040 
   1041 @_add_method(ttLib.getTableClass('GSUB'),
   1042              ttLib.getTableClass('GPOS'))
   1043 def subset_feature_tags(self, feature_tags):
   1044   if self.table.FeatureList:
   1045     feature_indices = [i for i,f in
   1046                        enumerate(self.table.FeatureList.FeatureRecord)
   1047                        if f.FeatureTag in feature_tags]
   1048     self.table.FeatureList.subset_features(feature_indices)
   1049   else:
   1050     feature_indices = []
   1051   if self.table.ScriptList:
   1052     self.table.ScriptList.subset_features(feature_indices)
   1053 
   1054 @_add_method(ttLib.getTableClass('GSUB'),
   1055              ttLib.getTableClass('GPOS'))
   1056 def prune_features(self):
   1057   "Remove unreferenced featurs"
   1058   if self.table.ScriptList:
   1059     feature_indices = self.table.ScriptList.collect_features()
   1060   else:
   1061     feature_indices = []
   1062   if self.table.FeatureList:
   1063     self.table.FeatureList.subset_features(feature_indices)
   1064   if self.table.ScriptList:
   1065     self.table.ScriptList.subset_features(feature_indices)
   1066 
   1067 @_add_method(ttLib.getTableClass('GSUB'),
   1068              ttLib.getTableClass('GPOS'))
   1069 def prune_pre_subset(self, options):
   1070   # Drop undesired features
   1071   if '*' not in options.layout_features:
   1072     self.subset_feature_tags(options.layout_features)
   1073   # Drop unreferenced lookups
   1074   self.prune_lookups()
   1075   # Prune lookups themselves
   1076   if self.table.LookupList:
   1077     self.table.LookupList.prune_pre_subset(options);
   1078   return True
   1079 
   1080 @_add_method(ttLib.getTableClass('GSUB'),
   1081              ttLib.getTableClass('GPOS'))
   1082 def remove_redundant_langsys(self):
   1083   table = self.table
   1084   if not table.ScriptList or not table.FeatureList:
   1085     return
   1086 
   1087   features = table.FeatureList.FeatureRecord
   1088 
   1089   for s in table.ScriptList.ScriptRecord:
   1090     d = s.Script.DefaultLangSys
   1091     if not d:
   1092       continue
   1093     for lr in s.Script.LangSysRecord[:]:
   1094       l = lr.LangSys
   1095       # Compare d and l
   1096       if len(d.FeatureIndex) != len(l.FeatureIndex):
   1097         continue
   1098       if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535):
   1099         continue
   1100 
   1101       if d.ReqFeatureIndex != 65535:
   1102         if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]:
   1103           continue
   1104 
   1105       for i in range(len(d.FeatureIndex)):
   1106         if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]:
   1107           break
   1108       else:
   1109         # LangSys and default are equal; delete LangSys
   1110         s.Script.LangSysRecord.remove(lr)
   1111 
   1112 @_add_method(ttLib.getTableClass('GSUB'),
   1113              ttLib.getTableClass('GPOS'))
   1114 def prune_post_subset(self, options):
   1115   table = self.table
   1116 
   1117   # LookupList looks good.  Just prune lookups themselves
   1118   if table.LookupList:
   1119     table.LookupList.prune_post_subset(options);
   1120     # XXX Next two lines disabled because OTS is stupid and
   1121     # doesn't like NULL offsetse here.
   1122     #if not table.LookupList.Lookup:
   1123     #  table.LookupList = None
   1124 
   1125   if not table.LookupList:
   1126     table.FeatureList = None
   1127 
   1128   if table.FeatureList:
   1129     self.remove_redundant_langsys()
   1130     # Remove unreferenced features
   1131     self.prune_features()
   1132 
   1133   # XXX Next two lines disabled because OTS is stupid and
   1134   # doesn't like NULL offsetse here.
   1135   #if table.FeatureList and not table.FeatureList.FeatureRecord:
   1136   #  table.FeatureList = None
   1137 
   1138   # Never drop scripts themselves as them just being available
   1139   # holds semantic significance.
   1140   # XXX Next two lines disabled because OTS is stupid and
   1141   # doesn't like NULL offsetse here.
   1142   #if table.ScriptList and not table.ScriptList.ScriptRecord:
   1143   #  table.ScriptList = None
   1144 
   1145   return True
   1146 
   1147 @_add_method(ttLib.getTableClass('GDEF'))
   1148 def subset_glyphs(self, s):
   1149   glyphs = s.glyphs_gsubed
   1150   table = self.table
   1151   if table.LigCaretList:
   1152     indices = table.LigCaretList.Coverage.subset(glyphs)
   1153     table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i]
   1154                                    for i in indices]
   1155     table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph)
   1156   if table.MarkAttachClassDef:
   1157     table.MarkAttachClassDef.classDefs = dict((g,v) for g,v in
   1158                                               table.MarkAttachClassDef.
   1159                                                 classDefs.items()
   1160                                               if g in glyphs)
   1161   if table.GlyphClassDef:
   1162     table.GlyphClassDef.classDefs = dict((g,v) for g,v in
   1163                                          table.GlyphClassDef.
   1164                                            classDefs.items()
   1165                                          if g in glyphs)
   1166   if table.AttachList:
   1167     indices = table.AttachList.Coverage.subset(glyphs)
   1168     GlyphCount = table.AttachList.GlyphCount
   1169     table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i]
   1170                                     for i in indices
   1171                                     if i < GlyphCount]
   1172     table.AttachList.GlyphCount = len(table.AttachList.AttachPoint)
   1173   if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef:
   1174     for coverage in table.MarkGlyphSetsDef.Coverage:
   1175       coverage.subset(glyphs)
   1176     # TODO: The following is disabled.  If enabling, we need to go fixup all
   1177     # lookups that use MarkFilteringSet and map their set.
   1178     #indices = table.MarkGlyphSetsDef.Coverage = [c for c in table.MarkGlyphSetsDef.Coverage if c.glyphs]
   1179   return True
   1180 
   1181 @_add_method(ttLib.getTableClass('GDEF'))
   1182 def prune_post_subset(self, options):
   1183   table = self.table
   1184   # XXX check these against OTS
   1185   if table.LigCaretList and not table.LigCaretList.LigGlyphCount:
   1186     table.LigCaretList = None
   1187   if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs:
   1188     table.MarkAttachClassDef = None
   1189   if table.GlyphClassDef and not table.GlyphClassDef.classDefs:
   1190     table.GlyphClassDef = None
   1191   if table.AttachList and not table.AttachList.GlyphCount:
   1192     table.AttachList = None
   1193   if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef and not table.MarkGlyphSetsDef.Coverage:
   1194     table.MarkGlyphSetsDef = None
   1195     if table.Version == 0x00010002/0x10000:
   1196       table.Version = 1.0
   1197   return bool(table.LigCaretList or
   1198               table.MarkAttachClassDef or
   1199               table.GlyphClassDef or
   1200               table.AttachList or
   1201               (table.Version >= 0x00010002/0x10000 and table.MarkGlyphSetsDef))
   1202 
   1203 @_add_method(ttLib.getTableClass('kern'))
   1204 def prune_pre_subset(self, options):
   1205   # Prune unknown kern table types
   1206   self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')]
   1207   return bool(self.kernTables)
   1208 
   1209 @_add_method(ttLib.getTableClass('kern'))
   1210 def subset_glyphs(self, s):
   1211   glyphs = s.glyphs_gsubed
   1212   for t in self.kernTables:
   1213     t.kernTable = dict(((a,b),v) for (a,b),v in t.kernTable.items()
   1214                        if a in glyphs and b in glyphs)
   1215   self.kernTables = [t for t in self.kernTables if t.kernTable]
   1216   return bool(self.kernTables)
   1217 
   1218 @_add_method(ttLib.getTableClass('vmtx'))
   1219 def subset_glyphs(self, s):
   1220   self.metrics = dict((g,v) for g,v in self.metrics.items() if g in s.glyphs)
   1221   return bool(self.metrics)
   1222 
   1223 @_add_method(ttLib.getTableClass('hmtx'))
   1224 def subset_glyphs(self, s):
   1225   self.metrics = dict((g,v) for g,v in self.metrics.items() if g in s.glyphs)
   1226   return True # Required table
   1227 
   1228 @_add_method(ttLib.getTableClass('hdmx'))
   1229 def subset_glyphs(self, s):
   1230   self.hdmx = dict((sz,dict((g,v) for g,v in l.items() if g in s.glyphs))
   1231                    for sz,l in self.hdmx.items())
   1232   return bool(self.hdmx)
   1233 
   1234 @_add_method(ttLib.getTableClass('VORG'))
   1235 def subset_glyphs(self, s):
   1236   self.VOriginRecords = dict((g,v) for g,v in self.VOriginRecords.items()
   1237                              if g in s.glyphs)
   1238   self.numVertOriginYMetrics = len(self.VOriginRecords)
   1239   return True  # Never drop; has default metrics
   1240 
   1241 @_add_method(ttLib.getTableClass('post'))
   1242 def prune_pre_subset(self, options):
   1243   if not options.glyph_names:
   1244     self.formatType = 3.0
   1245   return True # Required table
   1246 
   1247 @_add_method(ttLib.getTableClass('post'))
   1248 def subset_glyphs(self, s):
   1249   self.extraNames = []  # This seems to do it
   1250   return True # Required table
   1251 
   1252 @_add_method(ttLib.getTableModule('glyf').Glyph)
   1253 def remapComponentsFast(self, indices):
   1254   if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
   1255     return  # Not composite
   1256   data = array.array("B", self.data)
   1257   i = 10
   1258   more = 1
   1259   while more:
   1260     flags =(data[i] << 8) | data[i+1]
   1261     glyphID =(data[i+2] << 8) | data[i+3]
   1262     # Remap
   1263     glyphID = indices.index(glyphID)
   1264     data[i+2] = glyphID >> 8
   1265     data[i+3] = glyphID & 0xFF
   1266     i += 4
   1267     flags = int(flags)
   1268 
   1269     if flags & 0x0001: i += 4  # ARG_1_AND_2_ARE_WORDS
   1270     else: i += 2
   1271     if flags & 0x0008: i += 2  # WE_HAVE_A_SCALE
   1272     elif flags & 0x0040: i += 4  # WE_HAVE_AN_X_AND_Y_SCALE
   1273     elif flags & 0x0080: i += 8  # WE_HAVE_A_TWO_BY_TWO
   1274     more = flags & 0x0020  # MORE_COMPONENTS
   1275 
   1276   self.data = data.tostring()
   1277 
   1278 @_add_method(ttLib.getTableClass('glyf'))
   1279 def closure_glyphs(self, s):
   1280   decompose = s.glyphs
   1281   while True:
   1282     components = set()
   1283     for g in decompose:
   1284       if g not in self.glyphs:
   1285         continue
   1286       gl = self.glyphs[g]
   1287       for c in gl.getComponentNames(self):
   1288         if c not in s.glyphs:
   1289           components.add(c)
   1290     components = set(c for c in components if c not in s.glyphs)
   1291     if not components:
   1292       break
   1293     decompose = components
   1294     s.glyphs.update(components)
   1295 
   1296 @_add_method(ttLib.getTableClass('glyf'))
   1297 def prune_pre_subset(self, options):
   1298   if options.notdef_glyph and not options.notdef_outline:
   1299     g = self[self.glyphOrder[0]]
   1300     # Yay, easy!
   1301     g.__dict__.clear()
   1302     g.data = ""
   1303   return True
   1304 
   1305 @_add_method(ttLib.getTableClass('glyf'))
   1306 def subset_glyphs(self, s):
   1307   self.glyphs = dict((g,v) for g,v in self.glyphs.items() if g in s.glyphs)
   1308   indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs]
   1309   for v in self.glyphs.values():
   1310     if hasattr(v, "data"):
   1311       v.remapComponentsFast(indices)
   1312     else:
   1313       pass  # No need
   1314   self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs]
   1315   # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset.
   1316   return True
   1317 
   1318 @_add_method(ttLib.getTableClass('glyf'))
   1319 def prune_post_subset(self, options):
   1320   if not options.hinting:
   1321     for v in self.glyphs.values():
   1322       v.removeHinting()
   1323   return True
   1324 
   1325 @_add_method(ttLib.getTableClass('CFF '))
   1326 def prune_pre_subset(self, options):
   1327   cff = self.cff
   1328   # CFF table must have one font only
   1329   cff.fontNames = cff.fontNames[:1]
   1330 
   1331   if options.notdef_glyph and not options.notdef_outline:
   1332     for fontname in cff.keys():
   1333       font = cff[fontname]
   1334       c,_ = font.CharStrings.getItemAndSelector('.notdef')
   1335       # XXX we should preserve the glyph width
   1336       c.bytecode = '\x0e' # endchar
   1337       c.program = None
   1338 
   1339   return True # bool(cff.fontNames)
   1340 
   1341 @_add_method(ttLib.getTableClass('CFF '))
   1342 def subset_glyphs(self, s):
   1343   cff = self.cff
   1344   for fontname in cff.keys():
   1345     font = cff[fontname]
   1346     cs = font.CharStrings
   1347 
   1348     # Load all glyphs
   1349     for g in font.charset:
   1350       if g not in s.glyphs: continue
   1351       c,sel = cs.getItemAndSelector(g)
   1352 
   1353     if cs.charStringsAreIndexed:
   1354       indices = [i for i,g in enumerate(font.charset) if g in s.glyphs]
   1355       csi = cs.charStringsIndex
   1356       csi.items = [csi.items[i] for i in indices]
   1357       csi.count = len(csi.items)
   1358       del csi.file, csi.offsets
   1359       if hasattr(font, "FDSelect"):
   1360         sel = font.FDSelect
   1361         sel.format = None
   1362         sel.gidArray = [sel.gidArray[i] for i in indices]
   1363       cs.charStrings = dict((g,indices.index(v))
   1364                             for g,v in cs.charStrings.items()
   1365                             if g in s.glyphs)
   1366     else:
   1367       cs.charStrings = dict((g,v)
   1368                             for g,v in cs.charStrings.items()
   1369                             if g in s.glyphs)
   1370     font.charset = [g for g in font.charset if g in s.glyphs]
   1371     font.numGlyphs = len(font.charset)
   1372 
   1373   return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
   1374 
   1375 @_add_method(psCharStrings.T2CharString)
   1376 def subset_subroutines(self, subrs, gsubrs):
   1377   p = self.program
   1378   assert len(p)
   1379   for i in range(1, len(p)):
   1380     if p[i] == 'callsubr':
   1381       assert isinstance(p[i-1], int)
   1382       p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias
   1383     elif p[i] == 'callgsubr':
   1384       assert isinstance(p[i-1], int)
   1385       p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias
   1386 
   1387 @_add_method(psCharStrings.T2CharString)
   1388 def drop_hints(self):
   1389   hints = self._hints
   1390 
   1391   if hints.has_hint:
   1392     self.program = self.program[hints.last_hint:]
   1393     if hasattr(self, 'width'):
   1394       # Insert width back if needed
   1395       if self.width != self.private.defaultWidthX:
   1396         self.program.insert(0, self.width - self.private.nominalWidthX)
   1397 
   1398   if hints.has_hintmask:
   1399     i = 0
   1400     p = self.program
   1401     while i < len(p):
   1402       if p[i] in ['hintmask', 'cntrmask']:
   1403         assert i + 1 <= len(p)
   1404         del p[i:i+2]
   1405         continue
   1406       i += 1
   1407 
   1408   # TODO: we currently don't drop calls to "empty" subroutines.
   1409 
   1410   assert len(self.program)
   1411 
   1412   del self._hints
   1413 
   1414 class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler):
   1415 
   1416   def __init__(self, localSubrs, globalSubrs):
   1417     psCharStrings.SimpleT2Decompiler.__init__(self,
   1418                                               localSubrs,
   1419                                               globalSubrs)
   1420     for subrs in [localSubrs, globalSubrs]:
   1421       if subrs and not hasattr(subrs, "_used"):
   1422         subrs._used = set()
   1423 
   1424   def op_callsubr(self, index):
   1425     self.localSubrs._used.add(self.operandStack[-1]+self.localBias)
   1426     psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
   1427 
   1428   def op_callgsubr(self, index):
   1429     self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias)
   1430     psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
   1431 
   1432 class _DehintingT2Decompiler(psCharStrings.SimpleT2Decompiler):
   1433 
   1434   class Hints(object):
   1435     def __init__(self):
   1436       # Whether calling this charstring produces any hint stems
   1437       self.has_hint = False
   1438       # Index to start at to drop all hints
   1439       self.last_hint = 0
   1440       # Index up to which we know more hints are possible.  Only
   1441       # relevant if status is 0 or 1.
   1442       self.last_checked = 0
   1443       # The status means:
   1444       # 0: after dropping hints, this charstring is empty
   1445       # 1: after dropping hints, there may be more hints continuing after this
   1446       # 2: no more hints possible after this charstring
   1447       self.status = 0
   1448       # Has hintmask instructions; not recursive
   1449       self.has_hintmask = False
   1450     pass
   1451 
   1452   def __init__(self, css, localSubrs, globalSubrs):
   1453     self._css = css
   1454     psCharStrings.SimpleT2Decompiler.__init__(self,
   1455                                               localSubrs,
   1456                                               globalSubrs)
   1457 
   1458   def execute(self, charString):
   1459     old_hints = charString._hints if hasattr(charString, '_hints') else None
   1460     charString._hints = self.Hints()
   1461 
   1462     psCharStrings.SimpleT2Decompiler.execute(self, charString)
   1463 
   1464     hints = charString._hints
   1465 
   1466     if hints.has_hint or hints.has_hintmask:
   1467       self._css.add(charString)
   1468 
   1469     if hints.status != 2:
   1470       # Check from last_check, make sure we didn't have any operators.
   1471       for i in range(hints.last_checked, len(charString.program) - 1):
   1472         if isinstance(charString.program[i], str):
   1473           hints.status = 2
   1474           break;
   1475         else:
   1476           hints.status = 1 # There's *something* here
   1477       hints.last_checked = len(charString.program)
   1478 
   1479     if old_hints:
   1480       assert hints.__dict__ == old_hints.__dict__
   1481 
   1482   def op_callsubr(self, index):
   1483     subr = self.localSubrs[self.operandStack[-1]+self.localBias]
   1484     psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
   1485     self.processSubr(index, subr)
   1486 
   1487   def op_callgsubr(self, index):
   1488     subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
   1489     psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
   1490     self.processSubr(index, subr)
   1491 
   1492   def op_hstem(self, index):
   1493     psCharStrings.SimpleT2Decompiler.op_hstem(self, index)
   1494     self.processHint(index)
   1495   def op_vstem(self, index):
   1496     psCharStrings.SimpleT2Decompiler.op_vstem(self, index)
   1497     self.processHint(index)
   1498   def op_hstemhm(self, index):
   1499     psCharStrings.SimpleT2Decompiler.op_hstemhm(self, index)
   1500     self.processHint(index)
   1501   def op_vstemhm(self, index):
   1502     psCharStrings.SimpleT2Decompiler.op_vstemhm(self, index)
   1503     self.processHint(index)
   1504   def op_hintmask(self, index):
   1505     psCharStrings.SimpleT2Decompiler.op_hintmask(self, index)
   1506     self.processHintmask(index)
   1507   def op_cntrmask(self, index):
   1508     psCharStrings.SimpleT2Decompiler.op_cntrmask(self, index)
   1509     self.processHintmask(index)
   1510 
   1511   def processHintmask(self, index):
   1512     cs = self.callingStack[-1]
   1513     hints = cs._hints
   1514     hints.has_hintmask = True
   1515     if hints.status != 2 and hints.has_hint:
   1516       # Check from last_check, see if we may be an implicit vstem
   1517       for i in range(hints.last_checked, index - 1):
   1518         if isinstance(cs.program[i], str):
   1519           hints.status = 2
   1520           break;
   1521       if hints.status != 2:
   1522         # We are an implicit vstem
   1523         hints.last_hint = index + 1
   1524         hints.status = 0
   1525     hints.last_checked = index + 1
   1526 
   1527   def processHint(self, index):
   1528     cs = self.callingStack[-1]
   1529     hints = cs._hints
   1530     hints.has_hint = True
   1531     hints.last_hint = index
   1532     hints.last_checked = index
   1533 
   1534   def processSubr(self, index, subr):
   1535     cs = self.callingStack[-1]
   1536     hints = cs._hints
   1537     subr_hints = subr._hints
   1538 
   1539     if subr_hints.has_hint:
   1540       if hints.status != 2:
   1541         hints.has_hint = True
   1542         hints.last_checked = index
   1543         hints.status = subr_hints.status
   1544         # Decide where to chop off from
   1545         if subr_hints.status == 0:
   1546           hints.last_hint = index
   1547         else:
   1548           hints.last_hint = index - 2 # Leave the subr call in
   1549       else:
   1550         # In my understanding, this is a font bug.  Ie. it has hint stems
   1551         # *after* path construction.  I've seen this in widespread fonts.
   1552         # Best to ignore the hints I suppose...
   1553         pass
   1554         #assert 0
   1555     else:
   1556       hints.status = max(hints.status, subr_hints.status)
   1557       if hints.status != 2:
   1558         # Check from last_check, make sure we didn't have
   1559         # any operators.
   1560         for i in range(hints.last_checked, index - 1):
   1561           if isinstance(cs.program[i], str):
   1562             hints.status = 2
   1563             break;
   1564         hints.last_checked = index
   1565       if hints.status != 2:
   1566         # Decide where to chop off from
   1567         if subr_hints.status == 0:
   1568           hints.last_hint = index
   1569         else:
   1570           hints.last_hint = index - 2 # Leave the subr call in
   1571 
   1572 @_add_method(ttLib.getTableClass('CFF '))
   1573 def prune_post_subset(self, options):
   1574   cff = self.cff
   1575   for fontname in cff.keys():
   1576     font = cff[fontname]
   1577     cs = font.CharStrings
   1578 
   1579 
   1580     #
   1581     # Drop unused FontDictionaries
   1582     #
   1583     if hasattr(font, "FDSelect"):
   1584       sel = font.FDSelect
   1585       indices = _uniq_sort(sel.gidArray)
   1586       sel.gidArray = [indices.index (ss) for ss in sel.gidArray]
   1587       arr = font.FDArray
   1588       arr.items = [arr[i] for i in indices]
   1589       arr.count = len(arr.items)
   1590       del arr.file, arr.offsets
   1591 
   1592 
   1593     #
   1594     # Drop hints if not needed
   1595     #
   1596     if not options.hinting:
   1597 
   1598       #
   1599       # This can be tricky, but doesn't have to.  What we do is:
   1600       #
   1601       # - Run all used glyph charstrings and recurse into subroutines,
   1602       # - For each charstring (including subroutines), if it has any
   1603       #   of the hint stem operators, we mark it as such.  Upon returning,
   1604       #   for each charstring we note all the subroutine calls it makes
   1605       #   that (recursively) contain a stem,
   1606       # - Dropping hinting then consists of the following two ops:
   1607       #   * Drop the piece of the program in each charstring before the
   1608       #     last call to a stem op or a stem-calling subroutine,
   1609       #   * Drop all hintmask operations.
   1610       # - It's trickier... A hintmask right after hints and a few numbers
   1611       #   will act as an implicit vstemhm.  As such, we track whether
   1612       #   we have seen any non-hint operators so far and do the right
   1613       #   thing, recursively...  Good luck understanding that :(
   1614       #
   1615       css = set()
   1616       for g in font.charset:
   1617         c,sel = cs.getItemAndSelector(g)
   1618         # Make sure it's decompiled.  We want our "decompiler" to walk
   1619         # the program, not the bytecode.
   1620         c.draw(basePen.NullPen())
   1621         subrs = getattr(c.private, "Subrs", [])
   1622         decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs)
   1623         decompiler.execute(c)
   1624       for charstring in css:
   1625         charstring.drop_hints()
   1626 
   1627       # Drop font-wide hinting values
   1628       all_privs = []
   1629       if hasattr(font, 'FDSelect'):
   1630         all_privs.extend(fd.Private for fd in font.FDArray)
   1631       else:
   1632         all_privs.append(font.Private)
   1633       for priv in all_privs:
   1634         for k in ['BlueValues', 'OtherBlues', 'FamilyBlues', 'FamilyOtherBlues',
   1635                   'BlueScale', 'BlueShift', 'BlueFuzz',
   1636                   'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW']:
   1637           if hasattr(priv, k):
   1638             setattr(priv, k, None)
   1639 
   1640 
   1641     #
   1642     # Renumber subroutines to remove unused ones
   1643     #
   1644 
   1645     # Mark all used subroutines
   1646     for g in font.charset:
   1647       c,sel = cs.getItemAndSelector(g)
   1648       subrs = getattr(c.private, "Subrs", [])
   1649       decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs)
   1650       decompiler.execute(c)
   1651 
   1652     all_subrs = [font.GlobalSubrs]
   1653     if hasattr(font, 'FDSelect'):
   1654       all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs)
   1655     elif hasattr(font.Private, 'Subrs') and font.Private.Subrs:
   1656       all_subrs.append(font.Private.Subrs)
   1657 
   1658     subrs = set(subrs) # Remove duplicates
   1659 
   1660     # Prepare
   1661     for subrs in all_subrs:
   1662       if not hasattr(subrs, '_used'):
   1663         subrs._used = set()
   1664       subrs._used = _uniq_sort(subrs._used)
   1665       subrs._old_bias = psCharStrings.calcSubrBias(subrs)
   1666       subrs._new_bias = psCharStrings.calcSubrBias(subrs._used)
   1667 
   1668     # Renumber glyph charstrings
   1669     for g in font.charset:
   1670       c,sel = cs.getItemAndSelector(g)
   1671       subrs = getattr(c.private, "Subrs", [])
   1672       c.subset_subroutines (subrs, font.GlobalSubrs)
   1673 
   1674     # Renumber subroutines themselves
   1675     for subrs in all_subrs:
   1676 
   1677       if subrs == font.GlobalSubrs:
   1678         if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'):
   1679           local_subrs = font.Private.Subrs
   1680         else:
   1681           local_subrs = []
   1682       else:
   1683         local_subrs = subrs
   1684 
   1685       subrs.items = [subrs.items[i] for i in subrs._used]
   1686       subrs.count = len(subrs.items)
   1687       del subrs.file
   1688       if hasattr(subrs, 'offsets'):
   1689         del subrs.offsets
   1690 
   1691       for i in range (subrs.count):
   1692         subrs[i].subset_subroutines (local_subrs, font.GlobalSubrs)
   1693 
   1694     # Cleanup
   1695     for subrs in all_subrs:
   1696       del subrs._used, subrs._old_bias, subrs._new_bias
   1697 
   1698   return True
   1699 
   1700 @_add_method(ttLib.getTableClass('cmap'))
   1701 def closure_glyphs(self, s):
   1702   tables = [t for t in self.tables if t.isUnicode()]
   1703   for u in s.unicodes_requested:
   1704     found = False
   1705     for table in tables:
   1706       if table.format == 14:
   1707         for l in table.uvsDict.values():
   1708           # TODO(behdad) Speed this up!
   1709           gids = [g for uc,g in l if u == uc and g is not None]
   1710           s.glyphs.update(gids)
   1711           # Intentionally not setting found=True here.
   1712       else:
   1713         if u in table.cmap:
   1714           s.glyphs.add(table.cmap[u])
   1715           found = True
   1716     if not found:
   1717       s.log("No default glyph for Unicode %04X found." % u)
   1718 
   1719 @_add_method(ttLib.getTableClass('cmap'))
   1720 def prune_pre_subset(self, options):
   1721   if not options.legacy_cmap:
   1722     # Drop non-Unicode / non-Symbol cmaps
   1723     self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()]
   1724   if not options.symbol_cmap:
   1725     self.tables = [t for t in self.tables if not t.isSymbol()]
   1726   # TODO(behdad) Only keep one subtable?
   1727   # For now, drop format=0 which can't be subset_glyphs easily?
   1728   self.tables = [t for t in self.tables if t.format != 0]
   1729   self.numSubTables = len(self.tables)
   1730   return True # Required table
   1731 
   1732 @_add_method(ttLib.getTableClass('cmap'))
   1733 def subset_glyphs(self, s):
   1734   s.glyphs = s.glyphs_cmaped
   1735   for t in self.tables:
   1736     # For reasons I don't understand I need this here
   1737     # to force decompilation of the cmap format 14.
   1738     try:
   1739       getattr(t, "asdf")
   1740     except AttributeError:
   1741       pass
   1742     if t.format == 14:
   1743       # TODO(behdad) We drop all the default-UVS mappings for glyphs_requested.
   1744       # I don't think we care about that...
   1745       t.uvsDict = dict((v,[(u,g) for u,g in l
   1746                            if g in s.glyphs or u in s.unicodes_requested])
   1747                        for v,l in t.uvsDict.items())
   1748       t.uvsDict = dict((v,l) for v,l in t.uvsDict.items() if l)
   1749     elif t.isUnicode():
   1750       t.cmap = dict((u,g) for u,g in t.cmap.items()
   1751                     if g in s.glyphs_requested or u in s.unicodes_requested)
   1752     else:
   1753       t.cmap = dict((u,g) for u,g in t.cmap.items()
   1754                     if g in s.glyphs_requested)
   1755   self.tables = [t for t in self.tables
   1756                  if (t.cmap if t.format != 14 else t.uvsDict)]
   1757   self.numSubTables = len(self.tables)
   1758   # TODO(behdad) Convert formats when needed.
   1759   # In particular, if we have a format=12 without non-BMP
   1760   # characters, either drop format=12 one or convert it
   1761   # to format=4 if there's not one.
   1762   return True # Required table
   1763 
   1764 @_add_method(ttLib.getTableClass('name'))
   1765 def prune_pre_subset(self, options):
   1766   if '*' not in options.name_IDs:
   1767     self.names = [n for n in self.names if n.nameID in options.name_IDs]
   1768   if not options.name_legacy:
   1769     self.names = [n for n in self.names if n.isUnicode()]
   1770   # TODO(behdad) Option to keep only one platform's
   1771   if '*' not in options.name_languages:
   1772     # TODO(behdad) This is Windows-platform specific!
   1773     self.names = [n for n in self.names if n.langID in options.name_languages]
   1774   return True  # Required table
   1775 
   1776 
   1777 # TODO(behdad) OS/2 ulUnicodeRange / ulCodePageRange?
   1778 # TODO(behdad) Drop AAT tables.
   1779 # TODO(behdad) Drop unneeded GSUB/GPOS Script/LangSys entries.
   1780 # TODO(behdad) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left
   1781 # TODO(behdad) Drop GDEF subitems if unused by lookups
   1782 # TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF)
   1783 # TODO(behdad) Text direction considerations.
   1784 # TODO(behdad) Text script / language considerations.
   1785 # TODO(behdad) Optionally drop 'kern' table if GPOS available
   1786 # TODO(behdad) Implement --unicode='*' to choose all cmap'ed
   1787 # TODO(behdad) Drop old-spec Indic scripts
   1788 
   1789 
   1790 class Options(object):
   1791 
   1792   class UnknownOptionError(Exception):
   1793     pass
   1794 
   1795   _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'SVG ',
   1796                           'PCLT', 'LTSH']
   1797   _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill']  # Graphite
   1798   _drop_tables_default += ['CBLC', 'CBDT', 'sbix', 'COLR', 'CPAL']  # Color
   1799   _no_subset_tables_default = ['gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2',
   1800                                'loca', 'name', 'cvt ', 'fpgm', 'prep']
   1801   _hinting_tables_default = ['cvt ', 'fpgm', 'prep', 'hdmx', 'VDMX']
   1802 
   1803   # Based on HarfBuzz shapers
   1804   _layout_features_groups = {
   1805     # Default shaper
   1806     'common': ['ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'],
   1807     'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'],
   1808     'vertical':  ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'],
   1809     'ltr': ['ltra', 'ltrm'],
   1810     'rtl': ['rtla', 'rtlm'],
   1811     # Complex shapers
   1812     'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3',
   1813                'cswh', 'mset'],
   1814     'hangul': ['ljmo', 'vjmo', 'tjmo'],
   1815     'tibetan': ['abvs', 'blws', 'abvm', 'blwm'],
   1816     'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half',
   1817               'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres',
   1818               'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'],
   1819   }
   1820   _layout_features_default = _uniq_sort(sum(
   1821       iter(_layout_features_groups.values()), []))
   1822 
   1823   drop_tables = _drop_tables_default
   1824   no_subset_tables = _no_subset_tables_default
   1825   hinting_tables = _hinting_tables_default
   1826   layout_features = _layout_features_default
   1827   hinting = True
   1828   glyph_names = False
   1829   legacy_cmap = False
   1830   symbol_cmap = False
   1831   name_IDs = [1, 2]  # Family and Style
   1832   name_legacy = False
   1833   name_languages = [0x0409]  # English
   1834   notdef_glyph = True # gid0 for TrueType / .notdef for CFF
   1835   notdef_outline = False # No need for notdef to have an outline really
   1836   recommended_glyphs = False  # gid1, gid2, gid3 for TrueType
   1837   recalc_bounds = False # Recalculate font bounding boxes
   1838   recalc_timestamp = False # Recalculate font modified timestamp
   1839   canonical_order = False # Order tables as recommended
   1840   flavor = None # May be 'woff'
   1841 
   1842   def __init__(self, **kwargs):
   1843 
   1844     self.set(**kwargs)
   1845 
   1846   def set(self, **kwargs):
   1847     for k,v in kwargs.items():
   1848       if not hasattr(self, k):
   1849         raise self.UnknownOptionError("Unknown option '%s'" % k)
   1850       setattr(self, k, v)
   1851 
   1852   def parse_opts(self, argv, ignore_unknown=False):
   1853     ret = []
   1854     opts = {}
   1855     for a in argv:
   1856       orig_a = a
   1857       if not a.startswith('--'):
   1858         ret.append(a)
   1859         continue
   1860       a = a[2:]
   1861       i = a.find('=')
   1862       op = '='
   1863       if i == -1:
   1864         if a.startswith("no-"):
   1865           k = a[3:]
   1866           v = False
   1867         else:
   1868           k = a
   1869           v = True
   1870       else:
   1871         k = a[:i]
   1872         if k[-1] in "-+":
   1873           op = k[-1]+'='  # Ops is '-=' or '+=' now.
   1874           k = k[:-1]
   1875         v = a[i+1:]
   1876       k = k.replace('-', '_')
   1877       if not hasattr(self, k):
   1878         if ignore_unknown is True or k in ignore_unknown:
   1879           ret.append(orig_a)
   1880           continue
   1881         else:
   1882           raise self.UnknownOptionError("Unknown option '%s'" % a)
   1883 
   1884       ov = getattr(self, k)
   1885       if isinstance(ov, bool):
   1886         v = bool(v)
   1887       elif isinstance(ov, int):
   1888         v = int(v)
   1889       elif isinstance(ov, list):
   1890         vv = v.split(',')
   1891         if vv == ['']:
   1892           vv = []
   1893         vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
   1894         if op == '=':
   1895           v = vv
   1896         elif op == '+=':
   1897           v = ov
   1898           v.extend(vv)
   1899         elif op == '-=':
   1900           v = ov
   1901           for x in vv:
   1902             if x in v:
   1903               v.remove(x)
   1904         else:
   1905           assert False
   1906 
   1907       opts[k] = v
   1908     self.set(**opts)
   1909 
   1910     return ret
   1911 
   1912 
   1913 class Subsetter(object):
   1914 
   1915   def __init__(self, options=None, log=None):
   1916 
   1917     if not log:
   1918       log = Logger()
   1919     if not options:
   1920       options = Options()
   1921 
   1922     self.options = options
   1923     self.log = log
   1924     self.unicodes_requested = set()
   1925     self.glyphs_requested = set()
   1926     self.glyphs = set()
   1927 
   1928   def populate(self, glyphs=[], unicodes=[], text=""):
   1929     self.unicodes_requested.update(unicodes)
   1930     if isinstance(text, bytes):
   1931       text = text.decode("utf8")
   1932     for u in text:
   1933       self.unicodes_requested.add(ord(u))
   1934     self.glyphs_requested.update(glyphs)
   1935     self.glyphs.update(glyphs)
   1936 
   1937   def _prune_pre_subset(self, font):
   1938 
   1939     for tag in font.keys():
   1940       if tag == 'GlyphOrder': continue
   1941 
   1942       if(tag in self.options.drop_tables or
   1943          (tag in self.options.hinting_tables and not self.options.hinting)):
   1944         self.log(tag, "dropped")
   1945         del font[tag]
   1946         continue
   1947 
   1948       clazz = ttLib.getTableClass(tag)
   1949 
   1950       if hasattr(clazz, 'prune_pre_subset'):
   1951         table = font[tag]
   1952         self.log.lapse("load '%s'" % tag)
   1953         retain = table.prune_pre_subset(self.options)
   1954         self.log.lapse("prune  '%s'" % tag)
   1955         if not retain:
   1956           self.log(tag, "pruned to empty; dropped")
   1957           del font[tag]
   1958           continue
   1959         else:
   1960           self.log(tag, "pruned")
   1961 
   1962   def _closure_glyphs(self, font):
   1963 
   1964     realGlyphs = set(font.getGlyphOrder())
   1965 
   1966     self.glyphs = self.glyphs_requested.copy()
   1967 
   1968     if 'cmap' in font:
   1969       font['cmap'].closure_glyphs(self)
   1970       self.glyphs.intersection_update(realGlyphs)
   1971     self.glyphs_cmaped = self.glyphs
   1972 
   1973     if self.options.notdef_glyph:
   1974       if 'glyf' in font:
   1975         self.glyphs.add(font.getGlyphName(0))
   1976         self.log("Added gid0 to subset")
   1977       else:
   1978         self.glyphs.add('.notdef')
   1979         self.log("Added .notdef to subset")
   1980     if self.options.recommended_glyphs:
   1981       if 'glyf' in font:
   1982         for i in range(min(4, len(font.getGlyphOrder()))):
   1983           self.glyphs.add(font.getGlyphName(i))
   1984         self.log("Added first four glyphs to subset")
   1985 
   1986     if 'GSUB' in font:
   1987       self.log("Closing glyph list over 'GSUB': %d glyphs before" %
   1988                 len(self.glyphs))
   1989       self.log.glyphs(self.glyphs, font=font)
   1990       font['GSUB'].closure_glyphs(self)
   1991       self.glyphs.intersection_update(realGlyphs)
   1992       self.log("Closed  glyph list over 'GSUB': %d glyphs after" %
   1993                 len(self.glyphs))
   1994       self.log.glyphs(self.glyphs, font=font)
   1995       self.log.lapse("close glyph list over 'GSUB'")
   1996     self.glyphs_gsubed = self.glyphs.copy()
   1997 
   1998     if 'glyf' in font:
   1999       self.log("Closing glyph list over 'glyf': %d glyphs before" %
   2000                 len(self.glyphs))
   2001       self.log.glyphs(self.glyphs, font=font)
   2002       font['glyf'].closure_glyphs(self)
   2003       self.glyphs.intersection_update(realGlyphs)
   2004       self.log("Closed  glyph list over 'glyf': %d glyphs after" %
   2005                 len(self.glyphs))
   2006       self.log.glyphs(self.glyphs, font=font)
   2007       self.log.lapse("close glyph list over 'glyf'")
   2008     self.glyphs_glyfed = self.glyphs.copy()
   2009 
   2010     self.glyphs_all = self.glyphs.copy()
   2011 
   2012     self.log("Retaining %d glyphs: " % len(self.glyphs_all))
   2013 
   2014     del self.glyphs
   2015 
   2016 
   2017   def _subset_glyphs(self, font):
   2018     for tag in font.keys():
   2019       if tag == 'GlyphOrder': continue
   2020       clazz = ttLib.getTableClass(tag)
   2021 
   2022       if tag in self.options.no_subset_tables:
   2023         self.log(tag, "subsetting not needed")
   2024       elif hasattr(clazz, 'subset_glyphs'):
   2025         table = font[tag]
   2026         self.glyphs = self.glyphs_all
   2027         retain = table.subset_glyphs(self)
   2028         del self.glyphs
   2029         self.log.lapse("subset '%s'" % tag)
   2030         if not retain:
   2031           self.log(tag, "subsetted to empty; dropped")
   2032           del font[tag]
   2033         else:
   2034           self.log(tag, "subsetted")
   2035       else:
   2036         self.log(tag, "NOT subset; don't know how to subset; dropped")
   2037         del font[tag]
   2038 
   2039     glyphOrder = font.getGlyphOrder()
   2040     glyphOrder = [g for g in glyphOrder if g in self.glyphs_all]
   2041     font.setGlyphOrder(glyphOrder)
   2042     font._buildReverseGlyphOrderDict()
   2043     self.log.lapse("subset GlyphOrder")
   2044 
   2045   def _prune_post_subset(self, font):
   2046     for tag in font.keys():
   2047       if tag == 'GlyphOrder': continue
   2048       clazz = ttLib.getTableClass(tag)
   2049       if hasattr(clazz, 'prune_post_subset'):
   2050         table = font[tag]
   2051         retain = table.prune_post_subset(self.options)
   2052         self.log.lapse("prune  '%s'" % tag)
   2053         if not retain:
   2054           self.log(tag, "pruned to empty; dropped")
   2055           del font[tag]
   2056         else:
   2057           self.log(tag, "pruned")
   2058 
   2059   def subset(self, font):
   2060 
   2061     self._prune_pre_subset(font)
   2062     self._closure_glyphs(font)
   2063     self._subset_glyphs(font)
   2064     self._prune_post_subset(font)
   2065 
   2066 
   2067 class Logger(object):
   2068 
   2069   def __init__(self, verbose=False, xml=False, timing=False):
   2070     self.verbose = verbose
   2071     self.xml = xml
   2072     self.timing = timing
   2073     self.last_time = self.start_time = time.time()
   2074 
   2075   def parse_opts(self, argv):
   2076     argv = argv[:]
   2077     for v in ['verbose', 'xml', 'timing']:
   2078       if "--"+v in argv:
   2079         setattr(self, v, True)
   2080         argv.remove("--"+v)
   2081     return argv
   2082 
   2083   def __call__(self, *things):
   2084     if not self.verbose:
   2085       return
   2086     print(' '.join(str(x) for x in things))
   2087 
   2088   def lapse(self, *things):
   2089     if not self.timing:
   2090       return
   2091     new_time = time.time()
   2092     print("Took %0.3fs to %s" %(new_time - self.last_time,
   2093                                  ' '.join(str(x) for x in things)))
   2094     self.last_time = new_time
   2095 
   2096   def glyphs(self, glyphs, font=None):
   2097     if not self.verbose:
   2098       return
   2099     self("Names: ", sorted(glyphs))
   2100     if font:
   2101       reverseGlyphMap = font.getReverseGlyphMap()
   2102       self("Gids : ", sorted(reverseGlyphMap[g] for g in glyphs))
   2103 
   2104   def font(self, font, file=sys.stdout):
   2105     if not self.xml:
   2106       return
   2107     from fontTools.misc import xmlWriter
   2108     writer = xmlWriter.XMLWriter(file)
   2109     for tag in font.keys():
   2110       writer.begintag(tag)
   2111       writer.newline()
   2112       font[tag].toXML(writer, font)
   2113       writer.endtag(tag)
   2114       writer.newline()
   2115 
   2116 
   2117 def load_font(fontFile,
   2118               options,
   2119               allowVID=False,
   2120               checkChecksums=False,
   2121               dontLoadGlyphNames=False,
   2122               lazy=True):
   2123 
   2124   font = ttLib.TTFont(fontFile,
   2125                       allowVID=allowVID,
   2126                       checkChecksums=checkChecksums,
   2127                       recalcBBoxes=options.recalc_bounds,
   2128                       recalcTimestamp=options.recalc_timestamp,
   2129                       lazy=lazy)
   2130 
   2131   # Hack:
   2132   #
   2133   # If we don't need glyph names, change 'post' class to not try to
   2134   # load them.  It avoid lots of headache with broken fonts as well
   2135   # as loading time.
   2136   #
   2137   # Ideally ttLib should provide a way to ask it to skip loading
   2138   # glyph names.  But it currently doesn't provide such a thing.
   2139   #
   2140   if dontLoadGlyphNames:
   2141     post = ttLib.getTableClass('post')
   2142     saved = post.decode_format_2_0
   2143     post.decode_format_2_0 = post.decode_format_3_0
   2144     f = font['post']
   2145     if f.formatType == 2.0:
   2146       f.formatType = 3.0
   2147     post.decode_format_2_0 = saved
   2148 
   2149   return font
   2150 
   2151 def save_font(font, outfile, options):
   2152   if options.flavor and not hasattr(font, 'flavor'):
   2153     raise Exception("fonttools version does not support flavors.")
   2154   font.flavor = options.flavor
   2155   font.save(outfile, reorderTables=options.canonical_order)
   2156 
   2157 def main(args):
   2158 
   2159   log = Logger()
   2160   args = log.parse_opts(args)
   2161 
   2162   options = Options()
   2163   args = options.parse_opts(args, ignore_unknown=['text'])
   2164 
   2165   if len(args) < 2:
   2166     print("usage: pyftsubset font-file glyph... [--text=ABC]... [--option=value]...", file=sys.stderr)
   2167     sys.exit(1)
   2168 
   2169   fontfile = args[0]
   2170   args = args[1:]
   2171 
   2172   dontLoadGlyphNames =(not options.glyph_names and
   2173          all(any(g.startswith(p)
   2174              for p in ['gid', 'glyph', 'uni', 'U+'])
   2175               for g in args))
   2176 
   2177   font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames)
   2178   log.lapse("load font")
   2179   subsetter = Subsetter(options=options, log=log)
   2180 
   2181   names = font.getGlyphNames()
   2182   log.lapse("loading glyph names")
   2183 
   2184   glyphs = []
   2185   unicodes = []
   2186   text = ""
   2187   for g in args:
   2188     if g == '*':
   2189       glyphs.extend(font.getGlyphOrder())
   2190       continue
   2191     if g in names:
   2192       glyphs.append(g)
   2193       continue
   2194     if g.startswith('--text='):
   2195       text += g[7:]
   2196       continue
   2197     if g.startswith('uni') or g.startswith('U+'):
   2198       if g.startswith('uni') and len(g) > 3:
   2199         g = g[3:]
   2200       elif g.startswith('U+') and len(g) > 2:
   2201         g = g[2:]
   2202       u = int(g, 16)
   2203       unicodes.append(u)
   2204       continue
   2205     if g.startswith('gid') or g.startswith('glyph'):
   2206       if g.startswith('gid') and len(g) > 3:
   2207         g = g[3:]
   2208       elif g.startswith('glyph') and len(g) > 5:
   2209         g = g[5:]
   2210       try:
   2211         glyphs.append(font.getGlyphName(int(g), requireReal=True))
   2212       except ValueError:
   2213         raise Exception("Invalid glyph identifier: %s" % g)
   2214       continue
   2215     raise Exception("Invalid glyph identifier: %s" % g)
   2216   log.lapse("compile glyph list")
   2217   log("Unicodes:", unicodes)
   2218   log("Glyphs:", glyphs)
   2219 
   2220   subsetter.populate(glyphs=glyphs, unicodes=unicodes, text=text)
   2221   subsetter.subset(font)
   2222 
   2223   outfile = fontfile + '.subset'
   2224 
   2225   save_font (font, outfile, options)
   2226   log.lapse("compile and save font")
   2227 
   2228   log.last_time = log.start_time
   2229   log.lapse("make one with everything(TOTAL TIME)")
   2230 
   2231   if log.verbose:
   2232     import os
   2233     log("Input  font: %d bytes" % os.path.getsize(fontfile))
   2234     log("Subset font: %d bytes" % os.path.getsize(outfile))
   2235 
   2236   log.font(font)
   2237 
   2238   font.close()
   2239 
   2240 
   2241 __all__ = [
   2242   'Options',
   2243   'Subsetter',
   2244   'Logger',
   2245   'load_font',
   2246   'save_font',
   2247   'main'
   2248 ]
   2249 
   2250 if __name__ == '__main__':
   2251   main(sys.argv[1:])
   2252