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