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 step through each key in FontDict.Private. 167 For each key, step through each relevant source font Private dict, and 168 build a list of values to blend. 169 The 'relevant' source fonts are selected by first getting the right 170 submodel using vsindex_dict[vsindex]. The indices of the 171 subModel.locations are mapped to source font list indices by 172 assuming the latter order is the same as the order of the 173 var_model.locations. I can then get the index of each subModel 174 location in the list of var_model.locations. 175 """ 176 177 topDict = top_dicts[0] 178 region_top_dicts = top_dicts[1:] 179 if hasattr(region_top_dicts[0], 'FDArray'): 180 regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts] 181 else: 182 regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts] 183 for fd_index, font_dict in enumerate(topDict.FDArray): 184 private_dict = font_dict.Private 185 vsindex = getattr(private_dict, 'vsindex', 0) 186 # At the moment, no PrivateDict has a vsindex key, but let's support 187 # how it should work. See comment at end of 188 # merge_charstrings() - still need to optimize use of vsindex. 189 sub_model, _ = vsindex_dict[vsindex] 190 master_indices = [] 191 for loc in sub_model.locations[1:]: 192 i = var_model.locations.index(loc) - 1 193 master_indices.append(i) 194 pds = [private_dict] 195 last_pd = private_dict 196 for ri in master_indices: 197 pd = get_private(regionFDArrays, fd_index, ri, fd_map) 198 # If the region font doesn't have this FontDict, just reference 199 # the last one used. 200 if pd is None: 201 pd = last_pd 202 else: 203 last_pd = pd 204 pds.append(pd) 205 num_masters = len(pds) 206 for key, value in private_dict.rawDict.items(): 207 dataList = [] 208 if key not in pd_blend_fields: 209 continue 210 if isinstance(value, list): 211 try: 212 values = [pd.rawDict[key] for pd in pds] 213 except KeyError: 214 print( 215 "Warning: {key} in default font Private dict is " 216 "missing from another font, and was " 217 "discarded.".format(key=key)) 218 continue 219 try: 220 values = zip(*values) 221 except IndexError: 222 raise VarLibCFFDictMergeError(key, value, values) 223 """ 224 Row 0 contains the first value from each master. 225 Convert each row from absolute values to relative 226 values from the previous row. 227 e.g for three masters, a list of values was: 228 master 0 OtherBlues = [-217,-205] 229 master 1 OtherBlues = [-234,-222] 230 master 1 OtherBlues = [-188,-176] 231 The call to zip() converts this to: 232 [(-217, -234, -188), (-205, -222, -176)] 233 and is converted finally to: 234 OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]] 235 """ 236 prev_val_list = [0] * num_masters 237 any_points_differ = False 238 for val_list in values: 239 rel_list = [(val - prev_val_list[i]) for ( 240 i, val) in enumerate(val_list)] 241 if (not any_points_differ) and not allEqual(rel_list): 242 any_points_differ = True 243 prev_val_list = val_list 244 deltas = sub_model.getDeltas(rel_list) 245 # For PrivateDict BlueValues, the default font 246 # values are absolute, not relative to the prior value. 247 deltas[0] = val_list[0] 248 dataList.append(deltas) 249 # If there are no blend values,then 250 # we can collapse the blend lists. 251 if not any_points_differ: 252 dataList = [data[0] for data in dataList] 253 else: 254 values = [pd.rawDict[key] for pd in pds] 255 if not allEqual(values): 256 dataList = sub_model.getDeltas(values) 257 else: 258 dataList = values[0] 259 260 # Convert numbers with no decimal part to an int 261 if isinstance(dataList, list): 262 for i, item in enumerate(dataList): 263 if isinstance(item, list): 264 for j, jtem in enumerate(item): 265 dataList[i][j] = conv_to_int(jtem) 266 else: 267 dataList[i] = conv_to_int(item) 268 else: 269 dataList = conv_to_int(dataList) 270 271 private_dict.rawDict[key] = dataList 272 273 274def _cff_or_cff2(font): 275 if "CFF " in font: 276 return font["CFF "] 277 return font["CFF2"] 278 279 280def getfd_map(varFont, fonts_list): 281 """ Since a subset source font may have fewer FontDicts in their 282 FDArray than the default font, we have to match up the FontDicts in 283 the different fonts . We do this with the FDSelect array, and by 284 assuming that the same glyph will reference matching FontDicts in 285 each source font. We return a mapping from fdIndex in the default 286 font to a dictionary which maps each master list index of each 287 region font to the equivalent fdIndex in the region font.""" 288 fd_map = {} 289 default_font = fonts_list[0] 290 region_fonts = fonts_list[1:] 291 num_regions = len(region_fonts) 292 topDict = _cff_or_cff2(default_font).cff.topDictIndex[0] 293 if not hasattr(topDict, 'FDSelect'): 294 # All glyphs reference only one FontDict. 295 # Map the FD index for regions to index 0. 296 fd_map[0] = {ri:0 for ri in range(num_regions)} 297 return fd_map 298 299 gname_mapping = {} 300 default_fdSelect = topDict.FDSelect 301 glyphOrder = default_font.getGlyphOrder() 302 for gid, fdIndex in enumerate(default_fdSelect): 303 gname_mapping[glyphOrder[gid]] = fdIndex 304 if fdIndex not in fd_map: 305 fd_map[fdIndex] = {} 306 for ri, region_font in enumerate(region_fonts): 307 region_glyphOrder = region_font.getGlyphOrder() 308 region_topDict = _cff_or_cff2(region_font).cff.topDictIndex[0] 309 if not hasattr(region_topDict, 'FDSelect'): 310 # All the glyphs share the same FontDict. Pick any glyph. 311 default_fdIndex = gname_mapping[region_glyphOrder[0]] 312 fd_map[default_fdIndex][ri] = 0 313 else: 314 region_fdSelect = region_topDict.FDSelect 315 for gid, fdIndex in enumerate(region_fdSelect): 316 default_fdIndex = gname_mapping[region_glyphOrder[gid]] 317 region_map = fd_map[default_fdIndex] 318 if ri not in region_map: 319 region_map[ri] = fdIndex 320 return fd_map 321 322 323CVarData = namedtuple('CVarData', 'varDataList masterSupports vsindex_dict') 324def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder): 325 topDict = varFont['CFF2'].cff.topDictIndex[0] 326 top_dicts = [topDict] + [ 327 _cff_or_cff2(ttFont).cff.topDictIndex[0] 328 for ttFont in ordered_fonts_list[1:] 329 ] 330 num_masters = len(model.mapping) 331 cvData = merge_charstrings(glyphOrder, num_masters, top_dicts, model) 332 fd_map = getfd_map(varFont, ordered_fonts_list) 333 merge_PrivateDicts(top_dicts, cvData.vsindex_dict, model, fd_map) 334 addCFFVarStore(varFont, model, cvData.varDataList, 335 cvData.masterSupports) 336 337 338def _get_cs(charstrings, glyphName): 339 if glyphName not in charstrings: 340 return None 341 return charstrings[glyphName] 342 343def _add_new_vsindex(model, key, masterSupports, vsindex_dict, 344 vsindex_by_key, varDataList): 345 varTupleIndexes = [] 346 for support in model.supports[1:]: 347 if support not in masterSupports: 348 masterSupports.append(support) 349 varTupleIndexes.append(masterSupports.index(support)) 350 var_data = varLib.builder.buildVarData(varTupleIndexes, None, False) 351 vsindex = len(vsindex_dict) 352 vsindex_by_key[key] = vsindex 353 vsindex_dict[vsindex] = (model, [key]) 354 varDataList.append(var_data) 355 return vsindex 356 357def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel): 358 359 vsindex_dict = {} 360 vsindex_by_key = {} 361 varDataList = [] 362 masterSupports = [] 363 default_charstrings = top_dicts[0].CharStrings 364 for gid, gname in enumerate(glyphOrder): 365 all_cs = [ 366 _get_cs(td.CharStrings, gname) 367 for td in top_dicts] 368 if len([gs for gs in all_cs if gs is not None]) == 1: 369 continue 370 model, model_cs = masterModel.getSubModel(all_cs) 371 # create the first pass CFF2 charstring, from 372 # the default charstring. 373 default_charstring = model_cs[0] 374 var_pen = CFF2CharStringMergePen([], gname, num_masters, 0) 375 # We need to override outlineExtractor because these 376 # charstrings do have widths in the 'program'; we need to drop these 377 # values rather than post assertion error for them. 378 default_charstring.outlineExtractor = MergeOutlineExtractor 379 default_charstring.draw(var_pen) 380 381 # Add the coordinates from all the other regions to the 382 # blend lists in the CFF2 charstring. 383 region_cs = model_cs[1:] 384 for region_idx, region_charstring in enumerate(region_cs, start=1): 385 var_pen.restart(region_idx) 386 region_charstring.outlineExtractor = MergeOutlineExtractor 387 region_charstring.draw(var_pen) 388 389 # Collapse each coordinate list to a blend operator and its args. 390 new_cs = var_pen.getCharString( 391 private=default_charstring.private, 392 globalSubrs=default_charstring.globalSubrs, 393 var_model=model, optimize=True) 394 default_charstrings[gname] = new_cs 395 396 if (not var_pen.seen_moveto) or ('blend' not in new_cs.program): 397 # If this is not a marking glyph, or if there are no blend 398 # arguments, then we can use vsindex 0. No need to 399 # check if we need a new vsindex. 400 continue 401 402 # If the charstring required a new model, create 403 # a VarData table to go with, and set vsindex. 404 key = tuple(v is not None for v in all_cs) 405 try: 406 vsindex = vsindex_by_key[key] 407 except KeyError: 408 vsindex = _add_new_vsindex(model, key, masterSupports, vsindex_dict, 409 vsindex_by_key, varDataList) 410 # We do not need to check for an existing new_cs.private.vsindex, 411 # as we know it doesn't exist yet. 412 if vsindex != 0: 413 new_cs.program[:0] = [vsindex, 'vsindex'] 414 415 # If there is no variation in any of the charstrings, then vsindex_dict 416 # never gets built. This could still be needed if there is variation 417 # in the PrivatDict, so we will build the default data for vsindex = 0. 418 if not vsindex_dict: 419 key = (True,) * num_masters 420 _add_new_vsindex(masterModel, key, masterSupports, vsindex_dict, 421 vsindex_by_key, varDataList) 422 cvData = CVarData(varDataList=varDataList, masterSupports=masterSupports, 423 vsindex_dict=vsindex_dict) 424 # XXX To do: optimize use of vsindex between the PrivateDicts and 425 # charstrings 426 return cvData 427 428 429class CFFToCFF2OutlineExtractor(T2OutlineExtractor): 430 """ This class is used to remove the initial width from the CFF 431 charstring without trying to add the width to self.nominalWidthX, 432 which is None. """ 433 def popallWidth(self, evenOdd=0): 434 args = self.popall() 435 if not self.gotWidth: 436 if evenOdd ^ (len(args) % 2): 437 args = args[1:] 438 self.width = self.defaultWidthX 439 self.gotWidth = 1 440 return args 441 442 443class MergeOutlineExtractor(CFFToCFF2OutlineExtractor): 444 """ Used to extract the charstring commands - including hints - from a 445 CFF charstring in order to merge it as another set of region data 446 into a CFF2 variable font charstring.""" 447 448 def __init__(self, pen, localSubrs, globalSubrs, 449 nominalWidthX, defaultWidthX, private=None): 450 super().__init__(pen, localSubrs, 451 globalSubrs, nominalWidthX, defaultWidthX, private) 452 453 def countHints(self): 454 args = self.popallWidth() 455 self.hintCount = self.hintCount + len(args) // 2 456 return args 457 458 def _hint_op(self, type, args): 459 self.pen.add_hint(type, args) 460 461 def op_hstem(self, index): 462 args = self.countHints() 463 self._hint_op('hstem', args) 464 465 def op_vstem(self, index): 466 args = self.countHints() 467 self._hint_op('vstem', args) 468 469 def op_hstemhm(self, index): 470 args = self.countHints() 471 self._hint_op('hstemhm', args) 472 473 def op_vstemhm(self, index): 474 args = self.countHints() 475 self._hint_op('vstemhm', args) 476 477 def _get_hintmask(self, index): 478 if not self.hintMaskBytes: 479 args = self.countHints() 480 if args: 481 self._hint_op('vstemhm', args) 482 self.hintMaskBytes = (self.hintCount + 7) // 8 483 hintMaskBytes, index = self.callingStack[-1].getBytes(index, 484 self.hintMaskBytes) 485 return index, hintMaskBytes 486 487 def op_hintmask(self, index): 488 index, hintMaskBytes = self._get_hintmask(index) 489 self.pen.add_hintmask('hintmask', [hintMaskBytes]) 490 return hintMaskBytes, index 491 492 def op_cntrmask(self, index): 493 index, hintMaskBytes = self._get_hintmask(index) 494 self.pen.add_hintmask('cntrmask', [hintMaskBytes]) 495 return hintMaskBytes, index 496 497 498class CFF2CharStringMergePen(T2CharStringPen): 499 """Pen to merge Type 2 CharStrings. 500 """ 501 def __init__( 502 self, default_commands, glyphName, num_masters, master_idx, 503 roundTolerance=0.5): 504 super().__init__( 505 width=None, 506 glyphSet=None, CFF2=True, 507 roundTolerance=roundTolerance) 508 self.pt_index = 0 509 self._commands = default_commands 510 self.m_index = master_idx 511 self.num_masters = num_masters 512 self.prev_move_idx = 0 513 self.seen_moveto = False 514 self.glyphName = glyphName 515 self.round = roundFunc(roundTolerance, round=round) 516 517 def add_point(self, point_type, pt_coords): 518 if self.m_index == 0: 519 self._commands.append([point_type, [pt_coords]]) 520 else: 521 cmd = self._commands[self.pt_index] 522 if cmd[0] != point_type: 523 raise VarLibCFFPointTypeMergeError( 524 point_type, 525 self.pt_index, len(cmd[1]), 526 cmd[0], self.glyphName) 527 cmd[1].append(pt_coords) 528 self.pt_index += 1 529 530 def add_hint(self, hint_type, args): 531 if self.m_index == 0: 532 self._commands.append([hint_type, [args]]) 533 else: 534 cmd = self._commands[self.pt_index] 535 if cmd[0] != hint_type: 536 raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]), 537 cmd[0], self.glyphName) 538 cmd[1].append(args) 539 self.pt_index += 1 540 541 def add_hintmask(self, hint_type, abs_args): 542 # For hintmask, fonttools.cffLib.specializer.py expects 543 # each of these to be represented by two sequential commands: 544 # first holding only the operator name, with an empty arg list, 545 # second with an empty string as the op name, and the mask arg list. 546 if self.m_index == 0: 547 self._commands.append([hint_type, []]) 548 self._commands.append(["", [abs_args]]) 549 else: 550 cmd = self._commands[self.pt_index] 551 if cmd[0] != hint_type: 552 raise VarLibCFFHintTypeMergeError(hint_type, self.pt_index, len(cmd[1]), 553 cmd[0], self.glyphName) 554 self.pt_index += 1 555 cmd = self._commands[self.pt_index] 556 cmd[1].append(abs_args) 557 self.pt_index += 1 558 559 def _moveTo(self, pt): 560 if not self.seen_moveto: 561 self.seen_moveto = True 562 pt_coords = self._p(pt) 563 self.add_point('rmoveto', pt_coords) 564 # I set prev_move_idx here because add_point() 565 # can change self.pt_index. 566 self.prev_move_idx = self.pt_index - 1 567 568 def _lineTo(self, pt): 569 pt_coords = self._p(pt) 570 self.add_point('rlineto', pt_coords) 571 572 def _curveToOne(self, pt1, pt2, pt3): 573 _p = self._p 574 pt_coords = _p(pt1)+_p(pt2)+_p(pt3) 575 self.add_point('rrcurveto', pt_coords) 576 577 def _closePath(self): 578 pass 579 580 def _endPath(self): 581 pass 582 583 def restart(self, region_idx): 584 self.pt_index = 0 585 self.m_index = region_idx 586 self._p0 = (0, 0) 587 588 def getCommands(self): 589 return self._commands 590 591 def reorder_blend_args(self, commands, get_delta_func): 592 """ 593 We first re-order the master coordinate values. 594 For a moveto to lineto, the args are now arranged as: 595 [ [master_0 x,y], [master_1 x,y], [master_2 x,y] ] 596 We re-arrange this to 597 [ [master_0 x, master_1 x, master_2 x], 598 [master_0 y, master_1 y, master_2 y] 599 ] 600 If the master values are all the same, we collapse the list to 601 as single value instead of a list. 602 603 We then convert this to: 604 [ [master_0 x] + [x delta tuple] + [numBlends=1] 605 [master_0 y] + [y delta tuple] + [numBlends=1] 606 ] 607 """ 608 for cmd in commands: 609 # arg[i] is the set of arguments for this operator from master i. 610 args = cmd[1] 611 m_args = zip(*args) 612 # m_args[n] is now all num_master args for the i'th argument 613 # for this operation. 614 cmd[1] = list(m_args) 615 lastOp = None 616 for cmd in commands: 617 op = cmd[0] 618 # masks are represented by two cmd's: first has only op names, 619 # second has only args. 620 if lastOp in ['hintmask', 'cntrmask']: 621 coord = list(cmd[1]) 622 if not allEqual(coord): 623 raise VarLibMergeError("Hintmask values cannot differ between source fonts.") 624 cmd[1] = [coord[0][0]] 625 else: 626 coords = cmd[1] 627 new_coords = [] 628 for coord in coords: 629 if allEqual(coord): 630 new_coords.append(coord[0]) 631 else: 632 # convert to deltas 633 deltas = get_delta_func(coord)[1:] 634 coord = [coord[0]] + deltas 635 new_coords.append(coord) 636 cmd[1] = new_coords 637 lastOp = op 638 return commands 639 640 def getCharString( 641 self, private=None, globalSubrs=None, 642 var_model=None, optimize=True): 643 commands = self._commands 644 commands = self.reorder_blend_args(commands, partial (var_model.getDeltas, round=self.round)) 645 if optimize: 646 commands = specializeCommands( 647 commands, generalizeFirst=False, 648 maxstack=maxStackLimit) 649 program = commandsToProgram(commands) 650 charString = T2CharString( 651 program=program, private=private, 652 globalSubrs=globalSubrs) 653 return charString 654