1# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod
4
5"""Python OpenType Layout Subsetter.
6
7Later grown into full OpenType subsetter, supporting all standard tables.
8"""
9
10from __future__ import print_function, division, absolute_import
11from fontTools.misc.py23 import *
12from fontTools import ttLib
13from fontTools.ttLib.tables import otTables
14from fontTools.misc import psCharStrings
15from fontTools.pens import basePen
16import sys
17import struct
18import time
19import array
20
21
22def _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
35def _uniq_sort(l):
36  return sorted(set(l))
37
38def _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)
46def 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)
51def 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)
56def 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)
63def remap(self, coverage_map):
64  "Remaps coverage."
65  self.glyphs = [self.glyphs[i] for i in coverage_map]
66
67@_add_method(otTables.ClassDef)
68def 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)
75def 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)
83def 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)
96def 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)
102def 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)
107def 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)
113def 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)
119def 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)
131def 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)
137def 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)
145def 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)
153def 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)
164def 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)
177def 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)
194def 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)
206def 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)
213def 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)
242def 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)
250def 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)
260def prune_hints(self):
261  # Drop device tables / contour anchor point
262  self.ensureDecompiled()
263  self.Format = 1
264
265@_add_method(otTables.CursivePos)
266def 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)
274def 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)
298def 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)
310def 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)
335def 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)
348def 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)
372def 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)
395def 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)
409def 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)
427def 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)
439def prune_post_subset(self, options):
440  return True
441
442@_add_method(otTables.SingleSubst,
443             otTables.AlternateSubst,
444             otTables.ReverseChainSingleSubst)
445def may_have_non_1to1(self):
446  return False
447
448@_add_method(otTables.MultipleSubst,
449             otTables.LigatureSubst,
450             otTables.ContextSubst,
451             otTables.ChainContextSubst)
452def may_have_non_1to1(self):
453  return True
454
455@_add_method(otTables.ContextSubst,
456             otTables.ChainContextSubst,
457             otTables.ContextPos,
458             otTables.ChainContextPos)
459def __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)
555def 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)
640def 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)
701def 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)
729def 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)
744def 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)
751def 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)
759def 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)
767def 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)
775def 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)
783def 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)
791def 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)
798def 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)
804def 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)
812def 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)
818def 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)
826def subset_lookups(self, lookup_indices):
827  for s in self.SubTable:
828    s.subset_lookups(lookup_indices)
829
830@_add_method(otTables.Lookup)
831def 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)
836def 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)
840def 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)
848def 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)
853def 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)
861def 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)
870def 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)
885def 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)
895def collect_lookups(self):
896  return self.LookupListIndex[:]
897
898@_add_method(otTables.FeatureList)
899def 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)
912def 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)
918def 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)
926def 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)
940def 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)
947def 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)
957def 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)
964def 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)
971def 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'))
976def 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'))
999def 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'))
1011def 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'))
1025def 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'))
1043def 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'))
1056def 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'))
1069def 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'))
1082def 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'))
1114def 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'))
1148def 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'))
1182def 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'))
1204def 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'))
1210def 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'))
1219def 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'))
1224def 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'))
1229def 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'))
1235def 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'))
1242def 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'))
1248def 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)
1253def 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'))
1279def 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'))
1297def 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'))
1306def 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'))
1319def 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 '))
1326def 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 '))
1342def 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)
1376def 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)
1388def 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
1414class _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
1432class _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 '))
1573def 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'))
1701def 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'))
1720def 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'))
1733def 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'))
1765def 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
1790class 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
1913class 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
2067class 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
2117def 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
2151def 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
2157def 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
2250if __name__ == '__main__':
2251  main(sys.argv[1:])
2252