Coverage for /usr/lib/python3/dist-packages/fontTools/subset/cff.py: 15%

345 statements  

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

1from fontTools.misc import psCharStrings 

2from fontTools import ttLib 

3from fontTools.pens.basePen import NullPen 

4from fontTools.misc.roundTools import otRound 

5from fontTools.misc.loggingTools import deprecateFunction 

6from fontTools.subset.util import _add_method, _uniq_sort 

7 

8 

9class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler): 

10 def __init__(self, components, localSubrs, globalSubrs): 

11 psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs) 

12 self.components = components 

13 

14 def op_endchar(self, index): 

15 args = self.popall() 

16 if len(args) >= 4: 

17 from fontTools.encodings.StandardEncoding import StandardEncoding 

18 

19 # endchar can do seac accent bulding; The T2 spec says it's deprecated, 

20 # but recent software that shall remain nameless does output it. 

21 adx, ady, bchar, achar = args[-4:] 

22 baseGlyph = StandardEncoding[bchar] 

23 accentGlyph = StandardEncoding[achar] 

24 self.components.add(baseGlyph) 

25 self.components.add(accentGlyph) 

26 

27 

28@_add_method(ttLib.getTableClass("CFF ")) 

29def closure_glyphs(self, s): 

30 cff = self.cff 

31 assert len(cff) == 1 

32 font = cff[cff.keys()[0]] 

33 glyphSet = font.CharStrings 

34 

35 decompose = s.glyphs 

36 while decompose: 

37 components = set() 

38 for g in decompose: 

39 if g not in glyphSet: 

40 continue 

41 gl = glyphSet[g] 

42 

43 subrs = getattr(gl.private, "Subrs", []) 

44 decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs) 

45 decompiler.execute(gl) 

46 components -= s.glyphs 

47 s.glyphs.update(components) 

48 decompose = components 

49 

50 

51def _empty_charstring(font, glyphName, isCFF2, ignoreWidth=False): 

52 c, fdSelectIndex = font.CharStrings.getItemAndSelector(glyphName) 

53 if isCFF2 or ignoreWidth: 

54 # CFF2 charstrings have no widths nor 'endchar' operators 

55 c.setProgram([] if isCFF2 else ["endchar"]) 

56 else: 

57 if hasattr(font, "FDArray") and font.FDArray is not None: 

58 private = font.FDArray[fdSelectIndex].Private 

59 else: 

60 private = font.Private 

61 dfltWdX = private.defaultWidthX 

62 nmnlWdX = private.nominalWidthX 

63 pen = NullPen() 

64 c.draw(pen) # this will set the charstring's width 

65 if c.width != dfltWdX: 

66 c.program = [c.width - nmnlWdX, "endchar"] 

67 else: 

68 c.program = ["endchar"] 

69 

70 

71@_add_method(ttLib.getTableClass("CFF ")) 

72def prune_pre_subset(self, font, options): 

73 cff = self.cff 

74 # CFF table must have one font only 

75 cff.fontNames = cff.fontNames[:1] 

76 

77 if options.notdef_glyph and not options.notdef_outline: 

78 isCFF2 = cff.major > 1 

79 for fontname in cff.keys(): 

80 font = cff[fontname] 

81 _empty_charstring(font, ".notdef", isCFF2=isCFF2) 

82 

83 # Clear useless Encoding 

84 for fontname in cff.keys(): 

85 font = cff[fontname] 

86 # https://github.com/fonttools/fonttools/issues/620 

87 font.Encoding = "StandardEncoding" 

88 

89 return True # bool(cff.fontNames) 

90 

91 

92@_add_method(ttLib.getTableClass("CFF ")) 

93def subset_glyphs(self, s): 

94 cff = self.cff 

95 for fontname in cff.keys(): 

96 font = cff[fontname] 

97 cs = font.CharStrings 

98 

99 glyphs = s.glyphs.union(s.glyphs_emptied) 

100 

101 # Load all glyphs 

102 for g in font.charset: 

103 if g not in glyphs: 

104 continue 

105 c, _ = cs.getItemAndSelector(g) 

106 

107 if cs.charStringsAreIndexed: 

108 indices = [i for i, g in enumerate(font.charset) if g in glyphs] 

109 csi = cs.charStringsIndex 

110 csi.items = [csi.items[i] for i in indices] 

111 del csi.file, csi.offsets 

112 if hasattr(font, "FDSelect"): 

113 sel = font.FDSelect 

114 sel.format = None 

115 sel.gidArray = [sel.gidArray[i] for i in indices] 

116 newCharStrings = {} 

117 for indicesIdx, charsetIdx in enumerate(indices): 

118 g = font.charset[charsetIdx] 

119 if g in cs.charStrings: 

120 newCharStrings[g] = indicesIdx 

121 cs.charStrings = newCharStrings 

122 else: 

123 cs.charStrings = {g: v for g, v in cs.charStrings.items() if g in glyphs} 

124 font.charset = [g for g in font.charset if g in glyphs] 

125 font.numGlyphs = len(font.charset) 

126 

127 if s.options.retain_gids: 

128 isCFF2 = cff.major > 1 

129 for g in s.glyphs_emptied: 

130 _empty_charstring(font, g, isCFF2=isCFF2, ignoreWidth=True) 

131 

132 return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) 

133 

134 

135@_add_method(psCharStrings.T2CharString) 

136def subset_subroutines(self, subrs, gsubrs): 

137 p = self.program 

138 for i in range(1, len(p)): 

139 if p[i] == "callsubr": 

140 assert isinstance(p[i - 1], int) 

141 p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias 

142 elif p[i] == "callgsubr": 

143 assert isinstance(p[i - 1], int) 

144 p[i - 1] = ( 

145 gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias 

146 ) 

147 

148 

149@_add_method(psCharStrings.T2CharString) 

150def drop_hints(self): 

151 hints = self._hints 

152 

153 if hints.deletions: 

154 p = self.program 

155 for idx in reversed(hints.deletions): 

156 del p[idx - 2 : idx] 

157 

158 if hints.has_hint: 

159 assert not hints.deletions or hints.last_hint <= hints.deletions[0] 

160 self.program = self.program[hints.last_hint :] 

161 if not self.program: 

162 # TODO CFF2 no need for endchar. 

163 self.program.append("endchar") 

164 if hasattr(self, "width"): 

165 # Insert width back if needed 

166 if self.width != self.private.defaultWidthX: 

167 # For CFF2 charstrings, this should never happen 

168 assert ( 

169 self.private.defaultWidthX is not None 

170 ), "CFF2 CharStrings must not have an initial width value" 

171 self.program.insert(0, self.width - self.private.nominalWidthX) 

172 

173 if hints.has_hintmask: 

174 i = 0 

175 p = self.program 

176 while i < len(p): 

177 if p[i] in ["hintmask", "cntrmask"]: 

178 assert i + 1 <= len(p) 

179 del p[i : i + 2] 

180 continue 

181 i += 1 

182 

183 assert len(self.program) 

184 

185 del self._hints 

186 

187 

188class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): 

189 def __init__(self, localSubrs, globalSubrs, private): 

190 psCharStrings.SimpleT2Decompiler.__init__( 

191 self, localSubrs, globalSubrs, private 

192 ) 

193 for subrs in [localSubrs, globalSubrs]: 

194 if subrs and not hasattr(subrs, "_used"): 

195 subrs._used = set() 

196 

197 def op_callsubr(self, index): 

198 self.localSubrs._used.add(self.operandStack[-1] + self.localBias) 

199 psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) 

200 

201 def op_callgsubr(self, index): 

202 self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias) 

203 psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) 

204 

205 

206class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor): 

207 class Hints(object): 

208 def __init__(self): 

209 # Whether calling this charstring produces any hint stems 

210 # Note that if a charstring starts with hintmask, it will 

211 # have has_hint set to True, because it *might* produce an 

212 # implicit vstem if called under certain conditions. 

213 self.has_hint = False 

214 # Index to start at to drop all hints 

215 self.last_hint = 0 

216 # Index up to which we know more hints are possible. 

217 # Only relevant if status is 0 or 1. 

218 self.last_checked = 0 

219 # The status means: 

220 # 0: after dropping hints, this charstring is empty 

221 # 1: after dropping hints, there may be more hints 

222 # continuing after this, or there might be 

223 # other things. Not clear yet. 

224 # 2: no more hints possible after this charstring 

225 self.status = 0 

226 # Has hintmask instructions; not recursive 

227 self.has_hintmask = False 

228 # List of indices of calls to empty subroutines to remove. 

229 self.deletions = [] 

230 

231 pass 

232 

233 def __init__( 

234 self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None 

235 ): 

236 self._css = css 

237 psCharStrings.T2WidthExtractor.__init__( 

238 self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX 

239 ) 

240 self.private = private 

241 

242 def execute(self, charString): 

243 old_hints = charString._hints if hasattr(charString, "_hints") else None 

244 charString._hints = self.Hints() 

245 

246 psCharStrings.T2WidthExtractor.execute(self, charString) 

247 

248 hints = charString._hints 

249 

250 if hints.has_hint or hints.has_hintmask: 

251 self._css.add(charString) 

252 

253 if hints.status != 2: 

254 # Check from last_check, make sure we didn't have any operators. 

255 for i in range(hints.last_checked, len(charString.program) - 1): 

256 if isinstance(charString.program[i], str): 

257 hints.status = 2 

258 break 

259 else: 

260 hints.status = 1 # There's *something* here 

261 hints.last_checked = len(charString.program) 

262 

263 if old_hints: 

264 assert hints.__dict__ == old_hints.__dict__ 

265 

266 def op_callsubr(self, index): 

267 subr = self.localSubrs[self.operandStack[-1] + self.localBias] 

268 psCharStrings.T2WidthExtractor.op_callsubr(self, index) 

269 self.processSubr(index, subr) 

270 

271 def op_callgsubr(self, index): 

272 subr = self.globalSubrs[self.operandStack[-1] + self.globalBias] 

273 psCharStrings.T2WidthExtractor.op_callgsubr(self, index) 

274 self.processSubr(index, subr) 

275 

276 def op_hstem(self, index): 

277 psCharStrings.T2WidthExtractor.op_hstem(self, index) 

278 self.processHint(index) 

279 

280 def op_vstem(self, index): 

281 psCharStrings.T2WidthExtractor.op_vstem(self, index) 

282 self.processHint(index) 

283 

284 def op_hstemhm(self, index): 

285 psCharStrings.T2WidthExtractor.op_hstemhm(self, index) 

286 self.processHint(index) 

287 

288 def op_vstemhm(self, index): 

289 psCharStrings.T2WidthExtractor.op_vstemhm(self, index) 

290 self.processHint(index) 

291 

292 def op_hintmask(self, index): 

293 rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) 

294 self.processHintmask(index) 

295 return rv 

296 

297 def op_cntrmask(self, index): 

298 rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) 

299 self.processHintmask(index) 

300 return rv 

301 

302 def processHintmask(self, index): 

303 cs = self.callingStack[-1] 

304 hints = cs._hints 

305 hints.has_hintmask = True 

306 if hints.status != 2: 

307 # Check from last_check, see if we may be an implicit vstem 

308 for i in range(hints.last_checked, index - 1): 

309 if isinstance(cs.program[i], str): 

310 hints.status = 2 

311 break 

312 else: 

313 # We are an implicit vstem 

314 hints.has_hint = True 

315 hints.last_hint = index + 1 

316 hints.status = 0 

317 hints.last_checked = index + 1 

318 

319 def processHint(self, index): 

320 cs = self.callingStack[-1] 

321 hints = cs._hints 

322 hints.has_hint = True 

323 hints.last_hint = index 

324 hints.last_checked = index 

325 

326 def processSubr(self, index, subr): 

327 cs = self.callingStack[-1] 

328 hints = cs._hints 

329 subr_hints = subr._hints 

330 

331 # Check from last_check, make sure we didn't have 

332 # any operators. 

333 if hints.status != 2: 

334 for i in range(hints.last_checked, index - 1): 

335 if isinstance(cs.program[i], str): 

336 hints.status = 2 

337 break 

338 hints.last_checked = index 

339 

340 if hints.status != 2: 

341 if subr_hints.has_hint: 

342 hints.has_hint = True 

343 

344 # Decide where to chop off from 

345 if subr_hints.status == 0: 

346 hints.last_hint = index 

347 else: 

348 hints.last_hint = index - 2 # Leave the subr call in 

349 

350 elif subr_hints.status == 0: 

351 hints.deletions.append(index) 

352 

353 hints.status = max(hints.status, subr_hints.status) 

354 

355 

356@_add_method(ttLib.getTableClass("CFF ")) 

357def prune_post_subset(self, ttfFont, options): 

358 cff = self.cff 

359 for fontname in cff.keys(): 

360 font = cff[fontname] 

361 cs = font.CharStrings 

362 

363 # Drop unused FontDictionaries 

364 if hasattr(font, "FDSelect"): 

365 sel = font.FDSelect 

366 indices = _uniq_sort(sel.gidArray) 

367 sel.gidArray = [indices.index(ss) for ss in sel.gidArray] 

368 arr = font.FDArray 

369 arr.items = [arr[i] for i in indices] 

370 del arr.file, arr.offsets 

371 

372 # Desubroutinize if asked for 

373 if options.desubroutinize: 

374 cff.desubroutinize() 

375 

376 # Drop hints if not needed 

377 if not options.hinting: 

378 self.remove_hints() 

379 elif not options.desubroutinize: 

380 self.remove_unused_subroutines() 

381 return True 

382 

383 

384def _delete_empty_subrs(private_dict): 

385 if hasattr(private_dict, "Subrs") and not private_dict.Subrs: 

386 if "Subrs" in private_dict.rawDict: 

387 del private_dict.rawDict["Subrs"] 

388 del private_dict.Subrs 

389 

390 

391@deprecateFunction( 

392 "use 'CFFFontSet.desubroutinize()' instead", category=DeprecationWarning 

393) 

394@_add_method(ttLib.getTableClass("CFF ")) 

395def desubroutinize(self): 

396 self.cff.desubroutinize() 

397 

398 

399@_add_method(ttLib.getTableClass("CFF ")) 

400def remove_hints(self): 

401 cff = self.cff 

402 for fontname in cff.keys(): 

403 font = cff[fontname] 

404 cs = font.CharStrings 

405 # This can be tricky, but doesn't have to. What we do is: 

406 # 

407 # - Run all used glyph charstrings and recurse into subroutines, 

408 # - For each charstring (including subroutines), if it has any 

409 # of the hint stem operators, we mark it as such. 

410 # Upon returning, for each charstring we note all the 

411 # subroutine calls it makes that (recursively) contain a stem, 

412 # - Dropping hinting then consists of the following two ops: 

413 # * Drop the piece of the program in each charstring before the 

414 # last call to a stem op or a stem-calling subroutine, 

415 # * Drop all hintmask operations. 

416 # - It's trickier... A hintmask right after hints and a few numbers 

417 # will act as an implicit vstemhm. As such, we track whether 

418 # we have seen any non-hint operators so far and do the right 

419 # thing, recursively... Good luck understanding that :( 

420 css = set() 

421 for g in font.charset: 

422 c, _ = cs.getItemAndSelector(g) 

423 c.decompile() 

424 subrs = getattr(c.private, "Subrs", []) 

425 decompiler = _DehintingT2Decompiler( 

426 css, 

427 subrs, 

428 c.globalSubrs, 

429 c.private.nominalWidthX, 

430 c.private.defaultWidthX, 

431 c.private, 

432 ) 

433 decompiler.execute(c) 

434 c.width = decompiler.width 

435 for charstring in css: 

436 charstring.drop_hints() 

437 del css 

438 

439 # Drop font-wide hinting values 

440 all_privs = [] 

441 if hasattr(font, "FDArray"): 

442 all_privs.extend(fd.Private for fd in font.FDArray) 

443 else: 

444 all_privs.append(font.Private) 

445 for priv in all_privs: 

446 for k in [ 

447 "BlueValues", 

448 "OtherBlues", 

449 "FamilyBlues", 

450 "FamilyOtherBlues", 

451 "BlueScale", 

452 "BlueShift", 

453 "BlueFuzz", 

454 "StemSnapH", 

455 "StemSnapV", 

456 "StdHW", 

457 "StdVW", 

458 "ForceBold", 

459 "LanguageGroup", 

460 "ExpansionFactor", 

461 ]: 

462 if hasattr(priv, k): 

463 setattr(priv, k, None) 

464 self.remove_unused_subroutines() 

465 

466 

467@_add_method(ttLib.getTableClass("CFF ")) 

468def remove_unused_subroutines(self): 

469 cff = self.cff 

470 for fontname in cff.keys(): 

471 font = cff[fontname] 

472 cs = font.CharStrings 

473 # Renumber subroutines to remove unused ones 

474 

475 # Mark all used subroutines 

476 for g in font.charset: 

477 c, _ = cs.getItemAndSelector(g) 

478 subrs = getattr(c.private, "Subrs", []) 

479 decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private) 

480 decompiler.execute(c) 

481 

482 all_subrs = [font.GlobalSubrs] 

483 if hasattr(font, "FDArray"): 

484 all_subrs.extend( 

485 fd.Private.Subrs 

486 for fd in font.FDArray 

487 if hasattr(fd.Private, "Subrs") and fd.Private.Subrs 

488 ) 

489 elif hasattr(font.Private, "Subrs") and font.Private.Subrs: 

490 all_subrs.append(font.Private.Subrs) 

491 

492 subrs = set(subrs) # Remove duplicates 

493 

494 # Prepare 

495 for subrs in all_subrs: 

496 if not hasattr(subrs, "_used"): 

497 subrs._used = set() 

498 subrs._used = _uniq_sort(subrs._used) 

499 subrs._old_bias = psCharStrings.calcSubrBias(subrs) 

500 subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) 

501 

502 # Renumber glyph charstrings 

503 for g in font.charset: 

504 c, _ = cs.getItemAndSelector(g) 

505 subrs = getattr(c.private, "Subrs", None) 

506 c.subset_subroutines(subrs, font.GlobalSubrs) 

507 

508 # Renumber subroutines themselves 

509 for subrs in all_subrs: 

510 if subrs == font.GlobalSubrs: 

511 if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"): 

512 local_subrs = font.Private.Subrs 

513 else: 

514 local_subrs = None 

515 else: 

516 local_subrs = subrs 

517 

518 subrs.items = [subrs.items[i] for i in subrs._used] 

519 if hasattr(subrs, "file"): 

520 del subrs.file 

521 if hasattr(subrs, "offsets"): 

522 del subrs.offsets 

523 

524 for subr in subrs.items: 

525 subr.subset_subroutines(local_subrs, font.GlobalSubrs) 

526 

527 # Delete local SubrsIndex if empty 

528 if hasattr(font, "FDArray"): 

529 for fd in font.FDArray: 

530 _delete_empty_subrs(fd.Private) 

531 else: 

532 _delete_empty_subrs(font.Private) 

533 

534 # Cleanup 

535 for subrs in all_subrs: 

536 del subrs._used, subrs._old_bias, subrs._new_bias