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 537