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