Coverage for /usr/lib/python3/dist-packages/fontTools/subset/__init__.py: 17%
2025 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod
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
27__usage__ = "pyftsubset font-file [glyph...] [--option=value]..."
29__doc__ = (
30 """\
31pyftsubset -- OpenType font subsetter and optimizer
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.
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.
42Usage: """
43 + __usage__
44 + """
46At least one glyph or one of --gids, --gids-file, --glyphs, --glyphs-file,
47--text, --text-file, --unicodes, or --unicodes-file, must be specified.
49Args:
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.
57Initial glyph set specification
58^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
60These options populate the initial glyph set. Same option can appear
61multiple times, and the results are accummulated.
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.
68--gids-file=<path>
69 Like --gids but reads from a file. Anything after a '#' on any line is
70 ignored as comments.
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.
78--glyphs-file=<path>
79 Like --glyphs but reads from a file. Anything after a '#' on any line
80 is ignored as comments.
82--text=<text>
83 Specify characters to include in the subset, as UTF-8 string.
85--text-file=<path>
86 Like --text but reads from a file. Newline character are not added to
87 the subset.
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.
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.
101--ignore-missing-glyphs
102 Do not fail if some requested glyphs or gids are not available in
103 the font.
105--no-ignore-missing-glyphs
106 Stop and fail if some requested glyphs or gids are not available
107 in the font. [default]
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.
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.
121Other options
122^^^^^^^^^^^^^
124For the other options listed below, to see the current value of the option,
125pass a value of '?' to it, with or without a '='.
127Examples::
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
137Output options
138^^^^^^^^^^^^^^
140--output-file=<path>
141 The output font file. If not specified, the subsetted font
142 will be saved in as font-file.subset.
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
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
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).
161--no-harfbuzz-repacker
162 Always use the pure-python serializer even if uharfbuzz is available.
164Glyph set expansion
165^^^^^^^^^^^^^^^^^^^
167These options control how additional glyphs are added to the subset.
169--retain-gids
170 Retain glyph indices; just empty glyphs not needed in-place.
172--notdef-glyph
173 Add the '.notdef' glyph to the subset (ie, keep it). [default]
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.
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.
187--no-notdef-outline
188 When including a '.notdef' glyph, remove its outline. This saves
189 a few bytes. [default]
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.
196--no-recommended-glyphs
197 Do not add glyphs 0, 1, 2, and 3 to the subset, unless specified in
198 glyph set. [default]
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.
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:
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'.
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 ('*').
236Hinting options
237^^^^^^^^^^^^^^^
239--hinting
240 Keep hinting [default]
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.
251Optimization options
252^^^^^^^^^^^^^^^^^^^^
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.
261--no-desubroutinize [default]
262 Leave CFF subroutinizes as is, only throw away unused subroutinizes.
264Font table options
265^^^^^^^^^^^^^^^^^^
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.
275 Examples:
277 --drop-tables-=BASE
278 * Drop the default set of tables but keep 'BASE'.
280 --drop-tables+=GSUB
281 * Drop the default set of tables and 'GSUB'.
283 --drop-tables=DSIG
284 * Only drop the 'DSIG' table, keep all others.
286 --drop-tables=
287 * Keep all tables.
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.
299 Example:
301 --no-subset-tables+=FFTM
302 * Keep 'FFTM' table in the font by preventing subsetting.
304--passthrough-tables
305 Do not drop tables that the tool does not know how to subset.
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]
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.
315 Examples:
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).
322--legacy-kern
323 Keep TrueType 'kern' table even when OpenType 'GPOS' is available.
325--no-legacy-kern
326 Drop TrueType 'kern' table if OpenType 'GPOS' is available. [default]
328Font naming options
329^^^^^^^^^^^^^^^^^^^
331These options control what is retained in the 'name' table. For numerical
332codes, see: http://www.microsoft.com/typography/otspec/name.htm
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.
339 Examples:
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
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
353--no-name-legacy
354 Drop legacy (non-Unicode) 'name' table entries [default]
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.
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).
365Glyph naming and encoding options
366^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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]
385Other font-specific options
386^^^^^^^^^^^^^^^^^^^^^^^^^^^
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).
428Application options
429^^^^^^^^^^^^^^^^^^^
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.
438Example
439^^^^^^^
441Produce a subset containing the characters ' !"#$%' without performing
442size-reducing optimizations::
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)
452log = logging.getLogger("fontTools.subset")
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))
462# bind "glyphs" function to 'log' object
463log.glyphs = MethodType(_log_glyphs, log)
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"))
470def _dict_subset(d, glyphs):
471 return {g: d[g] for g in glyphs}
474def _list_subset(l, indices):
475 count = len(l)
476 return [l[i] for i in indices if i < count]
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]
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)
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
499@_add_method(otTables.Coverage)
500def remap(self, coverage_map):
501 """Remaps coverage."""
502 self.glyphs = [self.glyphs[i] for i in coverage_map]
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 )
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)
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
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()}
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)
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)
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)
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)
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))
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)
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 )
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)
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
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
654@_add_method(otTables.Device)
655def is_hinting(self):
656 return self.DeltaFormat in (1, 2, 3)
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)
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
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
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 )
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
711 return True
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
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]
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()
778 return bool(self.ValueFormat1 | self.ValueFormat2)
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
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
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
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
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
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
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
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
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
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
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 []
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
995@_add_method(
996 otTables.SingleSubst, otTables.AlternateSubst, otTables.ReverseChainSingleSubst
997)
998def may_have_non_1to1(self):
999 return False
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
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
1037 self.Typ = Typ
1038 self.Type = Type
1039 self.Chain = Chain
1040 self.ChainTyp = ChainTyp
1041 self.InputIdx = InputIdx
1042 self.DataLen = DataLen
1044 self.LookupRecord = Type + "LookupRecord"
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)
1056 def SetRuleData(r, d):
1057 (r.Input,) = d
1058 (r.GlyphCount,) = (len(x) + 1 for x in d)
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]))
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 )
1078 def SetContextData(r, d):
1079 (r.ClassDef,) = d
1081 def SetChainContextData(r, d):
1082 (r.BacktrackClassDef, r.InputClassDef, r.LookAheadClassDef) = d
1084 RuleData = lambda r: (r.Class,)
1085 ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
1087 def SetRuleData(r, d):
1088 (r.Class,) = d
1089 (r.GlyphCount,) = (len(x) + 1 for x in d)
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]))
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 )
1111 def SetRuleData(r, d):
1112 (r.Coverage,) = d
1113 (r.GlyphCount,) = (len(x) for x in d)
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)
1123 else:
1124 assert 0, "unknown format: %s" % Format
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
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 )
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"
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]
1172@_add_method(otTables.ContextSubst, otTables.ChainContextSubst)
1173def closure_glyphs(self, s, cur_glyphs):
1174 c = self.__subset_classify_context()
1176 indices = c.Coverage(self).intersect(cur_glyphs)
1177 if not indices:
1178 return []
1179 cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs)
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
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()
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 ]
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)]
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))
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 )
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))
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.
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
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()
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
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()
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
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
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
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
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
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
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
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)
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)
1515 for st in self.SubTable:
1516 if not st:
1517 continue
1518 st.closure_glyphs(s, cur_glyphs)
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)
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
1545@_add_method(otTables.Lookup)
1546def subset_lookups(self, lookup_indices):
1547 for s in self.SubTable:
1548 s.subset_lookups(lookup_indices)
1551@_add_method(otTables.Lookup)
1552def collect_lookups(self):
1553 return sum((st.collect_lookups() for st in self.SubTable if st), [])
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)
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)]
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
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)
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 ]
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
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 )
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 ]
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 )
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)
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 ]
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 )
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 )
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)
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)
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)
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)
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)
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, []))
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)
1792@_add_method(otTables.ScriptList)
1793def collect_features(self):
1794 return _uniq_sort(sum((s.Script.collect_features() for s in self.ScriptRecord), []))
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]
1806 return True
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
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}
1826 return True
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
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
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")
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 )
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)
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)
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 )
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))
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)
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
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 )
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
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
2012 features = table.FeatureList.FeatureRecord
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
2026 if d.ReqFeatureIndex != 65535:
2027 if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]:
2028 continue
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)
2038@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
2039def prune_post_subset(self, font, options):
2040 table = self.table
2042 self.prune_lookups() # XXX Is this actually needed?!
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
2051 if not table.LookupList:
2052 table.FeatureList = None
2054 if table.FeatureList:
2055 self.remove_redundant_langsys()
2056 # Remove unreferenced features
2057 self.prune_features()
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
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
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
2078 # downgrade table version if there are no FeatureVariations
2079 if not table.FeatureVariations and table.Version == 0x00010001:
2080 table.Version = 0x00010000
2082 return True
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)
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]
2117 return True
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
2128 store = table.VarStore
2130 usedVarIdxes = set()
2132 # Collect.
2133 table.collect_device_varidxes(usedVarIdxes)
2134 if "GPOS" in font:
2135 font["GPOS"].table.collect_device_varidxes(usedVarIdxes)
2137 # Subset.
2138 varidx_map = store.subset_varidxes(usedVarIdxes)
2140 # Map.
2141 table.remap_device_varidxes(varidx_map)
2142 if "GPOS" in font:
2143 font["GPOS"].table.remap_device_varidxes(varidx_map)
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 )
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)
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)
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)
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
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)
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
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)
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
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
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
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)
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_
2300@_add_method(ttLib.getTableClass("HVAR"))
2301def subset_glyphs(self, s):
2302 table = self.table
2304 used = set()
2305 advIdxes_ = set()
2306 retainAdvMap = False
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
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())
2323 varidx_map = table.VarStore.subset_varidxes(
2324 used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_
2325 )
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)
2334 # TODO Return emptiness...
2335 return True
2338@_add_method(ttLib.getTableClass("VVAR"))
2339def subset_glyphs(self, s):
2340 table = self.table
2342 used = set()
2343 advIdxes_ = set()
2344 retainAdvMap = False
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
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())
2364 varidx_map = table.VarStore.subset_varidxes(
2365 used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_
2366 )
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)
2377 # TODO Return emptiness...
2378 return True
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
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
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
2418@_add_method(ttLib.getTableClass("post"))
2419def subset_glyphs(self, s):
2420 self.extraNames = [] # This seems to do it
2421 return True # Required table
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
2447def _paint_glyph_names(paint, colr):
2448 result = set()
2450 def callback(paint):
2451 if paint.Format in {
2452 otTables.PaintFormat.PaintGlyph,
2453 otTables.PaintFormat.PaintColrGlyph,
2454 }:
2455 result.add(paint.Glyph)
2457 paint.traverse(colr, callback)
2458 return result
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 }
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)
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))
2486 layers -= s.glyphs
2487 s.glyphs.update(layers)
2488 decompose = layers
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
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
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)
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
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}
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)
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
2539 # TODO: also prune ununsed varIndices in COLR.VarStore
2540 return True
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
2553 colr = font.get("COLR")
2554 if not colr: # drop CPAL if COLR was subsetted to empty
2555 return False
2557 colors_by_index = defaultdict(list)
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)
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)
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)
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)
2593 self.numPaletteEntries = len(self.palettes[0])
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)
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
2615@_add_method(otTables.MathVariants)
2616def closure_glyphs(self, s):
2617 glyphs = frozenset(s.glyphs)
2618 variants = set()
2620 if self.VertGlyphCoverage:
2621 indices = self.VertGlyphCoverage.intersect(glyphs)
2622 for i in indices:
2623 variants.update(self.VertGlyphConstruction[i].closure_glyphs(glyphs))
2625 if self.HorizGlyphCoverage:
2626 indices = self.HorizGlyphCoverage.intersect(glyphs)
2627 for i in indices:
2628 variants.update(self.HorizGlyphConstruction[i].closure_glyphs(glyphs))
2630 s.glyphs.update(variants)
2633@_add_method(ttLib.getTableClass("MATH"))
2634def closure_glyphs(self, s):
2635 if self.table.MathVariants:
2636 self.table.MathVariants.closure_glyphs(s)
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)
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)
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)
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
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)
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)
2688 return True
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
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)
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
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
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
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
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
2786@_add_method(ttLib.getTableClass("cmap"))
2787def closure_glyphs(self, s):
2788 tables = [t for t in self.tables if t.isUnicode()]
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)
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)
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
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
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
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
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}
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()
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
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
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
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
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
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
2961class Options(object):
2962 class OptionError(Exception):
2963 pass
2965 class UnknownOptionError(OptionError):
2966 pass
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"]
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 )
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
3108 self.set(**kwargs)
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)
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)
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
3189 setattr(self, k, v)
3191 return posargs + passthru_options
3194class Subsetter(object):
3195 class SubsettingError(Exception):
3196 pass
3198 class MissingGlyphsSubsettingError(SubsettingError):
3199 pass
3201 class MissingUnicodesSubsettingError(SubsettingError):
3202 pass
3204 def __init__(self, options=None):
3205 if not options:
3206 options = Options()
3208 self.options = options
3209 self.unicodes_requested = set()
3210 self.glyph_names_requested = set()
3211 self.glyph_ids_requested = set()
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)
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
3238 clazz = ttLib.getTableClass(tag)
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)
3252 def _closure_glyphs(self, font):
3253 realGlyphs = set(font.getGlyphOrder())
3254 self.orig_glyph_order = glyph_order = font.getGlyphOrder()
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 )
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)
3272 self.glyphs = self.glyphs_requested.copy()
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
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")
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)
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)
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))
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)
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)
3375 self.glyphs_retained = frozenset(self.glyphs)
3377 order = font.getReverseGlyphMap()
3378 self.reverseOrigGlyphMap = {g: order[g] for g in self.glyphs_retained}
3380 self.last_retained_order = max(self.reverseOrigGlyphMap.values())
3381 self.last_retained_glyph = font.getGlyphOrder()[self.last_retained_order]
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 }
3391 self.reverseEmptiedGlyphMap = {g: order[g] for g in self.glyphs_emptied}
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 }
3408 log.info("Retaining %d glyphs", len(self.glyphs_retained))
3410 del self.glyphs
3412 def _subset_glyphs(self, font):
3413 self.used_mark_sets = []
3414 for tag in self._sort_tables(font):
3415 clazz = ttLib.getTableClass(tag)
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]
3436 with timer("subset GlyphOrder"):
3437 font.setGlyphOrder(self.new_glyph_order)
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)
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"]
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)
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 )
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
3532 return font
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
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)
3546def parse_unicodes(s):
3547 import re
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
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
3573def parse_glyphs(s):
3574 return s.replace(",", " ").split()
3577def usage():
3578 print("usage:", __usage__, file=sys.stderr)
3579 print("Try pyftsubset --help for more information.\n", file=sys.stderr)
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
3588 if args is None:
3589 args = sys.argv[1:]
3591 if "--help" in args:
3592 print(__doc__)
3593 return 0
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
3616 if len(args) < 2:
3617 usage()
3618 return 1
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
3626 fontfile = args[0]
3627 args = args[1:]
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)
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 )
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 )
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
3704 log.info("Text: '%s'" % text)
3705 log.info("Unicodes: %s", unicodes)
3706 log.info("Glyphs: %s", glyphs)
3707 log.info("Gids: %s", gids)
3709 subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text)
3710 subsetter.subset(font)
3712 save_font(font, outfile, options)
3714 if options.verbose:
3715 import os
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))
3720 if options.xml:
3721 font.saveXML(sys.stdout)
3723 font.close()
3726__all__ = [
3727 "Options",
3728 "Subsetter",
3729 "load_font",
3730 "save_font",
3731 "parse_gids",
3732 "parse_glyphs",
3733 "parse_unicodes",
3734 "main",
3735]
3737if __name__ == "__main__":
3738 sys.exit(main())