1import os 2from fontTools.misc.py23 import BytesIO 3from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor 4from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round 5from fontTools.cffLib import ( 6 maxStackLimit, 7 TopDictIndex, 8 buildOrder, 9 topDictOperators, 10 topDictOperators2, 11 privateDictOperators, 12 privateDictOperators2, 13 FDArrayIndex, 14 FontDict, 15 VarStoreData 16) 17from fontTools.cffLib.specializer import (commandsToProgram, specializeCommands) 18from fontTools.ttLib import newTable 19from fontTools import varLib 20from fontTools.varLib.models import allEqual 21 22 23def addCFFVarStore(varFont, varModel): 24 supports = varModel.supports[1:] 25 fvarTable = varFont['fvar'] 26 axisKeys = [axis.axisTag for axis in fvarTable.axes] 27 varTupleList = varLib.builder.buildVarRegionList(supports, axisKeys) 28 varTupleIndexes = list(range(len(supports))) 29 varDeltasCFFV = varLib.builder.buildVarData(varTupleIndexes, None, False) 30 varStoreCFFV = varLib.builder.buildVarStore(varTupleList, [varDeltasCFFV]) 31 32 topDict = varFont['CFF2'].cff.topDictIndex[0] 33 topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV) 34 35 36def lib_convertCFFToCFF2(cff, otFont): 37 # This assumes a decompiled CFF table. 38 cff2GetGlyphOrder = cff.otFont.getGlyphOrder 39 topDictData = TopDictIndex(None, cff2GetGlyphOrder, None) 40 topDictData.items = cff.topDictIndex.items 41 cff.topDictIndex = topDictData 42 topDict = topDictData[0] 43 if hasattr(topDict, 'Private'): 44 privateDict = topDict.Private 45 else: 46 privateDict = None 47 opOrder = buildOrder(topDictOperators2) 48 topDict.order = opOrder 49 topDict.cff2GetGlyphOrder = cff2GetGlyphOrder 50 if not hasattr(topDict, "FDArray"): 51 fdArray = topDict.FDArray = FDArrayIndex() 52 fdArray.strings = None 53 fdArray.GlobalSubrs = topDict.GlobalSubrs 54 topDict.GlobalSubrs.fdArray = fdArray 55 charStrings = topDict.CharStrings 56 if charStrings.charStringsAreIndexed: 57 charStrings.charStringsIndex.fdArray = fdArray 58 else: 59 charStrings.fdArray = fdArray 60 fontDict = FontDict() 61 fontDict.setCFF2(True) 62 fdArray.append(fontDict) 63 fontDict.Private = privateDict 64 privateOpOrder = buildOrder(privateDictOperators2) 65 for entry in privateDictOperators: 66 key = entry[1] 67 if key not in privateOpOrder: 68 if key in privateDict.rawDict: 69 # print "Removing private dict", key 70 del privateDict.rawDict[key] 71 if hasattr(privateDict, key): 72 delattr(privateDict, key) 73 # print "Removing privateDict attr", key 74 else: 75 # clean up the PrivateDicts in the fdArray 76 fdArray = topDict.FDArray 77 privateOpOrder = buildOrder(privateDictOperators2) 78 for fontDict in fdArray: 79 fontDict.setCFF2(True) 80 for key in list(fontDict.rawDict.keys()): 81 if key not in fontDict.order: 82 del fontDict.rawDict[key] 83 if hasattr(fontDict, key): 84 delattr(fontDict, key) 85 86 privateDict = fontDict.Private 87 for entry in privateDictOperators: 88 key = entry[1] 89 if key not in privateOpOrder: 90 if key in privateDict.rawDict: 91 # print "Removing private dict", key 92 del privateDict.rawDict[key] 93 if hasattr(privateDict, key): 94 delattr(privateDict, key) 95 # print "Removing privateDict attr", key 96 # Now delete up the decrecated topDict operators from CFF 1.0 97 for entry in topDictOperators: 98 key = entry[1] 99 if key not in opOrder: 100 if key in topDict.rawDict: 101 del topDict.rawDict[key] 102 if hasattr(topDict, key): 103 delattr(topDict, key) 104 105 # At this point, the Subrs and Charstrings are all still T2Charstring class 106 # easiest to fix this by compiling, then decompiling again 107 cff.major = 2 108 file = BytesIO() 109 cff.compile(file, otFont, isCFF2=True) 110 file.seek(0) 111 cff.decompile(file, otFont, isCFF2=True) 112 113 114def convertCFFtoCFF2(varFont): 115 # Convert base font to a single master CFF2 font. 116 cffTable = varFont['CFF '] 117 lib_convertCFFToCFF2(cffTable.cff, varFont) 118 newCFF2 = newTable("CFF2") 119 newCFF2.cff = cffTable.cff 120 varFont['CFF2'] = newCFF2 121 del varFont['CFF '] 122 123 124class MergeDictError(TypeError): 125 def __init__(self, key, value, values): 126 error_msg = ["For the Private Dict key '{}', ".format(key), 127 "the default font value list:", 128 "\t{}".format(value), 129 "had a different number of values than a region font:"] 130 error_msg += ["\t{}".format(region_value) for region_value in values] 131 error_msg = os.linesep.join(error_msg) 132 133 134def conv_to_int(num): 135 if num % 1 == 0: 136 return int(num) 137 return num 138 139 140pd_blend_fields = ("BlueValues", "OtherBlues", "FamilyBlues", 141 "FamilyOtherBlues", "BlueScale", "BlueShift", 142 "BlueFuzz", "StdHW", "StdVW", "StemSnapH", 143 "StemSnapV") 144 145 146def merge_PrivateDicts(topDict, region_top_dicts, num_masters, var_model): 147 if hasattr(region_top_dicts[0], 'FDArray'): 148 regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts] 149 else: 150 regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts] 151 for fd_index, font_dict in enumerate(topDict.FDArray): 152 private_dict = font_dict.Private 153 pds = [private_dict] + [ 154 regionFDArray[fd_index].Private for regionFDArray in regionFDArrays 155 ] 156 for key, value in private_dict.rawDict.items(): 157 if key not in pd_blend_fields: 158 continue 159 if isinstance(value, list): 160 try: 161 values = [pd.rawDict[key] for pd in pds] 162 except KeyError: 163 del private_dict.rawDict[key] 164 print( 165 b"Warning: {key} in default font Private dict is " 166 b"missing from another font, and was " 167 b"discarded.".format(key=key)) 168 continue 169 try: 170 values = zip(*values) 171 except IndexError: 172 raise MergeDictError(key, value, values) 173 """ 174 Row 0 contains the first value from each master. 175 Convert each row from absolute values to relative 176 values from the previous row. 177 e.g for three masters, a list of values was: 178 master 0 OtherBlues = [-217,-205] 179 master 1 OtherBlues = [-234,-222] 180 master 1 OtherBlues = [-188,-176] 181 The call to zip() converts this to: 182 [(-217, -234, -188), (-205, -222, -176)] 183 and is converted finally to: 184 OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]] 185 """ 186 dataList = [] 187 prev_val_list = [0] * num_masters 188 any_points_differ = False 189 for val_list in values: 190 rel_list = [(val - prev_val_list[i]) for ( 191 i, val) in enumerate(val_list)] 192 if (not any_points_differ) and not allEqual(rel_list): 193 any_points_differ = True 194 prev_val_list = val_list 195 deltas = var_model.getDeltas(rel_list) 196 # Convert numbers with no decimal part to an int. 197 deltas = [conv_to_int(delta) for delta in deltas] 198 # For PrivateDict BlueValues, the default font 199 # values are absolute, not relative to the prior value. 200 deltas[0] = val_list[0] 201 dataList.append(deltas) 202 # If there are no blend values,then 203 # we can collapse the blend lists. 204 if not any_points_differ: 205 dataList = [data[0] for data in dataList] 206 else: 207 values = [pd.rawDict[key] for pd in pds] 208 if not allEqual(values): 209 dataList = var_model.getDeltas(values) 210 else: 211 dataList = values[0] 212 private_dict.rawDict[key] = dataList 213 214 215def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder): 216 topDict = varFont['CFF2'].cff.topDictIndex[0] 217 default_charstrings = topDict.CharStrings 218 region_fonts = ordered_fonts_list[1:] 219 region_top_dicts = [ 220 ttFont['CFF '].cff.topDictIndex[0] for ttFont in region_fonts 221 ] 222 num_masters = len(model.mapping) 223 merge_PrivateDicts(topDict, region_top_dicts, num_masters, model) 224 merge_charstrings(default_charstrings, 225 glyphOrder, 226 num_masters, 227 region_top_dicts, model) 228 229 230def merge_charstrings(default_charstrings, 231 glyphOrder, 232 num_masters, 233 region_top_dicts, 234 var_model): 235 for gname in glyphOrder: 236 default_charstring = default_charstrings[gname] 237 var_pen = CFF2CharStringMergePen([], gname, num_masters, 0) 238 default_charstring.outlineExtractor = CFFToCFF2OutlineExtractor 239 default_charstring.draw(var_pen) 240 for region_idx, region_td in enumerate(region_top_dicts, start=1): 241 region_charstrings = region_td.CharStrings 242 region_charstring = region_charstrings[gname] 243 var_pen.restart(region_idx) 244 region_charstring.draw(var_pen) 245 new_charstring = var_pen.getCharString( 246 private=default_charstring.private, 247 globalSubrs=default_charstring.globalSubrs, 248 var_model=var_model, optimize=True) 249 default_charstrings[gname] = new_charstring 250 251 252class MergeTypeError(TypeError): 253 def __init__(self, point_type, pt_index, m_index, default_type, glyphName): 254 self.error_msg = [ 255 "In glyph '{gname}' " 256 "'{point_type}' at point index {pt_index} in master " 257 "index {m_index} differs from the default font point " 258 "type '{default_type}'" 259 "".format(gname=glyphName, 260 point_type=point_type, pt_index=pt_index, 261 m_index=m_index, default_type=default_type) 262 ][0] 263 super(MergeTypeError, self).__init__(self.error_msg) 264 265 266def makeRoundNumberFunc(tolerance): 267 if tolerance < 0: 268 raise ValueError("Rounding tolerance must be positive") 269 270 def roundNumber(val): 271 return t2c_round(val, tolerance) 272 273 return roundNumber 274 275 276class CFFToCFF2OutlineExtractor(T2OutlineExtractor): 277 """ This class is used to remove the initial width 278 from the CFF charstring without adding the width 279 to self.nominalWidthX, which is None. 280 """ 281 def popallWidth(self, evenOdd=0): 282 args = self.popall() 283 if not self.gotWidth: 284 if evenOdd ^ (len(args) % 2): 285 args = args[1:] 286 self.width = self.defaultWidthX 287 self.gotWidth = 1 288 return args 289 290 291class CFF2CharStringMergePen(T2CharStringPen): 292 """Pen to merge Type 2 CharStrings. 293 """ 294 def __init__(self, default_commands, 295 glyphName, num_masters, master_idx, roundTolerance=0.5): 296 super( 297 CFF2CharStringMergePen, 298 self).__init__(width=None, 299 glyphSet=None, CFF2=True, 300 roundTolerance=roundTolerance) 301 self.pt_index = 0 302 self._commands = default_commands 303 self.m_index = master_idx 304 self.num_masters = num_masters 305 self.prev_move_idx = 0 306 self.glyphName = glyphName 307 self.roundNumber = makeRoundNumberFunc(roundTolerance) 308 309 def _p(self, pt): 310 """ Unlike T2CharstringPen, this class stores absolute values. 311 This is to allow the logic in check_and_fix_closepath() to work, 312 where the current or previous absolute point has to be compared to 313 the path start-point. 314 """ 315 self._p0 = pt 316 return list(self._p0) 317 318 def add_point(self, point_type, pt_coords): 319 if self.m_index == 0: 320 self._commands.append([point_type, [pt_coords]]) 321 else: 322 cmd = self._commands[self.pt_index] 323 if cmd[0] != point_type: 324 # Fix some issues that show up in some 325 # CFF workflows, even when fonts are 326 # topologically merge compatible. 327 success, pt_coords = self.check_and_fix_flat_curve( 328 cmd, point_type, pt_coords) 329 if not success: 330 success = self.check_and_fix_closepath( 331 cmd, point_type, pt_coords) 332 if success: 333 # We may have incremented self.pt_index 334 cmd = self._commands[self.pt_index] 335 if cmd[0] != point_type: 336 success = False 337 if not success: 338 raise MergeTypeError(point_type, 339 self.pt_index, len(cmd[1]), 340 cmd[0], self.glyphName) 341 cmd[1].append(pt_coords) 342 self.pt_index += 1 343 344 def _moveTo(self, pt): 345 pt_coords = self._p(pt) 346 self.add_point('rmoveto', pt_coords) 347 # I set prev_move_idx here because add_point() 348 # can change self.pt_index. 349 self.prev_move_idx = self.pt_index - 1 350 351 def _lineTo(self, pt): 352 pt_coords = self._p(pt) 353 self.add_point('rlineto', pt_coords) 354 355 def _curveToOne(self, pt1, pt2, pt3): 356 _p = self._p 357 pt_coords = _p(pt1)+_p(pt2)+_p(pt3) 358 self.add_point('rrcurveto', pt_coords) 359 360 def _closePath(self): 361 pass 362 363 def _endPath(self): 364 pass 365 366 def restart(self, region_idx): 367 self.pt_index = 0 368 self.m_index = region_idx 369 self._p0 = (0, 0) 370 371 def getCommands(self): 372 return self._commands 373 374 def reorder_blend_args(self, commands): 375 """ 376 We first re-order the master coordinate values. 377 For a moveto to lineto, the args are now arranged as: 378 [ [master_0 x,y], [master_1 x,y], [master_2 x,y] ] 379 We re-arrange this to 380 [ [master_0 x, master_1 x, master_2 x], 381 [master_0 y, master_1 y, master_2 y] 382 ] 383 We also make the value relative. 384 If the master values are all the same, we collapse the list to 385 as single value instead of a list. 386 """ 387 for cmd in commands: 388 # arg[i] is the set of arguments for this operator from master i. 389 args = cmd[1] 390 m_args = zip(*args) 391 # m_args[n] is now all num_master args for the i'th argument 392 # for this operation. 393 cmd[1] = m_args 394 395 # Now convert from absolute to relative 396 x0 = [0]*self.num_masters 397 y0 = [0]*self.num_masters 398 for cmd in self._commands: 399 is_x = True 400 coords = cmd[1] 401 rel_coords = [] 402 for coord in coords: 403 prev_coord = x0 if is_x else y0 404 rel_coord = [pt[0] - pt[1] for pt in zip(coord, prev_coord)] 405 406 if allEqual(rel_coord): 407 rel_coord = rel_coord[0] 408 rel_coords.append(rel_coord) 409 if is_x: 410 x0 = coord 411 else: 412 y0 = coord 413 is_x = not is_x 414 cmd[1] = rel_coords 415 return commands 416 417 @staticmethod 418 def mergeCommandsToProgram(commands, var_model, round_func): 419 """ 420 Takes a commands list as returned by programToCommands() and 421 converts it back to a T2CharString or CFF2Charstring program list. I 422 need to use this rather than specialize.commandsToProgram, as the 423 commands produced by CFF2CharStringMergePen initially contains a 424 list of coordinate values, one for each master, wherever a single 425 coordinate value is expected by the regular logic. The problem with 426 doing using the specialize.py functions is that a commands list is 427 expected to be a op name with its associated argument list. For the 428 commands list here, some of the arguments may need to be converted 429 to a new argument list and opcode. 430 This version will convert each list of master arguments to a blend 431 op and its arguments, and will also combine successive blend ops up 432 to the stack limit. 433 """ 434 program = [] 435 for op, args in commands: 436 num_args = len(args) 437 # some of the args may be blend lists, and some may be 438 # single coordinate values. 439 i = 0 440 stack_use = 0 441 while i < num_args: 442 arg = args[i] 443 if not isinstance(arg, list): 444 program.append(arg) 445 i += 1 446 stack_use += 1 447 else: 448 prev_stack_use = stack_use 449 """ The arg is a tuple of blend values. 450 These are each (master 0,master 1..master n) 451 Combine as many successive tuples as we can, 452 up to the max stack limit. 453 """ 454 num_masters = len(arg) 455 blendlist = [arg] 456 i += 1 457 stack_use += 1 + num_masters # 1 for the num_blends arg 458 while (i < num_args) and isinstance(args[i], list): 459 blendlist.append(args[i]) 460 i += 1 461 stack_use += num_masters 462 if stack_use + num_masters > maxStackLimit: 463 # if we are here, max stack is is the CFF2 max stack. 464 break 465 num_blends = len(blendlist) 466 # append the 'num_blends' default font values 467 for arg in blendlist: 468 if round_func: 469 arg[0] = round_func(arg[0]) 470 program.append(arg[0]) 471 for arg in blendlist: 472 # for each coordinate tuple, append the region deltas 473 if len(arg) != 3: 474 print(arg) 475 import pdb 476 pdb.set_trace() 477 deltas = var_model.getDeltas(arg) 478 if round_func: 479 deltas = [round_func(delta) for delta in deltas] 480 # First item in 'deltas' is the default master value; 481 # for CFF2 data, that has already been written. 482 program.extend(deltas[1:]) 483 program.append(num_blends) 484 program.append('blend') 485 stack_use = prev_stack_use + num_blends 486 if op: 487 program.append(op) 488 return program 489 490 491 def getCharString(self, private=None, globalSubrs=None, 492 var_model=None, optimize=True): 493 commands = self._commands 494 commands = self.reorder_blend_args(commands) 495 if optimize: 496 commands = specializeCommands(commands, generalizeFirst=False, 497 maxstack=maxStackLimit) 498 program = self.mergeCommandsToProgram(commands, var_model=var_model, 499 round_func=self.roundNumber) 500 charString = T2CharString(program=program, private=private, 501 globalSubrs=globalSubrs) 502 return charString 503