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