1from fontTools.misc import psCharStrings 2from fontTools import ttLib 3from fontTools.pens.basePen import NullPen 4from fontTools.misc.fixedTools import otRound 5from fontTools.varLib.varStore import VarStoreInstancer 6 7def _add_method(*clazzes): 8 """Returns a decorator function that adds a new method to one or 9 more classes.""" 10 def wrapper(method): 11 done = [] 12 for clazz in clazzes: 13 if clazz in done: continue # Support multiple names of a clazz 14 done.append(clazz) 15 assert clazz.__name__ != 'DefaultTable', \ 16 'Oops, table class not found.' 17 assert not hasattr(clazz, method.__name__), \ 18 "Oops, class '%s' has method '%s'." % (clazz.__name__, 19 method.__name__) 20 setattr(clazz, method.__name__, method) 21 return None 22 return wrapper 23 24def _uniq_sort(l): 25 return sorted(set(l)) 26 27class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler): 28 29 def __init__(self, components, localSubrs, globalSubrs): 30 psCharStrings.SimpleT2Decompiler.__init__(self, 31 localSubrs, 32 globalSubrs) 33 self.components = components 34 35 def op_endchar(self, index): 36 args = self.popall() 37 if len(args) >= 4: 38 from fontTools.encodings.StandardEncoding import StandardEncoding 39 # endchar can do seac accent bulding; The T2 spec says it's deprecated, 40 # but recent software that shall remain nameless does output it. 41 adx, ady, bchar, achar = args[-4:] 42 baseGlyph = StandardEncoding[bchar] 43 accentGlyph = StandardEncoding[achar] 44 self.components.add(baseGlyph) 45 self.components.add(accentGlyph) 46 47@_add_method(ttLib.getTableClass('CFF ')) 48def closure_glyphs(self, s): 49 cff = self.cff 50 assert len(cff) == 1 51 font = cff[cff.keys()[0]] 52 glyphSet = font.CharStrings 53 54 decompose = s.glyphs 55 while decompose: 56 components = set() 57 for g in decompose: 58 if g not in glyphSet: 59 continue 60 gl = glyphSet[g] 61 62 subrs = getattr(gl.private, "Subrs", []) 63 decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs) 64 decompiler.execute(gl) 65 components -= s.glyphs 66 s.glyphs.update(components) 67 decompose = components 68 69def _empty_charstring(font, glyphName, isCFF2, ignoreWidth=False): 70 c, fdSelectIndex = font.CharStrings.getItemAndSelector(glyphName) 71 if isCFF2 or ignoreWidth: 72 # CFF2 charstrings have no widths nor 'endchar' operators 73 c.decompile() 74 c.program = [] if isCFF2 else ['endchar'] 75 else: 76 if hasattr(font, 'FDArray') and font.FDArray is not None: 77 private = font.FDArray[fdSelectIndex].Private 78 else: 79 private = font.Private 80 dfltWdX = private.defaultWidthX 81 nmnlWdX = private.nominalWidthX 82 pen = NullPen() 83 c.draw(pen) # this will set the charstring's width 84 if c.width != dfltWdX: 85 c.program = [c.width - nmnlWdX, 'endchar'] 86 else: 87 c.program = ['endchar'] 88 89@_add_method(ttLib.getTableClass('CFF ')) 90def prune_pre_subset(self, font, options): 91 cff = self.cff 92 # CFF table must have one font only 93 cff.fontNames = cff.fontNames[:1] 94 95 if options.notdef_glyph and not options.notdef_outline: 96 isCFF2 = cff.major > 1 97 for fontname in cff.keys(): 98 font = cff[fontname] 99 _empty_charstring(font, ".notdef", isCFF2=isCFF2) 100 101 # Clear useless Encoding 102 for fontname in cff.keys(): 103 font = cff[fontname] 104 # https://github.com/fonttools/fonttools/issues/620 105 font.Encoding = "StandardEncoding" 106 107 return True # bool(cff.fontNames) 108 109@_add_method(ttLib.getTableClass('CFF ')) 110def subset_glyphs(self, s): 111 cff = self.cff 112 for fontname in cff.keys(): 113 font = cff[fontname] 114 cs = font.CharStrings 115 116 if s.options.retain_gids: 117 isCFF2 = cff.major > 1 118 for g in s.glyphs_emptied: 119 _empty_charstring(font, g, isCFF2=isCFF2, ignoreWidth=True) 120 else: 121 # Load all glyphs 122 for g in font.charset: 123 if g not in s.glyphs: continue 124 c, _ = cs.getItemAndSelector(g) 125 126 if cs.charStringsAreIndexed: 127 indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] 128 csi = cs.charStringsIndex 129 csi.items = [csi.items[i] for i in indices] 130 del csi.file, csi.offsets 131 if hasattr(font, "FDSelect"): 132 sel = font.FDSelect 133 # XXX We want to set sel.format to None, such that the 134 # most compact format is selected. However, OTS was 135 # broken and couldn't parse a FDSelect format 0 that 136 # happened before CharStrings. As such, always force 137 # format 3 until we fix cffLib to always generate 138 # FDSelect after CharStrings. 139 # https://github.com/khaledhosny/ots/pull/31 140 #sel.format = None 141 sel.format = 3 142 sel.gidArray = [sel.gidArray[i] for i in indices] 143 cs.charStrings = {g:indices.index(v) 144 for g,v in cs.charStrings.items() 145 if g in s.glyphs} 146 else: 147 cs.charStrings = {g:v 148 for g,v in cs.charStrings.items() 149 if g in s.glyphs} 150 font.charset = [g for g in font.charset if g in s.glyphs] 151 font.numGlyphs = len(font.charset) 152 153 return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) 154 155@_add_method(psCharStrings.T2CharString) 156def subset_subroutines(self, subrs, gsubrs): 157 p = self.program 158 for i in range(1, len(p)): 159 if p[i] == 'callsubr': 160 assert isinstance(p[i-1], int) 161 p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias 162 elif p[i] == 'callgsubr': 163 assert isinstance(p[i-1], int) 164 p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias 165 166@_add_method(psCharStrings.T2CharString) 167def drop_hints(self): 168 hints = self._hints 169 170 if hints.deletions: 171 p = self.program 172 for idx in reversed(hints.deletions): 173 del p[idx-2:idx] 174 175 if hints.has_hint: 176 assert not hints.deletions or hints.last_hint <= hints.deletions[0] 177 self.program = self.program[hints.last_hint:] 178 if not self.program: 179 # TODO CFF2 no need for endchar. 180 self.program.append('endchar') 181 if hasattr(self, 'width'): 182 # Insert width back if needed 183 if self.width != self.private.defaultWidthX: 184 # For CFF2 charstrings, this should never happen 185 assert self.private.defaultWidthX is not None, "CFF2 CharStrings must not have an initial width value" 186 self.program.insert(0, self.width - self.private.nominalWidthX) 187 188 if hints.has_hintmask: 189 i = 0 190 p = self.program 191 while i < len(p): 192 if p[i] in ['hintmask', 'cntrmask']: 193 assert i + 1 <= len(p) 194 del p[i:i+2] 195 continue 196 i += 1 197 198 assert len(self.program) 199 200 del self._hints 201 202class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): 203 204 def __init__(self, localSubrs, globalSubrs, private): 205 psCharStrings.SimpleT2Decompiler.__init__(self, 206 localSubrs, 207 globalSubrs, 208 private) 209 for subrs in [localSubrs, globalSubrs]: 210 if subrs and not hasattr(subrs, "_used"): 211 subrs._used = set() 212 213 def op_callsubr(self, index): 214 self.localSubrs._used.add(self.operandStack[-1]+self.localBias) 215 psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) 216 217 def op_callgsubr(self, index): 218 self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) 219 psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) 220 221class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor): 222 223 class Hints(object): 224 def __init__(self): 225 # Whether calling this charstring produces any hint stems 226 # Note that if a charstring starts with hintmask, it will 227 # have has_hint set to True, because it *might* produce an 228 # implicit vstem if called under certain conditions. 229 self.has_hint = False 230 # Index to start at to drop all hints 231 self.last_hint = 0 232 # Index up to which we know more hints are possible. 233 # Only relevant if status is 0 or 1. 234 self.last_checked = 0 235 # The status means: 236 # 0: after dropping hints, this charstring is empty 237 # 1: after dropping hints, there may be more hints 238 # continuing after this, or there might be 239 # other things. Not clear yet. 240 # 2: no more hints possible after this charstring 241 self.status = 0 242 # Has hintmask instructions; not recursive 243 self.has_hintmask = False 244 # List of indices of calls to empty subroutines to remove. 245 self.deletions = [] 246 pass 247 248 def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None): 249 self._css = css 250 psCharStrings.T2WidthExtractor.__init__( 251 self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) 252 self.private = private 253 254 def execute(self, charString): 255 old_hints = charString._hints if hasattr(charString, '_hints') else None 256 charString._hints = self.Hints() 257 258 psCharStrings.T2WidthExtractor.execute(self, charString) 259 260 hints = charString._hints 261 262 if hints.has_hint or hints.has_hintmask: 263 self._css.add(charString) 264 265 if hints.status != 2: 266 # Check from last_check, make sure we didn't have any operators. 267 for i in range(hints.last_checked, len(charString.program) - 1): 268 if isinstance(charString.program[i], str): 269 hints.status = 2 270 break 271 else: 272 hints.status = 1 # There's *something* here 273 hints.last_checked = len(charString.program) 274 275 if old_hints: 276 assert hints.__dict__ == old_hints.__dict__ 277 278 def op_callsubr(self, index): 279 subr = self.localSubrs[self.operandStack[-1]+self.localBias] 280 psCharStrings.T2WidthExtractor.op_callsubr(self, index) 281 self.processSubr(index, subr) 282 283 def op_callgsubr(self, index): 284 subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] 285 psCharStrings.T2WidthExtractor.op_callgsubr(self, index) 286 self.processSubr(index, subr) 287 288 def op_hstem(self, index): 289 psCharStrings.T2WidthExtractor.op_hstem(self, index) 290 self.processHint(index) 291 def op_vstem(self, index): 292 psCharStrings.T2WidthExtractor.op_vstem(self, index) 293 self.processHint(index) 294 def op_hstemhm(self, index): 295 psCharStrings.T2WidthExtractor.op_hstemhm(self, index) 296 self.processHint(index) 297 def op_vstemhm(self, index): 298 psCharStrings.T2WidthExtractor.op_vstemhm(self, index) 299 self.processHint(index) 300 def op_hintmask(self, index): 301 rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) 302 self.processHintmask(index) 303 return rv 304 def op_cntrmask(self, index): 305 rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) 306 self.processHintmask(index) 307 return rv 308 309 def processHintmask(self, index): 310 cs = self.callingStack[-1] 311 hints = cs._hints 312 hints.has_hintmask = True 313 if hints.status != 2: 314 # Check from last_check, see if we may be an implicit vstem 315 for i in range(hints.last_checked, index - 1): 316 if isinstance(cs.program[i], str): 317 hints.status = 2 318 break 319 else: 320 # We are an implicit vstem 321 hints.has_hint = True 322 hints.last_hint = index + 1 323 hints.status = 0 324 hints.last_checked = index + 1 325 326 def processHint(self, index): 327 cs = self.callingStack[-1] 328 hints = cs._hints 329 hints.has_hint = True 330 hints.last_hint = index 331 hints.last_checked = index 332 333 def processSubr(self, index, subr): 334 cs = self.callingStack[-1] 335 hints = cs._hints 336 subr_hints = subr._hints 337 338 # Check from last_check, make sure we didn't have 339 # any operators. 340 if hints.status != 2: 341 for i in range(hints.last_checked, index - 1): 342 if isinstance(cs.program[i], str): 343 hints.status = 2 344 break 345 hints.last_checked = index 346 347 if hints.status != 2: 348 if subr_hints.has_hint: 349 hints.has_hint = True 350 351 # Decide where to chop off from 352 if subr_hints.status == 0: 353 hints.last_hint = index 354 else: 355 hints.last_hint = index - 2 # Leave the subr call in 356 357 elif subr_hints.status == 0: 358 hints.deletions.append(index) 359 360 hints.status = max(hints.status, subr_hints.status) 361 362class StopHintCountEvent(Exception): 363 pass 364 365 366 367 368class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler): 369 stop_hintcount_ops = ("op_hstem", "op_vstem", "op_rmoveto", "op_hmoveto", 370 "op_vmoveto") 371 372 def __init__(self, localSubrs, globalSubrs, private=None): 373 psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, 374 private) 375 376 def execute(self, charString): 377 self.need_hintcount = True # until proven otherwise 378 for op_name in self.stop_hintcount_ops: 379 setattr(self, op_name, self.stop_hint_count) 380 381 if hasattr(charString, '_desubroutinized'): 382 if self.need_hintcount and self.callingStack: 383 try: 384 psCharStrings.SimpleT2Decompiler.execute(self, charString) 385 except StopHintCountEvent: 386 del self.callingStack[-1] 387 return 388 389 charString._patches = [] 390 psCharStrings.SimpleT2Decompiler.execute(self, charString) 391 desubroutinized = charString.program[:] 392 for idx, expansion in reversed(charString._patches): 393 assert idx >= 2 394 assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1] 395 assert type(desubroutinized[idx - 2]) == int 396 if expansion[-1] == 'return': 397 expansion = expansion[:-1] 398 desubroutinized[idx-2:idx] = expansion 399 if not self.private.in_cff2: 400 if 'endchar' in desubroutinized: 401 # Cut off after first endchar 402 desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1] 403 else: 404 if not len(desubroutinized) or desubroutinized[-1] != 'return': 405 desubroutinized.append('return') 406 407 charString._desubroutinized = desubroutinized 408 del charString._patches 409 410 def op_callsubr(self, index): 411 subr = self.localSubrs[self.operandStack[-1]+self.localBias] 412 psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) 413 self.processSubr(index, subr) 414 415 def op_callgsubr(self, index): 416 subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] 417 psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) 418 self.processSubr(index, subr) 419 420 def stop_hint_count(self, *args): 421 self.need_hintcount = False 422 for op_name in self.stop_hintcount_ops: 423 setattr(self, op_name, None) 424 cs = self.callingStack[-1] 425 if hasattr(cs, '_desubroutinized'): 426 raise StopHintCountEvent() 427 428 def op_hintmask(self, index): 429 psCharStrings.SimpleT2Decompiler.op_hintmask(self, index) 430 if self.need_hintcount: 431 self.stop_hint_count() 432 433 def processSubr(self, index, subr): 434 cs = self.callingStack[-1] 435 if not hasattr(cs, '_desubroutinized'): 436 cs._patches.append((index, subr._desubroutinized)) 437 438 439@_add_method(ttLib.getTableClass('CFF ')) 440def prune_post_subset(self, ttfFont, options): 441 cff = self.cff 442 for fontname in cff.keys(): 443 font = cff[fontname] 444 cs = font.CharStrings 445 446 # Drop unused FontDictionaries 447 if hasattr(font, "FDSelect"): 448 sel = font.FDSelect 449 indices = _uniq_sort(sel.gidArray) 450 sel.gidArray = [indices.index (ss) for ss in sel.gidArray] 451 arr = font.FDArray 452 arr.items = [arr[i] for i in indices] 453 del arr.file, arr.offsets 454 455 # Desubroutinize if asked for 456 if options.desubroutinize: 457 self.desubroutinize() 458 459 # Drop hints if not needed 460 if not options.hinting: 461 self.remove_hints() 462 elif not options.desubroutinize: 463 self.remove_unused_subroutines() 464 return True 465 466 467def _delete_empty_subrs(private_dict): 468 if hasattr(private_dict, 'Subrs') and not private_dict.Subrs: 469 if 'Subrs' in private_dict.rawDict: 470 del private_dict.rawDict['Subrs'] 471 del private_dict.Subrs 472 473@_add_method(ttLib.getTableClass('CFF ')) 474def desubroutinize(self): 475 cff = self.cff 476 for fontname in cff.keys(): 477 font = cff[fontname] 478 cs = font.CharStrings 479 for g in font.charset: 480 c, _ = cs.getItemAndSelector(g) 481 c.decompile() 482 subrs = getattr(c.private, "Subrs", []) 483 decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private) 484 decompiler.execute(c) 485 c.program = c._desubroutinized 486 del c._desubroutinized 487 # Delete all the local subrs 488 if hasattr(font, 'FDArray'): 489 for fd in font.FDArray: 490 pd = fd.Private 491 if hasattr(pd, 'Subrs'): 492 del pd.Subrs 493 if 'Subrs' in pd.rawDict: 494 del pd.rawDict['Subrs'] 495 else: 496 pd = font.Private 497 if hasattr(pd, 'Subrs'): 498 del pd.Subrs 499 if 'Subrs' in pd.rawDict: 500 del pd.rawDict['Subrs'] 501 # as well as the global subrs 502 cff.GlobalSubrs.clear() 503 504 505@_add_method(ttLib.getTableClass('CFF ')) 506def remove_hints(self): 507 cff = self.cff 508 for fontname in cff.keys(): 509 font = cff[fontname] 510 cs = font.CharStrings 511 # This can be tricky, but doesn't have to. What we do is: 512 # 513 # - Run all used glyph charstrings and recurse into subroutines, 514 # - For each charstring (including subroutines), if it has any 515 # of the hint stem operators, we mark it as such. 516 # Upon returning, for each charstring we note all the 517 # subroutine calls it makes that (recursively) contain a stem, 518 # - Dropping hinting then consists of the following two ops: 519 # * Drop the piece of the program in each charstring before the 520 # last call to a stem op or a stem-calling subroutine, 521 # * Drop all hintmask operations. 522 # - It's trickier... A hintmask right after hints and a few numbers 523 # will act as an implicit vstemhm. As such, we track whether 524 # we have seen any non-hint operators so far and do the right 525 # thing, recursively... Good luck understanding that :( 526 css = set() 527 for g in font.charset: 528 c, _ = cs.getItemAndSelector(g) 529 c.decompile() 530 subrs = getattr(c.private, "Subrs", []) 531 decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs, 532 c.private.nominalWidthX, 533 c.private.defaultWidthX, 534 c.private) 535 decompiler.execute(c) 536 c.width = decompiler.width 537 for charstring in css: 538 charstring.drop_hints() 539 del css 540 541 # Drop font-wide hinting values 542 all_privs = [] 543 if hasattr(font, 'FDArray'): 544 all_privs.extend(fd.Private for fd in font.FDArray) 545 else: 546 all_privs.append(font.Private) 547 for priv in all_privs: 548 for k in ['BlueValues', 'OtherBlues', 549 'FamilyBlues', 'FamilyOtherBlues', 550 'BlueScale', 'BlueShift', 'BlueFuzz', 551 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW', 552 'ForceBold', 'LanguageGroup', 'ExpansionFactor']: 553 if hasattr(priv, k): 554 setattr(priv, k, None) 555 self.remove_unused_subroutines() 556 557 558@_add_method(ttLib.getTableClass('CFF ')) 559def remove_unused_subroutines(self): 560 cff = self.cff 561 for fontname in cff.keys(): 562 font = cff[fontname] 563 cs = font.CharStrings 564 # Renumber subroutines to remove unused ones 565 566 # Mark all used subroutines 567 for g in font.charset: 568 c, _ = cs.getItemAndSelector(g) 569 subrs = getattr(c.private, "Subrs", []) 570 decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private) 571 decompiler.execute(c) 572 573 all_subrs = [font.GlobalSubrs] 574 if hasattr(font, 'FDArray'): 575 all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) 576 elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: 577 all_subrs.append(font.Private.Subrs) 578 579 subrs = set(subrs) # Remove duplicates 580 581 # Prepare 582 for subrs in all_subrs: 583 if not hasattr(subrs, '_used'): 584 subrs._used = set() 585 subrs._used = _uniq_sort(subrs._used) 586 subrs._old_bias = psCharStrings.calcSubrBias(subrs) 587 subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) 588 589 # Renumber glyph charstrings 590 for g in font.charset: 591 c, _ = cs.getItemAndSelector(g) 592 subrs = getattr(c.private, "Subrs", []) 593 c.subset_subroutines (subrs, font.GlobalSubrs) 594 595 # Renumber subroutines themselves 596 for subrs in all_subrs: 597 if subrs == font.GlobalSubrs: 598 if not hasattr(font, 'FDArray') and hasattr(font.Private, 'Subrs'): 599 local_subrs = font.Private.Subrs 600 else: 601 local_subrs = [] 602 else: 603 local_subrs = subrs 604 605 subrs.items = [subrs.items[i] for i in subrs._used] 606 if hasattr(subrs, 'file'): 607 del subrs.file 608 if hasattr(subrs, 'offsets'): 609 del subrs.offsets 610 611 for subr in subrs.items: 612 subr.subset_subroutines (local_subrs, font.GlobalSubrs) 613 614 # Delete local SubrsIndex if empty 615 if hasattr(font, 'FDArray'): 616 for fd in font.FDArray: 617 _delete_empty_subrs(fd.Private) 618 else: 619 _delete_empty_subrs(font.Private) 620 621 # Cleanup 622 for subrs in all_subrs: 623 del subrs._used, subrs._old_bias, subrs._new_bias 624