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