xref: /aosp_15_r20/external/fonttools/Lib/fontTools/subset/__init__.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod
4
5from fontTools import config
6from fontTools.misc.roundTools import otRound
7from fontTools import ttLib
8from fontTools.ttLib.tables import otTables
9from fontTools.ttLib.tables.otBase import USE_HARFBUZZ_REPACKER
10from fontTools.otlLib.maxContextCalc import maxCtxFont
11from fontTools.pens.basePen import NullPen
12from fontTools.misc.loggingTools import Timer
13from fontTools.misc.cliTools import makeOutputFileName
14from fontTools.subset.util import _add_method, _uniq_sort
15from fontTools.subset.cff import *
16from fontTools.subset.svg import *
17from fontTools.varLib import varStore  # for subset_varidxes
18from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor
19import sys
20import struct
21import array
22import logging
23from collections import Counter, defaultdict
24from functools import reduce
25from types import MethodType
26
27__usage__ = "pyftsubset font-file [glyph...] [--option=value]..."
28
29__doc__ = (
30    """\
31pyftsubset -- OpenType font subsetter and optimizer
32
33pyftsubset is an OpenType font subsetter and optimizer, based on fontTools.
34It accepts any TT- or CFF-flavored OpenType (.otf or .ttf) or WOFF (.woff)
35font file. The subsetted glyph set is based on the specified glyphs
36or characters, and specified OpenType layout features.
37
38The tool also performs some size-reducing optimizations, aimed for using
39subset fonts as webfonts.  Individual optimizations can be enabled or
40disabled, and are enabled by default when they are safe.
41
42Usage: """
43    + __usage__
44    + """
45
46At least one glyph or one of --gids, --gids-file, --glyphs, --glyphs-file,
47--text, --text-file, --unicodes, or --unicodes-file, must be specified.
48
49Args:
50
51font-file
52  The input font file.
53glyph
54  Specify one or more glyph identifiers to include in the subset. Must be
55  PS glyph names, or the special string '*' to keep the entire glyph set.
56
57Initial glyph set specification
58^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59
60These options populate the initial glyph set. Same option can appear
61multiple times, and the results are accummulated.
62
63--gids=<NNN>[,<NNN>...]
64  Specify comma/whitespace-separated list of glyph IDs or ranges as decimal
65  numbers.  For example, --gids=10-12,14 adds glyphs with numbers 10, 11,
66  12, and 14.
67
68--gids-file=<path>
69  Like --gids but reads from a file. Anything after a '#' on any line is
70  ignored as comments.
71
72--glyphs=<glyphname>[,<glyphname>...]
73  Specify comma/whitespace-separated PS glyph names to add to the subset.
74  Note that only PS glyph names are accepted, not gidNNN, U+XXXX, etc
75  that are accepted on the command line.  The special string '*' will keep
76  the entire glyph set.
77
78--glyphs-file=<path>
79  Like --glyphs but reads from a file. Anything after a '#' on any line
80  is ignored as comments.
81
82--text=<text>
83  Specify characters to include in the subset, as UTF-8 string.
84
85--text-file=<path>
86  Like --text but reads from a file. Newline character are not added to
87  the subset.
88
89--unicodes=<XXXX>[,<XXXX>...]
90  Specify comma/whitespace-separated list of Unicode codepoints or
91  ranges as hex numbers, optionally prefixed with 'U+', 'u', etc.
92  For example, --unicodes=41-5a,61-7a adds ASCII letters, so does
93  the more verbose --unicodes=U+0041-005A,U+0061-007A.
94  The special strings '*' will choose all Unicode characters mapped
95  by the font.
96
97--unicodes-file=<path>
98  Like --unicodes, but reads from a file. Anything after a '#' on any
99  line in the file is ignored as comments.
100
101--ignore-missing-glyphs
102  Do not fail if some requested glyphs or gids are not available in
103  the font.
104
105--no-ignore-missing-glyphs
106  Stop and fail if some requested glyphs or gids are not available
107  in the font. [default]
108
109--ignore-missing-unicodes [default]
110  Do not fail if some requested Unicode characters (including those
111  indirectly specified using --text or --text-file) are not available
112  in the font.
113
114--no-ignore-missing-unicodes
115  Stop and fail if some requested Unicode characters are not available
116  in the font.
117  Note the default discrepancy between ignoring missing glyphs versus
118  unicodes.  This is for historical reasons and in the future
119  --no-ignore-missing-unicodes might become default.
120
121Other options
122^^^^^^^^^^^^^
123
124For the other options listed below, to see the current value of the option,
125pass a value of '?' to it, with or without a '='.
126
127Examples::
128
129    $ pyftsubset --glyph-names?
130    Current setting for 'glyph-names' is: False
131    $ ./pyftsubset --name-IDs=?
132    Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6]
133    $ ./pyftsubset --hinting? --no-hinting --hinting?
134    Current setting for 'hinting' is: True
135    Current setting for 'hinting' is: False
136
137Output options
138^^^^^^^^^^^^^^
139
140--output-file=<path>
141  The output font file. If not specified, the subsetted font
142  will be saved in as font-file.subset.
143
144--flavor=<type>
145  Specify flavor of output font file. May be 'woff' or 'woff2'.
146  Note that WOFF2 requires the Brotli Python extension, available
147  at https://github.com/google/brotli
148
149--with-zopfli
150  Use the Google Zopfli algorithm to compress WOFF. The output is 3-8 %
151  smaller than pure zlib, but the compression speed is much slower.
152  The Zopfli Python bindings are available at:
153  https://pypi.python.org/pypi/zopfli
154
155--harfbuzz-repacker
156  By default, we serialize GPOS/GSUB using the HarfBuzz Repacker when
157  uharfbuzz can be imported and is successful, otherwise fall back to
158  the pure-python serializer. Set the option to force using the HarfBuzz
159  Repacker (raises an error if uharfbuzz can't be found or fails).
160
161--no-harfbuzz-repacker
162  Always use the pure-python serializer even if uharfbuzz is available.
163
164Glyph set expansion
165^^^^^^^^^^^^^^^^^^^
166
167These options control how additional glyphs are added to the subset.
168
169--retain-gids
170  Retain glyph indices; just empty glyphs not needed in-place.
171
172--notdef-glyph
173  Add the '.notdef' glyph to the subset (ie, keep it). [default]
174
175--no-notdef-glyph
176  Drop the '.notdef' glyph unless specified in the glyph set. This
177  saves a few bytes, but is not possible for Postscript-flavored
178  fonts, as those require '.notdef'. For TrueType-flavored fonts,
179  this works fine as long as no unsupported glyphs are requested
180  from the font.
181
182--notdef-outline
183  Keep the outline of '.notdef' glyph. The '.notdef' glyph outline is
184  used when glyphs not supported by the font are to be shown. It is not
185  needed otherwise.
186
187--no-notdef-outline
188  When including a '.notdef' glyph, remove its outline. This saves
189  a few bytes. [default]
190
191--recommended-glyphs
192  Add glyphs 0, 1, 2, and 3 to the subset, as recommended for
193  TrueType-flavored fonts: '.notdef', 'NULL' or '.null', 'CR', 'space'.
194  Some legacy software might require this, but no modern system does.
195
196--no-recommended-glyphs
197  Do not add glyphs 0, 1, 2, and 3 to the subset, unless specified in
198  glyph set. [default]
199
200--no-layout-closure
201  Do not expand glyph set to add glyphs produced by OpenType layout
202  features.  Instead, OpenType layout features will be subset to only
203  rules that are relevant to the otherwise-specified glyph set.
204
205--layout-features[+|-]=<feature>[,<feature>...]
206  Specify (=), add to (+=) or exclude from (-=) the comma-separated
207  set of OpenType layout feature tags that will be preserved.
208  Glyph variants used by the preserved features are added to the
209  specified subset glyph set. By default, 'calt', 'ccmp', 'clig', 'curs',
210  'dnom', 'frac', 'kern', 'liga', 'locl', 'mark', 'mkmk', 'numr', 'rclt',
211  'rlig', 'rvrn', and all features required for script shaping are
212  preserved. To see the full list, try '--layout-features=?'.
213  Use '*' to keep all features.
214  Multiple --layout-features options can be provided if necessary.
215  Examples:
216
217    --layout-features+=onum,pnum,ss01
218        * Keep the default set of features and 'onum', 'pnum', 'ss01'.
219    --layout-features-='mark','mkmk'
220        * Keep the default set of features but drop 'mark' and 'mkmk'.
221    --layout-features='kern'
222        * Only keep the 'kern' feature, drop all others.
223    --layout-features=''
224        * Drop all features.
225    --layout-features='*'
226        * Keep all features.
227    --layout-features+=aalt --layout-features-=vrt2
228        * Keep default set of features plus 'aalt', but drop 'vrt2'.
229
230--layout-scripts[+|-]=<script>[,<script>...]
231  Specify (=), add to (+=) or exclude from (-=) the comma-separated
232  set of OpenType layout script tags that will be preserved. LangSys tags
233  can be appended to script tag, separated by '.', for example:
234  'arab.dflt,arab.URD,latn.TRK'. By default all scripts are retained ('*').
235
236Hinting options
237^^^^^^^^^^^^^^^
238
239--hinting
240  Keep hinting [default]
241
242--no-hinting
243  Drop glyph-specific hinting and font-wide hinting tables, as well
244  as remove hinting-related bits and pieces from other tables (eg. GPOS).
245  See --hinting-tables for list of tables that are dropped by default.
246  Instructions and hints are stripped from 'glyf' and 'CFF ' tables
247  respectively. This produces (sometimes up to 30%) smaller fonts that
248  are suitable for extremely high-resolution systems, like high-end
249  mobile devices and retina displays.
250
251Optimization options
252^^^^^^^^^^^^^^^^^^^^
253
254--desubroutinize
255  Remove CFF use of subroutinizes.  Subroutinization is a way to make CFF
256  fonts smaller.  For small subsets however, desubroutinizing might make
257  the font smaller.  It has even been reported that desubroutinized CFF
258  fonts compress better (produce smaller output) WOFF and WOFF2 fonts.
259  Also see note under --no-hinting.
260
261--no-desubroutinize [default]
262  Leave CFF subroutinizes as is, only throw away unused subroutinizes.
263
264Font table options
265^^^^^^^^^^^^^^^^^^
266
267--drop-tables[+|-]=<table>[,<table>...]
268  Specify (=), add to (+=) or exclude from (-=) the comma-separated
269  set of tables that will be be dropped.
270  By default, the following tables are dropped:
271  'BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'PCLT', 'LTSH'
272  and Graphite tables: 'Feat', 'Glat', 'Gloc', 'Silf', 'Sill'.
273  The tool will attempt to subset the remaining tables.
274
275  Examples:
276
277  --drop-tables-=BASE
278      * Drop the default set of tables but keep 'BASE'.
279
280  --drop-tables+=GSUB
281      * Drop the default set of tables and 'GSUB'.
282
283  --drop-tables=DSIG
284      * Only drop the 'DSIG' table, keep all others.
285
286  --drop-tables=
287      * Keep all tables.
288
289--no-subset-tables+=<table>[,<table>...]
290  Add to the set of tables that will not be subsetted.
291  By default, the following tables are included in this list, as
292  they do not need subsetting (ignore the fact that 'loca' is listed
293  here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', 'name',
294  'cvt ', 'fpgm', 'prep', 'VMDX', 'DSIG', 'CPAL', 'MVAR', 'cvar', 'STAT'.
295  By default, tables that the tool does not know how to subset and are not
296  specified here will be dropped from the font, unless --passthrough-tables
297  option is passed.
298
299  Example:
300
301   --no-subset-tables+=FFTM
302      * Keep 'FFTM' table in the font by preventing subsetting.
303
304--passthrough-tables
305  Do not drop tables that the tool does not know how to subset.
306
307--no-passthrough-tables
308  Tables that the tool does not know how to subset and are not specified
309  in --no-subset-tables will be dropped from the font. [default]
310
311--hinting-tables[-]=<table>[,<table>...]
312  Specify (=), add to (+=) or exclude from (-=) the list of font-wide
313  hinting tables that will be dropped if --no-hinting is specified.
314
315  Examples:
316
317  --hinting-tables-=VDMX
318      * Drop font-wide hinting tables except 'VDMX'.
319  --hinting-tables=
320      * Keep all font-wide hinting tables (but strip hints from glyphs).
321
322--legacy-kern
323  Keep TrueType 'kern' table even when OpenType 'GPOS' is available.
324
325--no-legacy-kern
326  Drop TrueType 'kern' table if OpenType 'GPOS' is available. [default]
327
328Font naming options
329^^^^^^^^^^^^^^^^^^^
330
331These options control what is retained in the 'name' table. For numerical
332codes, see: http://www.microsoft.com/typography/otspec/name.htm
333
334--name-IDs[+|-]=<nameID>[,<nameID>...]
335  Specify (=), add to (+=) or exclude from (-=) the set of 'name' table
336  entry nameIDs that will be preserved. By default, only nameIDs between 0
337  and 6 are preserved, the rest are dropped. Use '*' to keep all entries.
338
339  Examples:
340
341  --name-IDs+=7,8,9
342      * Also keep Trademark, Manufacturer and Designer name entries.
343  --name-IDs=
344      * Drop all 'name' table entries.
345  --name-IDs=*
346      * keep all 'name' table entries
347
348--name-legacy
349  Keep legacy (non-Unicode) 'name' table entries (0.x, 1.x etc.).
350  XXX Note: This might be needed for some fonts that have no Unicode name
351  entires for English. See: https://github.com/fonttools/fonttools/issues/146
352
353--no-name-legacy
354  Drop legacy (non-Unicode) 'name' table entries [default]
355
356--name-languages[+|-]=<langID>[,<langID>]
357  Specify (=), add to (+=) or exclude from (-=) the set of 'name' table
358  langIDs that will be preserved. By default only records with langID
359  0x0409 (English) are preserved. Use '*' to keep all langIDs.
360
361--obfuscate-names
362  Make the font unusable as a system font by replacing name IDs 1, 2, 3, 4,
363  and 6 with dummy strings (it is still fully functional as webfont).
364
365Glyph naming and encoding options
366^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
367
368--glyph-names
369  Keep PS glyph names in TT-flavored fonts. In general glyph names are
370  not needed for correct use of the font. However, some PDF generators
371  and PDF viewers might rely on glyph names to extract Unicode text
372  from PDF documents.
373--no-glyph-names
374  Drop PS glyph names in TT-flavored fonts, by using 'post' table
375  version 3.0. [default]
376--legacy-cmap
377  Keep the legacy 'cmap' subtables (0.x, 1.x, 4.x etc.).
378--no-legacy-cmap
379  Drop the legacy 'cmap' subtables. [default]
380--symbol-cmap
381  Keep the 3.0 symbol 'cmap'.
382--no-symbol-cmap
383  Drop the 3.0 symbol 'cmap'. [default]
384
385Other font-specific options
386^^^^^^^^^^^^^^^^^^^^^^^^^^^
387
388--recalc-bounds
389    Recalculate font bounding boxes.
390--no-recalc-bounds
391    Keep original font bounding boxes. This is faster and still safe
392    for all practical purposes. [default]
393--recalc-timestamp
394    Set font 'modified' timestamp to current time.
395--no-recalc-timestamp
396    Do not modify font 'modified' timestamp. [default]
397--canonical-order
398    Order tables as recommended in the OpenType standard. This is not
399    required by the standard, nor by any known implementation.
400--no-canonical-order
401    Keep original order of font tables. This is faster. [default]
402--prune-unicode-ranges
403    Update the 'OS/2 ulUnicodeRange*' bits after subsetting. The Unicode
404    ranges defined in the OpenType specification v1.7 are intersected with
405    the Unicode codepoints specified in the font's Unicode 'cmap' subtables:
406    when no overlap is found, the bit will be switched off. However, it will
407    *not* be switched on if an intersection is found.  [default]
408--no-prune-unicode-ranges
409    Don't change the 'OS/2 ulUnicodeRange*' bits.
410--prune-codepage-ranges
411    Update the 'OS/2 ulCodePageRange*' bits after subsetting.  [default]
412--no-prune-codepage-ranges
413    Don't change the 'OS/2 ulCodePageRange*' bits.
414--recalc-average-width
415    Update the 'OS/2 xAvgCharWidth' field after subsetting.
416--no-recalc-average-width
417    Don't change the 'OS/2 xAvgCharWidth' field. [default]
418--recalc-max-context
419    Update the 'OS/2 usMaxContext' field after subsetting.
420--no-recalc-max-context
421    Don't change the 'OS/2 usMaxContext' field. [default]
422--font-number=<number>
423    Select font number for TrueType Collection (.ttc/.otc), starting from 0.
424--pretty-svg
425    When subsetting SVG table, use lxml pretty_print=True option to indent
426    the XML output (only recommended for debugging purposes).
427
428Application options
429^^^^^^^^^^^^^^^^^^^
430
431--verbose
432    Display verbose information of the subsetting process.
433--timing
434    Display detailed timing information of the subsetting process.
435--xml
436    Display the TTX XML representation of subsetted font.
437
438Example
439^^^^^^^
440
441Produce a subset containing the characters ' !"#$%' without performing
442size-reducing optimizations::
443
444  $ pyftsubset font.ttf --unicodes="U+0020-0025" \\
445    --layout-features=* --glyph-names --symbol-cmap --legacy-cmap \\
446    --notdef-glyph --notdef-outline --recommended-glyphs \\
447    --name-IDs=* --name-legacy --name-languages=*
448"""
449)
450
451
452log = logging.getLogger("fontTools.subset")
453
454
455def _log_glyphs(self, glyphs, font=None):
456    self.info("Glyph names: %s", sorted(glyphs))
457    if font:
458        reverseGlyphMap = font.getReverseGlyphMap()
459        self.info("Glyph IDs:   %s", sorted(reverseGlyphMap[g] for g in glyphs))
460
461
462# bind "glyphs" function to 'log' object
463log.glyphs = MethodType(_log_glyphs, log)
464
465# I use a different timing channel so I can configure it separately from the
466# main module's logger
467timer = Timer(logger=logging.getLogger("fontTools.subset.timer"))
468
469
470def _dict_subset(d, glyphs):
471    return {g: d[g] for g in glyphs}
472
473
474def _list_subset(l, indices):
475    count = len(l)
476    return [l[i] for i in indices if i < count]
477
478
479@_add_method(otTables.Coverage)
480def intersect(self, glyphs):
481    """Returns ascending list of matching coverage values."""
482    return [i for i, g in enumerate(self.glyphs) if g in glyphs]
483
484
485@_add_method(otTables.Coverage)
486def intersect_glyphs(self, glyphs):
487    """Returns set of intersecting glyphs."""
488    return set(g for g in self.glyphs if g in glyphs)
489
490
491@_add_method(otTables.Coverage)
492def subset(self, glyphs):
493    """Returns ascending list of remaining coverage values."""
494    indices = self.intersect(glyphs)
495    self.glyphs = [g for g in self.glyphs if g in glyphs]
496    return indices
497
498
499@_add_method(otTables.Coverage)
500def remap(self, coverage_map):
501    """Remaps coverage."""
502    self.glyphs = [self.glyphs[i] for i in coverage_map]
503
504
505@_add_method(otTables.ClassDef)
506def intersect(self, glyphs):
507    """Returns ascending list of matching class values."""
508    return _uniq_sort(
509        ([0] if any(g not in self.classDefs for g in glyphs) else [])
510        + [v for g, v in self.classDefs.items() if g in glyphs]
511    )
512
513
514@_add_method(otTables.ClassDef)
515def intersect_class(self, glyphs, klass):
516    """Returns set of glyphs matching class."""
517    if klass == 0:
518        return set(g for g in glyphs if g not in self.classDefs)
519    return set(g for g, v in self.classDefs.items() if v == klass and g in glyphs)
520
521
522@_add_method(otTables.ClassDef)
523def subset(self, glyphs, remap=False, useClass0=True):
524    """Returns ascending list of remaining classes."""
525    self.classDefs = {g: v for g, v in self.classDefs.items() if g in glyphs}
526    # Note: while class 0 has the special meaning of "not matched",
527    # if no glyph will ever /not match/, we can optimize class 0 out too.
528    # Only do this if allowed.
529    indices = _uniq_sort(
530        (
531            [0]
532            if ((not useClass0) or any(g not in self.classDefs for g in glyphs))
533            else []
534        )
535        + list(self.classDefs.values())
536    )
537    if remap:
538        self.remap(indices)
539    return indices
540
541
542@_add_method(otTables.ClassDef)
543def remap(self, class_map):
544    """Remaps classes."""
545    self.classDefs = {g: class_map.index(v) for g, v in self.classDefs.items()}
546
547
548@_add_method(otTables.SingleSubst)
549def closure_glyphs(self, s, cur_glyphs):
550    s.glyphs.update(v for g, v in self.mapping.items() if g in cur_glyphs)
551
552
553@_add_method(otTables.SingleSubst)
554def subset_glyphs(self, s):
555    self.mapping = {
556        g: v for g, v in self.mapping.items() if g in s.glyphs and v in s.glyphs
557    }
558    return bool(self.mapping)
559
560
561@_add_method(otTables.MultipleSubst)
562def closure_glyphs(self, s, cur_glyphs):
563    for glyph, subst in self.mapping.items():
564        if glyph in cur_glyphs:
565            s.glyphs.update(subst)
566
567
568@_add_method(otTables.MultipleSubst)
569def subset_glyphs(self, s):
570    self.mapping = {
571        g: v
572        for g, v in self.mapping.items()
573        if g in s.glyphs and all(sub in s.glyphs for sub in v)
574    }
575    return bool(self.mapping)
576
577
578@_add_method(otTables.AlternateSubst)
579def closure_glyphs(self, s, cur_glyphs):
580    s.glyphs.update(*(vlist for g, vlist in self.alternates.items() if g in cur_glyphs))
581
582
583@_add_method(otTables.AlternateSubst)
584def subset_glyphs(self, s):
585    self.alternates = {
586        g: [v for v in vlist if v in s.glyphs]
587        for g, vlist in self.alternates.items()
588        if g in s.glyphs and any(v in s.glyphs for v in vlist)
589    }
590    return bool(self.alternates)
591
592
593@_add_method(otTables.LigatureSubst)
594def closure_glyphs(self, s, cur_glyphs):
595    s.glyphs.update(
596        *(
597            [seq.LigGlyph for seq in seqs if all(c in s.glyphs for c in seq.Component)]
598            for g, seqs in self.ligatures.items()
599            if g in cur_glyphs
600        )
601    )
602
603
604@_add_method(otTables.LigatureSubst)
605def subset_glyphs(self, s):
606    self.ligatures = {g: v for g, v in self.ligatures.items() if g in s.glyphs}
607    self.ligatures = {
608        g: [
609            seq
610            for seq in seqs
611            if seq.LigGlyph in s.glyphs and all(c in s.glyphs for c in seq.Component)
612        ]
613        for g, seqs in self.ligatures.items()
614    }
615    self.ligatures = {g: v for g, v in self.ligatures.items() if v}
616    return bool(self.ligatures)
617
618
619@_add_method(otTables.ReverseChainSingleSubst)
620def closure_glyphs(self, s, cur_glyphs):
621    if self.Format == 1:
622        indices = self.Coverage.intersect(cur_glyphs)
623        if not indices or not all(
624            c.intersect(s.glyphs)
625            for c in self.LookAheadCoverage + self.BacktrackCoverage
626        ):
627            return
628        s.glyphs.update(self.Substitute[i] for i in indices)
629    else:
630        assert 0, "unknown format: %s" % self.Format
631
632
633@_add_method(otTables.ReverseChainSingleSubst)
634def subset_glyphs(self, s):
635    if self.Format == 1:
636        indices = self.Coverage.subset(s.glyphs)
637        self.Substitute = _list_subset(self.Substitute, indices)
638        # Now drop rules generating glyphs we don't want
639        indices = [i for i, sub in enumerate(self.Substitute) if sub in s.glyphs]
640        self.Substitute = _list_subset(self.Substitute, indices)
641        self.Coverage.remap(indices)
642        self.GlyphCount = len(self.Substitute)
643        return bool(
644            self.GlyphCount
645            and all(
646                c.subset(s.glyphs)
647                for c in self.LookAheadCoverage + self.BacktrackCoverage
648            )
649        )
650    else:
651        assert 0, "unknown format: %s" % self.Format
652
653
654@_add_method(otTables.Device)
655def is_hinting(self):
656    return self.DeltaFormat in (1, 2, 3)
657
658
659@_add_method(otTables.ValueRecord)
660def prune_hints(self):
661    for name in ["XPlaDevice", "YPlaDevice", "XAdvDevice", "YAdvDevice"]:
662        v = getattr(self, name, None)
663        if v is not None and v.is_hinting():
664            delattr(self, name)
665
666
667@_add_method(otTables.SinglePos)
668def subset_glyphs(self, s):
669    if self.Format == 1:
670        return len(self.Coverage.subset(s.glyphs))
671    elif self.Format == 2:
672        indices = self.Coverage.subset(s.glyphs)
673        values = self.Value
674        count = len(values)
675        self.Value = [values[i] for i in indices if i < count]
676        self.ValueCount = len(self.Value)
677        return bool(self.ValueCount)
678    else:
679        assert 0, "unknown format: %s" % self.Format
680
681
682@_add_method(otTables.SinglePos)
683def prune_post_subset(self, font, options):
684    if self.Value is None:
685        assert self.ValueFormat == 0
686        return True
687
688    # Shrink ValueFormat
689    if self.Format == 1:
690        if not options.hinting:
691            self.Value.prune_hints()
692        self.ValueFormat = self.Value.getEffectiveFormat()
693    elif self.Format == 2:
694        if None in self.Value:
695            assert self.ValueFormat == 0
696            assert all(v is None for v in self.Value)
697        else:
698            if not options.hinting:
699                for v in self.Value:
700                    v.prune_hints()
701            self.ValueFormat = reduce(
702                int.__or__, [v.getEffectiveFormat() for v in self.Value], 0
703            )
704
705    # Downgrade to Format 1 if all ValueRecords are the same
706    if self.Format == 2 and all(v == self.Value[0] for v in self.Value):
707        self.Format = 1
708        self.Value = self.Value[0] if self.ValueFormat != 0 else None
709        del self.ValueCount
710
711    return True
712
713
714@_add_method(otTables.PairPos)
715def subset_glyphs(self, s):
716    if self.Format == 1:
717        indices = self.Coverage.subset(s.glyphs)
718        pairs = self.PairSet
719        count = len(pairs)
720        self.PairSet = [pairs[i] for i in indices if i < count]
721        for p in self.PairSet:
722            p.PairValueRecord = [
723                r for r in p.PairValueRecord if r.SecondGlyph in s.glyphs
724            ]
725            p.PairValueCount = len(p.PairValueRecord)
726        # Remove empty pairsets
727        indices = [i for i, p in enumerate(self.PairSet) if p.PairValueCount]
728        self.Coverage.remap(indices)
729        self.PairSet = _list_subset(self.PairSet, indices)
730        self.PairSetCount = len(self.PairSet)
731        return bool(self.PairSetCount)
732    elif self.Format == 2:
733        class1_map = [
734            c
735            for c in self.ClassDef1.subset(
736                s.glyphs.intersection(self.Coverage.glyphs), remap=True
737            )
738            if c < self.Class1Count
739        ]
740        class2_map = [
741            c
742            for c in self.ClassDef2.subset(s.glyphs, remap=True, useClass0=False)
743            if c < self.Class2Count
744        ]
745        self.Class1Record = [self.Class1Record[i] for i in class1_map]
746        for c in self.Class1Record:
747            c.Class2Record = [c.Class2Record[i] for i in class2_map]
748        self.Class1Count = len(class1_map)
749        self.Class2Count = len(class2_map)
750        # If only Class2 0 left, no need to keep anything.
751        return bool(
752            self.Class1Count
753            and (self.Class2Count > 1)
754            and self.Coverage.subset(s.glyphs)
755        )
756    else:
757        assert 0, "unknown format: %s" % self.Format
758
759
760@_add_method(otTables.PairPos)
761def prune_post_subset(self, font, options):
762    if not options.hinting:
763        attr1, attr2 = {
764            1: ("PairSet", "PairValueRecord"),
765            2: ("Class1Record", "Class2Record"),
766        }[self.Format]
767
768        self.ValueFormat1 = self.ValueFormat2 = 0
769        for row in getattr(self, attr1):
770            for r in getattr(row, attr2):
771                if r.Value1:
772                    r.Value1.prune_hints()
773                    self.ValueFormat1 |= r.Value1.getEffectiveFormat()
774                if r.Value2:
775                    r.Value2.prune_hints()
776                    self.ValueFormat2 |= r.Value2.getEffectiveFormat()
777
778    return bool(self.ValueFormat1 | self.ValueFormat2)
779
780
781@_add_method(otTables.CursivePos)
782def subset_glyphs(self, s):
783    if self.Format == 1:
784        indices = self.Coverage.subset(s.glyphs)
785        records = self.EntryExitRecord
786        count = len(records)
787        self.EntryExitRecord = [records[i] for i in indices if i < count]
788        self.EntryExitCount = len(self.EntryExitRecord)
789        return bool(self.EntryExitCount)
790    else:
791        assert 0, "unknown format: %s" % self.Format
792
793
794@_add_method(otTables.Anchor)
795def prune_hints(self):
796    if self.Format == 2:
797        self.Format = 1
798    elif self.Format == 3:
799        for name in ("XDeviceTable", "YDeviceTable"):
800            v = getattr(self, name, None)
801            if v is not None and v.is_hinting():
802                setattr(self, name, None)
803        if self.XDeviceTable is None and self.YDeviceTable is None:
804            self.Format = 1
805
806
807@_add_method(otTables.CursivePos)
808def prune_post_subset(self, font, options):
809    if not options.hinting:
810        for rec in self.EntryExitRecord:
811            if rec.EntryAnchor:
812                rec.EntryAnchor.prune_hints()
813            if rec.ExitAnchor:
814                rec.ExitAnchor.prune_hints()
815    return True
816
817
818@_add_method(otTables.MarkBasePos)
819def subset_glyphs(self, s):
820    if self.Format == 1:
821        mark_indices = self.MarkCoverage.subset(s.glyphs)
822        self.MarkArray.MarkRecord = _list_subset(
823            self.MarkArray.MarkRecord, mark_indices
824        )
825        self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
826        base_indices = self.BaseCoverage.subset(s.glyphs)
827        self.BaseArray.BaseRecord = _list_subset(
828            self.BaseArray.BaseRecord, base_indices
829        )
830        self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord)
831        # Prune empty classes
832        class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
833        self.ClassCount = len(class_indices)
834        for m in self.MarkArray.MarkRecord:
835            m.Class = class_indices.index(m.Class)
836        for b in self.BaseArray.BaseRecord:
837            b.BaseAnchor = _list_subset(b.BaseAnchor, class_indices)
838        return bool(
839            self.ClassCount and self.MarkArray.MarkCount and self.BaseArray.BaseCount
840        )
841    else:
842        assert 0, "unknown format: %s" % self.Format
843
844
845@_add_method(otTables.MarkBasePos)
846def prune_post_subset(self, font, options):
847    if not options.hinting:
848        for m in self.MarkArray.MarkRecord:
849            if m.MarkAnchor:
850                m.MarkAnchor.prune_hints()
851        for b in self.BaseArray.BaseRecord:
852            for a in b.BaseAnchor:
853                if a:
854                    a.prune_hints()
855    return True
856
857
858@_add_method(otTables.MarkLigPos)
859def subset_glyphs(self, s):
860    if self.Format == 1:
861        mark_indices = self.MarkCoverage.subset(s.glyphs)
862        self.MarkArray.MarkRecord = _list_subset(
863            self.MarkArray.MarkRecord, mark_indices
864        )
865        self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
866        ligature_indices = self.LigatureCoverage.subset(s.glyphs)
867        self.LigatureArray.LigatureAttach = _list_subset(
868            self.LigatureArray.LigatureAttach, ligature_indices
869        )
870        self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach)
871        # Prune empty classes
872        class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
873        self.ClassCount = len(class_indices)
874        for m in self.MarkArray.MarkRecord:
875            m.Class = class_indices.index(m.Class)
876        for l in self.LigatureArray.LigatureAttach:
877            if l is None:
878                continue
879            for c in l.ComponentRecord:
880                c.LigatureAnchor = _list_subset(c.LigatureAnchor, class_indices)
881        return bool(
882            self.ClassCount
883            and self.MarkArray.MarkCount
884            and self.LigatureArray.LigatureCount
885        )
886    else:
887        assert 0, "unknown format: %s" % self.Format
888
889
890@_add_method(otTables.MarkLigPos)
891def prune_post_subset(self, font, options):
892    if not options.hinting:
893        for m in self.MarkArray.MarkRecord:
894            if m.MarkAnchor:
895                m.MarkAnchor.prune_hints()
896        for l in self.LigatureArray.LigatureAttach:
897            if l is None:
898                continue
899            for c in l.ComponentRecord:
900                for a in c.LigatureAnchor:
901                    if a:
902                        a.prune_hints()
903    return True
904
905
906@_add_method(otTables.MarkMarkPos)
907def subset_glyphs(self, s):
908    if self.Format == 1:
909        mark1_indices = self.Mark1Coverage.subset(s.glyphs)
910        self.Mark1Array.MarkRecord = _list_subset(
911            self.Mark1Array.MarkRecord, mark1_indices
912        )
913        self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord)
914        mark2_indices = self.Mark2Coverage.subset(s.glyphs)
915        self.Mark2Array.Mark2Record = _list_subset(
916            self.Mark2Array.Mark2Record, mark2_indices
917        )
918        self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record)
919        # Prune empty classes
920        class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord)
921        self.ClassCount = len(class_indices)
922        for m in self.Mark1Array.MarkRecord:
923            m.Class = class_indices.index(m.Class)
924        for b in self.Mark2Array.Mark2Record:
925            b.Mark2Anchor = _list_subset(b.Mark2Anchor, class_indices)
926        return bool(
927            self.ClassCount and self.Mark1Array.MarkCount and self.Mark2Array.MarkCount
928        )
929    else:
930        assert 0, "unknown format: %s" % self.Format
931
932
933@_add_method(otTables.MarkMarkPos)
934def prune_post_subset(self, font, options):
935    if not options.hinting:
936        for m in self.Mark1Array.MarkRecord:
937            if m.MarkAnchor:
938                m.MarkAnchor.prune_hints()
939        for b in self.Mark2Array.Mark2Record:
940            for m in b.Mark2Anchor:
941                if m:
942                    m.prune_hints()
943    return True
944
945
946@_add_method(
947    otTables.SingleSubst,
948    otTables.MultipleSubst,
949    otTables.AlternateSubst,
950    otTables.LigatureSubst,
951    otTables.ReverseChainSingleSubst,
952    otTables.SinglePos,
953    otTables.PairPos,
954    otTables.CursivePos,
955    otTables.MarkBasePos,
956    otTables.MarkLigPos,
957    otTables.MarkMarkPos,
958)
959def subset_lookups(self, lookup_indices):
960    pass
961
962
963@_add_method(
964    otTables.SingleSubst,
965    otTables.MultipleSubst,
966    otTables.AlternateSubst,
967    otTables.LigatureSubst,
968    otTables.ReverseChainSingleSubst,
969    otTables.SinglePos,
970    otTables.PairPos,
971    otTables.CursivePos,
972    otTables.MarkBasePos,
973    otTables.MarkLigPos,
974    otTables.MarkMarkPos,
975)
976def collect_lookups(self):
977    return []
978
979
980@_add_method(
981    otTables.SingleSubst,
982    otTables.MultipleSubst,
983    otTables.AlternateSubst,
984    otTables.LigatureSubst,
985    otTables.ReverseChainSingleSubst,
986    otTables.ContextSubst,
987    otTables.ChainContextSubst,
988    otTables.ContextPos,
989    otTables.ChainContextPos,
990)
991def prune_post_subset(self, font, options):
992    return True
993
994
995@_add_method(
996    otTables.SingleSubst, otTables.AlternateSubst, otTables.ReverseChainSingleSubst
997)
998def may_have_non_1to1(self):
999    return False
1000
1001
1002@_add_method(
1003    otTables.MultipleSubst,
1004    otTables.LigatureSubst,
1005    otTables.ContextSubst,
1006    otTables.ChainContextSubst,
1007)
1008def may_have_non_1to1(self):
1009    return True
1010
1011
1012@_add_method(
1013    otTables.ContextSubst,
1014    otTables.ChainContextSubst,
1015    otTables.ContextPos,
1016    otTables.ChainContextPos,
1017)
1018def __subset_classify_context(self):
1019    class ContextHelper(object):
1020        def __init__(self, klass, Format):
1021            if klass.__name__.endswith("Subst"):
1022                Typ = "Sub"
1023                Type = "Subst"
1024            else:
1025                Typ = "Pos"
1026                Type = "Pos"
1027            if klass.__name__.startswith("Chain"):
1028                Chain = "Chain"
1029                InputIdx = 1
1030                DataLen = 3
1031            else:
1032                Chain = ""
1033                InputIdx = 0
1034                DataLen = 1
1035            ChainTyp = Chain + Typ
1036
1037            self.Typ = Typ
1038            self.Type = Type
1039            self.Chain = Chain
1040            self.ChainTyp = ChainTyp
1041            self.InputIdx = InputIdx
1042            self.DataLen = DataLen
1043
1044            self.LookupRecord = Type + "LookupRecord"
1045
1046            if Format == 1:
1047                Coverage = lambda r: r.Coverage
1048                ChainCoverage = lambda r: r.Coverage
1049                ContextData = lambda r: (None,)
1050                ChainContextData = lambda r: (None, None, None)
1051                SetContextData = None
1052                SetChainContextData = None
1053                RuleData = lambda r: (r.Input,)
1054                ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
1055
1056                def SetRuleData(r, d):
1057                    (r.Input,) = d
1058                    (r.GlyphCount,) = (len(x) + 1 for x in d)
1059
1060                def ChainSetRuleData(r, d):
1061                    (r.Backtrack, r.Input, r.LookAhead) = d
1062                    (
1063                        r.BacktrackGlyphCount,
1064                        r.InputGlyphCount,
1065                        r.LookAheadGlyphCount,
1066                    ) = (len(d[0]), len(d[1]) + 1, len(d[2]))
1067
1068            elif Format == 2:
1069                Coverage = lambda r: r.Coverage
1070                ChainCoverage = lambda r: r.Coverage
1071                ContextData = lambda r: (r.ClassDef,)
1072                ChainContextData = lambda r: (
1073                    r.BacktrackClassDef,
1074                    r.InputClassDef,
1075                    r.LookAheadClassDef,
1076                )
1077
1078                def SetContextData(r, d):
1079                    (r.ClassDef,) = d
1080
1081                def SetChainContextData(r, d):
1082                    (r.BacktrackClassDef, r.InputClassDef, r.LookAheadClassDef) = d
1083
1084                RuleData = lambda r: (r.Class,)
1085                ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
1086
1087                def SetRuleData(r, d):
1088                    (r.Class,) = d
1089                    (r.GlyphCount,) = (len(x) + 1 for x in d)
1090
1091                def ChainSetRuleData(r, d):
1092                    (r.Backtrack, r.Input, r.LookAhead) = d
1093                    (
1094                        r.BacktrackGlyphCount,
1095                        r.InputGlyphCount,
1096                        r.LookAheadGlyphCount,
1097                    ) = (len(d[0]), len(d[1]) + 1, len(d[2]))
1098
1099            elif Format == 3:
1100                Coverage = lambda r: r.Coverage[0]
1101                ChainCoverage = lambda r: r.InputCoverage[0]
1102                ContextData = None
1103                ChainContextData = None
1104                SetContextData = None
1105                SetChainContextData = None
1106                RuleData = lambda r: r.Coverage
1107                ChainRuleData = lambda r: (
1108                    r.BacktrackCoverage + r.InputCoverage + r.LookAheadCoverage
1109                )
1110
1111                def SetRuleData(r, d):
1112                    (r.Coverage,) = d
1113                    (r.GlyphCount,) = (len(x) for x in d)
1114
1115                def ChainSetRuleData(r, d):
1116                    (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d
1117                    (
1118                        r.BacktrackGlyphCount,
1119                        r.InputGlyphCount,
1120                        r.LookAheadGlyphCount,
1121                    ) = (len(x) for x in d)
1122
1123            else:
1124                assert 0, "unknown format: %s" % Format
1125
1126            if Chain:
1127                self.Coverage = ChainCoverage
1128                self.ContextData = ChainContextData
1129                self.SetContextData = SetChainContextData
1130                self.RuleData = ChainRuleData
1131                self.SetRuleData = ChainSetRuleData
1132            else:
1133                self.Coverage = Coverage
1134                self.ContextData = ContextData
1135                self.SetContextData = SetContextData
1136                self.RuleData = RuleData
1137                self.SetRuleData = SetRuleData
1138
1139            if Format == 1:
1140                self.Rule = ChainTyp + "Rule"
1141                self.RuleCount = ChainTyp + "RuleCount"
1142                self.RuleSet = ChainTyp + "RuleSet"
1143                self.RuleSetCount = ChainTyp + "RuleSetCount"
1144                self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
1145            elif Format == 2:
1146                self.Rule = ChainTyp + "ClassRule"
1147                self.RuleCount = ChainTyp + "ClassRuleCount"
1148                self.RuleSet = ChainTyp + "ClassSet"
1149                self.RuleSetCount = ChainTyp + "ClassSetCount"
1150                self.Intersect = lambda glyphs, c, r: (
1151                    c.intersect_class(glyphs, r)
1152                    if c
1153                    else (set(glyphs) if r == 0 else set())
1154                )
1155
1156                self.ClassDef = "InputClassDef" if Chain else "ClassDef"
1157                self.ClassDefIndex = 1 if Chain else 0
1158                self.Input = "Input" if Chain else "Class"
1159            elif Format == 3:
1160                self.Input = "InputCoverage" if Chain else "Coverage"
1161
1162    if self.Format not in [1, 2, 3]:
1163        return None  # Don't shoot the messenger; let it go
1164    if not hasattr(self.__class__, "_subset__ContextHelpers"):
1165        self.__class__._subset__ContextHelpers = {}
1166    if self.Format not in self.__class__._subset__ContextHelpers:
1167        helper = ContextHelper(self.__class__, self.Format)
1168        self.__class__._subset__ContextHelpers[self.Format] = helper
1169    return self.__class__._subset__ContextHelpers[self.Format]
1170
1171
1172@_add_method(otTables.ContextSubst, otTables.ChainContextSubst)
1173def closure_glyphs(self, s, cur_glyphs):
1174    c = self.__subset_classify_context()
1175
1176    indices = c.Coverage(self).intersect(cur_glyphs)
1177    if not indices:
1178        return []
1179    cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs)
1180
1181    if self.Format == 1:
1182        ContextData = c.ContextData(self)
1183        rss = getattr(self, c.RuleSet)
1184        rssCount = getattr(self, c.RuleSetCount)
1185        for i in indices:
1186            if i >= rssCount or not rss[i]:
1187                continue
1188            for r in getattr(rss[i], c.Rule):
1189                if not r:
1190                    continue
1191                if not all(
1192                    all(c.Intersect(s.glyphs, cd, k) for k in klist)
1193                    for cd, klist in zip(ContextData, c.RuleData(r))
1194                ):
1195                    continue
1196                chaos = set()
1197                for ll in getattr(r, c.LookupRecord):
1198                    if not ll:
1199                        continue
1200                    seqi = ll.SequenceIndex
1201                    if seqi in chaos:
1202                        # TODO Can we improve this?
1203                        pos_glyphs = None
1204                    else:
1205                        if seqi == 0:
1206                            pos_glyphs = frozenset([c.Coverage(self).glyphs[i]])
1207                        else:
1208                            pos_glyphs = frozenset([r.Input[seqi - 1]])
1209                    lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
1210                    chaos.add(seqi)
1211                    if lookup.may_have_non_1to1():
1212                        chaos.update(range(seqi, len(r.Input) + 2))
1213                    lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
1214    elif self.Format == 2:
1215        ClassDef = getattr(self, c.ClassDef)
1216        indices = ClassDef.intersect(cur_glyphs)
1217        ContextData = c.ContextData(self)
1218        rss = getattr(self, c.RuleSet)
1219        rssCount = getattr(self, c.RuleSetCount)
1220        for i in indices:
1221            if i >= rssCount or not rss[i]:
1222                continue
1223            for r in getattr(rss[i], c.Rule):
1224                if not r:
1225                    continue
1226                if not all(
1227                    all(c.Intersect(s.glyphs, cd, k) for k in klist)
1228                    for cd, klist in zip(ContextData, c.RuleData(r))
1229                ):
1230                    continue
1231                chaos = set()
1232                for ll in getattr(r, c.LookupRecord):
1233                    if not ll:
1234                        continue
1235                    seqi = ll.SequenceIndex
1236                    if seqi in chaos:
1237                        # TODO Can we improve this?
1238                        pos_glyphs = None
1239                    else:
1240                        if seqi == 0:
1241                            pos_glyphs = frozenset(
1242                                ClassDef.intersect_class(cur_glyphs, i)
1243                            )
1244                        else:
1245                            pos_glyphs = frozenset(
1246                                ClassDef.intersect_class(
1247                                    s.glyphs, getattr(r, c.Input)[seqi - 1]
1248                                )
1249                            )
1250                    lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
1251                    chaos.add(seqi)
1252                    if lookup.may_have_non_1to1():
1253                        chaos.update(range(seqi, len(getattr(r, c.Input)) + 2))
1254                    lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
1255    elif self.Format == 3:
1256        if not all(x is not None and x.intersect(s.glyphs) for x in c.RuleData(self)):
1257            return []
1258        r = self
1259        input_coverages = getattr(r, c.Input)
1260        chaos = set()
1261        for ll in getattr(r, c.LookupRecord):
1262            if not ll:
1263                continue
1264            seqi = ll.SequenceIndex
1265            if seqi in chaos:
1266                # TODO Can we improve this?
1267                pos_glyphs = None
1268            else:
1269                if seqi == 0:
1270                    pos_glyphs = frozenset(cur_glyphs)
1271                else:
1272                    pos_glyphs = frozenset(
1273                        input_coverages[seqi].intersect_glyphs(s.glyphs)
1274                    )
1275            lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
1276            chaos.add(seqi)
1277            if lookup.may_have_non_1to1():
1278                chaos.update(range(seqi, len(input_coverages) + 1))
1279            lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
1280    else:
1281        assert 0, "unknown format: %s" % self.Format
1282
1283
1284@_add_method(
1285    otTables.ContextSubst,
1286    otTables.ContextPos,
1287    otTables.ChainContextSubst,
1288    otTables.ChainContextPos,
1289)
1290def subset_glyphs(self, s):
1291    c = self.__subset_classify_context()
1292
1293    if self.Format == 1:
1294        indices = self.Coverage.subset(s.glyphs)
1295        rss = getattr(self, c.RuleSet)
1296        rssCount = getattr(self, c.RuleSetCount)
1297        rss = [rss[i] for i in indices if i < rssCount]
1298        for rs in rss:
1299            if not rs:
1300                continue
1301            ss = getattr(rs, c.Rule)
1302            ss = [
1303                r
1304                for r in ss
1305                if r
1306                and all(all(g in s.glyphs for g in glist) for glist in c.RuleData(r))
1307            ]
1308            setattr(rs, c.Rule, ss)
1309            setattr(rs, c.RuleCount, len(ss))
1310        # Prune empty rulesets
1311        indices = [i for i, rs in enumerate(rss) if rs and getattr(rs, c.Rule)]
1312        self.Coverage.remap(indices)
1313        rss = _list_subset(rss, indices)
1314        setattr(self, c.RuleSet, rss)
1315        setattr(self, c.RuleSetCount, len(rss))
1316        return bool(rss)
1317    elif self.Format == 2:
1318        if not self.Coverage.subset(s.glyphs):
1319            return False
1320        ContextData = c.ContextData(self)
1321        klass_maps = [
1322            x.subset(s.glyphs, remap=True) if x else None for x in ContextData
1323        ]
1324
1325        # Keep rulesets for class numbers that survived.
1326        indices = klass_maps[c.ClassDefIndex]
1327        rss = getattr(self, c.RuleSet)
1328        rssCount = getattr(self, c.RuleSetCount)
1329        rss = [rss[i] for i in indices if i < rssCount]
1330        del rssCount
1331        # Delete, but not renumber, unreachable rulesets.
1332        indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs)
1333        rss = [rss if i in indices else None for i, rss in enumerate(rss)]
1334
1335        for rs in rss:
1336            if not rs:
1337                continue
1338            ss = getattr(rs, c.Rule)
1339            ss = [
1340                r
1341                for r in ss
1342                if r
1343                and all(
1344                    all(k in klass_map for k in klist)
1345                    for klass_map, klist in zip(klass_maps, c.RuleData(r))
1346                )
1347            ]
1348            setattr(rs, c.Rule, ss)
1349            setattr(rs, c.RuleCount, len(ss))
1350
1351            # Remap rule classes
1352            for r in ss:
1353                c.SetRuleData(
1354                    r,
1355                    [
1356                        [klass_map.index(k) for k in klist]
1357                        for klass_map, klist in zip(klass_maps, c.RuleData(r))
1358                    ],
1359                )
1360
1361        # Prune empty rulesets
1362        rss = [rs if rs and getattr(rs, c.Rule) else None for rs in rss]
1363        while rss and rss[-1] is None:
1364            del rss[-1]
1365        setattr(self, c.RuleSet, rss)
1366        setattr(self, c.RuleSetCount, len(rss))
1367
1368        # TODO: We can do a second round of remapping class values based
1369        # on classes that are actually used in at least one rule.	Right
1370        # now we subset classes to c.glyphs only.	Or better, rewrite
1371        # the above to do that.
1372
1373        return bool(rss)
1374    elif self.Format == 3:
1375        return all(x is not None and x.subset(s.glyphs) for x in c.RuleData(self))
1376    else:
1377        assert 0, "unknown format: %s" % self.Format
1378
1379
1380@_add_method(
1381    otTables.ContextSubst,
1382    otTables.ChainContextSubst,
1383    otTables.ContextPos,
1384    otTables.ChainContextPos,
1385)
1386def subset_lookups(self, lookup_indices):
1387    c = self.__subset_classify_context()
1388
1389    if self.Format in [1, 2]:
1390        for rs in getattr(self, c.RuleSet):
1391            if not rs:
1392                continue
1393            for r in getattr(rs, c.Rule):
1394                if not r:
1395                    continue
1396                setattr(
1397                    r,
1398                    c.LookupRecord,
1399                    [
1400                        ll
1401                        for ll in getattr(r, c.LookupRecord)
1402                        if ll and ll.LookupListIndex in lookup_indices
1403                    ],
1404                )
1405                for ll in getattr(r, c.LookupRecord):
1406                    if not ll:
1407                        continue
1408                    ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
1409    elif self.Format == 3:
1410        setattr(
1411            self,
1412            c.LookupRecord,
1413            [
1414                ll
1415                for ll in getattr(self, c.LookupRecord)
1416                if ll and ll.LookupListIndex in lookup_indices
1417            ],
1418        )
1419        for ll in getattr(self, c.LookupRecord):
1420            if not ll:
1421                continue
1422            ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
1423    else:
1424        assert 0, "unknown format: %s" % self.Format
1425
1426
1427@_add_method(
1428    otTables.ContextSubst,
1429    otTables.ChainContextSubst,
1430    otTables.ContextPos,
1431    otTables.ChainContextPos,
1432)
1433def collect_lookups(self):
1434    c = self.__subset_classify_context()
1435
1436    if self.Format in [1, 2]:
1437        return [
1438            ll.LookupListIndex
1439            for rs in getattr(self, c.RuleSet)
1440            if rs
1441            for r in getattr(rs, c.Rule)
1442            if r
1443            for ll in getattr(r, c.LookupRecord)
1444            if ll
1445        ]
1446    elif self.Format == 3:
1447        return [ll.LookupListIndex for ll in getattr(self, c.LookupRecord) if ll]
1448    else:
1449        assert 0, "unknown format: %s" % self.Format
1450
1451
1452@_add_method(otTables.ExtensionSubst)
1453def closure_glyphs(self, s, cur_glyphs):
1454    if self.Format == 1:
1455        self.ExtSubTable.closure_glyphs(s, cur_glyphs)
1456    else:
1457        assert 0, "unknown format: %s" % self.Format
1458
1459
1460@_add_method(otTables.ExtensionSubst)
1461def may_have_non_1to1(self):
1462    if self.Format == 1:
1463        return self.ExtSubTable.may_have_non_1to1()
1464    else:
1465        assert 0, "unknown format: %s" % self.Format
1466
1467
1468@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
1469def subset_glyphs(self, s):
1470    if self.Format == 1:
1471        return self.ExtSubTable.subset_glyphs(s)
1472    else:
1473        assert 0, "unknown format: %s" % self.Format
1474
1475
1476@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
1477def prune_post_subset(self, font, options):
1478    if self.Format == 1:
1479        return self.ExtSubTable.prune_post_subset(font, options)
1480    else:
1481        assert 0, "unknown format: %s" % self.Format
1482
1483
1484@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
1485def subset_lookups(self, lookup_indices):
1486    if self.Format == 1:
1487        return self.ExtSubTable.subset_lookups(lookup_indices)
1488    else:
1489        assert 0, "unknown format: %s" % self.Format
1490
1491
1492@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
1493def collect_lookups(self):
1494    if self.Format == 1:
1495        return self.ExtSubTable.collect_lookups()
1496    else:
1497        assert 0, "unknown format: %s" % self.Format
1498
1499
1500@_add_method(otTables.Lookup)
1501def closure_glyphs(self, s, cur_glyphs=None):
1502    if cur_glyphs is None:
1503        cur_glyphs = frozenset(s.glyphs)
1504
1505    # Memoize
1506    key = id(self)
1507    doneLookups = s._doneLookups
1508    count, covered = doneLookups.get(key, (0, None))
1509    if count != len(s.glyphs):
1510        count, covered = doneLookups[key] = (len(s.glyphs), set())
1511    if cur_glyphs.issubset(covered):
1512        return
1513    covered.update(cur_glyphs)
1514
1515    for st in self.SubTable:
1516        if not st:
1517            continue
1518        st.closure_glyphs(s, cur_glyphs)
1519
1520
1521@_add_method(otTables.Lookup)
1522def subset_glyphs(self, s):
1523    self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)]
1524    self.SubTableCount = len(self.SubTable)
1525    if hasattr(self, "MarkFilteringSet") and self.MarkFilteringSet is not None:
1526        if self.MarkFilteringSet not in s.used_mark_sets:
1527            self.MarkFilteringSet = None
1528            self.LookupFlag &= ~0x10
1529        else:
1530            self.MarkFilteringSet = s.used_mark_sets.index(self.MarkFilteringSet)
1531    return bool(self.SubTableCount)
1532
1533
1534@_add_method(otTables.Lookup)
1535def prune_post_subset(self, font, options):
1536    ret = False
1537    for st in self.SubTable:
1538        if not st:
1539            continue
1540        if st.prune_post_subset(font, options):
1541            ret = True
1542    return ret
1543
1544
1545@_add_method(otTables.Lookup)
1546def subset_lookups(self, lookup_indices):
1547    for s in self.SubTable:
1548        s.subset_lookups(lookup_indices)
1549
1550
1551@_add_method(otTables.Lookup)
1552def collect_lookups(self):
1553    return sum((st.collect_lookups() for st in self.SubTable if st), [])
1554
1555
1556@_add_method(otTables.Lookup)
1557def may_have_non_1to1(self):
1558    return any(st.may_have_non_1to1() for st in self.SubTable if st)
1559
1560
1561@_add_method(otTables.LookupList)
1562def subset_glyphs(self, s):
1563    """Returns the indices of nonempty lookups."""
1564    return [i for i, l in enumerate(self.Lookup) if l and l.subset_glyphs(s)]
1565
1566
1567@_add_method(otTables.LookupList)
1568def prune_post_subset(self, font, options):
1569    ret = False
1570    for l in self.Lookup:
1571        if not l:
1572            continue
1573        if l.prune_post_subset(font, options):
1574            ret = True
1575    return ret
1576
1577
1578@_add_method(otTables.LookupList)
1579def subset_lookups(self, lookup_indices):
1580    self.ensureDecompiled()
1581    self.Lookup = [self.Lookup[i] for i in lookup_indices if i < self.LookupCount]
1582    self.LookupCount = len(self.Lookup)
1583    for l in self.Lookup:
1584        l.subset_lookups(lookup_indices)
1585
1586
1587@_add_method(otTables.LookupList)
1588def neuter_lookups(self, lookup_indices):
1589    """Sets lookups not in lookup_indices to None."""
1590    self.ensureDecompiled()
1591    self.Lookup = [
1592        l if i in lookup_indices else None for i, l in enumerate(self.Lookup)
1593    ]
1594
1595
1596@_add_method(otTables.LookupList)
1597def closure_lookups(self, lookup_indices):
1598    """Returns sorted index of all lookups reachable from lookup_indices."""
1599    lookup_indices = _uniq_sort(lookup_indices)
1600    recurse = lookup_indices
1601    while True:
1602        recurse_lookups = sum(
1603            (self.Lookup[i].collect_lookups() for i in recurse if i < self.LookupCount),
1604            [],
1605        )
1606        recurse_lookups = [
1607            l
1608            for l in recurse_lookups
1609            if l not in lookup_indices and l < self.LookupCount
1610        ]
1611        if not recurse_lookups:
1612            return _uniq_sort(lookup_indices)
1613        recurse_lookups = _uniq_sort(recurse_lookups)
1614        lookup_indices.extend(recurse_lookups)
1615        recurse = recurse_lookups
1616
1617
1618@_add_method(otTables.Feature)
1619def subset_lookups(self, lookup_indices):
1620    """ "Returns True if feature is non-empty afterwards."""
1621    self.LookupListIndex = [l for l in self.LookupListIndex if l in lookup_indices]
1622    # Now map them.
1623    self.LookupListIndex = [lookup_indices.index(l) for l in self.LookupListIndex]
1624    self.LookupCount = len(self.LookupListIndex)
1625    # keep 'size' feature even if it contains no lookups; but drop any other
1626    # empty feature (e.g. FeatureParams for stylistic set names)
1627    # https://github.com/fonttools/fonttools/issues/2324
1628    return self.LookupCount or isinstance(
1629        self.FeatureParams, otTables.FeatureParamsSize
1630    )
1631
1632
1633@_add_method(otTables.FeatureList)
1634def subset_lookups(self, lookup_indices):
1635    """Returns the indices of nonempty features."""
1636    # Note: Never ever drop feature 'pref', even if it's empty.
1637    # HarfBuzz chooses shaper for Khmer based on presence of this
1638    # feature.	See thread at:
1639    # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html
1640    return [
1641        i
1642        for i, f in enumerate(self.FeatureRecord)
1643        if (f.Feature.subset_lookups(lookup_indices) or f.FeatureTag == "pref")
1644    ]
1645
1646
1647@_add_method(otTables.FeatureList)
1648def collect_lookups(self, feature_indices):
1649    return sum(
1650        (
1651            self.FeatureRecord[i].Feature.LookupListIndex
1652            for i in feature_indices
1653            if i < self.FeatureCount
1654        ),
1655        [],
1656    )
1657
1658
1659@_add_method(otTables.FeatureList)
1660def subset_features(self, feature_indices):
1661    self.ensureDecompiled()
1662    self.FeatureRecord = _list_subset(self.FeatureRecord, feature_indices)
1663    self.FeatureCount = len(self.FeatureRecord)
1664    return bool(self.FeatureCount)
1665
1666
1667@_add_method(otTables.FeatureTableSubstitution)
1668def subset_lookups(self, lookup_indices):
1669    """Returns the indices of nonempty features."""
1670    return [
1671        r.FeatureIndex
1672        for r in self.SubstitutionRecord
1673        if r.Feature.subset_lookups(lookup_indices)
1674    ]
1675
1676
1677@_add_method(otTables.FeatureVariations)
1678def subset_lookups(self, lookup_indices):
1679    """Returns the indices of nonempty features."""
1680    return sum(
1681        (
1682            f.FeatureTableSubstitution.subset_lookups(lookup_indices)
1683            for f in self.FeatureVariationRecord
1684        ),
1685        [],
1686    )
1687
1688
1689@_add_method(otTables.FeatureVariations)
1690def collect_lookups(self, feature_indices):
1691    return sum(
1692        (
1693            r.Feature.LookupListIndex
1694            for vr in self.FeatureVariationRecord
1695            for r in vr.FeatureTableSubstitution.SubstitutionRecord
1696            if r.FeatureIndex in feature_indices
1697        ),
1698        [],
1699    )
1700
1701
1702@_add_method(otTables.FeatureTableSubstitution)
1703def subset_features(self, feature_indices):
1704    self.ensureDecompiled()
1705    self.SubstitutionRecord = [
1706        r for r in self.SubstitutionRecord if r.FeatureIndex in feature_indices
1707    ]
1708    # remap feature indices
1709    for r in self.SubstitutionRecord:
1710        r.FeatureIndex = feature_indices.index(r.FeatureIndex)
1711    self.SubstitutionCount = len(self.SubstitutionRecord)
1712    return bool(self.SubstitutionCount)
1713
1714
1715@_add_method(otTables.FeatureVariations)
1716def subset_features(self, feature_indices):
1717    self.ensureDecompiled()
1718    for r in self.FeatureVariationRecord:
1719        r.FeatureTableSubstitution.subset_features(feature_indices)
1720    # Prune empty records at the end only
1721    # https://github.com/fonttools/fonttools/issues/1881
1722    while (
1723        self.FeatureVariationRecord
1724        and not self.FeatureVariationRecord[
1725            -1
1726        ].FeatureTableSubstitution.SubstitutionCount
1727    ):
1728        self.FeatureVariationRecord.pop()
1729    self.FeatureVariationCount = len(self.FeatureVariationRecord)
1730    return bool(self.FeatureVariationCount)
1731
1732
1733@_add_method(otTables.DefaultLangSys, otTables.LangSys)
1734def subset_features(self, feature_indices):
1735    if self.ReqFeatureIndex in feature_indices:
1736        self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex)
1737    else:
1738        self.ReqFeatureIndex = 65535
1739    self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices]
1740    # Now map them.
1741    self.FeatureIndex = [
1742        feature_indices.index(f) for f in self.FeatureIndex if f in feature_indices
1743    ]
1744    self.FeatureCount = len(self.FeatureIndex)
1745    return bool(self.FeatureCount or self.ReqFeatureIndex != 65535)
1746
1747
1748@_add_method(otTables.DefaultLangSys, otTables.LangSys)
1749def collect_features(self):
1750    feature_indices = self.FeatureIndex[:]
1751    if self.ReqFeatureIndex != 65535:
1752        feature_indices.append(self.ReqFeatureIndex)
1753    return _uniq_sort(feature_indices)
1754
1755
1756@_add_method(otTables.Script)
1757def subset_features(self, feature_indices, keepEmptyDefaultLangSys=False):
1758    if (
1759        self.DefaultLangSys
1760        and not self.DefaultLangSys.subset_features(feature_indices)
1761        and not keepEmptyDefaultLangSys
1762    ):
1763        self.DefaultLangSys = None
1764    self.LangSysRecord = [
1765        l for l in self.LangSysRecord if l.LangSys.subset_features(feature_indices)
1766    ]
1767    self.LangSysCount = len(self.LangSysRecord)
1768    return bool(self.LangSysCount or self.DefaultLangSys)
1769
1770
1771@_add_method(otTables.Script)
1772def collect_features(self):
1773    feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord]
1774    if self.DefaultLangSys:
1775        feature_indices.append(self.DefaultLangSys.collect_features())
1776    return _uniq_sort(sum(feature_indices, []))
1777
1778
1779@_add_method(otTables.ScriptList)
1780def subset_features(self, feature_indices, retain_empty):
1781    # https://bugzilla.mozilla.org/show_bug.cgi?id=1331737#c32
1782    self.ScriptRecord = [
1783        s
1784        for s in self.ScriptRecord
1785        if s.Script.subset_features(feature_indices, s.ScriptTag == "DFLT")
1786        or retain_empty
1787    ]
1788    self.ScriptCount = len(self.ScriptRecord)
1789    return bool(self.ScriptCount)
1790
1791
1792@_add_method(otTables.ScriptList)
1793def collect_features(self):
1794    return _uniq_sort(sum((s.Script.collect_features() for s in self.ScriptRecord), []))
1795
1796
1797# CBLC will inherit it
1798@_add_method(ttLib.getTableClass("EBLC"))
1799def subset_glyphs(self, s):
1800    for strike in self.strikes:
1801        for indexSubTable in strike.indexSubTables:
1802            indexSubTable.names = [n for n in indexSubTable.names if n in s.glyphs]
1803        strike.indexSubTables = [i for i in strike.indexSubTables if i.names]
1804    self.strikes = [s for s in self.strikes if s.indexSubTables]
1805
1806    return True
1807
1808
1809# CBDT will inherit it
1810@_add_method(ttLib.getTableClass("EBDT"))
1811def subset_glyphs(self, s):
1812    strikeData = [
1813        {g: strike[g] for g in s.glyphs if g in strike} for strike in self.strikeData
1814    ]
1815    # Prune empty strikes
1816    # https://github.com/fonttools/fonttools/issues/1633
1817    self.strikeData = [strike for strike in strikeData if strike]
1818    return True
1819
1820
1821@_add_method(ttLib.getTableClass("sbix"))
1822def subset_glyphs(self, s):
1823    for strike in self.strikes.values():
1824        strike.glyphs = {g: strike.glyphs[g] for g in s.glyphs if g in strike.glyphs}
1825
1826    return True
1827
1828
1829@_add_method(ttLib.getTableClass("GSUB"))
1830def closure_glyphs(self, s):
1831    s.table = self.table
1832    if self.table.ScriptList:
1833        feature_indices = self.table.ScriptList.collect_features()
1834    else:
1835        feature_indices = []
1836    if self.table.FeatureList:
1837        lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
1838    else:
1839        lookup_indices = []
1840    if getattr(self.table, "FeatureVariations", None):
1841        lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices)
1842    lookup_indices = _uniq_sort(lookup_indices)
1843    if self.table.LookupList:
1844        s._doneLookups = {}
1845        while True:
1846            orig_glyphs = frozenset(s.glyphs)
1847            for i in lookup_indices:
1848                if i >= self.table.LookupList.LookupCount:
1849                    continue
1850                if not self.table.LookupList.Lookup[i]:
1851                    continue
1852                self.table.LookupList.Lookup[i].closure_glyphs(s)
1853            if orig_glyphs == s.glyphs:
1854                break
1855        del s._doneLookups
1856    del s.table
1857
1858
1859@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1860def subset_glyphs(self, s):
1861    s.glyphs = s.glyphs_gsubed
1862    if self.table.LookupList:
1863        lookup_indices = self.table.LookupList.subset_glyphs(s)
1864    else:
1865        lookup_indices = []
1866    self.subset_lookups(lookup_indices)
1867    return True
1868
1869
1870@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1871def retain_empty_scripts(self):
1872    # https://github.com/fonttools/fonttools/issues/518
1873    # https://bugzilla.mozilla.org/show_bug.cgi?id=1080739#c15
1874    return self.__class__ == ttLib.getTableClass("GSUB")
1875
1876
1877@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1878def subset_lookups(self, lookup_indices):
1879    """Retains specified lookups, then removes empty features, language
1880    systems, and scripts."""
1881    if self.table.LookupList:
1882        self.table.LookupList.subset_lookups(lookup_indices)
1883    if self.table.FeatureList:
1884        feature_indices = self.table.FeatureList.subset_lookups(lookup_indices)
1885    else:
1886        feature_indices = []
1887    if getattr(self.table, "FeatureVariations", None):
1888        feature_indices += self.table.FeatureVariations.subset_lookups(lookup_indices)
1889    feature_indices = _uniq_sort(feature_indices)
1890    if self.table.FeatureList:
1891        self.table.FeatureList.subset_features(feature_indices)
1892    if getattr(self.table, "FeatureVariations", None):
1893        self.table.FeatureVariations.subset_features(feature_indices)
1894    if self.table.ScriptList:
1895        self.table.ScriptList.subset_features(
1896            feature_indices, self.retain_empty_scripts()
1897        )
1898
1899
1900@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1901def neuter_lookups(self, lookup_indices):
1902    """Sets lookups not in lookup_indices to None."""
1903    if self.table.LookupList:
1904        self.table.LookupList.neuter_lookups(lookup_indices)
1905
1906
1907@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1908def prune_lookups(self, remap=True):
1909    """Remove (default) or neuter unreferenced lookups"""
1910    if self.table.ScriptList:
1911        feature_indices = self.table.ScriptList.collect_features()
1912    else:
1913        feature_indices = []
1914    if self.table.FeatureList:
1915        lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
1916    else:
1917        lookup_indices = []
1918    if getattr(self.table, "FeatureVariations", None):
1919        lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices)
1920    lookup_indices = _uniq_sort(lookup_indices)
1921    if self.table.LookupList:
1922        lookup_indices = self.table.LookupList.closure_lookups(lookup_indices)
1923    else:
1924        lookup_indices = []
1925    if remap:
1926        self.subset_lookups(lookup_indices)
1927    else:
1928        self.neuter_lookups(lookup_indices)
1929
1930
1931@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1932def subset_feature_tags(self, feature_tags):
1933    if self.table.FeatureList:
1934        feature_indices = [
1935            i
1936            for i, f in enumerate(self.table.FeatureList.FeatureRecord)
1937            if f.FeatureTag in feature_tags
1938        ]
1939        self.table.FeatureList.subset_features(feature_indices)
1940        if getattr(self.table, "FeatureVariations", None):
1941            self.table.FeatureVariations.subset_features(feature_indices)
1942    else:
1943        feature_indices = []
1944    if self.table.ScriptList:
1945        self.table.ScriptList.subset_features(
1946            feature_indices, self.retain_empty_scripts()
1947        )
1948
1949
1950@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1951def subset_script_tags(self, tags):
1952    langsys = {}
1953    script_tags = set()
1954    for tag in tags:
1955        script_tag, lang_tag = tag.split(".") if "." in tag else (tag, "*")
1956        script_tags.add(script_tag.ljust(4))
1957        langsys.setdefault(script_tag, set()).add(lang_tag.ljust(4))
1958
1959    if self.table.ScriptList:
1960        self.table.ScriptList.ScriptRecord = [
1961            s for s in self.table.ScriptList.ScriptRecord if s.ScriptTag in script_tags
1962        ]
1963        self.table.ScriptList.ScriptCount = len(self.table.ScriptList.ScriptRecord)
1964
1965        for record in self.table.ScriptList.ScriptRecord:
1966            if record.ScriptTag in langsys and "*   " not in langsys[record.ScriptTag]:
1967                record.Script.LangSysRecord = [
1968                    l
1969                    for l in record.Script.LangSysRecord
1970                    if l.LangSysTag in langsys[record.ScriptTag]
1971                ]
1972                record.Script.LangSysCount = len(record.Script.LangSysRecord)
1973                if "dflt" not in langsys[record.ScriptTag]:
1974                    record.Script.DefaultLangSys = None
1975
1976
1977@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1978def prune_features(self):
1979    """Remove unreferenced features"""
1980    if self.table.ScriptList:
1981        feature_indices = self.table.ScriptList.collect_features()
1982    else:
1983        feature_indices = []
1984    if self.table.FeatureList:
1985        self.table.FeatureList.subset_features(feature_indices)
1986    if getattr(self.table, "FeatureVariations", None):
1987        self.table.FeatureVariations.subset_features(feature_indices)
1988    if self.table.ScriptList:
1989        self.table.ScriptList.subset_features(
1990            feature_indices, self.retain_empty_scripts()
1991        )
1992
1993
1994@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1995def prune_pre_subset(self, font, options):
1996    # Drop undesired features
1997    if "*" not in options.layout_scripts:
1998        self.subset_script_tags(options.layout_scripts)
1999    if "*" not in options.layout_features:
2000        self.subset_feature_tags(options.layout_features)
2001    # Neuter unreferenced lookups
2002    self.prune_lookups(remap=False)
2003    return True
2004
2005
2006@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
2007def remove_redundant_langsys(self):
2008    table = self.table
2009    if not table.ScriptList or not table.FeatureList:
2010        return
2011
2012    features = table.FeatureList.FeatureRecord
2013
2014    for s in table.ScriptList.ScriptRecord:
2015        d = s.Script.DefaultLangSys
2016        if not d:
2017            continue
2018        for lr in s.Script.LangSysRecord[:]:
2019            l = lr.LangSys
2020            # Compare d and l
2021            if len(d.FeatureIndex) != len(l.FeatureIndex):
2022                continue
2023            if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535):
2024                continue
2025
2026            if d.ReqFeatureIndex != 65535:
2027                if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]:
2028                    continue
2029
2030            for i in range(len(d.FeatureIndex)):
2031                if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]:
2032                    break
2033            else:
2034                # LangSys and default are equal; delete LangSys
2035                s.Script.LangSysRecord.remove(lr)
2036
2037
2038@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
2039def prune_post_subset(self, font, options):
2040    table = self.table
2041
2042    self.prune_lookups()  # XXX Is this actually needed?!
2043
2044    if table.LookupList:
2045        table.LookupList.prune_post_subset(font, options)
2046        # XXX Next two lines disabled because OTS is stupid and
2047        # doesn't like NULL offsets here.
2048        # if not table.LookupList.Lookup:
2049        # 	table.LookupList = None
2050
2051    if not table.LookupList:
2052        table.FeatureList = None
2053
2054    if table.FeatureList:
2055        self.remove_redundant_langsys()
2056        # Remove unreferenced features
2057        self.prune_features()
2058
2059    # XXX Next two lines disabled because OTS is stupid and
2060    # doesn't like NULL offsets here.
2061    # if table.FeatureList and not table.FeatureList.FeatureRecord:
2062    # 	table.FeatureList = None
2063
2064    # Never drop scripts themselves as them just being available
2065    # holds semantic significance.
2066    # XXX Next two lines disabled because OTS is stupid and
2067    # doesn't like NULL offsets here.
2068    # if table.ScriptList and not table.ScriptList.ScriptRecord:
2069    # 	table.ScriptList = None
2070
2071    if hasattr(table, "FeatureVariations"):
2072        # drop FeatureVariations if there are no features to substitute
2073        if table.FeatureVariations and not (
2074            table.FeatureList and table.FeatureVariations.FeatureVariationRecord
2075        ):
2076            table.FeatureVariations = None
2077
2078        # downgrade table version if there are no FeatureVariations
2079        if not table.FeatureVariations and table.Version == 0x00010001:
2080            table.Version = 0x00010000
2081
2082    return True
2083
2084
2085@_add_method(ttLib.getTableClass("GDEF"))
2086def subset_glyphs(self, s):
2087    glyphs = s.glyphs_gsubed
2088    table = self.table
2089    if table.LigCaretList:
2090        indices = table.LigCaretList.Coverage.subset(glyphs)
2091        table.LigCaretList.LigGlyph = _list_subset(table.LigCaretList.LigGlyph, indices)
2092        table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph)
2093    if table.MarkAttachClassDef:
2094        table.MarkAttachClassDef.classDefs = {
2095            g: v for g, v in table.MarkAttachClassDef.classDefs.items() if g in glyphs
2096        }
2097    if table.GlyphClassDef:
2098        table.GlyphClassDef.classDefs = {
2099            g: v for g, v in table.GlyphClassDef.classDefs.items() if g in glyphs
2100        }
2101    if table.AttachList:
2102        indices = table.AttachList.Coverage.subset(glyphs)
2103        GlyphCount = table.AttachList.GlyphCount
2104        table.AttachList.AttachPoint = [
2105            table.AttachList.AttachPoint[i] for i in indices if i < GlyphCount
2106        ]
2107        table.AttachList.GlyphCount = len(table.AttachList.AttachPoint)
2108    if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef:
2109        markGlyphSets = table.MarkGlyphSetsDef
2110        for coverage in markGlyphSets.Coverage:
2111            if coverage:
2112                coverage.subset(glyphs)
2113
2114        s.used_mark_sets = [i for i, c in enumerate(markGlyphSets.Coverage) if c.glyphs]
2115        markGlyphSets.Coverage = [c for c in markGlyphSets.Coverage if c.glyphs]
2116
2117    return True
2118
2119
2120def _pruneGDEF(font):
2121    if "GDEF" not in font:
2122        return
2123    gdef = font["GDEF"]
2124    table = gdef.table
2125    if not hasattr(table, "VarStore"):
2126        return
2127
2128    store = table.VarStore
2129
2130    usedVarIdxes = set()
2131
2132    # Collect.
2133    table.collect_device_varidxes(usedVarIdxes)
2134    if "GPOS" in font:
2135        font["GPOS"].table.collect_device_varidxes(usedVarIdxes)
2136
2137    # Subset.
2138    varidx_map = store.subset_varidxes(usedVarIdxes)
2139
2140    # Map.
2141    table.remap_device_varidxes(varidx_map)
2142    if "GPOS" in font:
2143        font["GPOS"].table.remap_device_varidxes(varidx_map)
2144
2145
2146@_add_method(ttLib.getTableClass("GDEF"))
2147def prune_post_subset(self, font, options):
2148    table = self.table
2149    # XXX check these against OTS
2150    if table.LigCaretList and not table.LigCaretList.LigGlyphCount:
2151        table.LigCaretList = None
2152    if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs:
2153        table.MarkAttachClassDef = None
2154    if table.GlyphClassDef and not table.GlyphClassDef.classDefs:
2155        table.GlyphClassDef = None
2156    if table.AttachList and not table.AttachList.GlyphCount:
2157        table.AttachList = None
2158    if hasattr(table, "VarStore"):
2159        _pruneGDEF(font)
2160        if table.VarStore.VarDataCount == 0:
2161            if table.Version == 0x00010003:
2162                table.Version = 0x00010002
2163    if (
2164        not hasattr(table, "MarkGlyphSetsDef")
2165        or not table.MarkGlyphSetsDef
2166        or not table.MarkGlyphSetsDef.Coverage
2167    ):
2168        table.MarkGlyphSetsDef = None
2169        if table.Version == 0x00010002:
2170            table.Version = 0x00010000
2171    return bool(
2172        table.LigCaretList
2173        or table.MarkAttachClassDef
2174        or table.GlyphClassDef
2175        or table.AttachList
2176        or (table.Version >= 0x00010002 and table.MarkGlyphSetsDef)
2177        or (table.Version >= 0x00010003 and table.VarStore)
2178    )
2179
2180
2181@_add_method(ttLib.getTableClass("kern"))
2182def prune_pre_subset(self, font, options):
2183    # Prune unknown kern table types
2184    self.kernTables = [t for t in self.kernTables if hasattr(t, "kernTable")]
2185    return bool(self.kernTables)
2186
2187
2188@_add_method(ttLib.getTableClass("kern"))
2189def subset_glyphs(self, s):
2190    glyphs = s.glyphs_gsubed
2191    for t in self.kernTables:
2192        t.kernTable = {
2193            (a, b): v
2194            for (a, b), v in t.kernTable.items()
2195            if a in glyphs and b in glyphs
2196        }
2197    self.kernTables = [t for t in self.kernTables if t.kernTable]
2198    return bool(self.kernTables)
2199
2200
2201@_add_method(ttLib.getTableClass("vmtx"))
2202def subset_glyphs(self, s):
2203    self.metrics = _dict_subset(self.metrics, s.glyphs)
2204    for g in s.glyphs_emptied:
2205        self.metrics[g] = (0, 0)
2206    return bool(self.metrics)
2207
2208
2209@_add_method(ttLib.getTableClass("hmtx"))
2210def subset_glyphs(self, s):
2211    self.metrics = _dict_subset(self.metrics, s.glyphs)
2212    for g in s.glyphs_emptied:
2213        self.metrics[g] = (0, 0)
2214    return True  # Required table
2215
2216
2217@_add_method(ttLib.getTableClass("hdmx"))
2218def subset_glyphs(self, s):
2219    self.hdmx = {sz: _dict_subset(l, s.glyphs) for sz, l in self.hdmx.items()}
2220    for sz in self.hdmx:
2221        for g in s.glyphs_emptied:
2222            self.hdmx[sz][g] = 0
2223    return bool(self.hdmx)
2224
2225
2226@_add_method(ttLib.getTableClass("ankr"))
2227def subset_glyphs(self, s):
2228    table = self.table.AnchorPoints
2229    assert table.Format == 0, "unknown 'ankr' format %s" % table.Format
2230    table.Anchors = {
2231        glyph: table.Anchors[glyph] for glyph in s.glyphs if glyph in table.Anchors
2232    }
2233    return len(table.Anchors) > 0
2234
2235
2236@_add_method(ttLib.getTableClass("bsln"))
2237def closure_glyphs(self, s):
2238    table = self.table.Baseline
2239    if table.Format in (2, 3):
2240        s.glyphs.add(table.StandardGlyph)
2241
2242
2243@_add_method(ttLib.getTableClass("bsln"))
2244def subset_glyphs(self, s):
2245    table = self.table.Baseline
2246    if table.Format in (1, 3):
2247        baselines = {
2248            glyph: table.BaselineValues.get(glyph, table.DefaultBaseline)
2249            for glyph in s.glyphs
2250        }
2251        if len(baselines) > 0:
2252            mostCommon, _cnt = Counter(baselines.values()).most_common(1)[0]
2253            table.DefaultBaseline = mostCommon
2254            baselines = {glyph: b for glyph, b in baselines.items() if b != mostCommon}
2255        if len(baselines) > 0:
2256            table.BaselineValues = baselines
2257        else:
2258            table.Format = {1: 0, 3: 2}[table.Format]
2259            del table.BaselineValues
2260    return True
2261
2262
2263@_add_method(ttLib.getTableClass("lcar"))
2264def subset_glyphs(self, s):
2265    table = self.table.LigatureCarets
2266    if table.Format in (0, 1):
2267        table.Carets = {
2268            glyph: table.Carets[glyph] for glyph in s.glyphs if glyph in table.Carets
2269        }
2270        return len(table.Carets) > 0
2271    else:
2272        assert False, "unknown 'lcar' format %s" % table.Format
2273
2274
2275@_add_method(ttLib.getTableClass("gvar"))
2276def prune_pre_subset(self, font, options):
2277    if options.notdef_glyph and not options.notdef_outline:
2278        self.variations[font.glyphOrder[0]] = []
2279    return True
2280
2281
2282@_add_method(ttLib.getTableClass("gvar"))
2283def subset_glyphs(self, s):
2284    self.variations = _dict_subset(self.variations, s.glyphs)
2285    self.glyphCount = len(self.variations)
2286    return bool(self.variations)
2287
2288
2289def _remap_index_map(s, varidx_map, table_map):
2290    map_ = {k: varidx_map[v] for k, v in table_map.mapping.items()}
2291    # Emptied glyphs are remapped to:
2292    # if GID <= last retained GID, 0/0: delta set for 0/0 is expected to exist & zeros compress well
2293    # if GID > last retained GID, major/minor of the last retained glyph: will be optimized out by table compiler
2294    last_idx = varidx_map[table_map.mapping[s.last_retained_glyph]]
2295    for g, i in s.reverseEmptiedGlyphMap.items():
2296        map_[g] = last_idx if i > s.last_retained_order else 0
2297    return map_
2298
2299
2300@_add_method(ttLib.getTableClass("HVAR"))
2301def subset_glyphs(self, s):
2302    table = self.table
2303
2304    used = set()
2305    advIdxes_ = set()
2306    retainAdvMap = False
2307
2308    if table.AdvWidthMap:
2309        table.AdvWidthMap.mapping = _dict_subset(table.AdvWidthMap.mapping, s.glyphs)
2310        used.update(table.AdvWidthMap.mapping.values())
2311    else:
2312        used.update(s.reverseOrigGlyphMap.values())
2313        advIdxes_ = used.copy()
2314        retainAdvMap = s.options.retain_gids
2315
2316    if table.LsbMap:
2317        table.LsbMap.mapping = _dict_subset(table.LsbMap.mapping, s.glyphs)
2318        used.update(table.LsbMap.mapping.values())
2319    if table.RsbMap:
2320        table.RsbMap.mapping = _dict_subset(table.RsbMap.mapping, s.glyphs)
2321        used.update(table.RsbMap.mapping.values())
2322
2323    varidx_map = table.VarStore.subset_varidxes(
2324        used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_
2325    )
2326
2327    if table.AdvWidthMap:
2328        table.AdvWidthMap.mapping = _remap_index_map(s, varidx_map, table.AdvWidthMap)
2329    if table.LsbMap:
2330        table.LsbMap.mapping = _remap_index_map(s, varidx_map, table.LsbMap)
2331    if table.RsbMap:
2332        table.RsbMap.mapping = _remap_index_map(s, varidx_map, table.RsbMap)
2333
2334    # TODO Return emptiness...
2335    return True
2336
2337
2338@_add_method(ttLib.getTableClass("VVAR"))
2339def subset_glyphs(self, s):
2340    table = self.table
2341
2342    used = set()
2343    advIdxes_ = set()
2344    retainAdvMap = False
2345
2346    if table.AdvHeightMap:
2347        table.AdvHeightMap.mapping = _dict_subset(table.AdvHeightMap.mapping, s.glyphs)
2348        used.update(table.AdvHeightMap.mapping.values())
2349    else:
2350        used.update(s.reverseOrigGlyphMap.values())
2351        advIdxes_ = used.copy()
2352        retainAdvMap = s.options.retain_gids
2353
2354    if table.TsbMap:
2355        table.TsbMap.mapping = _dict_subset(table.TsbMap.mapping, s.glyphs)
2356        used.update(table.TsbMap.mapping.values())
2357    if table.BsbMap:
2358        table.BsbMap.mapping = _dict_subset(table.BsbMap.mapping, s.glyphs)
2359        used.update(table.BsbMap.mapping.values())
2360    if table.VOrgMap:
2361        table.VOrgMap.mapping = _dict_subset(table.VOrgMap.mapping, s.glyphs)
2362        used.update(table.VOrgMap.mapping.values())
2363
2364    varidx_map = table.VarStore.subset_varidxes(
2365        used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_
2366    )
2367
2368    if table.AdvHeightMap:
2369        table.AdvHeightMap.mapping = _remap_index_map(s, varidx_map, table.AdvHeightMap)
2370    if table.TsbMap:
2371        table.TsbMap.mapping = _remap_index_map(s, varidx_map, table.TsbMap)
2372    if table.BsbMap:
2373        table.BsbMap.mapping = _remap_index_map(s, varidx_map, table.BsbMap)
2374    if table.VOrgMap:
2375        table.VOrgMap.mapping = _remap_index_map(s, varidx_map, table.VOrgMap)
2376
2377    # TODO Return emptiness...
2378    return True
2379
2380
2381@_add_method(ttLib.getTableClass("VORG"))
2382def subset_glyphs(self, s):
2383    self.VOriginRecords = {
2384        g: v for g, v in self.VOriginRecords.items() if g in s.glyphs
2385    }
2386    self.numVertOriginYMetrics = len(self.VOriginRecords)
2387    return True  # Never drop; has default metrics
2388
2389
2390@_add_method(ttLib.getTableClass("opbd"))
2391def subset_glyphs(self, s):
2392    table = self.table.OpticalBounds
2393    if table.Format == 0:
2394        table.OpticalBoundsDeltas = {
2395            glyph: table.OpticalBoundsDeltas[glyph]
2396            for glyph in s.glyphs
2397            if glyph in table.OpticalBoundsDeltas
2398        }
2399        return len(table.OpticalBoundsDeltas) > 0
2400    elif table.Format == 1:
2401        table.OpticalBoundsPoints = {
2402            glyph: table.OpticalBoundsPoints[glyph]
2403            for glyph in s.glyphs
2404            if glyph in table.OpticalBoundsPoints
2405        }
2406        return len(table.OpticalBoundsPoints) > 0
2407    else:
2408        assert False, "unknown 'opbd' format %s" % table.Format
2409
2410
2411@_add_method(ttLib.getTableClass("post"))
2412def prune_pre_subset(self, font, options):
2413    if not options.glyph_names:
2414        self.formatType = 3.0
2415    return True  # Required table
2416
2417
2418@_add_method(ttLib.getTableClass("post"))
2419def subset_glyphs(self, s):
2420    self.extraNames = []  # This seems to do it
2421    return True  # Required table
2422
2423
2424@_add_method(ttLib.getTableClass("prop"))
2425def subset_glyphs(self, s):
2426    prop = self.table.GlyphProperties
2427    if prop.Format == 0:
2428        return prop.DefaultProperties != 0
2429    elif prop.Format == 1:
2430        prop.Properties = {
2431            g: prop.Properties.get(g, prop.DefaultProperties) for g in s.glyphs
2432        }
2433        mostCommon, _cnt = Counter(prop.Properties.values()).most_common(1)[0]
2434        prop.DefaultProperties = mostCommon
2435        prop.Properties = {
2436            g: prop for g, prop in prop.Properties.items() if prop != mostCommon
2437        }
2438        if len(prop.Properties) == 0:
2439            del prop.Properties
2440            prop.Format = 0
2441            return prop.DefaultProperties != 0
2442        return True
2443    else:
2444        assert False, "unknown 'prop' format %s" % prop.Format
2445
2446
2447def _paint_glyph_names(paint, colr):
2448    result = set()
2449
2450    def callback(paint):
2451        if paint.Format in {
2452            otTables.PaintFormat.PaintGlyph,
2453            otTables.PaintFormat.PaintColrGlyph,
2454        }:
2455            result.add(paint.Glyph)
2456
2457    paint.traverse(colr, callback)
2458    return result
2459
2460
2461@_add_method(ttLib.getTableClass("COLR"))
2462def closure_glyphs(self, s):
2463    if self.version > 0:
2464        # on decompiling COLRv1, we only keep around the raw otTables
2465        # but for subsetting we need dicts with fully decompiled layers;
2466        # we store them temporarily in the C_O_L_R_ instance and delete
2467        # them after we have finished subsetting.
2468        self.ColorLayers = self._decompileColorLayersV0(self.table)
2469        self.ColorLayersV1 = {
2470            rec.BaseGlyph: rec.Paint
2471            for rec in self.table.BaseGlyphList.BaseGlyphPaintRecord
2472        }
2473
2474    decompose = s.glyphs
2475    while decompose:
2476        layers = set()
2477        for g in decompose:
2478            for layer in self.ColorLayers.get(g, []):
2479                layers.add(layer.name)
2480
2481            if self.version > 0:
2482                paint = self.ColorLayersV1.get(g)
2483                if paint is not None:
2484                    layers.update(_paint_glyph_names(paint, self.table))
2485
2486        layers -= s.glyphs
2487        s.glyphs.update(layers)
2488        decompose = layers
2489
2490
2491@_add_method(ttLib.getTableClass("COLR"))
2492def subset_glyphs(self, s):
2493    from fontTools.colorLib.unbuilder import unbuildColrV1
2494    from fontTools.colorLib.builder import buildColrV1, populateCOLRv0
2495
2496    # only include glyphs after COLR closure, which in turn comes after cmap and GSUB
2497    # closure, but importantly before glyf/CFF closures. COLR layers can refer to
2498    # composite glyphs, and that's ok, since glyf/CFF closures happen after COLR closure
2499    # and take care of those. If we also included glyphs resulting from glyf/CFF closures
2500    # when deciding which COLR base glyphs to retain, then we may end up with a situation
2501    # whereby a COLR base glyph is kept, not because directly requested (cmap)
2502    # or substituted (GSUB) or referenced by another COLRv1 PaintColrGlyph, but because
2503    # it corresponds to (has same GID as) a non-COLR glyph that happens to be used as a
2504    # component in glyf or CFF table. Best case scenario we retain more glyphs than
2505    # required; worst case we retain incomplete COLR records that try to reference
2506    # glyphs that are no longer in the final subset font.
2507    # https://github.com/fonttools/fonttools/issues/2461
2508    s.glyphs = s.glyphs_colred
2509
2510    self.ColorLayers = {
2511        g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers
2512    }
2513    if self.version == 0:
2514        return bool(self.ColorLayers)
2515
2516    colorGlyphsV1 = unbuildColrV1(self.table.LayerList, self.table.BaseGlyphList)
2517    self.table.LayerList, self.table.BaseGlyphList = buildColrV1(
2518        {g: colorGlyphsV1[g] for g in colorGlyphsV1 if g in s.glyphs}
2519    )
2520    del self.ColorLayersV1
2521
2522    if self.table.ClipList is not None:
2523        clips = self.table.ClipList.clips
2524        self.table.ClipList.clips = {g: clips[g] for g in clips if g in s.glyphs}
2525
2526    layersV0 = self.ColorLayers
2527    if not self.table.BaseGlyphList.BaseGlyphPaintRecord:
2528        # no more COLRv1 glyphs: downgrade to version 0
2529        self.version = 0
2530        del self.table
2531        return bool(layersV0)
2532
2533    populateCOLRv0(
2534        self.table,
2535        {g: [(layer.name, layer.colorID) for layer in layersV0[g]] for g in layersV0},
2536    )
2537    del self.ColorLayers
2538
2539    # TODO: also prune ununsed varIndices in COLR.VarStore
2540    return True
2541
2542
2543@_add_method(ttLib.getTableClass("CPAL"))
2544def prune_post_subset(self, font, options):
2545    # Keep whole "CPAL" if "SVG " is present as it may be referenced by the latter
2546    # via 'var(--color{palette_entry_index}, ...)' CSS color variables.
2547    # For now we just assume this is the case by the mere presence of "SVG " table,
2548    # for parsing SVG to collect all the used indices is too much work...
2549    # TODO(anthrotype): Do The Right Thing (TM).
2550    if "SVG " in font:
2551        return True
2552
2553    colr = font.get("COLR")
2554    if not colr:  # drop CPAL if COLR was subsetted to empty
2555        return False
2556
2557    colors_by_index = defaultdict(list)
2558
2559    def collect_colors_by_index(paint):
2560        if hasattr(paint, "PaletteIndex"):  # either solid colors...
2561            colors_by_index[paint.PaletteIndex].append(paint)
2562        elif hasattr(paint, "ColorLine"):  # ... or gradient color stops
2563            for stop in paint.ColorLine.ColorStop:
2564                colors_by_index[stop.PaletteIndex].append(stop)
2565
2566    if colr.version == 0:
2567        for layers in colr.ColorLayers.values():
2568            for layer in layers:
2569                colors_by_index[layer.colorID].append(layer)
2570    else:
2571        if colr.table.LayerRecordArray:
2572            for layer in colr.table.LayerRecordArray.LayerRecord:
2573                colors_by_index[layer.PaletteIndex].append(layer)
2574        for record in colr.table.BaseGlyphList.BaseGlyphPaintRecord:
2575            record.Paint.traverse(colr.table, collect_colors_by_index)
2576
2577    # don't remap palette entry index 0xFFFF, this is always the foreground color
2578    # https://github.com/fonttools/fonttools/issues/2257
2579    retained_palette_indices = set(colors_by_index.keys()) - {0xFFFF}
2580    for palette in self.palettes:
2581        palette[:] = [c for i, c in enumerate(palette) if i in retained_palette_indices]
2582        assert len(palette) == len(retained_palette_indices)
2583
2584    for new_index, old_index in enumerate(sorted(retained_palette_indices)):
2585        for record in colors_by_index[old_index]:
2586            if hasattr(record, "colorID"):  # v0
2587                record.colorID = new_index
2588            elif hasattr(record, "PaletteIndex"):  # v1
2589                record.PaletteIndex = new_index
2590            else:
2591                raise AssertionError(record)
2592
2593    self.numPaletteEntries = len(self.palettes[0])
2594
2595    if self.version == 1:
2596        kept_labels = []
2597        for i, label in enumerate(self.paletteEntryLabels):
2598            if i in retained_palette_indices:
2599                kept_labels.append(label)
2600        self.paletteEntryLabels = kept_labels
2601    return bool(self.numPaletteEntries)
2602
2603
2604@_add_method(otTables.MathGlyphConstruction)
2605def closure_glyphs(self, glyphs):
2606    variants = set()
2607    for v in self.MathGlyphVariantRecord:
2608        variants.add(v.VariantGlyph)
2609    if self.GlyphAssembly:
2610        for p in self.GlyphAssembly.PartRecords:
2611            variants.add(p.glyph)
2612    return variants
2613
2614
2615@_add_method(otTables.MathVariants)
2616def closure_glyphs(self, s):
2617    glyphs = frozenset(s.glyphs)
2618    variants = set()
2619
2620    if self.VertGlyphCoverage:
2621        indices = self.VertGlyphCoverage.intersect(glyphs)
2622        for i in indices:
2623            variants.update(self.VertGlyphConstruction[i].closure_glyphs(glyphs))
2624
2625    if self.HorizGlyphCoverage:
2626        indices = self.HorizGlyphCoverage.intersect(glyphs)
2627        for i in indices:
2628            variants.update(self.HorizGlyphConstruction[i].closure_glyphs(glyphs))
2629
2630    s.glyphs.update(variants)
2631
2632
2633@_add_method(ttLib.getTableClass("MATH"))
2634def closure_glyphs(self, s):
2635    if self.table.MathVariants:
2636        self.table.MathVariants.closure_glyphs(s)
2637
2638
2639@_add_method(otTables.MathItalicsCorrectionInfo)
2640def subset_glyphs(self, s):
2641    indices = self.Coverage.subset(s.glyphs)
2642    self.ItalicsCorrection = _list_subset(self.ItalicsCorrection, indices)
2643    self.ItalicsCorrectionCount = len(self.ItalicsCorrection)
2644    return bool(self.ItalicsCorrectionCount)
2645
2646
2647@_add_method(otTables.MathTopAccentAttachment)
2648def subset_glyphs(self, s):
2649    indices = self.TopAccentCoverage.subset(s.glyphs)
2650    self.TopAccentAttachment = _list_subset(self.TopAccentAttachment, indices)
2651    self.TopAccentAttachmentCount = len(self.TopAccentAttachment)
2652    return bool(self.TopAccentAttachmentCount)
2653
2654
2655@_add_method(otTables.MathKernInfo)
2656def subset_glyphs(self, s):
2657    indices = self.MathKernCoverage.subset(s.glyphs)
2658    self.MathKernInfoRecords = _list_subset(self.MathKernInfoRecords, indices)
2659    self.MathKernCount = len(self.MathKernInfoRecords)
2660    return bool(self.MathKernCount)
2661
2662
2663@_add_method(otTables.MathGlyphInfo)
2664def subset_glyphs(self, s):
2665    if self.MathItalicsCorrectionInfo:
2666        self.MathItalicsCorrectionInfo.subset_glyphs(s)
2667    if self.MathTopAccentAttachment:
2668        self.MathTopAccentAttachment.subset_glyphs(s)
2669    if self.MathKernInfo:
2670        self.MathKernInfo.subset_glyphs(s)
2671    if self.ExtendedShapeCoverage:
2672        self.ExtendedShapeCoverage.subset(s.glyphs)
2673    return True
2674
2675
2676@_add_method(otTables.MathVariants)
2677def subset_glyphs(self, s):
2678    if self.VertGlyphCoverage:
2679        indices = self.VertGlyphCoverage.subset(s.glyphs)
2680        self.VertGlyphConstruction = _list_subset(self.VertGlyphConstruction, indices)
2681        self.VertGlyphCount = len(self.VertGlyphConstruction)
2682
2683    if self.HorizGlyphCoverage:
2684        indices = self.HorizGlyphCoverage.subset(s.glyphs)
2685        self.HorizGlyphConstruction = _list_subset(self.HorizGlyphConstruction, indices)
2686        self.HorizGlyphCount = len(self.HorizGlyphConstruction)
2687
2688    return True
2689
2690
2691@_add_method(ttLib.getTableClass("MATH"))
2692def subset_glyphs(self, s):
2693    s.glyphs = s.glyphs_mathed
2694    if self.table.MathGlyphInfo:
2695        self.table.MathGlyphInfo.subset_glyphs(s)
2696    if self.table.MathVariants:
2697        self.table.MathVariants.subset_glyphs(s)
2698    return True
2699
2700
2701@_add_method(ttLib.getTableModule("glyf").Glyph)
2702def remapComponentsFast(self, glyphidmap):
2703    if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
2704        return  # Not composite
2705    data = self.data = bytearray(self.data)
2706    i = 10
2707    more = 1
2708    while more:
2709        flags = (data[i] << 8) | data[i + 1]
2710        glyphID = (data[i + 2] << 8) | data[i + 3]
2711        # Remap
2712        glyphID = glyphidmap[glyphID]
2713        data[i + 2] = glyphID >> 8
2714        data[i + 3] = glyphID & 0xFF
2715        i += 4
2716        flags = int(flags)
2717
2718        if flags & 0x0001:
2719            i += 4  # ARG_1_AND_2_ARE_WORDS
2720        else:
2721            i += 2
2722        if flags & 0x0008:
2723            i += 2  # WE_HAVE_A_SCALE
2724        elif flags & 0x0040:
2725            i += 4  # WE_HAVE_AN_X_AND_Y_SCALE
2726        elif flags & 0x0080:
2727            i += 8  # WE_HAVE_A_TWO_BY_TWO
2728        more = flags & 0x0020  # MORE_COMPONENTS
2729
2730
2731@_add_method(ttLib.getTableClass("glyf"))
2732def closure_glyphs(self, s):
2733    glyphSet = self.glyphs
2734    decompose = s.glyphs
2735    while decompose:
2736        components = set()
2737        for g in decompose:
2738            if g not in glyphSet:
2739                continue
2740            gl = glyphSet[g]
2741            for c in gl.getComponentNames(self):
2742                components.add(c)
2743        components -= s.glyphs
2744        s.glyphs.update(components)
2745        decompose = components
2746
2747
2748@_add_method(ttLib.getTableClass("glyf"))
2749def prune_pre_subset(self, font, options):
2750    if options.notdef_glyph and not options.notdef_outline:
2751        g = self[self.glyphOrder[0]]
2752        # Yay, easy!
2753        g.__dict__.clear()
2754        g.data = b""
2755    return True
2756
2757
2758@_add_method(ttLib.getTableClass("glyf"))
2759def subset_glyphs(self, s):
2760    self.glyphs = _dict_subset(self.glyphs, s.glyphs)
2761    if not s.options.retain_gids:
2762        indices = [i for i, g in enumerate(self.glyphOrder) if g in s.glyphs]
2763        glyphmap = {o: n for n, o in enumerate(indices)}
2764        for v in self.glyphs.values():
2765            if hasattr(v, "data"):
2766                v.remapComponentsFast(glyphmap)
2767    Glyph = ttLib.getTableModule("glyf").Glyph
2768    for g in s.glyphs_emptied:
2769        self.glyphs[g] = Glyph()
2770        self.glyphs[g].data = b""
2771    self.glyphOrder = [
2772        g for g in self.glyphOrder if g in s.glyphs or g in s.glyphs_emptied
2773    ]
2774    # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset.
2775    return True
2776
2777
2778@_add_method(ttLib.getTableClass("glyf"))
2779def prune_post_subset(self, font, options):
2780    remove_hinting = not options.hinting
2781    for v in self.glyphs.values():
2782        v.trim(remove_hinting=remove_hinting)
2783    return True
2784
2785
2786@_add_method(ttLib.getTableClass("cmap"))
2787def closure_glyphs(self, s):
2788    tables = [t for t in self.tables if t.isUnicode()]
2789
2790    # Close glyphs
2791    for table in tables:
2792        if table.format == 14:
2793            for cmap in table.uvsDict.values():
2794                glyphs = {g for u, g in cmap if u in s.unicodes_requested}
2795                if None in glyphs:
2796                    glyphs.remove(None)
2797                s.glyphs.update(glyphs)
2798        else:
2799            cmap = table.cmap
2800            intersection = s.unicodes_requested.intersection(cmap.keys())
2801            s.glyphs.update(cmap[u] for u in intersection)
2802
2803    # Calculate unicodes_missing
2804    s.unicodes_missing = s.unicodes_requested.copy()
2805    for table in tables:
2806        s.unicodes_missing.difference_update(table.cmap)
2807
2808
2809@_add_method(ttLib.getTableClass("cmap"))
2810def prune_pre_subset(self, font, options):
2811    if not options.legacy_cmap:
2812        # Drop non-Unicode / non-Symbol cmaps
2813        self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()]
2814    if not options.symbol_cmap:
2815        self.tables = [t for t in self.tables if not t.isSymbol()]
2816    # TODO(behdad) Only keep one subtable?
2817    # For now, drop format=0 which can't be subset_glyphs easily?
2818    self.tables = [t for t in self.tables if t.format != 0]
2819    self.numSubTables = len(self.tables)
2820    return True  # Required table
2821
2822
2823@_add_method(ttLib.getTableClass("cmap"))
2824def subset_glyphs(self, s):
2825    s.glyphs = None  # We use s.glyphs_requested and s.unicodes_requested only
2826
2827    tables_format12_bmp = []
2828    table_plat0_enc3 = {}  # Unicode platform, Unicode BMP only, keyed by language
2829    table_plat3_enc1 = {}  # Windows platform, Unicode BMP, keyed by language
2830
2831    for t in self.tables:
2832        if t.platformID == 0 and t.platEncID == 3:
2833            table_plat0_enc3[t.language] = t
2834        if t.platformID == 3 and t.platEncID == 1:
2835            table_plat3_enc1[t.language] = t
2836
2837        if t.format == 14:
2838            # TODO(behdad) We drop all the default-UVS mappings
2839            # for glyphs_requested.  So it's the caller's responsibility to make
2840            # sure those are included.
2841            t.uvsDict = {
2842                v: [
2843                    (u, g)
2844                    for u, g in l
2845                    if g in s.glyphs_requested or u in s.unicodes_requested
2846                ]
2847                for v, l in t.uvsDict.items()
2848            }
2849            t.uvsDict = {v: l for v, l in t.uvsDict.items() if l}
2850        elif t.isUnicode():
2851            t.cmap = {
2852                u: g
2853                for u, g in t.cmap.items()
2854                if g in s.glyphs_requested or u in s.unicodes_requested
2855            }
2856            # Collect format 12 tables that hold only basic multilingual plane
2857            # codepoints.
2858            if t.format == 12 and t.cmap and max(t.cmap.keys()) < 0x10000:
2859                tables_format12_bmp.append(t)
2860        else:
2861            t.cmap = {u: g for u, g in t.cmap.items() if g in s.glyphs_requested}
2862
2863    # Fomat 12 tables are redundant if they contain just the same BMP codepoints
2864    # their little BMP-only encoding siblings contain.
2865    for t in tables_format12_bmp:
2866        if (
2867            t.platformID == 0  # Unicode platform
2868            and t.platEncID == 4  # Unicode full repertoire
2869            and t.language in table_plat0_enc3  # Have a BMP-only sibling?
2870            and table_plat0_enc3[t.language].cmap == t.cmap
2871        ):
2872            t.cmap.clear()
2873        elif (
2874            t.platformID == 3  # Windows platform
2875            and t.platEncID == 10  # Unicode full repertoire
2876            and t.language in table_plat3_enc1  # Have a BMP-only sibling?
2877            and table_plat3_enc1[t.language].cmap == t.cmap
2878        ):
2879            t.cmap.clear()
2880
2881    self.tables = [t for t in self.tables if (t.cmap if t.format != 14 else t.uvsDict)]
2882    self.numSubTables = len(self.tables)
2883    # TODO(behdad) Convert formats when needed.
2884    # In particular, if we have a format=12 without non-BMP
2885    # characters, convert it to format=4 if there's not one.
2886    return True  # Required table
2887
2888
2889@_add_method(ttLib.getTableClass("DSIG"))
2890def prune_pre_subset(self, font, options):
2891    # Drop all signatures since they will be invalid
2892    self.usNumSigs = 0
2893    self.signatureRecords = []
2894    return True
2895
2896
2897@_add_method(ttLib.getTableClass("maxp"))
2898def prune_pre_subset(self, font, options):
2899    if not options.hinting:
2900        if self.tableVersion == 0x00010000:
2901            self.maxZones = 1
2902            self.maxTwilightPoints = 0
2903            self.maxStorage = 0
2904            self.maxFunctionDefs = 0
2905            self.maxInstructionDefs = 0
2906            self.maxStackElements = 0
2907            self.maxSizeOfInstructions = 0
2908    return True
2909
2910
2911@_add_method(ttLib.getTableClass("name"))
2912def prune_post_subset(self, font, options):
2913    visitor = NameRecordVisitor()
2914    visitor.visit(font)
2915    nameIDs = set(options.name_IDs) | visitor.seen
2916    if "*" not in options.name_IDs:
2917        self.names = [n for n in self.names if n.nameID in nameIDs]
2918    if not options.name_legacy:
2919        # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman
2920        # entry for Latin and no Unicode names.
2921        self.names = [n for n in self.names if n.isUnicode()]
2922    # TODO(behdad) Option to keep only one platform's
2923    if "*" not in options.name_languages:
2924        # TODO(behdad) This is Windows-platform specific!
2925        self.names = [n for n in self.names if n.langID in options.name_languages]
2926    if options.obfuscate_names:
2927        namerecs = []
2928        for n in self.names:
2929            if n.nameID in [1, 4]:
2930                n.string = ".\x7f".encode("utf_16_be") if n.isUnicode() else ".\x7f"
2931            elif n.nameID in [2, 6]:
2932                n.string = "\x7f".encode("utf_16_be") if n.isUnicode() else "\x7f"
2933            elif n.nameID == 3:
2934                n.string = ""
2935            elif n.nameID in [16, 17, 18]:
2936                continue
2937            namerecs.append(n)
2938        self.names = namerecs
2939    return True  # Required table
2940
2941
2942@_add_method(ttLib.getTableClass("head"))
2943def prune_post_subset(self, font, options):
2944    # Force re-compiling head table, to update any recalculated values.
2945    return True
2946
2947
2948# TODO(behdad) OS/2 ulCodePageRange?
2949# TODO(behdad) Drop AAT tables.
2950# TODO(behdad) Drop unneeded GSUB/GPOS Script/LangSys entries.
2951# TODO(behdad) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left
2952# TODO(behdad) Drop GDEF subitems if unused by lookups
2953# TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF)
2954# TODO(behdad) Text direction considerations.
2955# TODO(behdad) Text script / language considerations.
2956# TODO(behdad) Optionally drop 'kern' table if GPOS available
2957# TODO(behdad) Implement --unicode='*' to choose all cmap'ed
2958# TODO(behdad) Drop old-spec Indic scripts
2959
2960
2961class Options(object):
2962    class OptionError(Exception):
2963        pass
2964
2965    class UnknownOptionError(OptionError):
2966        pass
2967
2968    # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser
2969    _drop_tables_default = [
2970        "BASE",
2971        "JSTF",
2972        "DSIG",
2973        "EBDT",
2974        "EBLC",
2975        "EBSC",
2976        "PCLT",
2977        "LTSH",
2978    ]
2979    _drop_tables_default += ["Feat", "Glat", "Gloc", "Silf", "Sill"]  # Graphite
2980    _no_subset_tables_default = [
2981        "avar",
2982        "fvar",
2983        "gasp",
2984        "head",
2985        "hhea",
2986        "maxp",
2987        "vhea",
2988        "OS/2",
2989        "loca",
2990        "name",
2991        "cvt",
2992        "fpgm",
2993        "prep",
2994        "VDMX",
2995        "DSIG",
2996        "CPAL",
2997        "MVAR",
2998        "cvar",
2999        "STAT",
3000    ]
3001    _hinting_tables_default = ["cvt", "cvar", "fpgm", "prep", "hdmx", "VDMX"]
3002
3003    # Based on HarfBuzz shapers
3004    _layout_features_groups = {
3005        # Default shaper
3006        "common": ["rvrn", "ccmp", "liga", "locl", "mark", "mkmk", "rlig"],
3007        "fractions": ["frac", "numr", "dnom"],
3008        "horizontal": ["calt", "clig", "curs", "kern", "rclt"],
3009        "vertical": ["valt", "vert", "vkrn", "vpal", "vrt2"],
3010        "ltr": ["ltra", "ltrm"],
3011        "rtl": ["rtla", "rtlm"],
3012        "rand": ["rand"],
3013        "justify": ["jalt"],
3014        "private": ["Harf", "HARF", "Buzz", "BUZZ"],
3015        "east_asian_spacing": ["chws", "vchw", "halt", "vhal"],
3016        # Complex shapers
3017        "arabic": [
3018            "init",
3019            "medi",
3020            "fina",
3021            "isol",
3022            "med2",
3023            "fin2",
3024            "fin3",
3025            "cswh",
3026            "mset",
3027            "stch",
3028        ],
3029        "hangul": ["ljmo", "vjmo", "tjmo"],
3030        "tibetan": ["abvs", "blws", "abvm", "blwm"],
3031        "indic": [
3032            "nukt",
3033            "akhn",
3034            "rphf",
3035            "rkrf",
3036            "pref",
3037            "blwf",
3038            "half",
3039            "abvf",
3040            "pstf",
3041            "cfar",
3042            "vatu",
3043            "cjct",
3044            "init",
3045            "pres",
3046            "abvs",
3047            "blws",
3048            "psts",
3049            "haln",
3050            "dist",
3051            "abvm",
3052            "blwm",
3053        ],
3054    }
3055    _layout_features_default = _uniq_sort(
3056        sum(iter(_layout_features_groups.values()), [])
3057    )
3058
3059    def __init__(self, **kwargs):
3060        self.drop_tables = self._drop_tables_default[:]
3061        self.no_subset_tables = self._no_subset_tables_default[:]
3062        self.passthrough_tables = False  # keep/drop tables we can't subset
3063        self.hinting_tables = self._hinting_tables_default[:]
3064        self.legacy_kern = False  # drop 'kern' table if GPOS available
3065        self.layout_closure = True
3066        self.layout_features = self._layout_features_default[:]
3067        self.layout_scripts = ["*"]
3068        self.ignore_missing_glyphs = False
3069        self.ignore_missing_unicodes = True
3070        self.hinting = True
3071        self.glyph_names = False
3072        self.legacy_cmap = False
3073        self.symbol_cmap = False
3074        self.name_IDs = [
3075            0,
3076            1,
3077            2,
3078            3,
3079            4,
3080            5,
3081            6,
3082        ]  # https://github.com/fonttools/fonttools/issues/1170#issuecomment-364631225
3083        self.name_legacy = False
3084        self.name_languages = [0x0409]  # English
3085        self.obfuscate_names = False  # to make webfont unusable as a system font
3086        self.retain_gids = False
3087        self.notdef_glyph = True  # gid0 for TrueType / .notdef for CFF
3088        self.notdef_outline = False  # No need for notdef to have an outline really
3089        self.recommended_glyphs = False  # gid1, gid2, gid3 for TrueType
3090        self.recalc_bounds = False  # Recalculate font bounding boxes
3091        self.recalc_timestamp = False  # Recalculate font modified timestamp
3092        self.prune_unicode_ranges = True  # Clear unused 'ulUnicodeRange' bits
3093        self.prune_codepage_ranges = True  # Clear unused 'ulCodePageRange' bits
3094        self.recalc_average_width = False  # update 'xAvgCharWidth'
3095        self.recalc_max_context = False  # update 'usMaxContext'
3096        self.canonical_order = None  # Order tables as recommended
3097        self.flavor = None  # May be 'woff' or 'woff2'
3098        self.with_zopfli = False  # use zopfli instead of zlib for WOFF 1.0
3099        self.desubroutinize = False  # Desubroutinize CFF CharStrings
3100        self.harfbuzz_repacker = USE_HARFBUZZ_REPACKER.default
3101        self.verbose = False
3102        self.timing = False
3103        self.xml = False
3104        self.font_number = -1
3105        self.pretty_svg = False
3106        self.lazy = True
3107
3108        self.set(**kwargs)
3109
3110    def set(self, **kwargs):
3111        for k, v in kwargs.items():
3112            if not hasattr(self, k):
3113                raise self.UnknownOptionError("Unknown option '%s'" % k)
3114            setattr(self, k, v)
3115
3116    def parse_opts(self, argv, ignore_unknown=[]):
3117        posargs = []
3118        passthru_options = []
3119        for a in argv:
3120            orig_a = a
3121            if not a.startswith("--"):
3122                posargs.append(a)
3123                continue
3124            a = a[2:]
3125            i = a.find("=")
3126            op = "="
3127            if i == -1:
3128                if a.startswith("no-"):
3129                    k = a[3:]
3130                    if k == "canonical-order":
3131                        # reorderTables=None is faster than False (the latter
3132                        # still reorders to "keep" the original table order)
3133                        v = None
3134                    else:
3135                        v = False
3136                else:
3137                    k = a
3138                    v = True
3139                if k.endswith("?"):
3140                    k = k[:-1]
3141                    v = "?"
3142            else:
3143                k = a[:i]
3144                if k[-1] in "-+":
3145                    op = k[-1] + "="  # Op is '-=' or '+=' now.
3146                    k = k[:-1]
3147                v = a[i + 1 :]
3148            ok = k
3149            k = k.replace("-", "_")
3150            if not hasattr(self, k):
3151                if ignore_unknown is True or ok in ignore_unknown:
3152                    passthru_options.append(orig_a)
3153                    continue
3154                else:
3155                    raise self.UnknownOptionError("Unknown option '%s'" % a)
3156
3157            ov = getattr(self, k)
3158            if v == "?":
3159                print("Current setting for '%s' is: %s" % (ok, ov))
3160                continue
3161            if isinstance(ov, bool):
3162                v = bool(v)
3163            elif isinstance(ov, int):
3164                v = int(v)
3165            elif isinstance(ov, str):
3166                v = str(v)  # redundant
3167            elif isinstance(ov, list):
3168                if isinstance(v, bool):
3169                    raise self.OptionError(
3170                        "Option '%s' requires values to be specified using '='" % a
3171                    )
3172                vv = v.replace(",", " ").split()
3173                if vv == [""]:
3174                    vv = []
3175                vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
3176                if op == "=":
3177                    v = vv
3178                elif op == "+=":
3179                    v = ov
3180                    v.extend(vv)
3181                elif op == "-=":
3182                    v = ov
3183                    for x in vv:
3184                        if x in v:
3185                            v.remove(x)
3186                else:
3187                    assert False
3188
3189            setattr(self, k, v)
3190
3191        return posargs + passthru_options
3192
3193
3194class Subsetter(object):
3195    class SubsettingError(Exception):
3196        pass
3197
3198    class MissingGlyphsSubsettingError(SubsettingError):
3199        pass
3200
3201    class MissingUnicodesSubsettingError(SubsettingError):
3202        pass
3203
3204    def __init__(self, options=None):
3205        if not options:
3206            options = Options()
3207
3208        self.options = options
3209        self.unicodes_requested = set()
3210        self.glyph_names_requested = set()
3211        self.glyph_ids_requested = set()
3212
3213    def populate(self, glyphs=[], gids=[], unicodes=[], text=""):
3214        self.unicodes_requested.update(unicodes)
3215        if isinstance(text, bytes):
3216            text = text.decode("utf_8")
3217        text_utf32 = text.encode("utf-32-be")
3218        nchars = len(text_utf32) // 4
3219        for u in struct.unpack(">%dL" % nchars, text_utf32):
3220            self.unicodes_requested.add(u)
3221        self.glyph_names_requested.update(glyphs)
3222        self.glyph_ids_requested.update(gids)
3223
3224    def _prune_pre_subset(self, font):
3225        for tag in self._sort_tables(font):
3226            if (
3227                tag.strip() in self.options.drop_tables
3228                or (
3229                    tag.strip() in self.options.hinting_tables
3230                    and not self.options.hinting
3231                )
3232                or (tag == "kern" and (not self.options.legacy_kern and "GPOS" in font))
3233            ):
3234                log.info("%s dropped", tag)
3235                del font[tag]
3236                continue
3237
3238            clazz = ttLib.getTableClass(tag)
3239
3240            if hasattr(clazz, "prune_pre_subset"):
3241                with timer("load '%s'" % tag):
3242                    table = font[tag]
3243                with timer("prune '%s'" % tag):
3244                    retain = table.prune_pre_subset(font, self.options)
3245                if not retain:
3246                    log.info("%s pruned to empty; dropped", tag)
3247                    del font[tag]
3248                    continue
3249                else:
3250                    log.info("%s pruned", tag)
3251
3252    def _closure_glyphs(self, font):
3253        realGlyphs = set(font.getGlyphOrder())
3254        self.orig_glyph_order = glyph_order = font.getGlyphOrder()
3255
3256        self.glyphs_requested = set()
3257        self.glyphs_requested.update(self.glyph_names_requested)
3258        self.glyphs_requested.update(
3259            glyph_order[i] for i in self.glyph_ids_requested if i < len(glyph_order)
3260        )
3261
3262        self.glyphs_missing = set()
3263        self.glyphs_missing.update(self.glyphs_requested.difference(realGlyphs))
3264        self.glyphs_missing.update(
3265            i for i in self.glyph_ids_requested if i >= len(glyph_order)
3266        )
3267        if self.glyphs_missing:
3268            log.info("Missing requested glyphs: %s", self.glyphs_missing)
3269            if not self.options.ignore_missing_glyphs:
3270                raise self.MissingGlyphsSubsettingError(self.glyphs_missing)
3271
3272        self.glyphs = self.glyphs_requested.copy()
3273
3274        self.unicodes_missing = set()
3275        if "cmap" in font:
3276            with timer("close glyph list over 'cmap'"):
3277                font["cmap"].closure_glyphs(self)
3278                self.glyphs.intersection_update(realGlyphs)
3279        self.glyphs_cmaped = frozenset(self.glyphs)
3280        if self.unicodes_missing:
3281            missing = ["U+%04X" % u for u in self.unicodes_missing]
3282            log.info("Missing glyphs for requested Unicodes: %s", missing)
3283            if not self.options.ignore_missing_unicodes:
3284                raise self.MissingUnicodesSubsettingError(missing)
3285            del missing
3286
3287        if self.options.notdef_glyph:
3288            if "glyf" in font:
3289                self.glyphs.add(font.getGlyphName(0))
3290                log.info("Added gid0 to subset")
3291            else:
3292                self.glyphs.add(".notdef")
3293                log.info("Added .notdef to subset")
3294        if self.options.recommended_glyphs:
3295            if "glyf" in font:
3296                for i in range(min(4, len(font.getGlyphOrder()))):
3297                    self.glyphs.add(font.getGlyphName(i))
3298                log.info("Added first four glyphs to subset")
3299
3300        if self.options.layout_closure and "GSUB" in font:
3301            with timer("close glyph list over 'GSUB'"):
3302                log.info(
3303                    "Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs)
3304                )
3305                log.glyphs(self.glyphs, font=font)
3306                font["GSUB"].closure_glyphs(self)
3307                self.glyphs.intersection_update(realGlyphs)
3308                log.info(
3309                    "Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs)
3310                )
3311                log.glyphs(self.glyphs, font=font)
3312        self.glyphs_gsubed = frozenset(self.glyphs)
3313
3314        if "MATH" in font:
3315            with timer("close glyph list over 'MATH'"):
3316                log.info(
3317                    "Closing glyph list over 'MATH': %d glyphs before", len(self.glyphs)
3318                )
3319                log.glyphs(self.glyphs, font=font)
3320                font["MATH"].closure_glyphs(self)
3321                self.glyphs.intersection_update(realGlyphs)
3322                log.info(
3323                    "Closed glyph list over 'MATH': %d glyphs after", len(self.glyphs)
3324                )
3325                log.glyphs(self.glyphs, font=font)
3326        self.glyphs_mathed = frozenset(self.glyphs)
3327
3328        for table in ("COLR", "bsln"):
3329            if table in font:
3330                with timer("close glyph list over '%s'" % table):
3331                    log.info(
3332                        "Closing glyph list over '%s': %d glyphs before",
3333                        table,
3334                        len(self.glyphs),
3335                    )
3336                    log.glyphs(self.glyphs, font=font)
3337                    font[table].closure_glyphs(self)
3338                    self.glyphs.intersection_update(realGlyphs)
3339                    log.info(
3340                        "Closed glyph list over '%s': %d glyphs after",
3341                        table,
3342                        len(self.glyphs),
3343                    )
3344                    log.glyphs(self.glyphs, font=font)
3345            setattr(self, f"glyphs_{table.lower()}ed", frozenset(self.glyphs))
3346
3347        if "glyf" in font:
3348            with timer("close glyph list over 'glyf'"):
3349                log.info(
3350                    "Closing glyph list over 'glyf': %d glyphs before", len(self.glyphs)
3351                )
3352                log.glyphs(self.glyphs, font=font)
3353                font["glyf"].closure_glyphs(self)
3354                self.glyphs.intersection_update(realGlyphs)
3355                log.info(
3356                    "Closed glyph list over 'glyf': %d glyphs after", len(self.glyphs)
3357                )
3358                log.glyphs(self.glyphs, font=font)
3359        self.glyphs_glyfed = frozenset(self.glyphs)
3360
3361        if "CFF " in font:
3362            with timer("close glyph list over 'CFF '"):
3363                log.info(
3364                    "Closing glyph list over 'CFF ': %d glyphs before", len(self.glyphs)
3365                )
3366                log.glyphs(self.glyphs, font=font)
3367                font["CFF "].closure_glyphs(self)
3368                self.glyphs.intersection_update(realGlyphs)
3369                log.info(
3370                    "Closed glyph list over 'CFF ': %d glyphs after", len(self.glyphs)
3371                )
3372                log.glyphs(self.glyphs, font=font)
3373        self.glyphs_cffed = frozenset(self.glyphs)
3374
3375        self.glyphs_retained = frozenset(self.glyphs)
3376
3377        order = font.getReverseGlyphMap()
3378        self.reverseOrigGlyphMap = {g: order[g] for g in self.glyphs_retained}
3379
3380        self.last_retained_order = max(self.reverseOrigGlyphMap.values())
3381        self.last_retained_glyph = font.getGlyphOrder()[self.last_retained_order]
3382
3383        self.glyphs_emptied = frozenset()
3384        if self.options.retain_gids:
3385            self.glyphs_emptied = {
3386                g
3387                for g in realGlyphs - self.glyphs_retained
3388                if order[g] <= self.last_retained_order
3389            }
3390
3391        self.reverseEmptiedGlyphMap = {g: order[g] for g in self.glyphs_emptied}
3392
3393        if not self.options.retain_gids:
3394            new_glyph_order = [g for g in glyph_order if g in self.glyphs_retained]
3395        else:
3396            new_glyph_order = [
3397                g for g in glyph_order if font.getGlyphID(g) <= self.last_retained_order
3398            ]
3399        # We'll call font.setGlyphOrder() at the end of _subset_glyphs when all
3400        # tables have been subsetted. Below, we use the new glyph order to get
3401        # a map from old to new glyph indices, which can be useful when
3402        # subsetting individual tables (e.g. SVG) that refer to GIDs.
3403        self.new_glyph_order = new_glyph_order
3404        self.glyph_index_map = {
3405            order[new_glyph_order[i]]: i for i in range(len(new_glyph_order))
3406        }
3407
3408        log.info("Retaining %d glyphs", len(self.glyphs_retained))
3409
3410        del self.glyphs
3411
3412    def _subset_glyphs(self, font):
3413        self.used_mark_sets = []
3414        for tag in self._sort_tables(font):
3415            clazz = ttLib.getTableClass(tag)
3416
3417            if tag.strip() in self.options.no_subset_tables:
3418                log.info("%s subsetting not needed", tag)
3419            elif hasattr(clazz, "subset_glyphs"):
3420                with timer("subset '%s'" % tag):
3421                    table = font[tag]
3422                    self.glyphs = self.glyphs_retained
3423                    retain = table.subset_glyphs(self)
3424                    del self.glyphs
3425                if not retain:
3426                    log.info("%s subsetted to empty; dropped", tag)
3427                    del font[tag]
3428                else:
3429                    log.info("%s subsetted", tag)
3430            elif self.options.passthrough_tables:
3431                log.info("%s NOT subset; don't know how to subset", tag)
3432            else:
3433                log.warning("%s NOT subset; don't know how to subset; dropped", tag)
3434                del font[tag]
3435
3436        with timer("subset GlyphOrder"):
3437            font.setGlyphOrder(self.new_glyph_order)
3438
3439    def _prune_post_subset(self, font):
3440        tableTags = font.keys()
3441        # Prune the name table last because when we're pruning the name table,
3442        # we visit each table in the font to see what name table records are
3443        # still in use.
3444        if "name" in tableTags:
3445            tableTags.remove("name")
3446            tableTags.append("name")
3447        for tag in tableTags:
3448            if tag == "GlyphOrder":
3449                continue
3450            if tag == "OS/2":
3451                if self.options.prune_unicode_ranges:
3452                    old_uniranges = font[tag].getUnicodeRanges()
3453                    new_uniranges = font[tag].recalcUnicodeRanges(font, pruneOnly=True)
3454                    if old_uniranges != new_uniranges:
3455                        log.info(
3456                            "%s Unicode ranges pruned: %s", tag, sorted(new_uniranges)
3457                        )
3458                if self.options.prune_codepage_ranges and font[tag].version >= 1:
3459                    # codepage range fields were added with OS/2 format 1
3460                    # https://learn.microsoft.com/en-us/typography/opentype/spec/os2#version-1
3461                    old_codepages = font[tag].getCodePageRanges()
3462                    new_codepages = font[tag].recalcCodePageRanges(font, pruneOnly=True)
3463                    if old_codepages != new_codepages:
3464                        log.info(
3465                            "%s CodePage ranges pruned: %s",
3466                            tag,
3467                            sorted(new_codepages),
3468                        )
3469                if self.options.recalc_average_width:
3470                    old_avg_width = font[tag].xAvgCharWidth
3471                    new_avg_width = font[tag].recalcAvgCharWidth(font)
3472                    if old_avg_width != new_avg_width:
3473                        log.info("%s xAvgCharWidth updated: %d", tag, new_avg_width)
3474                if self.options.recalc_max_context:
3475                    max_context = maxCtxFont(font)
3476                    if max_context != font[tag].usMaxContext:
3477                        font[tag].usMaxContext = max_context
3478                        log.info("%s usMaxContext updated: %d", tag, max_context)
3479            clazz = ttLib.getTableClass(tag)
3480            if hasattr(clazz, "prune_post_subset"):
3481                with timer("prune '%s'" % tag):
3482                    table = font[tag]
3483                    retain = table.prune_post_subset(font, self.options)
3484                if not retain:
3485                    log.info("%s pruned to empty; dropped", tag)
3486                    del font[tag]
3487                else:
3488                    log.info("%s pruned", tag)
3489
3490    def _sort_tables(self, font):
3491        tagOrder = ["GDEF", "GPOS", "GSUB", "fvar", "avar", "gvar", "name", "glyf"]
3492        tagOrder = {t: i + 1 for i, t in enumerate(tagOrder)}
3493        tags = sorted(font.keys(), key=lambda tag: tagOrder.get(tag, 0))
3494        return [t for t in tags if t != "GlyphOrder"]
3495
3496    def subset(self, font):
3497        self._prune_pre_subset(font)
3498        self._closure_glyphs(font)
3499        self._subset_glyphs(font)
3500        self._prune_post_subset(font)
3501
3502
3503@timer("load font")
3504def load_font(fontFile, options, checkChecksums=0, dontLoadGlyphNames=False, lazy=True):
3505    font = ttLib.TTFont(
3506        fontFile,
3507        checkChecksums=checkChecksums,
3508        recalcBBoxes=options.recalc_bounds,
3509        recalcTimestamp=options.recalc_timestamp,
3510        lazy=lazy,
3511        fontNumber=options.font_number,
3512    )
3513
3514    # Hack:
3515    #
3516    # If we don't need glyph names, change 'post' class to not try to
3517    # load them.	It avoid lots of headache with broken fonts as well
3518    # as loading time.
3519    #
3520    # Ideally ttLib should provide a way to ask it to skip loading
3521    # glyph names.	But it currently doesn't provide such a thing.
3522    #
3523    if dontLoadGlyphNames:
3524        post = ttLib.getTableClass("post")
3525        saved = post.decode_format_2_0
3526        post.decode_format_2_0 = post.decode_format_3_0
3527        f = font["post"]
3528        if f.formatType == 2.0:
3529            f.formatType = 3.0
3530        post.decode_format_2_0 = saved
3531
3532    return font
3533
3534
3535@timer("compile and save font")
3536def save_font(font, outfile, options):
3537    if options.with_zopfli and options.flavor == "woff":
3538        from fontTools.ttLib import sfnt
3539
3540        sfnt.USE_ZOPFLI = True
3541    font.flavor = options.flavor
3542    font.cfg[USE_HARFBUZZ_REPACKER] = options.harfbuzz_repacker
3543    font.save(outfile, reorderTables=options.canonical_order)
3544
3545
3546def parse_unicodes(s):
3547    import re
3548
3549    s = re.sub(r"0[xX]", " ", s)
3550    s = re.sub(r"[<+>,;&#\\xXuU\n	]", " ", s)
3551    l = []
3552    for item in s.split():
3553        fields = item.split("-")
3554        if len(fields) == 1:
3555            l.append(int(item, 16))
3556        else:
3557            start, end = fields
3558            l.extend(range(int(start, 16), int(end, 16) + 1))
3559    return l
3560
3561
3562def parse_gids(s):
3563    l = []
3564    for item in s.replace(",", " ").split():
3565        fields = item.split("-")
3566        if len(fields) == 1:
3567            l.append(int(fields[0]))
3568        else:
3569            l.extend(range(int(fields[0]), int(fields[1]) + 1))
3570    return l
3571
3572
3573def parse_glyphs(s):
3574    return s.replace(",", " ").split()
3575
3576
3577def usage():
3578    print("usage:", __usage__, file=sys.stderr)
3579    print("Try pyftsubset --help for more information.\n", file=sys.stderr)
3580
3581
3582@timer("make one with everything (TOTAL TIME)")
3583def main(args=None):
3584    """OpenType font subsetter and optimizer"""
3585    from os.path import splitext
3586    from fontTools import configLogger
3587
3588    if args is None:
3589        args = sys.argv[1:]
3590
3591    if "--help" in args:
3592        print(__doc__)
3593        return 0
3594
3595    options = Options()
3596    try:
3597        args = options.parse_opts(
3598            args,
3599            ignore_unknown=[
3600                "gids",
3601                "gids-file",
3602                "glyphs",
3603                "glyphs-file",
3604                "text",
3605                "text-file",
3606                "unicodes",
3607                "unicodes-file",
3608                "output-file",
3609            ],
3610        )
3611    except options.OptionError as e:
3612        usage()
3613        print("ERROR:", e, file=sys.stderr)
3614        return 2
3615
3616    if len(args) < 2:
3617        usage()
3618        return 1
3619
3620    configLogger(level=logging.INFO if options.verbose else logging.WARNING)
3621    if options.timing:
3622        timer.logger.setLevel(logging.DEBUG)
3623    else:
3624        timer.logger.disabled = True
3625
3626    fontfile = args[0]
3627    args = args[1:]
3628
3629    subsetter = Subsetter(options=options)
3630    outfile = None
3631    glyphs = []
3632    gids = []
3633    unicodes = []
3634    wildcard_glyphs = False
3635    wildcard_unicodes = False
3636    text = ""
3637    for g in args:
3638        if g == "*":
3639            wildcard_glyphs = True
3640            continue
3641        if g.startswith("--output-file="):
3642            outfile = g[14:]
3643            continue
3644        if g.startswith("--text="):
3645            text += g[7:]
3646            continue
3647        if g.startswith("--text-file="):
3648            with open(g[12:], encoding="utf-8") as f:
3649                text += f.read().replace("\n", "")
3650            continue
3651        if g.startswith("--unicodes="):
3652            if g[11:] == "*":
3653                wildcard_unicodes = True
3654            else:
3655                unicodes.extend(parse_unicodes(g[11:]))
3656            continue
3657        if g.startswith("--unicodes-file="):
3658            with open(g[16:]) as f:
3659                for line in f.readlines():
3660                    unicodes.extend(parse_unicodes(line.split("#")[0]))
3661            continue
3662        if g.startswith("--gids="):
3663            gids.extend(parse_gids(g[7:]))
3664            continue
3665        if g.startswith("--gids-file="):
3666            with open(g[12:]) as f:
3667                for line in f.readlines():
3668                    gids.extend(parse_gids(line.split("#")[0]))
3669            continue
3670        if g.startswith("--glyphs="):
3671            if g[9:] == "*":
3672                wildcard_glyphs = True
3673            else:
3674                glyphs.extend(parse_glyphs(g[9:]))
3675            continue
3676        if g.startswith("--glyphs-file="):
3677            with open(g[14:]) as f:
3678                for line in f.readlines():
3679                    glyphs.extend(parse_glyphs(line.split("#")[0]))
3680            continue
3681        glyphs.append(g)
3682
3683    dontLoadGlyphNames = not options.glyph_names and not glyphs
3684    lazy = options.lazy
3685    font = load_font(
3686        fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames, lazy=lazy
3687    )
3688
3689    if outfile is None:
3690        ext = "." + options.flavor.lower() if options.flavor is not None else None
3691        outfile = makeOutputFileName(
3692            fontfile, extension=ext, overWrite=True, suffix=".subset"
3693        )
3694
3695    with timer("compile glyph list"):
3696        if wildcard_glyphs:
3697            glyphs.extend(font.getGlyphOrder())
3698        if wildcard_unicodes:
3699            for t in font["cmap"].tables:
3700                if t.isUnicode():
3701                    unicodes.extend(t.cmap.keys())
3702        assert "" not in glyphs
3703
3704    log.info("Text: '%s'" % text)
3705    log.info("Unicodes: %s", unicodes)
3706    log.info("Glyphs: %s", glyphs)
3707    log.info("Gids: %s", gids)
3708
3709    subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text)
3710    subsetter.subset(font)
3711
3712    save_font(font, outfile, options)
3713
3714    if options.verbose:
3715        import os
3716
3717        log.info("Input font:% 7d bytes: %s" % (os.path.getsize(fontfile), fontfile))
3718        log.info("Subset font:% 7d bytes: %s" % (os.path.getsize(outfile), outfile))
3719
3720    if options.xml:
3721        font.saveXML(sys.stdout)
3722
3723    font.close()
3724
3725
3726__all__ = [
3727    "Options",
3728    "Subsetter",
3729    "load_font",
3730    "save_font",
3731    "parse_gids",
3732    "parse_glyphs",
3733    "parse_unicodes",
3734    "main",
3735]
3736