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