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