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