Coverage for /usr/lib/python3/dist-packages/fontTools/subset/__init__.py: 17%

2025 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1# Copyright 2013 Google, Inc. All Rights Reserved. 

2# 

3# Google Author(s): Behdad Esfahbod 

4 

5from fontTools import config 

6from fontTools.misc.roundTools import otRound 

7from fontTools import ttLib 

8from fontTools.ttLib.tables import otTables 

9from fontTools.ttLib.tables.otBase import USE_HARFBUZZ_REPACKER 

10from fontTools.otlLib.maxContextCalc import maxCtxFont 

11from fontTools.pens.basePen import NullPen 

12from fontTools.misc.loggingTools import Timer 

13from fontTools.misc.cliTools import makeOutputFileName 

14from fontTools.subset.util import _add_method, _uniq_sort 

15from fontTools.subset.cff import * 

16from fontTools.subset.svg import * 

17from fontTools.varLib import varStore # for subset_varidxes 

18from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor 

19import sys 

20import struct 

21import array 

22import logging 

23from collections import Counter, defaultdict 

24from functools import reduce 

25from types import MethodType 

26 

27__usage__ = "pyftsubset font-file [glyph...] [--option=value]..." 

28 

29__doc__ = ( 

30 """\ 

31pyftsubset -- OpenType font subsetter and optimizer 

32 

33pyftsubset is an OpenType font subsetter and optimizer, based on fontTools. 

34It accepts any TT- or CFF-flavored OpenType (.otf or .ttf) or WOFF (.woff) 

35font file. The subsetted glyph set is based on the specified glyphs 

36or characters, and specified OpenType layout features. 

37 

38The tool also performs some size-reducing optimizations, aimed for using 

39subset fonts as webfonts. Individual optimizations can be enabled or 

40disabled, and are enabled by default when they are safe. 

41 

42Usage: """ 

43 + __usage__ 

44 + """ 

45 

46At least one glyph or one of --gids, --gids-file, --glyphs, --glyphs-file, 

47--text, --text-file, --unicodes, or --unicodes-file, must be specified. 

48 

49Args: 

50 

51font-file 

52 The input font file. 

53glyph 

54 Specify one or more glyph identifiers to include in the subset. Must be 

55 PS glyph names, or the special string '*' to keep the entire glyph set. 

56 

57Initial glyph set specification 

58^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

59 

60These options populate the initial glyph set. Same option can appear 

61multiple times, and the results are accummulated. 

62 

63--gids=<NNN>[,<NNN>...] 

64 Specify comma/whitespace-separated list of glyph IDs or ranges as decimal 

65 numbers. For example, --gids=10-12,14 adds glyphs with numbers 10, 11, 

66 12, and 14. 

67 

68--gids-file=<path> 

69 Like --gids but reads from a file. Anything after a '#' on any line is 

70 ignored as comments. 

71 

72--glyphs=<glyphname>[,<glyphname>...] 

73 Specify comma/whitespace-separated PS glyph names to add to the subset. 

74 Note that only PS glyph names are accepted, not gidNNN, U+XXXX, etc 

75 that are accepted on the command line. The special string '*' will keep 

76 the entire glyph set. 

77 

78--glyphs-file=<path> 

79 Like --glyphs but reads from a file. Anything after a '#' on any line 

80 is ignored as comments. 

81 

82--text=<text> 

83 Specify characters to include in the subset, as UTF-8 string. 

84 

85--text-file=<path> 

86 Like --text but reads from a file. Newline character are not added to 

87 the subset. 

88 

89--unicodes=<XXXX>[,<XXXX>...] 

90 Specify comma/whitespace-separated list of Unicode codepoints or 

91 ranges as hex numbers, optionally prefixed with 'U+', 'u', etc. 

92 For example, --unicodes=41-5a,61-7a adds ASCII letters, so does 

93 the more verbose --unicodes=U+0041-005A,U+0061-007A. 

94 The special strings '*' will choose all Unicode characters mapped 

95 by the font. 

96 

97--unicodes-file=<path> 

98 Like --unicodes, but reads from a file. Anything after a '#' on any 

99 line in the file is ignored as comments. 

100 

101--ignore-missing-glyphs 

102 Do not fail if some requested glyphs or gids are not available in 

103 the font. 

104 

105--no-ignore-missing-glyphs 

106 Stop and fail if some requested glyphs or gids are not available 

107 in the font. [default] 

108 

109--ignore-missing-unicodes [default] 

110 Do not fail if some requested Unicode characters (including those 

111 indirectly specified using --text or --text-file) are not available 

112 in the font. 

113 

114--no-ignore-missing-unicodes 

115 Stop and fail if some requested Unicode characters are not available 

116 in the font. 

117 Note the default discrepancy between ignoring missing glyphs versus 

118 unicodes. This is for historical reasons and in the future 

119 --no-ignore-missing-unicodes might become default. 

120 

121Other options 

122^^^^^^^^^^^^^ 

123 

124For the other options listed below, to see the current value of the option, 

125pass a value of '?' to it, with or without a '='. 

126 

127Examples:: 

128 

129 $ pyftsubset --glyph-names? 

130 Current setting for 'glyph-names' is: False 

131 $ ./pyftsubset --name-IDs=? 

132 Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6] 

133 $ ./pyftsubset --hinting? --no-hinting --hinting? 

134 Current setting for 'hinting' is: True 

135 Current setting for 'hinting' is: False 

136 

137Output options 

138^^^^^^^^^^^^^^ 

139 

140--output-file=<path> 

141 The output font file. If not specified, the subsetted font 

142 will be saved in as font-file.subset. 

143 

144--flavor=<type> 

145 Specify flavor of output font file. May be 'woff' or 'woff2'. 

146 Note that WOFF2 requires the Brotli Python extension, available 

147 at https://github.com/google/brotli 

148 

149--with-zopfli 

150 Use the Google Zopfli algorithm to compress WOFF. The output is 3-8 % 

151 smaller than pure zlib, but the compression speed is much slower. 

152 The Zopfli Python bindings are available at: 

153 https://pypi.python.org/pypi/zopfli 

154 

155--harfbuzz-repacker 

156 By default, we serialize GPOS/GSUB using the HarfBuzz Repacker when 

157 uharfbuzz can be imported and is successful, otherwise fall back to 

158 the pure-python serializer. Set the option to force using the HarfBuzz 

159 Repacker (raises an error if uharfbuzz can't be found or fails). 

160 

161--no-harfbuzz-repacker 

162 Always use the pure-python serializer even if uharfbuzz is available. 

163 

164Glyph set expansion 

165^^^^^^^^^^^^^^^^^^^ 

166 

167These options control how additional glyphs are added to the subset. 

168 

169--retain-gids 

170 Retain glyph indices; just empty glyphs not needed in-place. 

171 

172--notdef-glyph 

173 Add the '.notdef' glyph to the subset (ie, keep it). [default] 

174 

175--no-notdef-glyph 

176 Drop the '.notdef' glyph unless specified in the glyph set. This 

177 saves a few bytes, but is not possible for Postscript-flavored 

178 fonts, as those require '.notdef'. For TrueType-flavored fonts, 

179 this works fine as long as no unsupported glyphs are requested 

180 from the font. 

181 

182--notdef-outline 

183 Keep the outline of '.notdef' glyph. The '.notdef' glyph outline is 

184 used when glyphs not supported by the font are to be shown. It is not 

185 needed otherwise. 

186 

187--no-notdef-outline 

188 When including a '.notdef' glyph, remove its outline. This saves 

189 a few bytes. [default] 

190 

191--recommended-glyphs 

192 Add glyphs 0, 1, 2, and 3 to the subset, as recommended for 

193 TrueType-flavored fonts: '.notdef', 'NULL' or '.null', 'CR', 'space'. 

194 Some legacy software might require this, but no modern system does. 

195 

196--no-recommended-glyphs 

197 Do not add glyphs 0, 1, 2, and 3 to the subset, unless specified in 

198 glyph set. [default] 

199 

200--no-layout-closure 

201 Do not expand glyph set to add glyphs produced by OpenType layout 

202 features. Instead, OpenType layout features will be subset to only 

203 rules that are relevant to the otherwise-specified glyph set. 

204 

205--layout-features[+|-]=<feature>[,<feature>...] 

206 Specify (=), add to (+=) or exclude from (-=) the comma-separated 

207 set of OpenType layout feature tags that will be preserved. 

208 Glyph variants used by the preserved features are added to the 

209 specified subset glyph set. By default, 'calt', 'ccmp', 'clig', 'curs', 

210 'dnom', 'frac', 'kern', 'liga', 'locl', 'mark', 'mkmk', 'numr', 'rclt', 

211 'rlig', 'rvrn', and all features required for script shaping are 

212 preserved. To see the full list, try '--layout-features=?'. 

213 Use '*' to keep all features. 

214 Multiple --layout-features options can be provided if necessary. 

215 Examples: 

216 

217 --layout-features+=onum,pnum,ss01 

218 * Keep the default set of features and 'onum', 'pnum', 'ss01'. 

219 --layout-features-='mark','mkmk' 

220 * Keep the default set of features but drop 'mark' and 'mkmk'. 

221 --layout-features='kern' 

222 * Only keep the 'kern' feature, drop all others. 

223 --layout-features='' 

224 * Drop all features. 

225 --layout-features='*' 

226 * Keep all features. 

227 --layout-features+=aalt --layout-features-=vrt2 

228 * Keep default set of features plus 'aalt', but drop 'vrt2'. 

229 

230--layout-scripts[+|-]=<script>[,<script>...] 

231 Specify (=), add to (+=) or exclude from (-=) the comma-separated 

232 set of OpenType layout script tags that will be preserved. LangSys tags 

233 can be appended to script tag, separated by '.', for example: 

234 'arab.dflt,arab.URD,latn.TRK'. By default all scripts are retained ('*'). 

235 

236Hinting options 

237^^^^^^^^^^^^^^^ 

238 

239--hinting 

240 Keep hinting [default] 

241 

242--no-hinting 

243 Drop glyph-specific hinting and font-wide hinting tables, as well 

244 as remove hinting-related bits and pieces from other tables (eg. GPOS). 

245 See --hinting-tables for list of tables that are dropped by default. 

246 Instructions and hints are stripped from 'glyf' and 'CFF ' tables 

247 respectively. This produces (sometimes up to 30%) smaller fonts that 

248 are suitable for extremely high-resolution systems, like high-end 

249 mobile devices and retina displays. 

250 

251Optimization options 

252^^^^^^^^^^^^^^^^^^^^ 

253 

254--desubroutinize 

255 Remove CFF use of subroutinizes. Subroutinization is a way to make CFF 

256 fonts smaller. For small subsets however, desubroutinizing might make 

257 the font smaller. It has even been reported that desubroutinized CFF 

258 fonts compress better (produce smaller output) WOFF and WOFF2 fonts. 

259 Also see note under --no-hinting. 

260 

261--no-desubroutinize [default] 

262 Leave CFF subroutinizes as is, only throw away unused subroutinizes. 

263 

264Font table options 

265^^^^^^^^^^^^^^^^^^ 

266 

267--drop-tables[+|-]=<table>[,<table>...] 

268 Specify (=), add to (+=) or exclude from (-=) the comma-separated 

269 set of tables that will be be dropped. 

270 By default, the following tables are dropped: 

271 'BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'PCLT', 'LTSH' 

272 and Graphite tables: 'Feat', 'Glat', 'Gloc', 'Silf', 'Sill'. 

273 The tool will attempt to subset the remaining tables. 

274 

275 Examples: 

276 

277 --drop-tables-=BASE 

278 * Drop the default set of tables but keep 'BASE'. 

279 

280 --drop-tables+=GSUB 

281 * Drop the default set of tables and 'GSUB'. 

282 

283 --drop-tables=DSIG 

284 * Only drop the 'DSIG' table, keep all others. 

285 

286 --drop-tables= 

287 * Keep all tables. 

288 

289--no-subset-tables+=<table>[,<table>...] 

290 Add to the set of tables that will not be subsetted. 

291 By default, the following tables are included in this list, as 

292 they do not need subsetting (ignore the fact that 'loca' is listed 

293 here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', 'name', 

294 'cvt ', 'fpgm', 'prep', 'VMDX', 'DSIG', 'CPAL', 'MVAR', 'cvar', 'STAT'. 

295 By default, tables that the tool does not know how to subset and are not 

296 specified here will be dropped from the font, unless --passthrough-tables 

297 option is passed. 

298 

299 Example: 

300 

301 --no-subset-tables+=FFTM 

302 * Keep 'FFTM' table in the font by preventing subsetting. 

303 

304--passthrough-tables 

305 Do not drop tables that the tool does not know how to subset. 

306 

307--no-passthrough-tables 

308 Tables that the tool does not know how to subset and are not specified 

309 in --no-subset-tables will be dropped from the font. [default] 

310 

311--hinting-tables[-]=<table>[,<table>...] 

312 Specify (=), add to (+=) or exclude from (-=) the list of font-wide 

313 hinting tables that will be dropped if --no-hinting is specified. 

314 

315 Examples: 

316 

317 --hinting-tables-=VDMX 

318 * Drop font-wide hinting tables except 'VDMX'. 

319 --hinting-tables= 

320 * Keep all font-wide hinting tables (but strip hints from glyphs). 

321 

322--legacy-kern 

323 Keep TrueType 'kern' table even when OpenType 'GPOS' is available. 

324 

325--no-legacy-kern 

326 Drop TrueType 'kern' table if OpenType 'GPOS' is available. [default] 

327 

328Font naming options 

329^^^^^^^^^^^^^^^^^^^ 

330 

331These options control what is retained in the 'name' table. For numerical 

332codes, see: http://www.microsoft.com/typography/otspec/name.htm 

333 

334--name-IDs[+|-]=<nameID>[,<nameID>...] 

335 Specify (=), add to (+=) or exclude from (-=) the set of 'name' table 

336 entry nameIDs that will be preserved. By default, only nameIDs between 0 

337 and 6 are preserved, the rest are dropped. Use '*' to keep all entries. 

338 

339 Examples: 

340 

341 --name-IDs+=7,8,9 

342 * Also keep Trademark, Manufacturer and Designer name entries. 

343 --name-IDs= 

344 * Drop all 'name' table entries. 

345 --name-IDs=* 

346 * keep all 'name' table entries 

347 

348--name-legacy 

349 Keep legacy (non-Unicode) 'name' table entries (0.x, 1.x etc.). 

350 XXX Note: This might be needed for some fonts that have no Unicode name 

351 entires for English. See: https://github.com/fonttools/fonttools/issues/146 

352 

353--no-name-legacy 

354 Drop legacy (non-Unicode) 'name' table entries [default] 

355 

356--name-languages[+|-]=<langID>[,<langID>] 

357 Specify (=), add to (+=) or exclude from (-=) the set of 'name' table 

358 langIDs that will be preserved. By default only records with langID 

359 0x0409 (English) are preserved. Use '*' to keep all langIDs. 

360 

361--obfuscate-names 

362 Make the font unusable as a system font by replacing name IDs 1, 2, 3, 4, 

363 and 6 with dummy strings (it is still fully functional as webfont). 

364 

365Glyph naming and encoding options 

366^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

367 

368--glyph-names 

369 Keep PS glyph names in TT-flavored fonts. In general glyph names are 

370 not needed for correct use of the font. However, some PDF generators 

371 and PDF viewers might rely on glyph names to extract Unicode text 

372 from PDF documents. 

373--no-glyph-names 

374 Drop PS glyph names in TT-flavored fonts, by using 'post' table 

375 version 3.0. [default] 

376--legacy-cmap 

377 Keep the legacy 'cmap' subtables (0.x, 1.x, 4.x etc.). 

378--no-legacy-cmap 

379 Drop the legacy 'cmap' subtables. [default] 

380--symbol-cmap 

381 Keep the 3.0 symbol 'cmap'. 

382--no-symbol-cmap 

383 Drop the 3.0 symbol 'cmap'. [default] 

384 

385Other font-specific options 

386^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

387 

388--recalc-bounds 

389 Recalculate font bounding boxes. 

390--no-recalc-bounds 

391 Keep original font bounding boxes. This is faster and still safe 

392 for all practical purposes. [default] 

393--recalc-timestamp 

394 Set font 'modified' timestamp to current time. 

395--no-recalc-timestamp 

396 Do not modify font 'modified' timestamp. [default] 

397--canonical-order 

398 Order tables as recommended in the OpenType standard. This is not 

399 required by the standard, nor by any known implementation. 

400--no-canonical-order 

401 Keep original order of font tables. This is faster. [default] 

402--prune-unicode-ranges 

403 Update the 'OS/2 ulUnicodeRange*' bits after subsetting. The Unicode 

404 ranges defined in the OpenType specification v1.7 are intersected with 

405 the Unicode codepoints specified in the font's Unicode 'cmap' subtables: 

406 when no overlap is found, the bit will be switched off. However, it will 

407 *not* be switched on if an intersection is found. [default] 

408--no-prune-unicode-ranges 

409 Don't change the 'OS/2 ulUnicodeRange*' bits. 

410--prune-codepage-ranges 

411 Update the 'OS/2 ulCodePageRange*' bits after subsetting. [default] 

412--no-prune-codepage-ranges 

413 Don't change the 'OS/2 ulCodePageRange*' bits. 

414--recalc-average-width 

415 Update the 'OS/2 xAvgCharWidth' field after subsetting. 

416--no-recalc-average-width 

417 Don't change the 'OS/2 xAvgCharWidth' field. [default] 

418--recalc-max-context 

419 Update the 'OS/2 usMaxContext' field after subsetting. 

420--no-recalc-max-context 

421 Don't change the 'OS/2 usMaxContext' field. [default] 

422--font-number=<number> 

423 Select font number for TrueType Collection (.ttc/.otc), starting from 0. 

424--pretty-svg 

425 When subsetting SVG table, use lxml pretty_print=True option to indent 

426 the XML output (only recommended for debugging purposes). 

427 

428Application options 

429^^^^^^^^^^^^^^^^^^^ 

430 

431--verbose 

432 Display verbose information of the subsetting process. 

433--timing 

434 Display detailed timing information of the subsetting process. 

435--xml 

436 Display the TTX XML representation of subsetted font. 

437 

438Example 

439^^^^^^^ 

440 

441Produce a subset containing the characters ' !"#$%' without performing 

442size-reducing optimizations:: 

443 

444 $ pyftsubset font.ttf --unicodes="U+0020-0025" \\ 

445 --layout-features=* --glyph-names --symbol-cmap --legacy-cmap \\ 

446 --notdef-glyph --notdef-outline --recommended-glyphs \\ 

447 --name-IDs=* --name-legacy --name-languages=* 

448""" 

449) 

450 

451 

452log = logging.getLogger("fontTools.subset") 

453 

454 

455def _log_glyphs(self, glyphs, font=None): 

456 self.info("Glyph names: %s", sorted(glyphs)) 

457 if font: 

458 reverseGlyphMap = font.getReverseGlyphMap() 

459 self.info("Glyph IDs: %s", sorted(reverseGlyphMap[g] for g in glyphs)) 

460 

461 

462# bind "glyphs" function to 'log' object 

463log.glyphs = MethodType(_log_glyphs, log) 

464 

465# I use a different timing channel so I can configure it separately from the 

466# main module's logger 

467timer = Timer(logger=logging.getLogger("fontTools.subset.timer")) 

468 

469 

470def _dict_subset(d, glyphs): 

471 return {g: d[g] for g in glyphs} 

472 

473 

474def _list_subset(l, indices): 

475 count = len(l) 

476 return [l[i] for i in indices if i < count] 

477 

478 

479@_add_method(otTables.Coverage) 

480def intersect(self, glyphs): 

481 """Returns ascending list of matching coverage values.""" 

482 return [i for i, g in enumerate(self.glyphs) if g in glyphs] 

483 

484 

485@_add_method(otTables.Coverage) 

486def intersect_glyphs(self, glyphs): 

487 """Returns set of intersecting glyphs.""" 

488 return set(g for g in self.glyphs if g in glyphs) 

489 

490 

491@_add_method(otTables.Coverage) 

492def subset(self, glyphs): 

493 """Returns ascending list of remaining coverage values.""" 

494 indices = self.intersect(glyphs) 

495 self.glyphs = [g for g in self.glyphs if g in glyphs] 

496 return indices 

497 

498 

499@_add_method(otTables.Coverage) 

500def remap(self, coverage_map): 

501 """Remaps coverage.""" 

502 self.glyphs = [self.glyphs[i] for i in coverage_map] 

503 

504 

505@_add_method(otTables.ClassDef) 

506def intersect(self, glyphs): 

507 """Returns ascending list of matching class values.""" 

508 return _uniq_sort( 

509 ([0] if any(g not in self.classDefs for g in glyphs) else []) 

510 + [v for g, v in self.classDefs.items() if g in glyphs] 

511 ) 

512 

513 

514@_add_method(otTables.ClassDef) 

515def intersect_class(self, glyphs, klass): 

516 """Returns set of glyphs matching class.""" 

517 if klass == 0: 

518 return set(g for g in glyphs if g not in self.classDefs) 

519 return set(g for g, v in self.classDefs.items() if v == klass and g in glyphs) 

520 

521 

522@_add_method(otTables.ClassDef) 

523def subset(self, glyphs, remap=False, useClass0=True): 

524 """Returns ascending list of remaining classes.""" 

525 self.classDefs = {g: v for g, v in self.classDefs.items() if g in glyphs} 

526 # Note: while class 0 has the special meaning of "not matched", 

527 # if no glyph will ever /not match/, we can optimize class 0 out too. 

528 # Only do this if allowed. 

529 indices = _uniq_sort( 

530 ( 

531 [0] 

532 if ((not useClass0) or any(g not in self.classDefs for g in glyphs)) 

533 else [] 

534 ) 

535 + list(self.classDefs.values()) 

536 ) 

537 if remap: 

538 self.remap(indices) 

539 return indices 

540 

541 

542@_add_method(otTables.ClassDef) 

543def remap(self, class_map): 

544 """Remaps classes.""" 

545 self.classDefs = {g: class_map.index(v) for g, v in self.classDefs.items()} 

546 

547 

548@_add_method(otTables.SingleSubst) 

549def closure_glyphs(self, s, cur_glyphs): 

550 s.glyphs.update(v for g, v in self.mapping.items() if g in cur_glyphs) 

551 

552 

553@_add_method(otTables.SingleSubst) 

554def subset_glyphs(self, s): 

555 self.mapping = { 

556 g: v for g, v in self.mapping.items() if g in s.glyphs and v in s.glyphs 

557 } 

558 return bool(self.mapping) 

559 

560 

561@_add_method(otTables.MultipleSubst) 

562def closure_glyphs(self, s, cur_glyphs): 

563 for glyph, subst in self.mapping.items(): 

564 if glyph in cur_glyphs: 

565 s.glyphs.update(subst) 

566 

567 

568@_add_method(otTables.MultipleSubst) 

569def subset_glyphs(self, s): 

570 self.mapping = { 

571 g: v 

572 for g, v in self.mapping.items() 

573 if g in s.glyphs and all(sub in s.glyphs for sub in v) 

574 } 

575 return bool(self.mapping) 

576 

577 

578@_add_method(otTables.AlternateSubst) 

579def closure_glyphs(self, s, cur_glyphs): 

580 s.glyphs.update(*(vlist for g, vlist in self.alternates.items() if g in cur_glyphs)) 

581 

582 

583@_add_method(otTables.AlternateSubst) 

584def subset_glyphs(self, s): 

585 self.alternates = { 

586 g: [v for v in vlist if v in s.glyphs] 

587 for g, vlist in self.alternates.items() 

588 if g in s.glyphs and any(v in s.glyphs for v in vlist) 

589 } 

590 return bool(self.alternates) 

591 

592 

593@_add_method(otTables.LigatureSubst) 

594def closure_glyphs(self, s, cur_glyphs): 

595 s.glyphs.update( 

596 *( 

597 [seq.LigGlyph for seq in seqs if all(c in s.glyphs for c in seq.Component)] 

598 for g, seqs in self.ligatures.items() 

599 if g in cur_glyphs 

600 ) 

601 ) 

602 

603 

604@_add_method(otTables.LigatureSubst) 

605def subset_glyphs(self, s): 

606 self.ligatures = {g: v for g, v in self.ligatures.items() if g in s.glyphs} 

607 self.ligatures = { 

608 g: [ 

609 seq 

610 for seq in seqs 

611 if seq.LigGlyph in s.glyphs and all(c in s.glyphs for c in seq.Component) 

612 ] 

613 for g, seqs in self.ligatures.items() 

614 } 

615 self.ligatures = {g: v for g, v in self.ligatures.items() if v} 

616 return bool(self.ligatures) 

617 

618 

619@_add_method(otTables.ReverseChainSingleSubst) 

620def closure_glyphs(self, s, cur_glyphs): 

621 if self.Format == 1: 

622 indices = self.Coverage.intersect(cur_glyphs) 

623 if not indices or not all( 

624 c.intersect(s.glyphs) 

625 for c in self.LookAheadCoverage + self.BacktrackCoverage 

626 ): 

627 return 

628 s.glyphs.update(self.Substitute[i] for i in indices) 

629 else: 

630 assert 0, "unknown format: %s" % self.Format 

631 

632 

633@_add_method(otTables.ReverseChainSingleSubst) 

634def subset_glyphs(self, s): 

635 if self.Format == 1: 

636 indices = self.Coverage.subset(s.glyphs) 

637 self.Substitute = _list_subset(self.Substitute, indices) 

638 # Now drop rules generating glyphs we don't want 

639 indices = [i for i, sub in enumerate(self.Substitute) if sub in s.glyphs] 

640 self.Substitute = _list_subset(self.Substitute, indices) 

641 self.Coverage.remap(indices) 

642 self.GlyphCount = len(self.Substitute) 

643 return bool( 

644 self.GlyphCount 

645 and all( 

646 c.subset(s.glyphs) 

647 for c in self.LookAheadCoverage + self.BacktrackCoverage 

648 ) 

649 ) 

650 else: 

651 assert 0, "unknown format: %s" % self.Format 

652 

653 

654@_add_method(otTables.Device) 

655def is_hinting(self): 

656 return self.DeltaFormat in (1, 2, 3) 

657 

658 

659@_add_method(otTables.ValueRecord) 

660def prune_hints(self): 

661 for name in ["XPlaDevice", "YPlaDevice", "XAdvDevice", "YAdvDevice"]: 

662 v = getattr(self, name, None) 

663 if v is not None and v.is_hinting(): 

664 delattr(self, name) 

665 

666 

667@_add_method(otTables.SinglePos) 

668def subset_glyphs(self, s): 

669 if self.Format == 1: 

670 return len(self.Coverage.subset(s.glyphs)) 

671 elif self.Format == 2: 

672 indices = self.Coverage.subset(s.glyphs) 

673 values = self.Value 

674 count = len(values) 

675 self.Value = [values[i] for i in indices if i < count] 

676 self.ValueCount = len(self.Value) 

677 return bool(self.ValueCount) 

678 else: 

679 assert 0, "unknown format: %s" % self.Format 

680 

681 

682@_add_method(otTables.SinglePos) 

683def prune_post_subset(self, font, options): 

684 if self.Value is None: 

685 assert self.ValueFormat == 0 

686 return True 

687 

688 # Shrink ValueFormat 

689 if self.Format == 1: 

690 if not options.hinting: 

691 self.Value.prune_hints() 

692 self.ValueFormat = self.Value.getEffectiveFormat() 

693 elif self.Format == 2: 

694 if None in self.Value: 

695 assert self.ValueFormat == 0 

696 assert all(v is None for v in self.Value) 

697 else: 

698 if not options.hinting: 

699 for v in self.Value: 

700 v.prune_hints() 

701 self.ValueFormat = reduce( 

702 int.__or__, [v.getEffectiveFormat() for v in self.Value], 0 

703 ) 

704 

705 # Downgrade to Format 1 if all ValueRecords are the same 

706 if self.Format == 2 and all(v == self.Value[0] for v in self.Value): 

707 self.Format = 1 

708 self.Value = self.Value[0] if self.ValueFormat != 0 else None 

709 del self.ValueCount 

710 

711 return True 

712 

713 

714@_add_method(otTables.PairPos) 

715def subset_glyphs(self, s): 

716 if self.Format == 1: 

717 indices = self.Coverage.subset(s.glyphs) 

718 pairs = self.PairSet 

719 count = len(pairs) 

720 self.PairSet = [pairs[i] for i in indices if i < count] 

721 for p in self.PairSet: 

722 p.PairValueRecord = [ 

723 r for r in p.PairValueRecord if r.SecondGlyph in s.glyphs 

724 ] 

725 p.PairValueCount = len(p.PairValueRecord) 

726 # Remove empty pairsets 

727 indices = [i for i, p in enumerate(self.PairSet) if p.PairValueCount] 

728 self.Coverage.remap(indices) 

729 self.PairSet = _list_subset(self.PairSet, indices) 

730 self.PairSetCount = len(self.PairSet) 

731 return bool(self.PairSetCount) 

732 elif self.Format == 2: 

733 class1_map = [ 

734 c 

735 for c in self.ClassDef1.subset( 

736 s.glyphs.intersection(self.Coverage.glyphs), remap=True 

737 ) 

738 if c < self.Class1Count 

739 ] 

740 class2_map = [ 

741 c 

742 for c in self.ClassDef2.subset(s.glyphs, remap=True, useClass0=False) 

743 if c < self.Class2Count 

744 ] 

745 self.Class1Record = [self.Class1Record[i] for i in class1_map] 

746 for c in self.Class1Record: 

747 c.Class2Record = [c.Class2Record[i] for i in class2_map] 

748 self.Class1Count = len(class1_map) 

749 self.Class2Count = len(class2_map) 

750 # If only Class2 0 left, no need to keep anything. 

751 return bool( 

752 self.Class1Count 

753 and (self.Class2Count > 1) 

754 and self.Coverage.subset(s.glyphs) 

755 ) 

756 else: 

757 assert 0, "unknown format: %s" % self.Format 

758 

759 

760@_add_method(otTables.PairPos) 

761def prune_post_subset(self, font, options): 

762 if not options.hinting: 

763 attr1, attr2 = { 

764 1: ("PairSet", "PairValueRecord"), 

765 2: ("Class1Record", "Class2Record"), 

766 }[self.Format] 

767 

768 self.ValueFormat1 = self.ValueFormat2 = 0 

769 for row in getattr(self, attr1): 

770 for r in getattr(row, attr2): 

771 if r.Value1: 

772 r.Value1.prune_hints() 

773 self.ValueFormat1 |= r.Value1.getEffectiveFormat() 

774 if r.Value2: 

775 r.Value2.prune_hints() 

776 self.ValueFormat2 |= r.Value2.getEffectiveFormat() 

777 

778 return bool(self.ValueFormat1 | self.ValueFormat2) 

779 

780 

781@_add_method(otTables.CursivePos) 

782def subset_glyphs(self, s): 

783 if self.Format == 1: 

784 indices = self.Coverage.subset(s.glyphs) 

785 records = self.EntryExitRecord 

786 count = len(records) 

787 self.EntryExitRecord = [records[i] for i in indices if i < count] 

788 self.EntryExitCount = len(self.EntryExitRecord) 

789 return bool(self.EntryExitCount) 

790 else: 

791 assert 0, "unknown format: %s" % self.Format 

792 

793 

794@_add_method(otTables.Anchor) 

795def prune_hints(self): 

796 if self.Format == 2: 

797 self.Format = 1 

798 elif self.Format == 3: 

799 for name in ("XDeviceTable", "YDeviceTable"): 

800 v = getattr(self, name, None) 

801 if v is not None and v.is_hinting(): 

802 setattr(self, name, None) 

803 if self.XDeviceTable is None and self.YDeviceTable is None: 

804 self.Format = 1 

805 

806 

807@_add_method(otTables.CursivePos) 

808def prune_post_subset(self, font, options): 

809 if not options.hinting: 

810 for rec in self.EntryExitRecord: 

811 if rec.EntryAnchor: 

812 rec.EntryAnchor.prune_hints() 

813 if rec.ExitAnchor: 

814 rec.ExitAnchor.prune_hints() 

815 return True 

816 

817 

818@_add_method(otTables.MarkBasePos) 

819def subset_glyphs(self, s): 

820 if self.Format == 1: 

821 mark_indices = self.MarkCoverage.subset(s.glyphs) 

822 self.MarkArray.MarkRecord = _list_subset( 

823 self.MarkArray.MarkRecord, mark_indices 

824 ) 

825 self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) 

826 base_indices = self.BaseCoverage.subset(s.glyphs) 

827 self.BaseArray.BaseRecord = _list_subset( 

828 self.BaseArray.BaseRecord, base_indices 

829 ) 

830 self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord) 

831 # Prune empty classes 

832 class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) 

833 self.ClassCount = len(class_indices) 

834 for m in self.MarkArray.MarkRecord: 

835 m.Class = class_indices.index(m.Class) 

836 for b in self.BaseArray.BaseRecord: 

837 b.BaseAnchor = _list_subset(b.BaseAnchor, class_indices) 

838 return bool( 

839 self.ClassCount and self.MarkArray.MarkCount and self.BaseArray.BaseCount 

840 ) 

841 else: 

842 assert 0, "unknown format: %s" % self.Format 

843 

844 

845@_add_method(otTables.MarkBasePos) 

846def prune_post_subset(self, font, options): 

847 if not options.hinting: 

848 for m in self.MarkArray.MarkRecord: 

849 if m.MarkAnchor: 

850 m.MarkAnchor.prune_hints() 

851 for b in self.BaseArray.BaseRecord: 

852 for a in b.BaseAnchor: 

853 if a: 

854 a.prune_hints() 

855 return True 

856 

857 

858@_add_method(otTables.MarkLigPos) 

859def subset_glyphs(self, s): 

860 if self.Format == 1: 

861 mark_indices = self.MarkCoverage.subset(s.glyphs) 

862 self.MarkArray.MarkRecord = _list_subset( 

863 self.MarkArray.MarkRecord, mark_indices 

864 ) 

865 self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) 

866 ligature_indices = self.LigatureCoverage.subset(s.glyphs) 

867 self.LigatureArray.LigatureAttach = _list_subset( 

868 self.LigatureArray.LigatureAttach, ligature_indices 

869 ) 

870 self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach) 

871 # Prune empty classes 

872 class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) 

873 self.ClassCount = len(class_indices) 

874 for m in self.MarkArray.MarkRecord: 

875 m.Class = class_indices.index(m.Class) 

876 for l in self.LigatureArray.LigatureAttach: 

877 if l is None: 

878 continue 

879 for c in l.ComponentRecord: 

880 c.LigatureAnchor = _list_subset(c.LigatureAnchor, class_indices) 

881 return bool( 

882 self.ClassCount 

883 and self.MarkArray.MarkCount 

884 and self.LigatureArray.LigatureCount 

885 ) 

886 else: 

887 assert 0, "unknown format: %s" % self.Format 

888 

889 

890@_add_method(otTables.MarkLigPos) 

891def prune_post_subset(self, font, options): 

892 if not options.hinting: 

893 for m in self.MarkArray.MarkRecord: 

894 if m.MarkAnchor: 

895 m.MarkAnchor.prune_hints() 

896 for l in self.LigatureArray.LigatureAttach: 

897 if l is None: 

898 continue 

899 for c in l.ComponentRecord: 

900 for a in c.LigatureAnchor: 

901 if a: 

902 a.prune_hints() 

903 return True 

904 

905 

906@_add_method(otTables.MarkMarkPos) 

907def subset_glyphs(self, s): 

908 if self.Format == 1: 

909 mark1_indices = self.Mark1Coverage.subset(s.glyphs) 

910 self.Mark1Array.MarkRecord = _list_subset( 

911 self.Mark1Array.MarkRecord, mark1_indices 

912 ) 

913 self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord) 

914 mark2_indices = self.Mark2Coverage.subset(s.glyphs) 

915 self.Mark2Array.Mark2Record = _list_subset( 

916 self.Mark2Array.Mark2Record, mark2_indices 

917 ) 

918 self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record) 

919 # Prune empty classes 

920 class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord) 

921 self.ClassCount = len(class_indices) 

922 for m in self.Mark1Array.MarkRecord: 

923 m.Class = class_indices.index(m.Class) 

924 for b in self.Mark2Array.Mark2Record: 

925 b.Mark2Anchor = _list_subset(b.Mark2Anchor, class_indices) 

926 return bool( 

927 self.ClassCount and self.Mark1Array.MarkCount and self.Mark2Array.MarkCount 

928 ) 

929 else: 

930 assert 0, "unknown format: %s" % self.Format 

931 

932 

933@_add_method(otTables.MarkMarkPos) 

934def prune_post_subset(self, font, options): 

935 if not options.hinting: 

936 for m in self.Mark1Array.MarkRecord: 

937 if m.MarkAnchor: 

938 m.MarkAnchor.prune_hints() 

939 for b in self.Mark2Array.Mark2Record: 

940 for m in b.Mark2Anchor: 

941 if m: 

942 m.prune_hints() 

943 return True 

944 

945 

946@_add_method( 

947 otTables.SingleSubst, 

948 otTables.MultipleSubst, 

949 otTables.AlternateSubst, 

950 otTables.LigatureSubst, 

951 otTables.ReverseChainSingleSubst, 

952 otTables.SinglePos, 

953 otTables.PairPos, 

954 otTables.CursivePos, 

955 otTables.MarkBasePos, 

956 otTables.MarkLigPos, 

957 otTables.MarkMarkPos, 

958) 

959def subset_lookups(self, lookup_indices): 

960 pass 

961 

962 

963@_add_method( 

964 otTables.SingleSubst, 

965 otTables.MultipleSubst, 

966 otTables.AlternateSubst, 

967 otTables.LigatureSubst, 

968 otTables.ReverseChainSingleSubst, 

969 otTables.SinglePos, 

970 otTables.PairPos, 

971 otTables.CursivePos, 

972 otTables.MarkBasePos, 

973 otTables.MarkLigPos, 

974 otTables.MarkMarkPos, 

975) 

976def collect_lookups(self): 

977 return [] 

978 

979 

980@_add_method( 

981 otTables.SingleSubst, 

982 otTables.MultipleSubst, 

983 otTables.AlternateSubst, 

984 otTables.LigatureSubst, 

985 otTables.ReverseChainSingleSubst, 

986 otTables.ContextSubst, 

987 otTables.ChainContextSubst, 

988 otTables.ContextPos, 

989 otTables.ChainContextPos, 

990) 

991def prune_post_subset(self, font, options): 

992 return True 

993 

994 

995@_add_method( 

996 otTables.SingleSubst, otTables.AlternateSubst, otTables.ReverseChainSingleSubst 

997) 

998def may_have_non_1to1(self): 

999 return False 

1000 

1001 

1002@_add_method( 

1003 otTables.MultipleSubst, 

1004 otTables.LigatureSubst, 

1005 otTables.ContextSubst, 

1006 otTables.ChainContextSubst, 

1007) 

1008def may_have_non_1to1(self): 

1009 return True 

1010 

1011 

1012@_add_method( 

1013 otTables.ContextSubst, 

1014 otTables.ChainContextSubst, 

1015 otTables.ContextPos, 

1016 otTables.ChainContextPos, 

1017) 

1018def __subset_classify_context(self): 

1019 class ContextHelper(object): 

1020 def __init__(self, klass, Format): 

1021 if klass.__name__.endswith("Subst"): 

1022 Typ = "Sub" 

1023 Type = "Subst" 

1024 else: 

1025 Typ = "Pos" 

1026 Type = "Pos" 

1027 if klass.__name__.startswith("Chain"): 

1028 Chain = "Chain" 

1029 InputIdx = 1 

1030 DataLen = 3 

1031 else: 

1032 Chain = "" 

1033 InputIdx = 0 

1034 DataLen = 1 

1035 ChainTyp = Chain + Typ 

1036 

1037 self.Typ = Typ 

1038 self.Type = Type 

1039 self.Chain = Chain 

1040 self.ChainTyp = ChainTyp 

1041 self.InputIdx = InputIdx 

1042 self.DataLen = DataLen 

1043 

1044 self.LookupRecord = Type + "LookupRecord" 

1045 

1046 if Format == 1: 

1047 Coverage = lambda r: r.Coverage 

1048 ChainCoverage = lambda r: r.Coverage 

1049 ContextData = lambda r: (None,) 

1050 ChainContextData = lambda r: (None, None, None) 

1051 SetContextData = None 

1052 SetChainContextData = None 

1053 RuleData = lambda r: (r.Input,) 

1054 ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead) 

1055 

1056 def SetRuleData(r, d): 

1057 (r.Input,) = d 

1058 (r.GlyphCount,) = (len(x) + 1 for x in d) 

1059 

1060 def ChainSetRuleData(r, d): 

1061 (r.Backtrack, r.Input, r.LookAhead) = d 

1062 ( 

1063 r.BacktrackGlyphCount, 

1064 r.InputGlyphCount, 

1065 r.LookAheadGlyphCount, 

1066 ) = (len(d[0]), len(d[1]) + 1, len(d[2])) 

1067 

1068 elif Format == 2: 

1069 Coverage = lambda r: r.Coverage 

1070 ChainCoverage = lambda r: r.Coverage 

1071 ContextData = lambda r: (r.ClassDef,) 

1072 ChainContextData = lambda r: ( 

1073 r.BacktrackClassDef, 

1074 r.InputClassDef, 

1075 r.LookAheadClassDef, 

1076 ) 

1077 

1078 def SetContextData(r, d): 

1079 (r.ClassDef,) = d 

1080 

1081 def SetChainContextData(r, d): 

1082 (r.BacktrackClassDef, r.InputClassDef, r.LookAheadClassDef) = d 

1083 

1084 RuleData = lambda r: (r.Class,) 

1085 ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead) 

1086 

1087 def SetRuleData(r, d): 

1088 (r.Class,) = d 

1089 (r.GlyphCount,) = (len(x) + 1 for x in d) 

1090 

1091 def ChainSetRuleData(r, d): 

1092 (r.Backtrack, r.Input, r.LookAhead) = d 

1093 ( 

1094 r.BacktrackGlyphCount, 

1095 r.InputGlyphCount, 

1096 r.LookAheadGlyphCount, 

1097 ) = (len(d[0]), len(d[1]) + 1, len(d[2])) 

1098 

1099 elif Format == 3: 

1100 Coverage = lambda r: r.Coverage[0] 

1101 ChainCoverage = lambda r: r.InputCoverage[0] 

1102 ContextData = None 

1103 ChainContextData = None 

1104 SetContextData = None 

1105 SetChainContextData = None 

1106 RuleData = lambda r: r.Coverage 

1107 ChainRuleData = lambda r: ( 

1108 r.BacktrackCoverage + r.InputCoverage + r.LookAheadCoverage 

1109 ) 

1110 

1111 def SetRuleData(r, d): 

1112 (r.Coverage,) = d 

1113 (r.GlyphCount,) = (len(x) for x in d) 

1114 

1115 def ChainSetRuleData(r, d): 

1116 (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d 

1117 ( 

1118 r.BacktrackGlyphCount, 

1119 r.InputGlyphCount, 

1120 r.LookAheadGlyphCount, 

1121 ) = (len(x) for x in d) 

1122 

1123 else: 

1124 assert 0, "unknown format: %s" % Format 

1125 

1126 if Chain: 

1127 self.Coverage = ChainCoverage 

1128 self.ContextData = ChainContextData 

1129 self.SetContextData = SetChainContextData 

1130 self.RuleData = ChainRuleData 

1131 self.SetRuleData = ChainSetRuleData 

1132 else: 

1133 self.Coverage = Coverage 

1134 self.ContextData = ContextData 

1135 self.SetContextData = SetContextData 

1136 self.RuleData = RuleData 

1137 self.SetRuleData = SetRuleData 

1138 

1139 if Format == 1: 

1140 self.Rule = ChainTyp + "Rule" 

1141 self.RuleCount = ChainTyp + "RuleCount" 

1142 self.RuleSet = ChainTyp + "RuleSet" 

1143 self.RuleSetCount = ChainTyp + "RuleSetCount" 

1144 self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] 

1145 elif Format == 2: 

1146 self.Rule = ChainTyp + "ClassRule" 

1147 self.RuleCount = ChainTyp + "ClassRuleCount" 

1148 self.RuleSet = ChainTyp + "ClassSet" 

1149 self.RuleSetCount = ChainTyp + "ClassSetCount" 

1150 self.Intersect = lambda glyphs, c, r: ( 

1151 c.intersect_class(glyphs, r) 

1152 if c 

1153 else (set(glyphs) if r == 0 else set()) 

1154 ) 

1155 

1156 self.ClassDef = "InputClassDef" if Chain else "ClassDef" 

1157 self.ClassDefIndex = 1 if Chain else 0 

1158 self.Input = "Input" if Chain else "Class" 

1159 elif Format == 3: 

1160 self.Input = "InputCoverage" if Chain else "Coverage" 

1161 

1162 if self.Format not in [1, 2, 3]: 

1163 return None # Don't shoot the messenger; let it go 

1164 if not hasattr(self.__class__, "_subset__ContextHelpers"): 

1165 self.__class__._subset__ContextHelpers = {} 

1166 if self.Format not in self.__class__._subset__ContextHelpers: 

1167 helper = ContextHelper(self.__class__, self.Format) 

1168 self.__class__._subset__ContextHelpers[self.Format] = helper 

1169 return self.__class__._subset__ContextHelpers[self.Format] 

1170 

1171 

1172@_add_method(otTables.ContextSubst, otTables.ChainContextSubst) 

1173def closure_glyphs(self, s, cur_glyphs): 

1174 c = self.__subset_classify_context() 

1175 

1176 indices = c.Coverage(self).intersect(cur_glyphs) 

1177 if not indices: 

1178 return [] 

1179 cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs) 

1180 

1181 if self.Format == 1: 

1182 ContextData = c.ContextData(self) 

1183 rss = getattr(self, c.RuleSet) 

1184 rssCount = getattr(self, c.RuleSetCount) 

1185 for i in indices: 

1186 if i >= rssCount or not rss[i]: 

1187 continue 

1188 for r in getattr(rss[i], c.Rule): 

1189 if not r: 

1190 continue 

1191 if not all( 

1192 all(c.Intersect(s.glyphs, cd, k) for k in klist) 

1193 for cd, klist in zip(ContextData, c.RuleData(r)) 

1194 ): 

1195 continue 

1196 chaos = set() 

1197 for ll in getattr(r, c.LookupRecord): 

1198 if not ll: 

1199 continue 

1200 seqi = ll.SequenceIndex 

1201 if seqi in chaos: 

1202 # TODO Can we improve this? 

1203 pos_glyphs = None 

1204 else: 

1205 if seqi == 0: 

1206 pos_glyphs = frozenset([c.Coverage(self).glyphs[i]]) 

1207 else: 

1208 pos_glyphs = frozenset([r.Input[seqi - 1]]) 

1209 lookup = s.table.LookupList.Lookup[ll.LookupListIndex] 

1210 chaos.add(seqi) 

1211 if lookup.may_have_non_1to1(): 

1212 chaos.update(range(seqi, len(r.Input) + 2)) 

1213 lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) 

1214 elif self.Format == 2: 

1215 ClassDef = getattr(self, c.ClassDef) 

1216 indices = ClassDef.intersect(cur_glyphs) 

1217 ContextData = c.ContextData(self) 

1218 rss = getattr(self, c.RuleSet) 

1219 rssCount = getattr(self, c.RuleSetCount) 

1220 for i in indices: 

1221 if i >= rssCount or not rss[i]: 

1222 continue 

1223 for r in getattr(rss[i], c.Rule): 

1224 if not r: 

1225 continue 

1226 if not all( 

1227 all(c.Intersect(s.glyphs, cd, k) for k in klist) 

1228 for cd, klist in zip(ContextData, c.RuleData(r)) 

1229 ): 

1230 continue 

1231 chaos = set() 

1232 for ll in getattr(r, c.LookupRecord): 

1233 if not ll: 

1234 continue 

1235 seqi = ll.SequenceIndex 

1236 if seqi in chaos: 

1237 # TODO Can we improve this? 

1238 pos_glyphs = None 

1239 else: 

1240 if seqi == 0: 

1241 pos_glyphs = frozenset( 

1242 ClassDef.intersect_class(cur_glyphs, i) 

1243 ) 

1244 else: 

1245 pos_glyphs = frozenset( 

1246 ClassDef.intersect_class( 

1247 s.glyphs, getattr(r, c.Input)[seqi - 1] 

1248 ) 

1249 ) 

1250 lookup = s.table.LookupList.Lookup[ll.LookupListIndex] 

1251 chaos.add(seqi) 

1252 if lookup.may_have_non_1to1(): 

1253 chaos.update(range(seqi, len(getattr(r, c.Input)) + 2)) 

1254 lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) 

1255 elif self.Format == 3: 

1256 if not all(x is not None and x.intersect(s.glyphs) for x in c.RuleData(self)): 

1257 return [] 

1258 r = self 

1259 input_coverages = getattr(r, c.Input) 

1260 chaos = set() 

1261 for ll in getattr(r, c.LookupRecord): 

1262 if not ll: 

1263 continue 

1264 seqi = ll.SequenceIndex 

1265 if seqi in chaos: 

1266 # TODO Can we improve this? 

1267 pos_glyphs = None 

1268 else: 

1269 if seqi == 0: 

1270 pos_glyphs = frozenset(cur_glyphs) 

1271 else: 

1272 pos_glyphs = frozenset( 

1273 input_coverages[seqi].intersect_glyphs(s.glyphs) 

1274 ) 

1275 lookup = s.table.LookupList.Lookup[ll.LookupListIndex] 

1276 chaos.add(seqi) 

1277 if lookup.may_have_non_1to1(): 

1278 chaos.update(range(seqi, len(input_coverages) + 1)) 

1279 lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) 

1280 else: 

1281 assert 0, "unknown format: %s" % self.Format 

1282 

1283 

1284@_add_method( 

1285 otTables.ContextSubst, 

1286 otTables.ContextPos, 

1287 otTables.ChainContextSubst, 

1288 otTables.ChainContextPos, 

1289) 

1290def subset_glyphs(self, s): 

1291 c = self.__subset_classify_context() 

1292 

1293 if self.Format == 1: 

1294 indices = self.Coverage.subset(s.glyphs) 

1295 rss = getattr(self, c.RuleSet) 

1296 rssCount = getattr(self, c.RuleSetCount) 

1297 rss = [rss[i] for i in indices if i < rssCount] 

1298 for rs in rss: 

1299 if not rs: 

1300 continue 

1301 ss = getattr(rs, c.Rule) 

1302 ss = [ 

1303 r 

1304 for r in ss 

1305 if r 

1306 and all(all(g in s.glyphs for g in glist) for glist in c.RuleData(r)) 

1307 ] 

1308 setattr(rs, c.Rule, ss) 

1309 setattr(rs, c.RuleCount, len(ss)) 

1310 # Prune empty rulesets 

1311 indices = [i for i, rs in enumerate(rss) if rs and getattr(rs, c.Rule)] 

1312 self.Coverage.remap(indices) 

1313 rss = _list_subset(rss, indices) 

1314 setattr(self, c.RuleSet, rss) 

1315 setattr(self, c.RuleSetCount, len(rss)) 

1316 return bool(rss) 

1317 elif self.Format == 2: 

1318 if not self.Coverage.subset(s.glyphs): 

1319 return False 

1320 ContextData = c.ContextData(self) 

1321 klass_maps = [ 

1322 x.subset(s.glyphs, remap=True) if x else None for x in ContextData 

1323 ] 

1324 

1325 # Keep rulesets for class numbers that survived. 

1326 indices = klass_maps[c.ClassDefIndex] 

1327 rss = getattr(self, c.RuleSet) 

1328 rssCount = getattr(self, c.RuleSetCount) 

1329 rss = [rss[i] for i in indices if i < rssCount] 

1330 del rssCount 

1331 # Delete, but not renumber, unreachable rulesets. 

1332 indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs) 

1333 rss = [rss if i in indices else None for i, rss in enumerate(rss)] 

1334 

1335 for rs in rss: 

1336 if not rs: 

1337 continue 

1338 ss = getattr(rs, c.Rule) 

1339 ss = [ 

1340 r 

1341 for r in ss 

1342 if r 

1343 and all( 

1344 all(k in klass_map for k in klist) 

1345 for klass_map, klist in zip(klass_maps, c.RuleData(r)) 

1346 ) 

1347 ] 

1348 setattr(rs, c.Rule, ss) 

1349 setattr(rs, c.RuleCount, len(ss)) 

1350 

1351 # Remap rule classes 

1352 for r in ss: 

1353 c.SetRuleData( 

1354 r, 

1355 [ 

1356 [klass_map.index(k) for k in klist] 

1357 for klass_map, klist in zip(klass_maps, c.RuleData(r)) 

1358 ], 

1359 ) 

1360 

1361 # Prune empty rulesets 

1362 rss = [rs if rs and getattr(rs, c.Rule) else None for rs in rss] 

1363 while rss and rss[-1] is None: 

1364 del rss[-1] 

1365 setattr(self, c.RuleSet, rss) 

1366 setattr(self, c.RuleSetCount, len(rss)) 

1367 

1368 # TODO: We can do a second round of remapping class values based 

1369 # on classes that are actually used in at least one rule. Right 

1370 # now we subset classes to c.glyphs only. Or better, rewrite 

1371 # the above to do that. 

1372 

1373 return bool(rss) 

1374 elif self.Format == 3: 

1375 return all(x is not None and x.subset(s.glyphs) for x in c.RuleData(self)) 

1376 else: 

1377 assert 0, "unknown format: %s" % self.Format 

1378 

1379 

1380@_add_method( 

1381 otTables.ContextSubst, 

1382 otTables.ChainContextSubst, 

1383 otTables.ContextPos, 

1384 otTables.ChainContextPos, 

1385) 

1386def subset_lookups(self, lookup_indices): 

1387 c = self.__subset_classify_context() 

1388 

1389 if self.Format in [1, 2]: 

1390 for rs in getattr(self, c.RuleSet): 

1391 if not rs: 

1392 continue 

1393 for r in getattr(rs, c.Rule): 

1394 if not r: 

1395 continue 

1396 setattr( 

1397 r, 

1398 c.LookupRecord, 

1399 [ 

1400 ll 

1401 for ll in getattr(r, c.LookupRecord) 

1402 if ll and ll.LookupListIndex in lookup_indices 

1403 ], 

1404 ) 

1405 for ll in getattr(r, c.LookupRecord): 

1406 if not ll: 

1407 continue 

1408 ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) 

1409 elif self.Format == 3: 

1410 setattr( 

1411 self, 

1412 c.LookupRecord, 

1413 [ 

1414 ll 

1415 for ll in getattr(self, c.LookupRecord) 

1416 if ll and ll.LookupListIndex in lookup_indices 

1417 ], 

1418 ) 

1419 for ll in getattr(self, c.LookupRecord): 

1420 if not ll: 

1421 continue 

1422 ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) 

1423 else: 

1424 assert 0, "unknown format: %s" % self.Format 

1425 

1426 

1427@_add_method( 

1428 otTables.ContextSubst, 

1429 otTables.ChainContextSubst, 

1430 otTables.ContextPos, 

1431 otTables.ChainContextPos, 

1432) 

1433def collect_lookups(self): 

1434 c = self.__subset_classify_context() 

1435 

1436 if self.Format in [1, 2]: 

1437 return [ 

1438 ll.LookupListIndex 

1439 for rs in getattr(self, c.RuleSet) 

1440 if rs 

1441 for r in getattr(rs, c.Rule) 

1442 if r 

1443 for ll in getattr(r, c.LookupRecord) 

1444 if ll 

1445 ] 

1446 elif self.Format == 3: 

1447 return [ll.LookupListIndex for ll in getattr(self, c.LookupRecord) if ll] 

1448 else: 

1449 assert 0, "unknown format: %s" % self.Format 

1450 

1451 

1452@_add_method(otTables.ExtensionSubst) 

1453def closure_glyphs(self, s, cur_glyphs): 

1454 if self.Format == 1: 

1455 self.ExtSubTable.closure_glyphs(s, cur_glyphs) 

1456 else: 

1457 assert 0, "unknown format: %s" % self.Format 

1458 

1459 

1460@_add_method(otTables.ExtensionSubst) 

1461def may_have_non_1to1(self): 

1462 if self.Format == 1: 

1463 return self.ExtSubTable.may_have_non_1to1() 

1464 else: 

1465 assert 0, "unknown format: %s" % self.Format 

1466 

1467 

1468@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos) 

1469def subset_glyphs(self, s): 

1470 if self.Format == 1: 

1471 return self.ExtSubTable.subset_glyphs(s) 

1472 else: 

1473 assert 0, "unknown format: %s" % self.Format 

1474 

1475 

1476@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos) 

1477def prune_post_subset(self, font, options): 

1478 if self.Format == 1: 

1479 return self.ExtSubTable.prune_post_subset(font, options) 

1480 else: 

1481 assert 0, "unknown format: %s" % self.Format 

1482 

1483 

1484@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos) 

1485def subset_lookups(self, lookup_indices): 

1486 if self.Format == 1: 

1487 return self.ExtSubTable.subset_lookups(lookup_indices) 

1488 else: 

1489 assert 0, "unknown format: %s" % self.Format 

1490 

1491 

1492@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos) 

1493def collect_lookups(self): 

1494 if self.Format == 1: 

1495 return self.ExtSubTable.collect_lookups() 

1496 else: 

1497 assert 0, "unknown format: %s" % self.Format 

1498 

1499 

1500@_add_method(otTables.Lookup) 

1501def closure_glyphs(self, s, cur_glyphs=None): 

1502 if cur_glyphs is None: 

1503 cur_glyphs = frozenset(s.glyphs) 

1504 

1505 # Memoize 

1506 key = id(self) 

1507 doneLookups = s._doneLookups 

1508 count, covered = doneLookups.get(key, (0, None)) 

1509 if count != len(s.glyphs): 

1510 count, covered = doneLookups[key] = (len(s.glyphs), set()) 

1511 if cur_glyphs.issubset(covered): 

1512 return 

1513 covered.update(cur_glyphs) 

1514 

1515 for st in self.SubTable: 

1516 if not st: 

1517 continue 

1518 st.closure_glyphs(s, cur_glyphs) 

1519 

1520 

1521@_add_method(otTables.Lookup) 

1522def subset_glyphs(self, s): 

1523 self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)] 

1524 self.SubTableCount = len(self.SubTable) 

1525 if hasattr(self, "MarkFilteringSet") and self.MarkFilteringSet is not None: 

1526 if self.MarkFilteringSet not in s.used_mark_sets: 

1527 self.MarkFilteringSet = None 

1528 self.LookupFlag &= ~0x10 

1529 else: 

1530 self.MarkFilteringSet = s.used_mark_sets.index(self.MarkFilteringSet) 

1531 return bool(self.SubTableCount) 

1532 

1533 

1534@_add_method(otTables.Lookup) 

1535def prune_post_subset(self, font, options): 

1536 ret = False 

1537 for st in self.SubTable: 

1538 if not st: 

1539 continue 

1540 if st.prune_post_subset(font, options): 

1541 ret = True 

1542 return ret 

1543 

1544 

1545@_add_method(otTables.Lookup) 

1546def subset_lookups(self, lookup_indices): 

1547 for s in self.SubTable: 

1548 s.subset_lookups(lookup_indices) 

1549 

1550 

1551@_add_method(otTables.Lookup) 

1552def collect_lookups(self): 

1553 return sum((st.collect_lookups() for st in self.SubTable if st), []) 

1554 

1555 

1556@_add_method(otTables.Lookup) 

1557def may_have_non_1to1(self): 

1558 return any(st.may_have_non_1to1() for st in self.SubTable if st) 

1559 

1560 

1561@_add_method(otTables.LookupList) 

1562def subset_glyphs(self, s): 

1563 """Returns the indices of nonempty lookups.""" 

1564 return [i for i, l in enumerate(self.Lookup) if l and l.subset_glyphs(s)] 

1565 

1566 

1567@_add_method(otTables.LookupList) 

1568def prune_post_subset(self, font, options): 

1569 ret = False 

1570 for l in self.Lookup: 

1571 if not l: 

1572 continue 

1573 if l.prune_post_subset(font, options): 

1574 ret = True 

1575 return ret 

1576 

1577 

1578@_add_method(otTables.LookupList) 

1579def subset_lookups(self, lookup_indices): 

1580 self.ensureDecompiled() 

1581 self.Lookup = [self.Lookup[i] for i in lookup_indices if i < self.LookupCount] 

1582 self.LookupCount = len(self.Lookup) 

1583 for l in self.Lookup: 

1584 l.subset_lookups(lookup_indices) 

1585 

1586 

1587@_add_method(otTables.LookupList) 

1588def neuter_lookups(self, lookup_indices): 

1589 """Sets lookups not in lookup_indices to None.""" 

1590 self.ensureDecompiled() 

1591 self.Lookup = [ 

1592 l if i in lookup_indices else None for i, l in enumerate(self.Lookup) 

1593 ] 

1594 

1595 

1596@_add_method(otTables.LookupList) 

1597def closure_lookups(self, lookup_indices): 

1598 """Returns sorted index of all lookups reachable from lookup_indices.""" 

1599 lookup_indices = _uniq_sort(lookup_indices) 

1600 recurse = lookup_indices 

1601 while True: 

1602 recurse_lookups = sum( 

1603 (self.Lookup[i].collect_lookups() for i in recurse if i < self.LookupCount), 

1604 [], 

1605 ) 

1606 recurse_lookups = [ 

1607 l 

1608 for l in recurse_lookups 

1609 if l not in lookup_indices and l < self.LookupCount 

1610 ] 

1611 if not recurse_lookups: 

1612 return _uniq_sort(lookup_indices) 

1613 recurse_lookups = _uniq_sort(recurse_lookups) 

1614 lookup_indices.extend(recurse_lookups) 

1615 recurse = recurse_lookups 

1616 

1617 

1618@_add_method(otTables.Feature) 

1619def subset_lookups(self, lookup_indices): 

1620 """ "Returns True if feature is non-empty afterwards.""" 

1621 self.LookupListIndex = [l for l in self.LookupListIndex if l in lookup_indices] 

1622 # Now map them. 

1623 self.LookupListIndex = [lookup_indices.index(l) for l in self.LookupListIndex] 

1624 self.LookupCount = len(self.LookupListIndex) 

1625 # keep 'size' feature even if it contains no lookups; but drop any other 

1626 # empty feature (e.g. FeatureParams for stylistic set names) 

1627 # https://github.com/fonttools/fonttools/issues/2324 

1628 return self.LookupCount or isinstance( 

1629 self.FeatureParams, otTables.FeatureParamsSize 

1630 ) 

1631 

1632 

1633@_add_method(otTables.FeatureList) 

1634def subset_lookups(self, lookup_indices): 

1635 """Returns the indices of nonempty features.""" 

1636 # Note: Never ever drop feature 'pref', even if it's empty. 

1637 # HarfBuzz chooses shaper for Khmer based on presence of this 

1638 # feature. See thread at: 

1639 # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html 

1640 return [ 

1641 i 

1642 for i, f in enumerate(self.FeatureRecord) 

1643 if (f.Feature.subset_lookups(lookup_indices) or f.FeatureTag == "pref") 

1644 ] 

1645 

1646 

1647@_add_method(otTables.FeatureList) 

1648def collect_lookups(self, feature_indices): 

1649 return sum( 

1650 ( 

1651 self.FeatureRecord[i].Feature.LookupListIndex 

1652 for i in feature_indices 

1653 if i < self.FeatureCount 

1654 ), 

1655 [], 

1656 ) 

1657 

1658 

1659@_add_method(otTables.FeatureList) 

1660def subset_features(self, feature_indices): 

1661 self.ensureDecompiled() 

1662 self.FeatureRecord = _list_subset(self.FeatureRecord, feature_indices) 

1663 self.FeatureCount = len(self.FeatureRecord) 

1664 return bool(self.FeatureCount) 

1665 

1666 

1667@_add_method(otTables.FeatureTableSubstitution) 

1668def subset_lookups(self, lookup_indices): 

1669 """Returns the indices of nonempty features.""" 

1670 return [ 

1671 r.FeatureIndex 

1672 for r in self.SubstitutionRecord 

1673 if r.Feature.subset_lookups(lookup_indices) 

1674 ] 

1675 

1676 

1677@_add_method(otTables.FeatureVariations) 

1678def subset_lookups(self, lookup_indices): 

1679 """Returns the indices of nonempty features.""" 

1680 return sum( 

1681 ( 

1682 f.FeatureTableSubstitution.subset_lookups(lookup_indices) 

1683 for f in self.FeatureVariationRecord 

1684 ), 

1685 [], 

1686 ) 

1687 

1688 

1689@_add_method(otTables.FeatureVariations) 

1690def collect_lookups(self, feature_indices): 

1691 return sum( 

1692 ( 

1693 r.Feature.LookupListIndex 

1694 for vr in self.FeatureVariationRecord 

1695 for r in vr.FeatureTableSubstitution.SubstitutionRecord 

1696 if r.FeatureIndex in feature_indices 

1697 ), 

1698 [], 

1699 ) 

1700 

1701 

1702@_add_method(otTables.FeatureTableSubstitution) 

1703def subset_features(self, feature_indices): 

1704 self.ensureDecompiled() 

1705 self.SubstitutionRecord = [ 

1706 r for r in self.SubstitutionRecord if r.FeatureIndex in feature_indices 

1707 ] 

1708 # remap feature indices 

1709 for r in self.SubstitutionRecord: 

1710 r.FeatureIndex = feature_indices.index(r.FeatureIndex) 

1711 self.SubstitutionCount = len(self.SubstitutionRecord) 

1712 return bool(self.SubstitutionCount) 

1713 

1714 

1715@_add_method(otTables.FeatureVariations) 

1716def subset_features(self, feature_indices): 

1717 self.ensureDecompiled() 

1718 for r in self.FeatureVariationRecord: 

1719 r.FeatureTableSubstitution.subset_features(feature_indices) 

1720 # Prune empty records at the end only 

1721 # https://github.com/fonttools/fonttools/issues/1881 

1722 while ( 

1723 self.FeatureVariationRecord 

1724 and not self.FeatureVariationRecord[ 

1725 -1 

1726 ].FeatureTableSubstitution.SubstitutionCount 

1727 ): 

1728 self.FeatureVariationRecord.pop() 

1729 self.FeatureVariationCount = len(self.FeatureVariationRecord) 

1730 return bool(self.FeatureVariationCount) 

1731 

1732 

1733@_add_method(otTables.DefaultLangSys, otTables.LangSys) 

1734def subset_features(self, feature_indices): 

1735 if self.ReqFeatureIndex in feature_indices: 

1736 self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex) 

1737 else: 

1738 self.ReqFeatureIndex = 65535 

1739 self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices] 

1740 # Now map them. 

1741 self.FeatureIndex = [ 

1742 feature_indices.index(f) for f in self.FeatureIndex if f in feature_indices 

1743 ] 

1744 self.FeatureCount = len(self.FeatureIndex) 

1745 return bool(self.FeatureCount or self.ReqFeatureIndex != 65535) 

1746 

1747 

1748@_add_method(otTables.DefaultLangSys, otTables.LangSys) 

1749def collect_features(self): 

1750 feature_indices = self.FeatureIndex[:] 

1751 if self.ReqFeatureIndex != 65535: 

1752 feature_indices.append(self.ReqFeatureIndex) 

1753 return _uniq_sort(feature_indices) 

1754 

1755 

1756@_add_method(otTables.Script) 

1757def subset_features(self, feature_indices, keepEmptyDefaultLangSys=False): 

1758 if ( 

1759 self.DefaultLangSys 

1760 and not self.DefaultLangSys.subset_features(feature_indices) 

1761 and not keepEmptyDefaultLangSys 

1762 ): 

1763 self.DefaultLangSys = None 

1764 self.LangSysRecord = [ 

1765 l for l in self.LangSysRecord if l.LangSys.subset_features(feature_indices) 

1766 ] 

1767 self.LangSysCount = len(self.LangSysRecord) 

1768 return bool(self.LangSysCount or self.DefaultLangSys) 

1769 

1770 

1771@_add_method(otTables.Script) 

1772def collect_features(self): 

1773 feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord] 

1774 if self.DefaultLangSys: 

1775 feature_indices.append(self.DefaultLangSys.collect_features()) 

1776 return _uniq_sort(sum(feature_indices, [])) 

1777 

1778 

1779@_add_method(otTables.ScriptList) 

1780def subset_features(self, feature_indices, retain_empty): 

1781 # https://bugzilla.mozilla.org/show_bug.cgi?id=1331737#c32 

1782 self.ScriptRecord = [ 

1783 s 

1784 for s in self.ScriptRecord 

1785 if s.Script.subset_features(feature_indices, s.ScriptTag == "DFLT") 

1786 or retain_empty 

1787 ] 

1788 self.ScriptCount = len(self.ScriptRecord) 

1789 return bool(self.ScriptCount) 

1790 

1791 

1792@_add_method(otTables.ScriptList) 

1793def collect_features(self): 

1794 return _uniq_sort(sum((s.Script.collect_features() for s in self.ScriptRecord), [])) 

1795 

1796 

1797# CBLC will inherit it 

1798@_add_method(ttLib.getTableClass("EBLC")) 

1799def subset_glyphs(self, s): 

1800 for strike in self.strikes: 

1801 for indexSubTable in strike.indexSubTables: 

1802 indexSubTable.names = [n for n in indexSubTable.names if n in s.glyphs] 

1803 strike.indexSubTables = [i for i in strike.indexSubTables if i.names] 

1804 self.strikes = [s for s in self.strikes if s.indexSubTables] 

1805 

1806 return True 

1807 

1808 

1809# CBDT will inherit it 

1810@_add_method(ttLib.getTableClass("EBDT")) 

1811def subset_glyphs(self, s): 

1812 strikeData = [ 

1813 {g: strike[g] for g in s.glyphs if g in strike} for strike in self.strikeData 

1814 ] 

1815 # Prune empty strikes 

1816 # https://github.com/fonttools/fonttools/issues/1633 

1817 self.strikeData = [strike for strike in strikeData if strike] 

1818 return True 

1819 

1820 

1821@_add_method(ttLib.getTableClass("sbix")) 

1822def subset_glyphs(self, s): 

1823 for strike in self.strikes.values(): 

1824 strike.glyphs = {g: strike.glyphs[g] for g in s.glyphs if g in strike.glyphs} 

1825 

1826 return True 

1827 

1828 

1829@_add_method(ttLib.getTableClass("GSUB")) 

1830def closure_glyphs(self, s): 

1831 s.table = self.table 

1832 if self.table.ScriptList: 

1833 feature_indices = self.table.ScriptList.collect_features() 

1834 else: 

1835 feature_indices = [] 

1836 if self.table.FeatureList: 

1837 lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) 

1838 else: 

1839 lookup_indices = [] 

1840 if getattr(self.table, "FeatureVariations", None): 

1841 lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) 

1842 lookup_indices = _uniq_sort(lookup_indices) 

1843 if self.table.LookupList: 

1844 s._doneLookups = {} 

1845 while True: 

1846 orig_glyphs = frozenset(s.glyphs) 

1847 for i in lookup_indices: 

1848 if i >= self.table.LookupList.LookupCount: 

1849 continue 

1850 if not self.table.LookupList.Lookup[i]: 

1851 continue 

1852 self.table.LookupList.Lookup[i].closure_glyphs(s) 

1853 if orig_glyphs == s.glyphs: 

1854 break 

1855 del s._doneLookups 

1856 del s.table 

1857 

1858 

1859@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

1860def subset_glyphs(self, s): 

1861 s.glyphs = s.glyphs_gsubed 

1862 if self.table.LookupList: 

1863 lookup_indices = self.table.LookupList.subset_glyphs(s) 

1864 else: 

1865 lookup_indices = [] 

1866 self.subset_lookups(lookup_indices) 

1867 return True 

1868 

1869 

1870@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

1871def retain_empty_scripts(self): 

1872 # https://github.com/fonttools/fonttools/issues/518 

1873 # https://bugzilla.mozilla.org/show_bug.cgi?id=1080739#c15 

1874 return self.__class__ == ttLib.getTableClass("GSUB") 

1875 

1876 

1877@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

1878def subset_lookups(self, lookup_indices): 

1879 """Retains specified lookups, then removes empty features, language 

1880 systems, and scripts.""" 

1881 if self.table.LookupList: 

1882 self.table.LookupList.subset_lookups(lookup_indices) 

1883 if self.table.FeatureList: 

1884 feature_indices = self.table.FeatureList.subset_lookups(lookup_indices) 

1885 else: 

1886 feature_indices = [] 

1887 if getattr(self.table, "FeatureVariations", None): 

1888 feature_indices += self.table.FeatureVariations.subset_lookups(lookup_indices) 

1889 feature_indices = _uniq_sort(feature_indices) 

1890 if self.table.FeatureList: 

1891 self.table.FeatureList.subset_features(feature_indices) 

1892 if getattr(self.table, "FeatureVariations", None): 

1893 self.table.FeatureVariations.subset_features(feature_indices) 

1894 if self.table.ScriptList: 

1895 self.table.ScriptList.subset_features( 

1896 feature_indices, self.retain_empty_scripts() 

1897 ) 

1898 

1899 

1900@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

1901def neuter_lookups(self, lookup_indices): 

1902 """Sets lookups not in lookup_indices to None.""" 

1903 if self.table.LookupList: 

1904 self.table.LookupList.neuter_lookups(lookup_indices) 

1905 

1906 

1907@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

1908def prune_lookups(self, remap=True): 

1909 """Remove (default) or neuter unreferenced lookups""" 

1910 if self.table.ScriptList: 

1911 feature_indices = self.table.ScriptList.collect_features() 

1912 else: 

1913 feature_indices = [] 

1914 if self.table.FeatureList: 

1915 lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) 

1916 else: 

1917 lookup_indices = [] 

1918 if getattr(self.table, "FeatureVariations", None): 

1919 lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) 

1920 lookup_indices = _uniq_sort(lookup_indices) 

1921 if self.table.LookupList: 

1922 lookup_indices = self.table.LookupList.closure_lookups(lookup_indices) 

1923 else: 

1924 lookup_indices = [] 

1925 if remap: 

1926 self.subset_lookups(lookup_indices) 

1927 else: 

1928 self.neuter_lookups(lookup_indices) 

1929 

1930 

1931@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

1932def subset_feature_tags(self, feature_tags): 

1933 if self.table.FeatureList: 

1934 feature_indices = [ 

1935 i 

1936 for i, f in enumerate(self.table.FeatureList.FeatureRecord) 

1937 if f.FeatureTag in feature_tags 

1938 ] 

1939 self.table.FeatureList.subset_features(feature_indices) 

1940 if getattr(self.table, "FeatureVariations", None): 

1941 self.table.FeatureVariations.subset_features(feature_indices) 

1942 else: 

1943 feature_indices = [] 

1944 if self.table.ScriptList: 

1945 self.table.ScriptList.subset_features( 

1946 feature_indices, self.retain_empty_scripts() 

1947 ) 

1948 

1949 

1950@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

1951def subset_script_tags(self, tags): 

1952 langsys = {} 

1953 script_tags = set() 

1954 for tag in tags: 

1955 script_tag, lang_tag = tag.split(".") if "." in tag else (tag, "*") 

1956 script_tags.add(script_tag.ljust(4)) 

1957 langsys.setdefault(script_tag, set()).add(lang_tag.ljust(4)) 

1958 

1959 if self.table.ScriptList: 

1960 self.table.ScriptList.ScriptRecord = [ 

1961 s for s in self.table.ScriptList.ScriptRecord if s.ScriptTag in script_tags 

1962 ] 

1963 self.table.ScriptList.ScriptCount = len(self.table.ScriptList.ScriptRecord) 

1964 

1965 for record in self.table.ScriptList.ScriptRecord: 

1966 if record.ScriptTag in langsys and "* " not in langsys[record.ScriptTag]: 

1967 record.Script.LangSysRecord = [ 

1968 l 

1969 for l in record.Script.LangSysRecord 

1970 if l.LangSysTag in langsys[record.ScriptTag] 

1971 ] 

1972 record.Script.LangSysCount = len(record.Script.LangSysRecord) 

1973 if "dflt" not in langsys[record.ScriptTag]: 

1974 record.Script.DefaultLangSys = None 

1975 

1976 

1977@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

1978def prune_features(self): 

1979 """Remove unreferenced features""" 

1980 if self.table.ScriptList: 

1981 feature_indices = self.table.ScriptList.collect_features() 

1982 else: 

1983 feature_indices = [] 

1984 if self.table.FeatureList: 

1985 self.table.FeatureList.subset_features(feature_indices) 

1986 if getattr(self.table, "FeatureVariations", None): 

1987 self.table.FeatureVariations.subset_features(feature_indices) 

1988 if self.table.ScriptList: 

1989 self.table.ScriptList.subset_features( 

1990 feature_indices, self.retain_empty_scripts() 

1991 ) 

1992 

1993 

1994@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

1995def prune_pre_subset(self, font, options): 

1996 # Drop undesired features 

1997 if "*" not in options.layout_scripts: 

1998 self.subset_script_tags(options.layout_scripts) 

1999 if "*" not in options.layout_features: 

2000 self.subset_feature_tags(options.layout_features) 

2001 # Neuter unreferenced lookups 

2002 self.prune_lookups(remap=False) 

2003 return True 

2004 

2005 

2006@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

2007def remove_redundant_langsys(self): 

2008 table = self.table 

2009 if not table.ScriptList or not table.FeatureList: 

2010 return 

2011 

2012 features = table.FeatureList.FeatureRecord 

2013 

2014 for s in table.ScriptList.ScriptRecord: 

2015 d = s.Script.DefaultLangSys 

2016 if not d: 

2017 continue 

2018 for lr in s.Script.LangSysRecord[:]: 

2019 l = lr.LangSys 

2020 # Compare d and l 

2021 if len(d.FeatureIndex) != len(l.FeatureIndex): 

2022 continue 

2023 if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535): 

2024 continue 

2025 

2026 if d.ReqFeatureIndex != 65535: 

2027 if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]: 

2028 continue 

2029 

2030 for i in range(len(d.FeatureIndex)): 

2031 if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]: 

2032 break 

2033 else: 

2034 # LangSys and default are equal; delete LangSys 

2035 s.Script.LangSysRecord.remove(lr) 

2036 

2037 

2038@_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS")) 

2039def prune_post_subset(self, font, options): 

2040 table = self.table 

2041 

2042 self.prune_lookups() # XXX Is this actually needed?! 

2043 

2044 if table.LookupList: 

2045 table.LookupList.prune_post_subset(font, options) 

2046 # XXX Next two lines disabled because OTS is stupid and 

2047 # doesn't like NULL offsets here. 

2048 # if not table.LookupList.Lookup: 

2049 # table.LookupList = None 

2050 

2051 if not table.LookupList: 

2052 table.FeatureList = None 

2053 

2054 if table.FeatureList: 

2055 self.remove_redundant_langsys() 

2056 # Remove unreferenced features 

2057 self.prune_features() 

2058 

2059 # XXX Next two lines disabled because OTS is stupid and 

2060 # doesn't like NULL offsets here. 

2061 # if table.FeatureList and not table.FeatureList.FeatureRecord: 

2062 # table.FeatureList = None 

2063 

2064 # Never drop scripts themselves as them just being available 

2065 # holds semantic significance. 

2066 # XXX Next two lines disabled because OTS is stupid and 

2067 # doesn't like NULL offsets here. 

2068 # if table.ScriptList and not table.ScriptList.ScriptRecord: 

2069 # table.ScriptList = None 

2070 

2071 if hasattr(table, "FeatureVariations"): 

2072 # drop FeatureVariations if there are no features to substitute 

2073 if table.FeatureVariations and not ( 

2074 table.FeatureList and table.FeatureVariations.FeatureVariationRecord 

2075 ): 

2076 table.FeatureVariations = None 

2077 

2078 # downgrade table version if there are no FeatureVariations 

2079 if not table.FeatureVariations and table.Version == 0x00010001: 

2080 table.Version = 0x00010000 

2081 

2082 return True 

2083 

2084 

2085@_add_method(ttLib.getTableClass("GDEF")) 

2086def subset_glyphs(self, s): 

2087 glyphs = s.glyphs_gsubed 

2088 table = self.table 

2089 if table.LigCaretList: 

2090 indices = table.LigCaretList.Coverage.subset(glyphs) 

2091 table.LigCaretList.LigGlyph = _list_subset(table.LigCaretList.LigGlyph, indices) 

2092 table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph) 

2093 if table.MarkAttachClassDef: 

2094 table.MarkAttachClassDef.classDefs = { 

2095 g: v for g, v in table.MarkAttachClassDef.classDefs.items() if g in glyphs 

2096 } 

2097 if table.GlyphClassDef: 

2098 table.GlyphClassDef.classDefs = { 

2099 g: v for g, v in table.GlyphClassDef.classDefs.items() if g in glyphs 

2100 } 

2101 if table.AttachList: 

2102 indices = table.AttachList.Coverage.subset(glyphs) 

2103 GlyphCount = table.AttachList.GlyphCount 

2104 table.AttachList.AttachPoint = [ 

2105 table.AttachList.AttachPoint[i] for i in indices if i < GlyphCount 

2106 ] 

2107 table.AttachList.GlyphCount = len(table.AttachList.AttachPoint) 

2108 if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef: 

2109 markGlyphSets = table.MarkGlyphSetsDef 

2110 for coverage in markGlyphSets.Coverage: 

2111 if coverage: 

2112 coverage.subset(glyphs) 

2113 

2114 s.used_mark_sets = [i for i, c in enumerate(markGlyphSets.Coverage) if c.glyphs] 

2115 markGlyphSets.Coverage = [c for c in markGlyphSets.Coverage if c.glyphs] 

2116 

2117 return True 

2118 

2119 

2120def _pruneGDEF(font): 

2121 if "GDEF" not in font: 

2122 return 

2123 gdef = font["GDEF"] 

2124 table = gdef.table 

2125 if not hasattr(table, "VarStore"): 

2126 return 

2127 

2128 store = table.VarStore 

2129 

2130 usedVarIdxes = set() 

2131 

2132 # Collect. 

2133 table.collect_device_varidxes(usedVarIdxes) 

2134 if "GPOS" in font: 

2135 font["GPOS"].table.collect_device_varidxes(usedVarIdxes) 

2136 

2137 # Subset. 

2138 varidx_map = store.subset_varidxes(usedVarIdxes) 

2139 

2140 # Map. 

2141 table.remap_device_varidxes(varidx_map) 

2142 if "GPOS" in font: 

2143 font["GPOS"].table.remap_device_varidxes(varidx_map) 

2144 

2145 

2146@_add_method(ttLib.getTableClass("GDEF")) 

2147def prune_post_subset(self, font, options): 

2148 table = self.table 

2149 # XXX check these against OTS 

2150 if table.LigCaretList and not table.LigCaretList.LigGlyphCount: 

2151 table.LigCaretList = None 

2152 if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs: 

2153 table.MarkAttachClassDef = None 

2154 if table.GlyphClassDef and not table.GlyphClassDef.classDefs: 

2155 table.GlyphClassDef = None 

2156 if table.AttachList and not table.AttachList.GlyphCount: 

2157 table.AttachList = None 

2158 if hasattr(table, "VarStore"): 

2159 _pruneGDEF(font) 

2160 if table.VarStore.VarDataCount == 0: 

2161 if table.Version == 0x00010003: 

2162 table.Version = 0x00010002 

2163 if ( 

2164 not hasattr(table, "MarkGlyphSetsDef") 

2165 or not table.MarkGlyphSetsDef 

2166 or not table.MarkGlyphSetsDef.Coverage 

2167 ): 

2168 table.MarkGlyphSetsDef = None 

2169 if table.Version == 0x00010002: 

2170 table.Version = 0x00010000 

2171 return bool( 

2172 table.LigCaretList 

2173 or table.MarkAttachClassDef 

2174 or table.GlyphClassDef 

2175 or table.AttachList 

2176 or (table.Version >= 0x00010002 and table.MarkGlyphSetsDef) 

2177 or (table.Version >= 0x00010003 and table.VarStore) 

2178 ) 

2179 

2180 

2181@_add_method(ttLib.getTableClass("kern")) 

2182def prune_pre_subset(self, font, options): 

2183 # Prune unknown kern table types 

2184 self.kernTables = [t for t in self.kernTables if hasattr(t, "kernTable")] 

2185 return bool(self.kernTables) 

2186 

2187 

2188@_add_method(ttLib.getTableClass("kern")) 

2189def subset_glyphs(self, s): 

2190 glyphs = s.glyphs_gsubed 

2191 for t in self.kernTables: 

2192 t.kernTable = { 

2193 (a, b): v 

2194 for (a, b), v in t.kernTable.items() 

2195 if a in glyphs and b in glyphs 

2196 } 

2197 self.kernTables = [t for t in self.kernTables if t.kernTable] 

2198 return bool(self.kernTables) 

2199 

2200 

2201@_add_method(ttLib.getTableClass("vmtx")) 

2202def subset_glyphs(self, s): 

2203 self.metrics = _dict_subset(self.metrics, s.glyphs) 

2204 for g in s.glyphs_emptied: 

2205 self.metrics[g] = (0, 0) 

2206 return bool(self.metrics) 

2207 

2208 

2209@_add_method(ttLib.getTableClass("hmtx")) 

2210def subset_glyphs(self, s): 

2211 self.metrics = _dict_subset(self.metrics, s.glyphs) 

2212 for g in s.glyphs_emptied: 

2213 self.metrics[g] = (0, 0) 

2214 return True # Required table 

2215 

2216 

2217@_add_method(ttLib.getTableClass("hdmx")) 

2218def subset_glyphs(self, s): 

2219 self.hdmx = {sz: _dict_subset(l, s.glyphs) for sz, l in self.hdmx.items()} 

2220 for sz in self.hdmx: 

2221 for g in s.glyphs_emptied: 

2222 self.hdmx[sz][g] = 0 

2223 return bool(self.hdmx) 

2224 

2225 

2226@_add_method(ttLib.getTableClass("ankr")) 

2227def subset_glyphs(self, s): 

2228 table = self.table.AnchorPoints 

2229 assert table.Format == 0, "unknown 'ankr' format %s" % table.Format 

2230 table.Anchors = { 

2231 glyph: table.Anchors[glyph] for glyph in s.glyphs if glyph in table.Anchors 

2232 } 

2233 return len(table.Anchors) > 0 

2234 

2235 

2236@_add_method(ttLib.getTableClass("bsln")) 

2237def closure_glyphs(self, s): 

2238 table = self.table.Baseline 

2239 if table.Format in (2, 3): 

2240 s.glyphs.add(table.StandardGlyph) 

2241 

2242 

2243@_add_method(ttLib.getTableClass("bsln")) 

2244def subset_glyphs(self, s): 

2245 table = self.table.Baseline 

2246 if table.Format in (1, 3): 

2247 baselines = { 

2248 glyph: table.BaselineValues.get(glyph, table.DefaultBaseline) 

2249 for glyph in s.glyphs 

2250 } 

2251 if len(baselines) > 0: 

2252 mostCommon, _cnt = Counter(baselines.values()).most_common(1)[0] 

2253 table.DefaultBaseline = mostCommon 

2254 baselines = {glyph: b for glyph, b in baselines.items() if b != mostCommon} 

2255 if len(baselines) > 0: 

2256 table.BaselineValues = baselines 

2257 else: 

2258 table.Format = {1: 0, 3: 2}[table.Format] 

2259 del table.BaselineValues 

2260 return True 

2261 

2262 

2263@_add_method(ttLib.getTableClass("lcar")) 

2264def subset_glyphs(self, s): 

2265 table = self.table.LigatureCarets 

2266 if table.Format in (0, 1): 

2267 table.Carets = { 

2268 glyph: table.Carets[glyph] for glyph in s.glyphs if glyph in table.Carets 

2269 } 

2270 return len(table.Carets) > 0 

2271 else: 

2272 assert False, "unknown 'lcar' format %s" % table.Format 

2273 

2274 

2275@_add_method(ttLib.getTableClass("gvar")) 

2276def prune_pre_subset(self, font, options): 

2277 if options.notdef_glyph and not options.notdef_outline: 

2278 self.variations[font.glyphOrder[0]] = [] 

2279 return True 

2280 

2281 

2282@_add_method(ttLib.getTableClass("gvar")) 

2283def subset_glyphs(self, s): 

2284 self.variations = _dict_subset(self.variations, s.glyphs) 

2285 self.glyphCount = len(self.variations) 

2286 return bool(self.variations) 

2287 

2288 

2289def _remap_index_map(s, varidx_map, table_map): 

2290 map_ = {k: varidx_map[v] for k, v in table_map.mapping.items()} 

2291 # Emptied glyphs are remapped to: 

2292 # if GID <= last retained GID, 0/0: delta set for 0/0 is expected to exist & zeros compress well 

2293 # if GID > last retained GID, major/minor of the last retained glyph: will be optimized out by table compiler 

2294 last_idx = varidx_map[table_map.mapping[s.last_retained_glyph]] 

2295 for g, i in s.reverseEmptiedGlyphMap.items(): 

2296 map_[g] = last_idx if i > s.last_retained_order else 0 

2297 return map_ 

2298 

2299 

2300@_add_method(ttLib.getTableClass("HVAR")) 

2301def subset_glyphs(self, s): 

2302 table = self.table 

2303 

2304 used = set() 

2305 advIdxes_ = set() 

2306 retainAdvMap = False 

2307 

2308 if table.AdvWidthMap: 

2309 table.AdvWidthMap.mapping = _dict_subset(table.AdvWidthMap.mapping, s.glyphs) 

2310 used.update(table.AdvWidthMap.mapping.values()) 

2311 else: 

2312 used.update(s.reverseOrigGlyphMap.values()) 

2313 advIdxes_ = used.copy() 

2314 retainAdvMap = s.options.retain_gids 

2315 

2316 if table.LsbMap: 

2317 table.LsbMap.mapping = _dict_subset(table.LsbMap.mapping, s.glyphs) 

2318 used.update(table.LsbMap.mapping.values()) 

2319 if table.RsbMap: 

2320 table.RsbMap.mapping = _dict_subset(table.RsbMap.mapping, s.glyphs) 

2321 used.update(table.RsbMap.mapping.values()) 

2322 

2323 varidx_map = table.VarStore.subset_varidxes( 

2324 used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_ 

2325 ) 

2326 

2327 if table.AdvWidthMap: 

2328 table.AdvWidthMap.mapping = _remap_index_map(s, varidx_map, table.AdvWidthMap) 

2329 if table.LsbMap: 

2330 table.LsbMap.mapping = _remap_index_map(s, varidx_map, table.LsbMap) 

2331 if table.RsbMap: 

2332 table.RsbMap.mapping = _remap_index_map(s, varidx_map, table.RsbMap) 

2333 

2334 # TODO Return emptiness... 

2335 return True 

2336 

2337 

2338@_add_method(ttLib.getTableClass("VVAR")) 

2339def subset_glyphs(self, s): 

2340 table = self.table 

2341 

2342 used = set() 

2343 advIdxes_ = set() 

2344 retainAdvMap = False 

2345 

2346 if table.AdvHeightMap: 

2347 table.AdvHeightMap.mapping = _dict_subset(table.AdvHeightMap.mapping, s.glyphs) 

2348 used.update(table.AdvHeightMap.mapping.values()) 

2349 else: 

2350 used.update(s.reverseOrigGlyphMap.values()) 

2351 advIdxes_ = used.copy() 

2352 retainAdvMap = s.options.retain_gids 

2353 

2354 if table.TsbMap: 

2355 table.TsbMap.mapping = _dict_subset(table.TsbMap.mapping, s.glyphs) 

2356 used.update(table.TsbMap.mapping.values()) 

2357 if table.BsbMap: 

2358 table.BsbMap.mapping = _dict_subset(table.BsbMap.mapping, s.glyphs) 

2359 used.update(table.BsbMap.mapping.values()) 

2360 if table.VOrgMap: 

2361 table.VOrgMap.mapping = _dict_subset(table.VOrgMap.mapping, s.glyphs) 

2362 used.update(table.VOrgMap.mapping.values()) 

2363 

2364 varidx_map = table.VarStore.subset_varidxes( 

2365 used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_ 

2366 ) 

2367 

2368 if table.AdvHeightMap: 

2369 table.AdvHeightMap.mapping = _remap_index_map(s, varidx_map, table.AdvHeightMap) 

2370 if table.TsbMap: 

2371 table.TsbMap.mapping = _remap_index_map(s, varidx_map, table.TsbMap) 

2372 if table.BsbMap: 

2373 table.BsbMap.mapping = _remap_index_map(s, varidx_map, table.BsbMap) 

2374 if table.VOrgMap: 

2375 table.VOrgMap.mapping = _remap_index_map(s, varidx_map, table.VOrgMap) 

2376 

2377 # TODO Return emptiness... 

2378 return True 

2379 

2380 

2381@_add_method(ttLib.getTableClass("VORG")) 

2382def subset_glyphs(self, s): 

2383 self.VOriginRecords = { 

2384 g: v for g, v in self.VOriginRecords.items() if g in s.glyphs 

2385 } 

2386 self.numVertOriginYMetrics = len(self.VOriginRecords) 

2387 return True # Never drop; has default metrics 

2388 

2389 

2390@_add_method(ttLib.getTableClass("opbd")) 

2391def subset_glyphs(self, s): 

2392 table = self.table.OpticalBounds 

2393 if table.Format == 0: 

2394 table.OpticalBoundsDeltas = { 

2395 glyph: table.OpticalBoundsDeltas[glyph] 

2396 for glyph in s.glyphs 

2397 if glyph in table.OpticalBoundsDeltas 

2398 } 

2399 return len(table.OpticalBoundsDeltas) > 0 

2400 elif table.Format == 1: 

2401 table.OpticalBoundsPoints = { 

2402 glyph: table.OpticalBoundsPoints[glyph] 

2403 for glyph in s.glyphs 

2404 if glyph in table.OpticalBoundsPoints 

2405 } 

2406 return len(table.OpticalBoundsPoints) > 0 

2407 else: 

2408 assert False, "unknown 'opbd' format %s" % table.Format 

2409 

2410 

2411@_add_method(ttLib.getTableClass("post")) 

2412def prune_pre_subset(self, font, options): 

2413 if not options.glyph_names: 

2414 self.formatType = 3.0 

2415 return True # Required table 

2416 

2417 

2418@_add_method(ttLib.getTableClass("post")) 

2419def subset_glyphs(self, s): 

2420 self.extraNames = [] # This seems to do it 

2421 return True # Required table 

2422 

2423 

2424@_add_method(ttLib.getTableClass("prop")) 

2425def subset_glyphs(self, s): 

2426 prop = self.table.GlyphProperties 

2427 if prop.Format == 0: 

2428 return prop.DefaultProperties != 0 

2429 elif prop.Format == 1: 

2430 prop.Properties = { 

2431 g: prop.Properties.get(g, prop.DefaultProperties) for g in s.glyphs 

2432 } 

2433 mostCommon, _cnt = Counter(prop.Properties.values()).most_common(1)[0] 

2434 prop.DefaultProperties = mostCommon 

2435 prop.Properties = { 

2436 g: prop for g, prop in prop.Properties.items() if prop != mostCommon 

2437 } 

2438 if len(prop.Properties) == 0: 

2439 del prop.Properties 

2440 prop.Format = 0 

2441 return prop.DefaultProperties != 0 

2442 return True 

2443 else: 

2444 assert False, "unknown 'prop' format %s" % prop.Format 

2445 

2446 

2447def _paint_glyph_names(paint, colr): 

2448 result = set() 

2449 

2450 def callback(paint): 

2451 if paint.Format in { 

2452 otTables.PaintFormat.PaintGlyph, 

2453 otTables.PaintFormat.PaintColrGlyph, 

2454 }: 

2455 result.add(paint.Glyph) 

2456 

2457 paint.traverse(colr, callback) 

2458 return result 

2459 

2460 

2461@_add_method(ttLib.getTableClass("COLR")) 

2462def closure_glyphs(self, s): 

2463 if self.version > 0: 

2464 # on decompiling COLRv1, we only keep around the raw otTables 

2465 # but for subsetting we need dicts with fully decompiled layers; 

2466 # we store them temporarily in the C_O_L_R_ instance and delete 

2467 # them after we have finished subsetting. 

2468 self.ColorLayers = self._decompileColorLayersV0(self.table) 

2469 self.ColorLayersV1 = { 

2470 rec.BaseGlyph: rec.Paint 

2471 for rec in self.table.BaseGlyphList.BaseGlyphPaintRecord 

2472 } 

2473 

2474 decompose = s.glyphs 

2475 while decompose: 

2476 layers = set() 

2477 for g in decompose: 

2478 for layer in self.ColorLayers.get(g, []): 

2479 layers.add(layer.name) 

2480 

2481 if self.version > 0: 

2482 paint = self.ColorLayersV1.get(g) 

2483 if paint is not None: 

2484 layers.update(_paint_glyph_names(paint, self.table)) 

2485 

2486 layers -= s.glyphs 

2487 s.glyphs.update(layers) 

2488 decompose = layers 

2489 

2490 

2491@_add_method(ttLib.getTableClass("COLR")) 

2492def subset_glyphs(self, s): 

2493 from fontTools.colorLib.unbuilder import unbuildColrV1 

2494 from fontTools.colorLib.builder import buildColrV1, populateCOLRv0 

2495 

2496 # only include glyphs after COLR closure, which in turn comes after cmap and GSUB 

2497 # closure, but importantly before glyf/CFF closures. COLR layers can refer to 

2498 # composite glyphs, and that's ok, since glyf/CFF closures happen after COLR closure 

2499 # and take care of those. If we also included glyphs resulting from glyf/CFF closures 

2500 # when deciding which COLR base glyphs to retain, then we may end up with a situation 

2501 # whereby a COLR base glyph is kept, not because directly requested (cmap) 

2502 # or substituted (GSUB) or referenced by another COLRv1 PaintColrGlyph, but because 

2503 # it corresponds to (has same GID as) a non-COLR glyph that happens to be used as a 

2504 # component in glyf or CFF table. Best case scenario we retain more glyphs than 

2505 # required; worst case we retain incomplete COLR records that try to reference 

2506 # glyphs that are no longer in the final subset font. 

2507 # https://github.com/fonttools/fonttools/issues/2461 

2508 s.glyphs = s.glyphs_colred 

2509 

2510 self.ColorLayers = { 

2511 g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers 

2512 } 

2513 if self.version == 0: 

2514 return bool(self.ColorLayers) 

2515 

2516 colorGlyphsV1 = unbuildColrV1(self.table.LayerList, self.table.BaseGlyphList) 

2517 self.table.LayerList, self.table.BaseGlyphList = buildColrV1( 

2518 {g: colorGlyphsV1[g] for g in colorGlyphsV1 if g in s.glyphs} 

2519 ) 

2520 del self.ColorLayersV1 

2521 

2522 if self.table.ClipList is not None: 

2523 clips = self.table.ClipList.clips 

2524 self.table.ClipList.clips = {g: clips[g] for g in clips if g in s.glyphs} 

2525 

2526 layersV0 = self.ColorLayers 

2527 if not self.table.BaseGlyphList.BaseGlyphPaintRecord: 

2528 # no more COLRv1 glyphs: downgrade to version 0 

2529 self.version = 0 

2530 del self.table 

2531 return bool(layersV0) 

2532 

2533 populateCOLRv0( 

2534 self.table, 

2535 {g: [(layer.name, layer.colorID) for layer in layersV0[g]] for g in layersV0}, 

2536 ) 

2537 del self.ColorLayers 

2538 

2539 # TODO: also prune ununsed varIndices in COLR.VarStore 

2540 return True 

2541 

2542 

2543@_add_method(ttLib.getTableClass("CPAL")) 

2544def prune_post_subset(self, font, options): 

2545 # Keep whole "CPAL" if "SVG " is present as it may be referenced by the latter 

2546 # via 'var(--color{palette_entry_index}, ...)' CSS color variables. 

2547 # For now we just assume this is the case by the mere presence of "SVG " table, 

2548 # for parsing SVG to collect all the used indices is too much work... 

2549 # TODO(anthrotype): Do The Right Thing (TM). 

2550 if "SVG " in font: 

2551 return True 

2552 

2553 colr = font.get("COLR") 

2554 if not colr: # drop CPAL if COLR was subsetted to empty 

2555 return False 

2556 

2557 colors_by_index = defaultdict(list) 

2558 

2559 def collect_colors_by_index(paint): 

2560 if hasattr(paint, "PaletteIndex"): # either solid colors... 

2561 colors_by_index[paint.PaletteIndex].append(paint) 

2562 elif hasattr(paint, "ColorLine"): # ... or gradient color stops 

2563 for stop in paint.ColorLine.ColorStop: 

2564 colors_by_index[stop.PaletteIndex].append(stop) 

2565 

2566 if colr.version == 0: 

2567 for layers in colr.ColorLayers.values(): 

2568 for layer in layers: 

2569 colors_by_index[layer.colorID].append(layer) 

2570 else: 

2571 if colr.table.LayerRecordArray: 

2572 for layer in colr.table.LayerRecordArray.LayerRecord: 

2573 colors_by_index[layer.PaletteIndex].append(layer) 

2574 for record in colr.table.BaseGlyphList.BaseGlyphPaintRecord: 

2575 record.Paint.traverse(colr.table, collect_colors_by_index) 

2576 

2577 # don't remap palette entry index 0xFFFF, this is always the foreground color 

2578 # https://github.com/fonttools/fonttools/issues/2257 

2579 retained_palette_indices = set(colors_by_index.keys()) - {0xFFFF} 

2580 for palette in self.palettes: 

2581 palette[:] = [c for i, c in enumerate(palette) if i in retained_palette_indices] 

2582 assert len(palette) == len(retained_palette_indices) 

2583 

2584 for new_index, old_index in enumerate(sorted(retained_palette_indices)): 

2585 for record in colors_by_index[old_index]: 

2586 if hasattr(record, "colorID"): # v0 

2587 record.colorID = new_index 

2588 elif hasattr(record, "PaletteIndex"): # v1 

2589 record.PaletteIndex = new_index 

2590 else: 

2591 raise AssertionError(record) 

2592 

2593 self.numPaletteEntries = len(self.palettes[0]) 

2594 

2595 if self.version == 1: 

2596 kept_labels = [] 

2597 for i, label in enumerate(self.paletteEntryLabels): 

2598 if i in retained_palette_indices: 

2599 kept_labels.append(label) 

2600 self.paletteEntryLabels = kept_labels 

2601 return bool(self.numPaletteEntries) 

2602 

2603 

2604@_add_method(otTables.MathGlyphConstruction) 

2605def closure_glyphs(self, glyphs): 

2606 variants = set() 

2607 for v in self.MathGlyphVariantRecord: 

2608 variants.add(v.VariantGlyph) 

2609 if self.GlyphAssembly: 

2610 for p in self.GlyphAssembly.PartRecords: 

2611 variants.add(p.glyph) 

2612 return variants 

2613 

2614 

2615@_add_method(otTables.MathVariants) 

2616def closure_glyphs(self, s): 

2617 glyphs = frozenset(s.glyphs) 

2618 variants = set() 

2619 

2620 if self.VertGlyphCoverage: 

2621 indices = self.VertGlyphCoverage.intersect(glyphs) 

2622 for i in indices: 

2623 variants.update(self.VertGlyphConstruction[i].closure_glyphs(glyphs)) 

2624 

2625 if self.HorizGlyphCoverage: 

2626 indices = self.HorizGlyphCoverage.intersect(glyphs) 

2627 for i in indices: 

2628 variants.update(self.HorizGlyphConstruction[i].closure_glyphs(glyphs)) 

2629 

2630 s.glyphs.update(variants) 

2631 

2632 

2633@_add_method(ttLib.getTableClass("MATH")) 

2634def closure_glyphs(self, s): 

2635 if self.table.MathVariants: 

2636 self.table.MathVariants.closure_glyphs(s) 

2637 

2638 

2639@_add_method(otTables.MathItalicsCorrectionInfo) 

2640def subset_glyphs(self, s): 

2641 indices = self.Coverage.subset(s.glyphs) 

2642 self.ItalicsCorrection = _list_subset(self.ItalicsCorrection, indices) 

2643 self.ItalicsCorrectionCount = len(self.ItalicsCorrection) 

2644 return bool(self.ItalicsCorrectionCount) 

2645 

2646 

2647@_add_method(otTables.MathTopAccentAttachment) 

2648def subset_glyphs(self, s): 

2649 indices = self.TopAccentCoverage.subset(s.glyphs) 

2650 self.TopAccentAttachment = _list_subset(self.TopAccentAttachment, indices) 

2651 self.TopAccentAttachmentCount = len(self.TopAccentAttachment) 

2652 return bool(self.TopAccentAttachmentCount) 

2653 

2654 

2655@_add_method(otTables.MathKernInfo) 

2656def subset_glyphs(self, s): 

2657 indices = self.MathKernCoverage.subset(s.glyphs) 

2658 self.MathKernInfoRecords = _list_subset(self.MathKernInfoRecords, indices) 

2659 self.MathKernCount = len(self.MathKernInfoRecords) 

2660 return bool(self.MathKernCount) 

2661 

2662 

2663@_add_method(otTables.MathGlyphInfo) 

2664def subset_glyphs(self, s): 

2665 if self.MathItalicsCorrectionInfo: 

2666 self.MathItalicsCorrectionInfo.subset_glyphs(s) 

2667 if self.MathTopAccentAttachment: 

2668 self.MathTopAccentAttachment.subset_glyphs(s) 

2669 if self.MathKernInfo: 

2670 self.MathKernInfo.subset_glyphs(s) 

2671 if self.ExtendedShapeCoverage: 

2672 self.ExtendedShapeCoverage.subset(s.glyphs) 

2673 return True 

2674 

2675 

2676@_add_method(otTables.MathVariants) 

2677def subset_glyphs(self, s): 

2678 if self.VertGlyphCoverage: 

2679 indices = self.VertGlyphCoverage.subset(s.glyphs) 

2680 self.VertGlyphConstruction = _list_subset(self.VertGlyphConstruction, indices) 

2681 self.VertGlyphCount = len(self.VertGlyphConstruction) 

2682 

2683 if self.HorizGlyphCoverage: 

2684 indices = self.HorizGlyphCoverage.subset(s.glyphs) 

2685 self.HorizGlyphConstruction = _list_subset(self.HorizGlyphConstruction, indices) 

2686 self.HorizGlyphCount = len(self.HorizGlyphConstruction) 

2687 

2688 return True 

2689 

2690 

2691@_add_method(ttLib.getTableClass("MATH")) 

2692def subset_glyphs(self, s): 

2693 s.glyphs = s.glyphs_mathed 

2694 if self.table.MathGlyphInfo: 

2695 self.table.MathGlyphInfo.subset_glyphs(s) 

2696 if self.table.MathVariants: 

2697 self.table.MathVariants.subset_glyphs(s) 

2698 return True 

2699 

2700 

2701@_add_method(ttLib.getTableModule("glyf").Glyph) 

2702def remapComponentsFast(self, glyphidmap): 

2703 if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: 

2704 return # Not composite 

2705 data = self.data = bytearray(self.data) 

2706 i = 10 

2707 more = 1 

2708 while more: 

2709 flags = (data[i] << 8) | data[i + 1] 

2710 glyphID = (data[i + 2] << 8) | data[i + 3] 

2711 # Remap 

2712 glyphID = glyphidmap[glyphID] 

2713 data[i + 2] = glyphID >> 8 

2714 data[i + 3] = glyphID & 0xFF 

2715 i += 4 

2716 flags = int(flags) 

2717 

2718 if flags & 0x0001: 

2719 i += 4 # ARG_1_AND_2_ARE_WORDS 

2720 else: 

2721 i += 2 

2722 if flags & 0x0008: 

2723 i += 2 # WE_HAVE_A_SCALE 

2724 elif flags & 0x0040: 

2725 i += 4 # WE_HAVE_AN_X_AND_Y_SCALE 

2726 elif flags & 0x0080: 

2727 i += 8 # WE_HAVE_A_TWO_BY_TWO 

2728 more = flags & 0x0020 # MORE_COMPONENTS 

2729 

2730 

2731@_add_method(ttLib.getTableClass("glyf")) 

2732def closure_glyphs(self, s): 

2733 glyphSet = self.glyphs 

2734 decompose = s.glyphs 

2735 while decompose: 

2736 components = set() 

2737 for g in decompose: 

2738 if g not in glyphSet: 

2739 continue 

2740 gl = glyphSet[g] 

2741 for c in gl.getComponentNames(self): 

2742 components.add(c) 

2743 components -= s.glyphs 

2744 s.glyphs.update(components) 

2745 decompose = components 

2746 

2747 

2748@_add_method(ttLib.getTableClass("glyf")) 

2749def prune_pre_subset(self, font, options): 

2750 if options.notdef_glyph and not options.notdef_outline: 

2751 g = self[self.glyphOrder[0]] 

2752 # Yay, easy! 

2753 g.__dict__.clear() 

2754 g.data = b"" 

2755 return True 

2756 

2757 

2758@_add_method(ttLib.getTableClass("glyf")) 

2759def subset_glyphs(self, s): 

2760 self.glyphs = _dict_subset(self.glyphs, s.glyphs) 

2761 if not s.options.retain_gids: 

2762 indices = [i for i, g in enumerate(self.glyphOrder) if g in s.glyphs] 

2763 glyphmap = {o: n for n, o in enumerate(indices)} 

2764 for v in self.glyphs.values(): 

2765 if hasattr(v, "data"): 

2766 v.remapComponentsFast(glyphmap) 

2767 Glyph = ttLib.getTableModule("glyf").Glyph 

2768 for g in s.glyphs_emptied: 

2769 self.glyphs[g] = Glyph() 

2770 self.glyphs[g].data = b"" 

2771 self.glyphOrder = [ 

2772 g for g in self.glyphOrder if g in s.glyphs or g in s.glyphs_emptied 

2773 ] 

2774 # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset. 

2775 return True 

2776 

2777 

2778@_add_method(ttLib.getTableClass("glyf")) 

2779def prune_post_subset(self, font, options): 

2780 remove_hinting = not options.hinting 

2781 for v in self.glyphs.values(): 

2782 v.trim(remove_hinting=remove_hinting) 

2783 return True 

2784 

2785 

2786@_add_method(ttLib.getTableClass("cmap")) 

2787def closure_glyphs(self, s): 

2788 tables = [t for t in self.tables if t.isUnicode()] 

2789 

2790 # Close glyphs 

2791 for table in tables: 

2792 if table.format == 14: 

2793 for cmap in table.uvsDict.values(): 

2794 glyphs = {g for u, g in cmap if u in s.unicodes_requested} 

2795 if None in glyphs: 

2796 glyphs.remove(None) 

2797 s.glyphs.update(glyphs) 

2798 else: 

2799 cmap = table.cmap 

2800 intersection = s.unicodes_requested.intersection(cmap.keys()) 

2801 s.glyphs.update(cmap[u] for u in intersection) 

2802 

2803 # Calculate unicodes_missing 

2804 s.unicodes_missing = s.unicodes_requested.copy() 

2805 for table in tables: 

2806 s.unicodes_missing.difference_update(table.cmap) 

2807 

2808 

2809@_add_method(ttLib.getTableClass("cmap")) 

2810def prune_pre_subset(self, font, options): 

2811 if not options.legacy_cmap: 

2812 # Drop non-Unicode / non-Symbol cmaps 

2813 self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()] 

2814 if not options.symbol_cmap: 

2815 self.tables = [t for t in self.tables if not t.isSymbol()] 

2816 # TODO(behdad) Only keep one subtable? 

2817 # For now, drop format=0 which can't be subset_glyphs easily? 

2818 self.tables = [t for t in self.tables if t.format != 0] 

2819 self.numSubTables = len(self.tables) 

2820 return True # Required table 

2821 

2822 

2823@_add_method(ttLib.getTableClass("cmap")) 

2824def subset_glyphs(self, s): 

2825 s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only 

2826 

2827 tables_format12_bmp = [] 

2828 table_plat0_enc3 = {} # Unicode platform, Unicode BMP only, keyed by language 

2829 table_plat3_enc1 = {} # Windows platform, Unicode BMP, keyed by language 

2830 

2831 for t in self.tables: 

2832 if t.platformID == 0 and t.platEncID == 3: 

2833 table_plat0_enc3[t.language] = t 

2834 if t.platformID == 3 and t.platEncID == 1: 

2835 table_plat3_enc1[t.language] = t 

2836 

2837 if t.format == 14: 

2838 # TODO(behdad) We drop all the default-UVS mappings 

2839 # for glyphs_requested. So it's the caller's responsibility to make 

2840 # sure those are included. 

2841 t.uvsDict = { 

2842 v: [ 

2843 (u, g) 

2844 for u, g in l 

2845 if g in s.glyphs_requested or u in s.unicodes_requested 

2846 ] 

2847 for v, l in t.uvsDict.items() 

2848 } 

2849 t.uvsDict = {v: l for v, l in t.uvsDict.items() if l} 

2850 elif t.isUnicode(): 

2851 t.cmap = { 

2852 u: g 

2853 for u, g in t.cmap.items() 

2854 if g in s.glyphs_requested or u in s.unicodes_requested 

2855 } 

2856 # Collect format 12 tables that hold only basic multilingual plane 

2857 # codepoints. 

2858 if t.format == 12 and t.cmap and max(t.cmap.keys()) < 0x10000: 

2859 tables_format12_bmp.append(t) 

2860 else: 

2861 t.cmap = {u: g for u, g in t.cmap.items() if g in s.glyphs_requested} 

2862 

2863 # Fomat 12 tables are redundant if they contain just the same BMP codepoints 

2864 # their little BMP-only encoding siblings contain. 

2865 for t in tables_format12_bmp: 

2866 if ( 

2867 t.platformID == 0 # Unicode platform 

2868 and t.platEncID == 4 # Unicode full repertoire 

2869 and t.language in table_plat0_enc3 # Have a BMP-only sibling? 

2870 and table_plat0_enc3[t.language].cmap == t.cmap 

2871 ): 

2872 t.cmap.clear() 

2873 elif ( 

2874 t.platformID == 3 # Windows platform 

2875 and t.platEncID == 10 # Unicode full repertoire 

2876 and t.language in table_plat3_enc1 # Have a BMP-only sibling? 

2877 and table_plat3_enc1[t.language].cmap == t.cmap 

2878 ): 

2879 t.cmap.clear() 

2880 

2881 self.tables = [t for t in self.tables if (t.cmap if t.format != 14 else t.uvsDict)] 

2882 self.numSubTables = len(self.tables) 

2883 # TODO(behdad) Convert formats when needed. 

2884 # In particular, if we have a format=12 without non-BMP 

2885 # characters, convert it to format=4 if there's not one. 

2886 return True # Required table 

2887 

2888 

2889@_add_method(ttLib.getTableClass("DSIG")) 

2890def prune_pre_subset(self, font, options): 

2891 # Drop all signatures since they will be invalid 

2892 self.usNumSigs = 0 

2893 self.signatureRecords = [] 

2894 return True 

2895 

2896 

2897@_add_method(ttLib.getTableClass("maxp")) 

2898def prune_pre_subset(self, font, options): 

2899 if not options.hinting: 

2900 if self.tableVersion == 0x00010000: 

2901 self.maxZones = 1 

2902 self.maxTwilightPoints = 0 

2903 self.maxStorage = 0 

2904 self.maxFunctionDefs = 0 

2905 self.maxInstructionDefs = 0 

2906 self.maxStackElements = 0 

2907 self.maxSizeOfInstructions = 0 

2908 return True 

2909 

2910 

2911@_add_method(ttLib.getTableClass("name")) 

2912def prune_post_subset(self, font, options): 

2913 visitor = NameRecordVisitor() 

2914 visitor.visit(font) 

2915 nameIDs = set(options.name_IDs) | visitor.seen 

2916 if "*" not in options.name_IDs: 

2917 self.names = [n for n in self.names if n.nameID in nameIDs] 

2918 if not options.name_legacy: 

2919 # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman 

2920 # entry for Latin and no Unicode names. 

2921 self.names = [n for n in self.names if n.isUnicode()] 

2922 # TODO(behdad) Option to keep only one platform's 

2923 if "*" not in options.name_languages: 

2924 # TODO(behdad) This is Windows-platform specific! 

2925 self.names = [n for n in self.names if n.langID in options.name_languages] 

2926 if options.obfuscate_names: 

2927 namerecs = [] 

2928 for n in self.names: 

2929 if n.nameID in [1, 4]: 

2930 n.string = ".\x7f".encode("utf_16_be") if n.isUnicode() else ".\x7f" 

2931 elif n.nameID in [2, 6]: 

2932 n.string = "\x7f".encode("utf_16_be") if n.isUnicode() else "\x7f" 

2933 elif n.nameID == 3: 

2934 n.string = "" 

2935 elif n.nameID in [16, 17, 18]: 

2936 continue 

2937 namerecs.append(n) 

2938 self.names = namerecs 

2939 return True # Required table 

2940 

2941 

2942@_add_method(ttLib.getTableClass("head")) 

2943def prune_post_subset(self, font, options): 

2944 # Force re-compiling head table, to update any recalculated values. 

2945 return True 

2946 

2947 

2948# TODO(behdad) OS/2 ulCodePageRange? 

2949# TODO(behdad) Drop AAT tables. 

2950# TODO(behdad) Drop unneeded GSUB/GPOS Script/LangSys entries. 

2951# TODO(behdad) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left 

2952# TODO(behdad) Drop GDEF subitems if unused by lookups 

2953# TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF) 

2954# TODO(behdad) Text direction considerations. 

2955# TODO(behdad) Text script / language considerations. 

2956# TODO(behdad) Optionally drop 'kern' table if GPOS available 

2957# TODO(behdad) Implement --unicode='*' to choose all cmap'ed 

2958# TODO(behdad) Drop old-spec Indic scripts 

2959 

2960 

2961class Options(object): 

2962 class OptionError(Exception): 

2963 pass 

2964 

2965 class UnknownOptionError(OptionError): 

2966 pass 

2967 

2968 # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser 

2969 _drop_tables_default = [ 

2970 "BASE", 

2971 "JSTF", 

2972 "DSIG", 

2973 "EBDT", 

2974 "EBLC", 

2975 "EBSC", 

2976 "PCLT", 

2977 "LTSH", 

2978 ] 

2979 _drop_tables_default += ["Feat", "Glat", "Gloc", "Silf", "Sill"] # Graphite 

2980 _no_subset_tables_default = [ 

2981 "avar", 

2982 "fvar", 

2983 "gasp", 

2984 "head", 

2985 "hhea", 

2986 "maxp", 

2987 "vhea", 

2988 "OS/2", 

2989 "loca", 

2990 "name", 

2991 "cvt", 

2992 "fpgm", 

2993 "prep", 

2994 "VDMX", 

2995 "DSIG", 

2996 "CPAL", 

2997 "MVAR", 

2998 "cvar", 

2999 "STAT", 

3000 ] 

3001 _hinting_tables_default = ["cvt", "cvar", "fpgm", "prep", "hdmx", "VDMX"] 

3002 

3003 # Based on HarfBuzz shapers 

3004 _layout_features_groups = { 

3005 # Default shaper 

3006 "common": ["rvrn", "ccmp", "liga", "locl", "mark", "mkmk", "rlig"], 

3007 "fractions": ["frac", "numr", "dnom"], 

3008 "horizontal": ["calt", "clig", "curs", "kern", "rclt"], 

3009 "vertical": ["valt", "vert", "vkrn", "vpal", "vrt2"], 

3010 "ltr": ["ltra", "ltrm"], 

3011 "rtl": ["rtla", "rtlm"], 

3012 "rand": ["rand"], 

3013 "justify": ["jalt"], 

3014 "private": ["Harf", "HARF", "Buzz", "BUZZ"], 

3015 "east_asian_spacing": ["chws", "vchw", "halt", "vhal"], 

3016 # Complex shapers 

3017 "arabic": [ 

3018 "init", 

3019 "medi", 

3020 "fina", 

3021 "isol", 

3022 "med2", 

3023 "fin2", 

3024 "fin3", 

3025 "cswh", 

3026 "mset", 

3027 "stch", 

3028 ], 

3029 "hangul": ["ljmo", "vjmo", "tjmo"], 

3030 "tibetan": ["abvs", "blws", "abvm", "blwm"], 

3031 "indic": [ 

3032 "nukt", 

3033 "akhn", 

3034 "rphf", 

3035 "rkrf", 

3036 "pref", 

3037 "blwf", 

3038 "half", 

3039 "abvf", 

3040 "pstf", 

3041 "cfar", 

3042 "vatu", 

3043 "cjct", 

3044 "init", 

3045 "pres", 

3046 "abvs", 

3047 "blws", 

3048 "psts", 

3049 "haln", 

3050 "dist", 

3051 "abvm", 

3052 "blwm", 

3053 ], 

3054 } 

3055 _layout_features_default = _uniq_sort( 

3056 sum(iter(_layout_features_groups.values()), []) 

3057 ) 

3058 

3059 def __init__(self, **kwargs): 

3060 self.drop_tables = self._drop_tables_default[:] 

3061 self.no_subset_tables = self._no_subset_tables_default[:] 

3062 self.passthrough_tables = False # keep/drop tables we can't subset 

3063 self.hinting_tables = self._hinting_tables_default[:] 

3064 self.legacy_kern = False # drop 'kern' table if GPOS available 

3065 self.layout_closure = True 

3066 self.layout_features = self._layout_features_default[:] 

3067 self.layout_scripts = ["*"] 

3068 self.ignore_missing_glyphs = False 

3069 self.ignore_missing_unicodes = True 

3070 self.hinting = True 

3071 self.glyph_names = False 

3072 self.legacy_cmap = False 

3073 self.symbol_cmap = False 

3074 self.name_IDs = [ 

3075 0, 

3076 1, 

3077 2, 

3078 3, 

3079 4, 

3080 5, 

3081 6, 

3082 ] # https://github.com/fonttools/fonttools/issues/1170#issuecomment-364631225 

3083 self.name_legacy = False 

3084 self.name_languages = [0x0409] # English 

3085 self.obfuscate_names = False # to make webfont unusable as a system font 

3086 self.retain_gids = False 

3087 self.notdef_glyph = True # gid0 for TrueType / .notdef for CFF 

3088 self.notdef_outline = False # No need for notdef to have an outline really 

3089 self.recommended_glyphs = False # gid1, gid2, gid3 for TrueType 

3090 self.recalc_bounds = False # Recalculate font bounding boxes 

3091 self.recalc_timestamp = False # Recalculate font modified timestamp 

3092 self.prune_unicode_ranges = True # Clear unused 'ulUnicodeRange' bits 

3093 self.prune_codepage_ranges = True # Clear unused 'ulCodePageRange' bits 

3094 self.recalc_average_width = False # update 'xAvgCharWidth' 

3095 self.recalc_max_context = False # update 'usMaxContext' 

3096 self.canonical_order = None # Order tables as recommended 

3097 self.flavor = None # May be 'woff' or 'woff2' 

3098 self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0 

3099 self.desubroutinize = False # Desubroutinize CFF CharStrings 

3100 self.harfbuzz_repacker = USE_HARFBUZZ_REPACKER.default 

3101 self.verbose = False 

3102 self.timing = False 

3103 self.xml = False 

3104 self.font_number = -1 

3105 self.pretty_svg = False 

3106 self.lazy = True 

3107 

3108 self.set(**kwargs) 

3109 

3110 def set(self, **kwargs): 

3111 for k, v in kwargs.items(): 

3112 if not hasattr(self, k): 

3113 raise self.UnknownOptionError("Unknown option '%s'" % k) 

3114 setattr(self, k, v) 

3115 

3116 def parse_opts(self, argv, ignore_unknown=[]): 

3117 posargs = [] 

3118 passthru_options = [] 

3119 for a in argv: 

3120 orig_a = a 

3121 if not a.startswith("--"): 

3122 posargs.append(a) 

3123 continue 

3124 a = a[2:] 

3125 i = a.find("=") 

3126 op = "=" 

3127 if i == -1: 

3128 if a.startswith("no-"): 

3129 k = a[3:] 

3130 if k == "canonical-order": 

3131 # reorderTables=None is faster than False (the latter 

3132 # still reorders to "keep" the original table order) 

3133 v = None 

3134 else: 

3135 v = False 

3136 else: 

3137 k = a 

3138 v = True 

3139 if k.endswith("?"): 

3140 k = k[:-1] 

3141 v = "?" 

3142 else: 

3143 k = a[:i] 

3144 if k[-1] in "-+": 

3145 op = k[-1] + "=" # Op is '-=' or '+=' now. 

3146 k = k[:-1] 

3147 v = a[i + 1 :] 

3148 ok = k 

3149 k = k.replace("-", "_") 

3150 if not hasattr(self, k): 

3151 if ignore_unknown is True or ok in ignore_unknown: 

3152 passthru_options.append(orig_a) 

3153 continue 

3154 else: 

3155 raise self.UnknownOptionError("Unknown option '%s'" % a) 

3156 

3157 ov = getattr(self, k) 

3158 if v == "?": 

3159 print("Current setting for '%s' is: %s" % (ok, ov)) 

3160 continue 

3161 if isinstance(ov, bool): 

3162 v = bool(v) 

3163 elif isinstance(ov, int): 

3164 v = int(v) 

3165 elif isinstance(ov, str): 

3166 v = str(v) # redundant 

3167 elif isinstance(ov, list): 

3168 if isinstance(v, bool): 

3169 raise self.OptionError( 

3170 "Option '%s' requires values to be specified using '='" % a 

3171 ) 

3172 vv = v.replace(",", " ").split() 

3173 if vv == [""]: 

3174 vv = [] 

3175 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] 

3176 if op == "=": 

3177 v = vv 

3178 elif op == "+=": 

3179 v = ov 

3180 v.extend(vv) 

3181 elif op == "-=": 

3182 v = ov 

3183 for x in vv: 

3184 if x in v: 

3185 v.remove(x) 

3186 else: 

3187 assert False 

3188 

3189 setattr(self, k, v) 

3190 

3191 return posargs + passthru_options 

3192 

3193 

3194class Subsetter(object): 

3195 class SubsettingError(Exception): 

3196 pass 

3197 

3198 class MissingGlyphsSubsettingError(SubsettingError): 

3199 pass 

3200 

3201 class MissingUnicodesSubsettingError(SubsettingError): 

3202 pass 

3203 

3204 def __init__(self, options=None): 

3205 if not options: 

3206 options = Options() 

3207 

3208 self.options = options 

3209 self.unicodes_requested = set() 

3210 self.glyph_names_requested = set() 

3211 self.glyph_ids_requested = set() 

3212 

3213 def populate(self, glyphs=[], gids=[], unicodes=[], text=""): 

3214 self.unicodes_requested.update(unicodes) 

3215 if isinstance(text, bytes): 

3216 text = text.decode("utf_8") 

3217 text_utf32 = text.encode("utf-32-be") 

3218 nchars = len(text_utf32) // 4 

3219 for u in struct.unpack(">%dL" % nchars, text_utf32): 

3220 self.unicodes_requested.add(u) 

3221 self.glyph_names_requested.update(glyphs) 

3222 self.glyph_ids_requested.update(gids) 

3223 

3224 def _prune_pre_subset(self, font): 

3225 for tag in self._sort_tables(font): 

3226 if ( 

3227 tag.strip() in self.options.drop_tables 

3228 or ( 

3229 tag.strip() in self.options.hinting_tables 

3230 and not self.options.hinting 

3231 ) 

3232 or (tag == "kern" and (not self.options.legacy_kern and "GPOS" in font)) 

3233 ): 

3234 log.info("%s dropped", tag) 

3235 del font[tag] 

3236 continue 

3237 

3238 clazz = ttLib.getTableClass(tag) 

3239 

3240 if hasattr(clazz, "prune_pre_subset"): 

3241 with timer("load '%s'" % tag): 

3242 table = font[tag] 

3243 with timer("prune '%s'" % tag): 

3244 retain = table.prune_pre_subset(font, self.options) 

3245 if not retain: 

3246 log.info("%s pruned to empty; dropped", tag) 

3247 del font[tag] 

3248 continue 

3249 else: 

3250 log.info("%s pruned", tag) 

3251 

3252 def _closure_glyphs(self, font): 

3253 realGlyphs = set(font.getGlyphOrder()) 

3254 self.orig_glyph_order = glyph_order = font.getGlyphOrder() 

3255 

3256 self.glyphs_requested = set() 

3257 self.glyphs_requested.update(self.glyph_names_requested) 

3258 self.glyphs_requested.update( 

3259 glyph_order[i] for i in self.glyph_ids_requested if i < len(glyph_order) 

3260 ) 

3261 

3262 self.glyphs_missing = set() 

3263 self.glyphs_missing.update(self.glyphs_requested.difference(realGlyphs)) 

3264 self.glyphs_missing.update( 

3265 i for i in self.glyph_ids_requested if i >= len(glyph_order) 

3266 ) 

3267 if self.glyphs_missing: 

3268 log.info("Missing requested glyphs: %s", self.glyphs_missing) 

3269 if not self.options.ignore_missing_glyphs: 

3270 raise self.MissingGlyphsSubsettingError(self.glyphs_missing) 

3271 

3272 self.glyphs = self.glyphs_requested.copy() 

3273 

3274 self.unicodes_missing = set() 

3275 if "cmap" in font: 

3276 with timer("close glyph list over 'cmap'"): 

3277 font["cmap"].closure_glyphs(self) 

3278 self.glyphs.intersection_update(realGlyphs) 

3279 self.glyphs_cmaped = frozenset(self.glyphs) 

3280 if self.unicodes_missing: 

3281 missing = ["U+%04X" % u for u in self.unicodes_missing] 

3282 log.info("Missing glyphs for requested Unicodes: %s", missing) 

3283 if not self.options.ignore_missing_unicodes: 

3284 raise self.MissingUnicodesSubsettingError(missing) 

3285 del missing 

3286 

3287 if self.options.notdef_glyph: 

3288 if "glyf" in font: 

3289 self.glyphs.add(font.getGlyphName(0)) 

3290 log.info("Added gid0 to subset") 

3291 else: 

3292 self.glyphs.add(".notdef") 

3293 log.info("Added .notdef to subset") 

3294 if self.options.recommended_glyphs: 

3295 if "glyf" in font: 

3296 for i in range(min(4, len(font.getGlyphOrder()))): 

3297 self.glyphs.add(font.getGlyphName(i)) 

3298 log.info("Added first four glyphs to subset") 

3299 

3300 if self.options.layout_closure and "GSUB" in font: 

3301 with timer("close glyph list over 'GSUB'"): 

3302 log.info( 

3303 "Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs) 

3304 ) 

3305 log.glyphs(self.glyphs, font=font) 

3306 font["GSUB"].closure_glyphs(self) 

3307 self.glyphs.intersection_update(realGlyphs) 

3308 log.info( 

3309 "Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs) 

3310 ) 

3311 log.glyphs(self.glyphs, font=font) 

3312 self.glyphs_gsubed = frozenset(self.glyphs) 

3313 

3314 if "MATH" in font: 

3315 with timer("close glyph list over 'MATH'"): 

3316 log.info( 

3317 "Closing glyph list over 'MATH': %d glyphs before", len(self.glyphs) 

3318 ) 

3319 log.glyphs(self.glyphs, font=font) 

3320 font["MATH"].closure_glyphs(self) 

3321 self.glyphs.intersection_update(realGlyphs) 

3322 log.info( 

3323 "Closed glyph list over 'MATH': %d glyphs after", len(self.glyphs) 

3324 ) 

3325 log.glyphs(self.glyphs, font=font) 

3326 self.glyphs_mathed = frozenset(self.glyphs) 

3327 

3328 for table in ("COLR", "bsln"): 

3329 if table in font: 

3330 with timer("close glyph list over '%s'" % table): 

3331 log.info( 

3332 "Closing glyph list over '%s': %d glyphs before", 

3333 table, 

3334 len(self.glyphs), 

3335 ) 

3336 log.glyphs(self.glyphs, font=font) 

3337 font[table].closure_glyphs(self) 

3338 self.glyphs.intersection_update(realGlyphs) 

3339 log.info( 

3340 "Closed glyph list over '%s': %d glyphs after", 

3341 table, 

3342 len(self.glyphs), 

3343 ) 

3344 log.glyphs(self.glyphs, font=font) 

3345 setattr(self, f"glyphs_{table.lower()}ed", frozenset(self.glyphs)) 

3346 

3347 if "glyf" in font: 

3348 with timer("close glyph list over 'glyf'"): 

3349 log.info( 

3350 "Closing glyph list over 'glyf': %d glyphs before", len(self.glyphs) 

3351 ) 

3352 log.glyphs(self.glyphs, font=font) 

3353 font["glyf"].closure_glyphs(self) 

3354 self.glyphs.intersection_update(realGlyphs) 

3355 log.info( 

3356 "Closed glyph list over 'glyf': %d glyphs after", len(self.glyphs) 

3357 ) 

3358 log.glyphs(self.glyphs, font=font) 

3359 self.glyphs_glyfed = frozenset(self.glyphs) 

3360 

3361 if "CFF " in font: 

3362 with timer("close glyph list over 'CFF '"): 

3363 log.info( 

3364 "Closing glyph list over 'CFF ': %d glyphs before", len(self.glyphs) 

3365 ) 

3366 log.glyphs(self.glyphs, font=font) 

3367 font["CFF "].closure_glyphs(self) 

3368 self.glyphs.intersection_update(realGlyphs) 

3369 log.info( 

3370 "Closed glyph list over 'CFF ': %d glyphs after", len(self.glyphs) 

3371 ) 

3372 log.glyphs(self.glyphs, font=font) 

3373 self.glyphs_cffed = frozenset(self.glyphs) 

3374 

3375 self.glyphs_retained = frozenset(self.glyphs) 

3376 

3377 order = font.getReverseGlyphMap() 

3378 self.reverseOrigGlyphMap = {g: order[g] for g in self.glyphs_retained} 

3379 

3380 self.last_retained_order = max(self.reverseOrigGlyphMap.values()) 

3381 self.last_retained_glyph = font.getGlyphOrder()[self.last_retained_order] 

3382 

3383 self.glyphs_emptied = frozenset() 

3384 if self.options.retain_gids: 

3385 self.glyphs_emptied = { 

3386 g 

3387 for g in realGlyphs - self.glyphs_retained 

3388 if order[g] <= self.last_retained_order 

3389 } 

3390 

3391 self.reverseEmptiedGlyphMap = {g: order[g] for g in self.glyphs_emptied} 

3392 

3393 if not self.options.retain_gids: 

3394 new_glyph_order = [g for g in glyph_order if g in self.glyphs_retained] 

3395 else: 

3396 new_glyph_order = [ 

3397 g for g in glyph_order if font.getGlyphID(g) <= self.last_retained_order 

3398 ] 

3399 # We'll call font.setGlyphOrder() at the end of _subset_glyphs when all 

3400 # tables have been subsetted. Below, we use the new glyph order to get 

3401 # a map from old to new glyph indices, which can be useful when 

3402 # subsetting individual tables (e.g. SVG) that refer to GIDs. 

3403 self.new_glyph_order = new_glyph_order 

3404 self.glyph_index_map = { 

3405 order[new_glyph_order[i]]: i for i in range(len(new_glyph_order)) 

3406 } 

3407 

3408 log.info("Retaining %d glyphs", len(self.glyphs_retained)) 

3409 

3410 del self.glyphs 

3411 

3412 def _subset_glyphs(self, font): 

3413 self.used_mark_sets = [] 

3414 for tag in self._sort_tables(font): 

3415 clazz = ttLib.getTableClass(tag) 

3416 

3417 if tag.strip() in self.options.no_subset_tables: 

3418 log.info("%s subsetting not needed", tag) 

3419 elif hasattr(clazz, "subset_glyphs"): 

3420 with timer("subset '%s'" % tag): 

3421 table = font[tag] 

3422 self.glyphs = self.glyphs_retained 

3423 retain = table.subset_glyphs(self) 

3424 del self.glyphs 

3425 if not retain: 

3426 log.info("%s subsetted to empty; dropped", tag) 

3427 del font[tag] 

3428 else: 

3429 log.info("%s subsetted", tag) 

3430 elif self.options.passthrough_tables: 

3431 log.info("%s NOT subset; don't know how to subset", tag) 

3432 else: 

3433 log.warning("%s NOT subset; don't know how to subset; dropped", tag) 

3434 del font[tag] 

3435 

3436 with timer("subset GlyphOrder"): 

3437 font.setGlyphOrder(self.new_glyph_order) 

3438 

3439 def _prune_post_subset(self, font): 

3440 tableTags = font.keys() 

3441 # Prune the name table last because when we're pruning the name table, 

3442 # we visit each table in the font to see what name table records are 

3443 # still in use. 

3444 if "name" in tableTags: 

3445 tableTags.remove("name") 

3446 tableTags.append("name") 

3447 for tag in tableTags: 

3448 if tag == "GlyphOrder": 

3449 continue 

3450 if tag == "OS/2": 

3451 if self.options.prune_unicode_ranges: 

3452 old_uniranges = font[tag].getUnicodeRanges() 

3453 new_uniranges = font[tag].recalcUnicodeRanges(font, pruneOnly=True) 

3454 if old_uniranges != new_uniranges: 

3455 log.info( 

3456 "%s Unicode ranges pruned: %s", tag, sorted(new_uniranges) 

3457 ) 

3458 if self.options.prune_codepage_ranges and font[tag].version >= 1: 

3459 # codepage range fields were added with OS/2 format 1 

3460 # https://learn.microsoft.com/en-us/typography/opentype/spec/os2#version-1 

3461 old_codepages = font[tag].getCodePageRanges() 

3462 new_codepages = font[tag].recalcCodePageRanges(font, pruneOnly=True) 

3463 if old_codepages != new_codepages: 

3464 log.info( 

3465 "%s CodePage ranges pruned: %s", 

3466 tag, 

3467 sorted(new_codepages), 

3468 ) 

3469 if self.options.recalc_average_width: 

3470 old_avg_width = font[tag].xAvgCharWidth 

3471 new_avg_width = font[tag].recalcAvgCharWidth(font) 

3472 if old_avg_width != new_avg_width: 

3473 log.info("%s xAvgCharWidth updated: %d", tag, new_avg_width) 

3474 if self.options.recalc_max_context: 

3475 max_context = maxCtxFont(font) 

3476 if max_context != font[tag].usMaxContext: 

3477 font[tag].usMaxContext = max_context 

3478 log.info("%s usMaxContext updated: %d", tag, max_context) 

3479 clazz = ttLib.getTableClass(tag) 

3480 if hasattr(clazz, "prune_post_subset"): 

3481 with timer("prune '%s'" % tag): 

3482 table = font[tag] 

3483 retain = table.prune_post_subset(font, self.options) 

3484 if not retain: 

3485 log.info("%s pruned to empty; dropped", tag) 

3486 del font[tag] 

3487 else: 

3488 log.info("%s pruned", tag) 

3489 

3490 def _sort_tables(self, font): 

3491 tagOrder = ["GDEF", "GPOS", "GSUB", "fvar", "avar", "gvar", "name", "glyf"] 

3492 tagOrder = {t: i + 1 for i, t in enumerate(tagOrder)} 

3493 tags = sorted(font.keys(), key=lambda tag: tagOrder.get(tag, 0)) 

3494 return [t for t in tags if t != "GlyphOrder"] 

3495 

3496 def subset(self, font): 

3497 self._prune_pre_subset(font) 

3498 self._closure_glyphs(font) 

3499 self._subset_glyphs(font) 

3500 self._prune_post_subset(font) 

3501 

3502 

3503@timer("load font") 

3504def load_font(fontFile, options, checkChecksums=0, dontLoadGlyphNames=False, lazy=True): 

3505 font = ttLib.TTFont( 

3506 fontFile, 

3507 checkChecksums=checkChecksums, 

3508 recalcBBoxes=options.recalc_bounds, 

3509 recalcTimestamp=options.recalc_timestamp, 

3510 lazy=lazy, 

3511 fontNumber=options.font_number, 

3512 ) 

3513 

3514 # Hack: 

3515 # 

3516 # If we don't need glyph names, change 'post' class to not try to 

3517 # load them. It avoid lots of headache with broken fonts as well 

3518 # as loading time. 

3519 # 

3520 # Ideally ttLib should provide a way to ask it to skip loading 

3521 # glyph names. But it currently doesn't provide such a thing. 

3522 # 

3523 if dontLoadGlyphNames: 

3524 post = ttLib.getTableClass("post") 

3525 saved = post.decode_format_2_0 

3526 post.decode_format_2_0 = post.decode_format_3_0 

3527 f = font["post"] 

3528 if f.formatType == 2.0: 

3529 f.formatType = 3.0 

3530 post.decode_format_2_0 = saved 

3531 

3532 return font 

3533 

3534 

3535@timer("compile and save font") 

3536def save_font(font, outfile, options): 

3537 if options.with_zopfli and options.flavor == "woff": 

3538 from fontTools.ttLib import sfnt 

3539 

3540 sfnt.USE_ZOPFLI = True 

3541 font.flavor = options.flavor 

3542 font.cfg[USE_HARFBUZZ_REPACKER] = options.harfbuzz_repacker 

3543 font.save(outfile, reorderTables=options.canonical_order) 

3544 

3545 

3546def parse_unicodes(s): 

3547 import re 

3548 

3549 s = re.sub(r"0[xX]", " ", s) 

3550 s = re.sub(r"[<+>,;&#\\xXuU\n ]", " ", s) 

3551 l = [] 

3552 for item in s.split(): 

3553 fields = item.split("-") 

3554 if len(fields) == 1: 

3555 l.append(int(item, 16)) 

3556 else: 

3557 start, end = fields 

3558 l.extend(range(int(start, 16), int(end, 16) + 1)) 

3559 return l 

3560 

3561 

3562def parse_gids(s): 

3563 l = [] 

3564 for item in s.replace(",", " ").split(): 

3565 fields = item.split("-") 

3566 if len(fields) == 1: 

3567 l.append(int(fields[0])) 

3568 else: 

3569 l.extend(range(int(fields[0]), int(fields[1]) + 1)) 

3570 return l 

3571 

3572 

3573def parse_glyphs(s): 

3574 return s.replace(",", " ").split() 

3575 

3576 

3577def usage(): 

3578 print("usage:", __usage__, file=sys.stderr) 

3579 print("Try pyftsubset --help for more information.\n", file=sys.stderr) 

3580 

3581 

3582@timer("make one with everything (TOTAL TIME)") 

3583def main(args=None): 

3584 """OpenType font subsetter and optimizer""" 

3585 from os.path import splitext 

3586 from fontTools import configLogger 

3587 

3588 if args is None: 

3589 args = sys.argv[1:] 

3590 

3591 if "--help" in args: 

3592 print(__doc__) 

3593 return 0 

3594 

3595 options = Options() 

3596 try: 

3597 args = options.parse_opts( 

3598 args, 

3599 ignore_unknown=[ 

3600 "gids", 

3601 "gids-file", 

3602 "glyphs", 

3603 "glyphs-file", 

3604 "text", 

3605 "text-file", 

3606 "unicodes", 

3607 "unicodes-file", 

3608 "output-file", 

3609 ], 

3610 ) 

3611 except options.OptionError as e: 

3612 usage() 

3613 print("ERROR:", e, file=sys.stderr) 

3614 return 2 

3615 

3616 if len(args) < 2: 

3617 usage() 

3618 return 1 

3619 

3620 configLogger(level=logging.INFO if options.verbose else logging.WARNING) 

3621 if options.timing: 

3622 timer.logger.setLevel(logging.DEBUG) 

3623 else: 

3624 timer.logger.disabled = True 

3625 

3626 fontfile = args[0] 

3627 args = args[1:] 

3628 

3629 subsetter = Subsetter(options=options) 

3630 outfile = None 

3631 glyphs = [] 

3632 gids = [] 

3633 unicodes = [] 

3634 wildcard_glyphs = False 

3635 wildcard_unicodes = False 

3636 text = "" 

3637 for g in args: 

3638 if g == "*": 

3639 wildcard_glyphs = True 

3640 continue 

3641 if g.startswith("--output-file="): 

3642 outfile = g[14:] 

3643 continue 

3644 if g.startswith("--text="): 

3645 text += g[7:] 

3646 continue 

3647 if g.startswith("--text-file="): 

3648 with open(g[12:], encoding="utf-8") as f: 

3649 text += f.read().replace("\n", "") 

3650 continue 

3651 if g.startswith("--unicodes="): 

3652 if g[11:] == "*": 

3653 wildcard_unicodes = True 

3654 else: 

3655 unicodes.extend(parse_unicodes(g[11:])) 

3656 continue 

3657 if g.startswith("--unicodes-file="): 

3658 with open(g[16:]) as f: 

3659 for line in f.readlines(): 

3660 unicodes.extend(parse_unicodes(line.split("#")[0])) 

3661 continue 

3662 if g.startswith("--gids="): 

3663 gids.extend(parse_gids(g[7:])) 

3664 continue 

3665 if g.startswith("--gids-file="): 

3666 with open(g[12:]) as f: 

3667 for line in f.readlines(): 

3668 gids.extend(parse_gids(line.split("#")[0])) 

3669 continue 

3670 if g.startswith("--glyphs="): 

3671 if g[9:] == "*": 

3672 wildcard_glyphs = True 

3673 else: 

3674 glyphs.extend(parse_glyphs(g[9:])) 

3675 continue 

3676 if g.startswith("--glyphs-file="): 

3677 with open(g[14:]) as f: 

3678 for line in f.readlines(): 

3679 glyphs.extend(parse_glyphs(line.split("#")[0])) 

3680 continue 

3681 glyphs.append(g) 

3682 

3683 dontLoadGlyphNames = not options.glyph_names and not glyphs 

3684 lazy = options.lazy 

3685 font = load_font( 

3686 fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames, lazy=lazy 

3687 ) 

3688 

3689 if outfile is None: 

3690 ext = "." + options.flavor.lower() if options.flavor is not None else None 

3691 outfile = makeOutputFileName( 

3692 fontfile, extension=ext, overWrite=True, suffix=".subset" 

3693 ) 

3694 

3695 with timer("compile glyph list"): 

3696 if wildcard_glyphs: 

3697 glyphs.extend(font.getGlyphOrder()) 

3698 if wildcard_unicodes: 

3699 for t in font["cmap"].tables: 

3700 if t.isUnicode(): 

3701 unicodes.extend(t.cmap.keys()) 

3702 assert "" not in glyphs 

3703 

3704 log.info("Text: '%s'" % text) 

3705 log.info("Unicodes: %s", unicodes) 

3706 log.info("Glyphs: %s", glyphs) 

3707 log.info("Gids: %s", gids) 

3708 

3709 subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text) 

3710 subsetter.subset(font) 

3711 

3712 save_font(font, outfile, options) 

3713 

3714 if options.verbose: 

3715 import os 

3716 

3717 log.info("Input font:% 7d bytes: %s" % (os.path.getsize(fontfile), fontfile)) 

3718 log.info("Subset font:% 7d bytes: %s" % (os.path.getsize(outfile), outfile)) 

3719 

3720 if options.xml: 

3721 font.saveXML(sys.stdout) 

3722 

3723 font.close() 

3724 

3725 

3726__all__ = [ 

3727 "Options", 

3728 "Subsetter", 

3729 "load_font", 

3730 "save_font", 

3731 "parse_gids", 

3732 "parse_glyphs", 

3733 "parse_unicodes", 

3734 "main", 

3735] 

3736 

3737if __name__ == "__main__": 

3738 sys.exit(main())