1from collections import namedtuple 2from fontTools.cffLib import ( 3 maxStackLimit, 4 TopDictIndex, 5 buildOrder, 6 topDictOperators, 7 topDictOperators2, 8 privateDictOperators, 9 privateDictOperators2, 10 FDArrayIndex, 11 FontDict, 12 VarStoreData 13) 14from io import BytesIO 15from fontTools.cffLib.specializer import ( 16 specializeCommands, commandsToProgram) 17from fontTools.ttLib import newTable 18from fontTools import varLib 19from fontTools.varLib.models import allEqual 20from fontTools.misc.roundTools import roundFunc 21from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor 22from fontTools.pens.t2CharStringPen import T2CharStringPen 23from functools import partial 24 25from .errors import ( 26 VarLibCFFDictMergeError, VarLibCFFPointTypeMergeError, 27 VarLibCFFHintTypeMergeError,VarLibMergeError) 28 29 30# Backwards compatibility 31MergeDictError = VarLibCFFDictMergeError 32MergeTypeError = VarLibCFFPointTypeMergeError 33 34 35def addCFFVarStore(varFont, varModel, varDataList, masterSupports): 36 fvarTable = varFont['fvar'] 37 axisKeys = [axis.axisTag for axis in fvarTable.axes] 38 varTupleList = varLib.builder.buildVarRegionList(masterSupports, axisKeys) 39 varStoreCFFV = varLib.builder.buildVarStore(varTupleList, varDataList) 40 41 topDict = varFont['CFF2'].cff.topDictIndex[0] 42 topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV) 43 if topDict.FDArray[0].vstore is None: 44 fdArray = topDict.FDArray 45 for fontDict in fdArray: 46 if hasattr(fontDict, "Private"): 47 fontDict.Private.vstore = topDict.VarStore 48 49 50def lib_convertCFFToCFF2(cff, otFont): 51 # This assumes a decompiled CFF table. 52 cff2GetGlyphOrder = cff.otFont.getGlyphOrder 53 topDictData = TopDictIndex(None, cff2GetGlyphOrder, None) 54 topDictData.items = cff.topDictIndex.items 55 cff.topDictIndex = topDictData 56 topDict = topDictData[0] 57 if hasattr(topDict, 'Private'): 58 privateDict = topDict.Private 59 else: 60 privateDict = None 61 opOrder = buildOrder(topDictOperators2) 62 topDict.order = opOrder 63 topDict.cff2GetGlyphOrder = cff2GetGlyphOrder 64 if not hasattr(topDict, "FDArray"): 65 fdArray = topDict.FDArray = FDArrayIndex() 66 fdArray.strings = None 67 fdArray.GlobalSubrs = topDict.GlobalSubrs 68 topDict.GlobalSubrs.fdArray = fdArray 69 charStrings = topDict.CharStrings 70 if charStrings.charStringsAreIndexed: 71 charStrings.charStringsIndex.fdArray = fdArray 72 else: 73 charStrings.fdArray = fdArray 74 fontDict = FontDict() 75 fontDict.setCFF2(True) 76 fdArray.append(fontDict) 77 fontDict.Private = privateDict 78 privateOpOrder = buildOrder(privateDictOperators2) 79 if privateDict is not None: 80 for entry in privateDictOperators: 81 key = entry[1] 82 if key not in privateOpOrder: 83 if key in privateDict.rawDict: 84 # print "Removing private dict", key 85 del privateDict.rawDict[key] 86 if hasattr(privateDict, key): 87 delattr(privateDict, key) 88 # print "Removing privateDict attr", key 89 else: 90 # clean up the PrivateDicts in the fdArray 91 fdArray = topDict.FDArray 92 privateOpOrder = buildOrder(privateDictOperators2) 93 for fontDict in fdArray: 94 fontDict.setCFF2(True) 95 for key in list(fontDict.rawDict.keys()): 96 if key not in fontDict.order: 97 del fontDict.rawDict[key] 98 if hasattr(fontDict, key): 99 delattr(fontDict, key) 100 101 privateDict = fontDict.Private 102 for entry in privateDictOperators: 103 key = entry[1] 104 if key not in privateOpOrder: 105 if key in privateDict.rawDict: 106 # print "Removing private dict", key 107 del privateDict.rawDict[key] 108 if hasattr(privateDict, key): 109 delattr(privateDict, key) 110 # print "Removing privateDict attr", key 111 # Now delete up the decrecated topDict operators from CFF 1.0 112 for entry in topDictOperators: 113 key = entry[1] 114 if key not in opOrder: 115 if key in topDict.rawDict: 116 del topDict.rawDict[key] 117 if hasattr(topDict, key): 118 delattr(topDict, key) 119 120 # At this point, the Subrs and Charstrings are all still T2Charstring class 121 # easiest to fix this by compiling, then decompiling again 122 cff.major = 2 123 file = BytesIO() 124 cff.compile(file, otFont, isCFF2=True) 125 file.seek(0) 126 cff.decompile(file, otFont, isCFF2=True) 127 128 129def convertCFFtoCFF2(varFont): 130 # Convert base font to a single master CFF2 font. 131 cffTable = varFont['CFF '] 132 lib_convertCFFToCFF2(cffTable.cff, varFont) 133 newCFF2 = newTable("CFF2") 134 newCFF2.cff = cffTable.cff 135 varFont['CFF2'] = newCFF2 136 del varFont['CFF '] 137 138 139def conv_to_int(num): 140 if isinstance(num, float) and num.is_integer(): 141 return int(num) 142 return num 143 144 145pd_blend_fields = ("BlueValues", "OtherBlues", "FamilyBlues", 146 "FamilyOtherBlues", "BlueScale", "BlueShift", 147 "BlueFuzz", "StdHW", "StdVW", "StemSnapH", 148 "StemSnapV") 149 150 151def get_private(regionFDArrays, fd_index, ri, fd_map): 152 region_fdArray = regionFDArrays[ri] 153 region_fd_map = fd_map[fd_index] 154 if ri in region_fd_map: 155 region_fdIndex = region_fd_map[ri] 156 private = region_fdArray[region_fdIndex].Private 157 else: 158 private = None 159 return private 160 161 162def merge_PrivateDicts(top_dicts, vsindex_dict, var_model, fd_map): 163 """ 164 I step through the FontDicts in the FDArray of the varfont TopDict. 165 For each varfont FontDict: 166 167 * step through each key in FontDict.Private. 168 * For each key, step through each relevant source font Private dict, and 169 build a list of values to blend. 170 171 The 'relevant' source fonts are selected by first getting the right 172 submodel using ``vsindex_dict[vsindex]``. The indices of the 173 ``subModel.locations`` are mapped to source font list indices by 174 assuming the latter order is the same as the order of the 175 ``var_model.locations``. I can then get the index of each subModel 176 location in the list of ``var_model.locations``. 177 """ 178 179 topDict = top_dicts[0] 180 region_top_dicts = top_dicts[1:] 181 if hasattr(region_top_dicts[0], 'FDArray'): 182 regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts] 183 else: 184 regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts] 185 for fd_index, font_dict in enumerate(topDict.FDArray): 186 private_dict = font_dict.Private 187 vsindex = getattr(private_dict, 'vsindex', 0) 188 # At the moment, no PrivateDict has a vsindex key, but let's support 189 # how it should work. See comment at end of 190 # merge_charstrings() - still need to optimize use of vsindex. 191 sub_model, _ = vsindex_dict[vsindex] 192 master_indices = [] 193 for loc in sub_model.locations[1:]: 194 i = var_model.locations.index(loc) - 1 195 master_indices.append(i) 196 pds = [private_dict] 197 last_pd = private_dict 198 for ri in master_indices: 199 pd = get_private(regionFDArrays, fd_index, ri, fd_map) 200 # If the region font doesn't have this FontDict, just reference 201 # the last one used. 202 if pd is None: 203 pd = last_pd 204 else: 205 last_pd = pd 206 pds.append(pd) 207 num_masters = len(pds) 208 for key, value in private_dict.rawDict.items(): 209 dataList = [] 210 if key not in pd_blend_fields: 211 continue 212 if isinstance(value, list): 213 try: 214 values = [pd.rawDict[key] for pd in pds] 215 except KeyError: 216 print( 217 "Warning: {key} in default font Private dict is " 218 "missing from another font, and was " 219 "discarded.".format(key=key)) 220 continue 221 try: 222 values = zip(*values) 223 except IndexError: 224 raise VarLibCFFDictMergeError(key, value, values) 225 """ 226 Row 0 contains the first value from each master. 227 Convert each row from absolute values to relative 228 values from the previous row. 229 e.g for three masters, a list of values was: 230 master 0 OtherBlues = [-217,-205] 231 master 1 OtherBlues = [-234,-222] 232 master 1 OtherBlues = [-188,-176] 233 The call to zip() converts this to: 234 [(-217, -234, -188), (-205, -222, -176)] 235 and is converted finally to: 236 OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]] 237 """ 238 prev_val_list = [0] * num_masters 239 any_points_differ = False 240 for val_list in values: 241 rel_list = [(val - prev_val_list[i]) for ( 242 i, val) in enumerate(val_list)] 243 if (not any_points_differ) and not allEqual(rel_list): 244 any_points_differ = True 245 prev_val_list = val_list 246 deltas = sub_model.getDeltas(rel_list) 247 # For PrivateDict BlueValues, the default font 248 # values are absolute, not relative to the prior value. 249 deltas[0] = val_list[0] 250 dataList.append(deltas) 251 # If there are no blend values,then 252 # we can collapse the blend lists. 253 if not any_points_differ: 254 dataList = [data[0] for data in dataList] 255 else: 256 values = [pd.rawDict[key] for pd in pds] 257 if not allEqual(values): 258 dataList = sub_model.getDeltas(values) 259 else: 260 dataList = values[0] 261 262 # Convert numbers with no decimal part to an int 263 if isinstance(dataList, list): 264 for i, item in enumerate(dataList): 265 if isinstance(item, list): 266 for j, jtem in enumerate(item): 267 dataList[i][j] = conv_to_int(jtem) 268 else: 269 dataList[i] = conv_to_int(item) 270 else: 271 dataList = conv_to_int(dataList) 272 273 private_dict.rawDict[key] = dataList 274 275 276def _cff_or_cff2(font): 277 if "CFF " in font: 278 return font["CFF "] 279 return font["CFF2"] 280 281 282def getfd_map(varFont, fonts_list): 283 """ Since a subset source font may have fewer FontDicts in their 284 FDArray than the default font, we have to match up the FontDicts in 285 the different fonts . We do this with the FDSelect array, and by 286 assuming that the same glyph will reference matching FontDicts in 287 each source font. We return a mapping from fdIndex in the default 288 font to a dictionary which maps each master list index of each 289 region font to the equivalent fdIndex in the region font.""" 290 fd_map = {} 291 default_font = fonts_list[0] 292 region_fonts = fonts_list[1:] 293 num_regions = len(region_fonts) 294 topDict = _cff_or_cff2(default_font).cff.topDictIndex[0] 295 if not hasattr(topDict, 'FDSelect'): 296 # All glyphs reference only one FontDict. 297 # Map the FD index for regions to index 0. 298 fd_map[0] = {ri:0 for ri in range(num_regions)} 299 return fd_map 300 301 gname_mapping = {} 302 default_fdSelect = topDict.FDSelect 303 glyphOrder = default_font.getGlyphOrder() 304 for gid, fdIndex in enumerate(default_fdSelect): 305 gname_mapping[glyphOrder[gid]] = fdIndex 306 if fdIndex not in fd_map: 307 fd_map[fdIndex] = {} 308 for ri, region_font in enumerate(region_fonts): 309 region_glyphOrder = region_font.getGlyphOrder() 310 region_topDict = _cff_or_cff2(region_font).cff.topDictIndex[0] 311 if not hasattr(region_topDict, 'FDSelect'): 312 # All the glyphs share the same FontDict. Pick any glyph. 313 default_fdIndex = gname_mapping[region_glyphOrder[0]] 314 fd_map[default_fdIndex][ri] = 0 315 else: 316 region_fdSelect = region_topDict.FDSelect 317 for gid, fdIndex in enumerate(region_fdSelect): 318 default_fdIndex = gname_mapping[region_glyphOrder[gid]] 319 region_map = fd_map[default_fdIndex] 320 if ri not in region_map: 321 region_map[ri] = fdIndex 322 return fd_map 323 324 325CVarData = namedtuple('CVarData', 'varDataList masterSupports vsindex_dict') 326def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder): 327 topDict = varFont['CFF2'].cff.topDictIndex[0] 328 top_dicts = [topDict] + [ 329 _cff_or_cff2(ttFont).cff.topDictIndex[0] 330 for ttFont in ordered_fonts_list[1:] 331 ] 332 num_masters = len(model.mapping) 333 cvData = merge_charstrings(glyphOrder, num_masters, top_dicts, model) 334 fd_map = getfd_map(varFont, ordered_fonts_list) 335 merge_PrivateDicts(top_dicts, cvData.vsindex_dict, model, fd_map) 336 addCFFVarStore(varFont, model, cvData.varDataList, 337 cvData.masterSupports) 338 339 340def _get_cs(charstrings, glyphName): 341 if glyphName not in charstrings: 342 return None 343 return charstrings[glyphName] 344 345def _add_new_vsindex(model, key, masterSupports, vsindex_dict, 346 vsindex_by_key, varDataList): 347 varTupleIndexes = [] 348 for support in model.supports[1:]: 349 if support not in masterSupports: 350 masterSupports.append(support) 351 varTupleIndexes.append(masterSupports.index(support)) 352 var_data = varLib.builder.buildVarData(varTupleIndexes, None, False) 353 vsindex = len(vsindex_dict) 354 vsindex_by_key[key] = vsindex 355 vsindex_dict[vsindex] = (model, [key]) 356 varDataList.append(var_data) 357 return vsindex 358 359def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel): 360 361 vsindex_dict = {} 362 vsindex_by_key = {} 363 varDataList = [] 364 masterSupports = [] 365 default_charstrings = top_dicts[0].CharStrings 366 for gid, gname in enumerate(glyphOrder): 367 all_cs = [ 368 _get_cs(td.CharStrings, gname) 369 for td in top_dicts] 370 if len([gs for gs in all_cs if gs is not None]) == 1: 371 continue 372 model, model_cs = masterModel.getSubModel(all_cs) 373 # create the first pass CFF2 charstring, from 374 # the default charstring. 375 default_charstring = model_cs[0] 376 var_pen = CFF2CharStringMergePen([], gname, num_masters, 0) 377 # We need to override outlineExtractor because these 378 # charstrings do have widths in the 'program'; we need to drop these 379 # values rather than post assertion error for them. 380 default_charstring.outlineExtractor = MergeOutlineExtractor 381 default_charstring.draw(var_pen) 382 383 # Add the coordinates from all the other regions to the 384 # blend lists in the CFF2 charstring. 385 region_cs = model_cs[1:] 386 for region_idx, region_charstring in enumerate(region_cs, start=1): 387 var_pen.restart(region_idx) 388 region_charstring.outlineExtractor = MergeOutlineExtractor 389 region_charstring.draw(var_pen) 390 391 # Collapse each coordinate list to a blend operator and its args. 392 new_cs = var_pen.getCharString( 393 private=default_charstring.private, 394 globalSubrs=default_charstring.globalSubrs, 395 var_model=model, optimize=True) 396 default_charstrings[gname] = new_cs 397 398 if (not var_pen.seen_moveto) or ('blend' not in new_cs.program): 399 # If this is not a marking glyph, or if there are no blend 400 # arguments, then we can use vsindex 0. No need to 401 # check if we need a new vsindex. 402 continue 403 404 # If the charstring required a new model, create 405 # a VarData table to go with, and set vsindex. 406 key = tuple(v is not None for v in all_cs) 407 try: 408 vsindex = vsindex_by_key[key] 409 except KeyError: 410 vsindex = _add_new_vsindex(model, key, masterSupports, vsindex_dict, 411 vsindex_by_key, varDataList) 412 # We do not need to check for an existing new_cs.private.vsindex, 413 # as we know it doesn't exist yet. 414 if vsindex != 0: 415 new_cs.program[:0] = [vsindex, 'vsindex'] 416 417 # If there is no variation in any of the charstrings, then vsindex_dict 418 # never gets built. This could still be needed if there is variation 419 # in the PrivatDict, so we will build the default data for vsindex = 0. 420 if not vsindex_dict: 421 key = (True,) * num_masters 422 _add_new_vsindex(masterModel, key, masterSupports, vsindex_dict, 423 vsindex_by_key, varDataList) 424 cvData = CVarData(varDataList=varDataList, masterSupports=masterSupports, 425 vsindex_dict=vsindex_dict) 426 # XXX To do: optimize use of vsindex between the PrivateDicts and 427 # charstrings 428 return cvData 429 430 431class CFFToCFF2OutlineExtractor(T2OutlineExtractor): 432 """ This class is used to remove the initial width from the CFF 433 charstring without trying to add the width to self.nominalWidthX, 434 which is None. """ 435 def popallWidth(self, evenOdd=0): 436 args = self.popall() 437 if not self.gotWidth: 438 if evenOdd ^ (len(args) % 2): 439 args = args[1:] 440 self.width = self.defaultWidthX 441 self.gotWidth = 1 442 return args 443 444 445class MergeOutlineExtractor(CFFToCFF2OutlineExtractor): 446 """ Used to extract the charstring commands - including hints - from a 447 CFF charstring in order to merge it as another set of region data 448 into a CFF2 variable font charstring.""" 449 450 def __init__(self, pen, localSubrs, globalSubrs, 451 nominalWidthX, defaultWidthX, private=None): 452 super().__init__(pen, localSubrs, 453 globalSubrs, nominalWidthX, defaultWidthX, private) 454 455 def countHints(self): 456 args = self.popallWidth() 457 self.hintCount = self.hintCount + len(args) // 2 458 return args 459 460 def _hint_op(self, type, args): 461 self.pen.add_hint(type, args) 462 463 def op_hstem(self, index): 464 args = self.countHints() 465 self._hint_op('hstem', args) 466 467 def op_vstem(self, index): 468 args = self.countHints() 469 self._hint_op('vstem', args) 470 471 def op_hstemhm(self, index): 472 args = self.countHints() 473 self._hint_op('hstemhm', args) 474 475 def op_vstemhm(self, index): 476 args = self.countHints() 477 self._hint_op('vstemhm', args) 478 479 def _get_hintmask(self, index): 480 if not self.hintMaskBytes: 481 args = self.countHints() 482 if args: 483 self._hint_op('vstemhm', args) 484 self.hintMaskBytes = (self.hintCount + 7) // 8 485 hintMaskBytes, index = self.callingStack[-1].getBytes(index, 486 self.hintMaskBytes) 487 return index, hintMaskBytes 488 489 def op_hintmask(self, index): 490 index, hintMaskBytes = self._get_hintmask(index) 491 self.pen.add_hintmask('hintmask', [hintMaskBytes]) 492 return hintMaskBytes, index 493 494 def op_cntrmask(self, index): 495 index, hintMaskBytes = self._get_hintmask(index) 496 self.pen.add_hintmask('cntrmask', [hintMaskBytes]) 497 return hintMaskBytes, index 498 499 500class CFF2CharStringMergePen(T2CharStringPen): 501 """Pen to merge Type 2 CharStrings. 502 """ 503 def __init__( 504 self, default_commands, glyphName, num_masters, master_idx, 505 roundTolerance=0.5): 506 super().__init__( 507 width=None, 508 glyphSet=None, CFF2=True, 509 roundTolerance=roundTolerance) 510 self.pt_index = 0 511 self._commands = default_commands 512 self.m_index = master_idx 513 self.num_masters = num_masters 514 self.prev_move_idx = 0 515 self.seen_moveto = False 516 self.glyphName = glyphName 517 self.round = roundFunc(roundTolerance, round=round) 518 519 def add_point(self, point_type, pt_coords): 520 if self.m_index == 0: 521 self._commands.append([point_type, [pt_coords]]) 522 else: 523 cmd = self._commands[self.pt_index] 524 if cmd[0] != point_type: 525 raise VarLibCFFPointTypeMergeError( 526 point_type, 527 self.pt_index, len(cmd[1]), 528 cmd[0], self.glyphName) 529 cmd[1].append(pt_coords) 530 self.pt_index += 1 531 532 def add_hint(self, hint_type, args): 533 if self.m_index == 0: 534 self._commands.append([hint_type, [args]]) 535 else: 536 cmd = self._commands[self.pt_index] 537 if cmd[0] != hint_type: 538 raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]), 539 cmd[0], self.glyphName) 540 cmd[1].append(args) 541 self.pt_index += 1 542 543 def add_hintmask(self, hint_type, abs_args): 544 # For hintmask, fonttools.cffLib.specializer.py expects 545 # each of these to be represented by two sequential commands: 546 # first holding only the operator name, with an empty arg list, 547 # second with an empty string as the op name, and the mask arg list. 548 if self.m_index == 0: 549 self._commands.append([hint_type, []]) 550 self._commands.append(["", [abs_args]]) 551 else: 552 cmd = self._commands[self.pt_index] 553 if cmd[0] != hint_type: 554 raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]), 555 cmd[0], self.glyphName) 556 self.pt_index += 1 557 cmd = self._commands[self.pt_index] 558 cmd[1].append(abs_args) 559 self.pt_index += 1 560 561 def _moveTo(self, pt): 562 if not self.seen_moveto: 563 self.seen_moveto = True 564 pt_coords = self._p(pt) 565 self.add_point('rmoveto', pt_coords) 566 # I set prev_move_idx here because add_point() 567 # can change self.pt_index. 568 self.prev_move_idx = self.pt_index - 1 569 570 def _lineTo(self, pt): 571 pt_coords = self._p(pt) 572 self.add_point('rlineto', pt_coords) 573 574 def _curveToOne(self, pt1, pt2, pt3): 575 _p = self._p 576 pt_coords = _p(pt1)+_p(pt2)+_p(pt3) 577 self.add_point('rrcurveto', pt_coords) 578 579 def _closePath(self): 580 pass 581 582 def _endPath(self): 583 pass 584 585 def restart(self, region_idx): 586 self.pt_index = 0 587 self.m_index = region_idx 588 self._p0 = (0, 0) 589 590 def getCommands(self): 591 return self._commands 592 593 def reorder_blend_args(self, commands, get_delta_func): 594 """ 595 We first re-order the master coordinate values. 596 For a moveto to lineto, the args are now arranged as:: 597 598 [ [master_0 x,y], [master_1 x,y], [master_2 x,y] ] 599 600 We re-arrange this to:: 601 602 [ [master_0 x, master_1 x, master_2 x], 603 [master_0 y, master_1 y, master_2 y] 604 ] 605 606 If the master values are all the same, we collapse the list to 607 as single value instead of a list. 608 609 We then convert this to:: 610 611 [ [master_0 x] + [x delta tuple] + [numBlends=1] 612 [master_0 y] + [y delta tuple] + [numBlends=1] 613 ] 614 """ 615 for cmd in commands: 616 # arg[i] is the set of arguments for this operator from master i. 617 args = cmd[1] 618 m_args = zip(*args) 619 # m_args[n] is now all num_master args for the i'th argument 620 # for this operation. 621 cmd[1] = list(m_args) 622 lastOp = None 623 for cmd in commands: 624 op = cmd[0] 625 # masks are represented by two cmd's: first has only op names, 626 # second has only args. 627 if lastOp in ['hintmask', 'cntrmask']: 628 coord = list(cmd[1]) 629 if not allEqual(coord): 630 raise VarLibMergeError("Hintmask values cannot differ between source fonts.") 631 cmd[1] = [coord[0][0]] 632 else: 633 coords = cmd[1] 634 new_coords = [] 635 for coord in coords: 636 if allEqual(coord): 637 new_coords.append(coord[0]) 638 else: 639 # convert to deltas 640 deltas = get_delta_func(coord)[1:] 641 coord = [coord[0]] + deltas 642 new_coords.append(coord) 643 cmd[1] = new_coords 644 lastOp = op 645 return commands 646 647 def getCharString( 648 self, private=None, globalSubrs=None, 649 var_model=None, optimize=True): 650 commands = self._commands 651 commands = self.reorder_blend_args(commands, partial (var_model.getDeltas, round=self.round)) 652 if optimize: 653 commands = specializeCommands( 654 commands, generalizeFirst=False, 655 maxstack=maxStackLimit) 656 program = commandsToProgram(commands) 657 charString = T2CharString( 658 program=program, private=private, 659 globalSubrs=globalSubrs) 660 return charString 661