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