1# Copyright 2014 Adobe. All rights reserved. 2 3""" 4Utilities for converting between T2 charstrings and the bez data format. 5""" 6 7import copy 8import logging 9import os 10import re 11import subprocess 12import tempfile 13import itertools 14 15from fontTools.misc.psCharStrings import (T2OutlineExtractor, 16 SimpleT2Decompiler) 17from fontTools.ttLib import TTFont, newTable 18from fontTools.misc.fixedTools import otRound 19from fontTools.varLib.varStore import VarStoreInstancer 20from fontTools.varLib.cff import CFF2CharStringMergePen, MergeOutlineExtractor 21# import subset.cff is needed to load the implementation for 22# CFF.desubroutinize: the module adds this class method to the CFF and CFF2 23# classes. 24import fontTools.subset.cff 25 26from . import fdTools, FontParseError 27 28# keep linting tools quiet about unused import 29assert fontTools.subset.cff is not None 30 31log = logging.getLogger(__name__) 32 33kStackLimit = 46 34kStemLimit = 96 35 36 37class SEACError(Exception): 38 pass 39 40 41def _add_method(*clazzes): 42 """Returns a decorator function that adds a new method to one or 43 more classes.""" 44 def wrapper(method): 45 done = [] 46 for clazz in clazzes: 47 if clazz in done: 48 continue # Support multiple names of a clazz 49 done.append(clazz) 50 assert clazz.__name__ != 'DefaultTable', \ 51 'Oops, table class not found.' 52 assert not hasattr(clazz, method.__name__), \ 53 "Oops, class '%s' has method '%s'." % (clazz.__name__, 54 method.__name__) 55 setattr(clazz, method.__name__, method) 56 return None 57 return wrapper 58 59 60def hintOn(i, hintMaskBytes): 61 # used to add the active hints to the bez string, 62 # when a T2 hintmask operator is encountered. 63 byteIndex = int(i / 8) 64 byteValue = hintMaskBytes[byteIndex] 65 offset = 7 - (i % 8) 66 return ((2**offset) & byteValue) > 0 67 68 69class T2ToBezExtractor(T2OutlineExtractor): 70 # The T2OutlineExtractor class calls a class method as the handler for each 71 # T2 operator. 72 # I use this to convert the T2 operands and arguments to bez operators. 73 # Note: flex is converted to regular rrcurveto's. 74 # cntrmasks just map to hint replacement blocks with the specified stems. 75 def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, 76 read_hints=True, round_coords=True): 77 T2OutlineExtractor.__init__(self, None, localSubrs, globalSubrs, 78 nominalWidthX, defaultWidthX) 79 self.vhints = [] 80 self.hhints = [] 81 self.bezProgram = [] 82 self.read_hints = read_hints 83 self.firstMarkingOpSeen = False 84 self.closePathSeen = False 85 self.subrLevel = 0 86 self.round_coords = round_coords 87 self.hintMaskBytes = None 88 89 def execute(self, charString): 90 self.subrLevel += 1 91 SimpleT2Decompiler.execute(self, charString) 92 self.subrLevel -= 1 93 if (not self.closePathSeen) and (self.subrLevel == 0): 94 self.closePath() 95 96 def _point(self, point): 97 if self.round_coords: 98 return " ".join("%d" % round(pt) for pt in point) 99 return " ".join("%3f" % pt for pt in point) 100 101 def rMoveTo(self, point): 102 point = self._nextPoint(point) 103 if not self.firstMarkingOpSeen: 104 self.firstMarkingOpSeen = True 105 self.bezProgram.append("sc\n") 106 log.debug("moveto %s, curpos %s", point, self.currentPoint) 107 self.bezProgram.append("%s mt\n" % self._point(point)) 108 self.sawMoveTo = True 109 110 def rLineTo(self, point): 111 if not self.sawMoveTo: 112 self.rMoveTo((0, 0)) 113 point = self._nextPoint(point) 114 log.debug("lineto %s, curpos %s", point, self.currentPoint) 115 self.bezProgram.append("%s dt\n" % self._point(point)) 116 117 def rCurveTo(self, pt1, pt2, pt3): 118 if not self.sawMoveTo: 119 self.rMoveTo((0, 0)) 120 pt1 = list(self._nextPoint(pt1)) 121 pt2 = list(self._nextPoint(pt2)) 122 pt3 = list(self._nextPoint(pt3)) 123 log.debug("curveto %s %s %s, curpos %s", pt1, pt2, pt3, 124 self.currentPoint) 125 self.bezProgram.append("%s ct\n" % self._point(pt1 + pt2 + pt3)) 126 127 def op_endchar(self, index): 128 self.endPath() 129 args = self.popallWidth() 130 if args: # It is a 'seac' composite character. Don't process 131 raise SEACError 132 133 def endPath(self): 134 # In T2 there are no open paths, so always do a closePath when 135 # finishing a sub path. 136 if self.sawMoveTo: 137 log.debug("endPath") 138 self.bezProgram.append("cp\n") 139 self.sawMoveTo = False 140 141 def closePath(self): 142 self.closePathSeen = True 143 log.debug("closePath") 144 if self.bezProgram and self.bezProgram[-1] != "cp\n": 145 self.bezProgram.append("cp\n") 146 self.bezProgram.append("ed\n") 147 148 def updateHints(self, args, hint_list, bezCommand): 149 self.countHints(args) 150 151 # first hint value is absolute hint coordinate, second is hint width 152 if not self.read_hints: 153 return 154 155 lastval = args[0] 156 arg = str(lastval) 157 hint_list.append(arg) 158 self.bezProgram.append(arg + " ") 159 160 for i in range(len(args))[1:]: 161 val = args[i] 162 lastval += val 163 164 if i % 2: 165 arg = str(val) 166 hint_list.append(arg) 167 self.bezProgram.append("%s %s\n" % (arg, bezCommand)) 168 else: 169 arg = str(lastval) 170 hint_list.append(arg) 171 self.bezProgram.append(arg + " ") 172 173 def op_hstem(self, index): 174 args = self.popallWidth() 175 self.hhints = [] 176 self.updateHints(args, self.hhints, "rb") 177 log.debug("hstem %s", self.hhints) 178 179 def op_vstem(self, index): 180 args = self.popallWidth() 181 self.vhints = [] 182 self.updateHints(args, self.vhints, "ry") 183 log.debug("vstem %s", self.vhints) 184 185 def op_hstemhm(self, index): 186 args = self.popallWidth() 187 self.hhints = [] 188 self.updateHints(args, self.hhints, "rb") 189 log.debug("stemhm %s %s", self.hhints, args) 190 191 def op_vstemhm(self, index): 192 args = self.popallWidth() 193 self.vhints = [] 194 self.updateHints(args, self.vhints, "ry") 195 log.debug("vstemhm %s %s", self.vhints, args) 196 197 def getCurHints(self, hintMaskBytes): 198 curhhints = [] 199 curvhints = [] 200 numhhints = len(self.hhints) 201 202 for i in range(int(numhhints / 2)): 203 if hintOn(i, hintMaskBytes): 204 curhhints.extend(self.hhints[2 * i:2 * i + 2]) 205 numvhints = len(self.vhints) 206 for i in range(int(numvhints / 2)): 207 if hintOn(i + int(numhhints / 2), hintMaskBytes): 208 curvhints.extend(self.vhints[2 * i:2 * i + 2]) 209 return curhhints, curvhints 210 211 def doMask(self, index, bezCommand): 212 args = [] 213 if not self.hintMaskBytes: 214 args = self.popallWidth() 215 if args: 216 self.vhints = [] 217 self.updateHints(args, self.vhints, "ry") 218 self.hintMaskBytes = int((self.hintCount + 7) / 8) 219 220 self.hintMaskString, index = self.callingStack[-1].getBytes( 221 index, self.hintMaskBytes) 222 223 if self.read_hints: 224 curhhints, curvhints = self.getCurHints(self.hintMaskString) 225 strout = "" 226 mask = [strout + hex(ch) for ch in self.hintMaskString] 227 log.debug("%s %s %s %s %s", bezCommand, mask, curhhints, curvhints, 228 args) 229 230 self.bezProgram.append("beginsubr snc\n") 231 for i, hint in enumerate(curhhints): 232 self.bezProgram.append("%s " % hint) 233 if i % 2: 234 self.bezProgram.append("rb\n") 235 for i, hint in enumerate(curvhints): 236 self.bezProgram.append("%s " % hint) 237 if i % 2: 238 self.bezProgram.append("ry\n") 239 self.bezProgram.extend(["endsubr enc\n", "newcolors\n"]) 240 return self.hintMaskString, index 241 242 def op_hintmask(self, index): 243 hintMaskString, index = self.doMask(index, "hintmask") 244 return hintMaskString, index 245 246 def op_cntrmask(self, index): 247 hintMaskString, index = self.doMask(index, "cntrmask") 248 return hintMaskString, index 249 250 def countHints(self, args): 251 self.hintCount = self.hintCount + int(len(args) / 2) 252 253 254def convertT2GlyphToBez(t2CharString, read_hints=True, round_coords=True): 255 # wrapper for T2ToBezExtractor which 256 # applies it to the supplied T2 charstring 257 subrs = getattr(t2CharString.private, "Subrs", []) 258 extractor = T2ToBezExtractor(subrs, 259 t2CharString.globalSubrs, 260 t2CharString.private.nominalWidthX, 261 t2CharString.private.defaultWidthX, 262 read_hints, 263 round_coords) 264 extractor.execute(t2CharString) 265 t2_width_arg = None 266 if extractor.gotWidth and (extractor.width is not None): 267 t2_width_arg = extractor.width - t2CharString.private.nominalWidthX 268 return "".join(extractor.bezProgram), t2_width_arg 269 270 271class HintMask: 272 # class used to collect hints for the current 273 # hint mask when converting bez to T2. 274 def __init__(self, listPos): 275 # The index into the t2list is kept so we can quickly find them later. 276 # Note that t2list has one item per operator, and does not include the 277 # initial hint operators - first op is always [rhv]moveto or endchar. 278 self.listPos = listPos 279 # These contain the actual hint values. 280 self.h_list = [] 281 self.v_list = [] 282 self.mask = None 283 284 def maskByte(self, hHints, vHints): 285 # return hintmask bytes for known hints. 286 num_hhints = len(hHints) 287 num_vhints = len(vHints) 288 self.byteLength = byteLength = int((7 + num_hhints + num_vhints) / 8) 289 maskVal = 0 290 byteIndex = 0 291 mask = b"" 292 if self.h_list: 293 mask, maskVal, byteIndex = self.addMaskBits( 294 hHints, self.h_list, 0, mask, maskVal, byteIndex) 295 if self.v_list: 296 mask, maskVal, byteIndex = self.addMaskBits( 297 vHints, self.v_list, num_hhints, mask, maskVal, byteIndex) 298 299 if maskVal: 300 mask += bytes([maskVal]) 301 302 if len(mask) < byteLength: 303 mask += b"\0" * (byteLength - len(mask)) 304 self.mask = mask 305 return mask 306 307 @staticmethod 308 def addMaskBits(allHints, maskHints, numPriorHints, mask, maskVal, 309 byteIndex): 310 # sort in allhints order. 311 sort_list = [[allHints.index(hint) + numPriorHints, hint] for hint in 312 maskHints if hint in allHints] 313 if not sort_list: 314 # we get here if some hints have been dropped # because of 315 # the stack limit, so that none of the items in maskHints are 316 # not in allHints 317 return mask, maskVal, byteIndex 318 319 sort_list.sort() 320 (idx_list, maskHints) = zip(*sort_list) 321 for i in idx_list: 322 newbyteIndex = int(i / 8) 323 if newbyteIndex != byteIndex: 324 mask += bytes([maskVal]) 325 byteIndex += 1 326 while byteIndex < newbyteIndex: 327 mask += b"\0" 328 byteIndex += 1 329 maskVal = 0 330 maskVal += 2**(7 - (i % 8)) 331 return mask, maskVal, byteIndex 332 333 @property 334 def num_bits(self): 335 count = sum( 336 [bin(mask_byte).count('1') for mask_byte in bytearray(self.mask)]) 337 return count 338 339 340def make_hint_list(hints, need_hint_masks, is_h): 341 # Add the list of T2 tokens that make up the initial hint operators 342 hint_list = [] 343 lastPos = 0 344 # In bez terms, the first coordinate in each pair is absolute, 345 # second is relative. 346 # In T2, each term is relative to the previous one. 347 for hint in hints: 348 if not hint: 349 continue 350 pos1 = hint[0] 351 pos = pos1 - lastPos 352 if pos % 1 == 0: 353 pos = int(pos) 354 hint_list.append(pos) 355 pos2 = hint[1] 356 if pos2 % 1 == 0: 357 pos2 = int(pos2) 358 lastPos = pos1 + pos2 359 hint_list.append(pos2) 360 361 if need_hint_masks: 362 if is_h: 363 op = "hstemhm" 364 hint_list.append(op) 365 # never need to append vstemhm: if we are using it, it is followed 366 # by a mask command and vstemhm is inferred. 367 else: 368 if is_h: 369 op = "hstem" 370 else: 371 op = "vstem" 372 hint_list.append(op) 373 return hint_list 374 375 376bezToT2 = { 377 "mt": 'rmoveto', 378 "rmt": 'rmoveto', 379 "dt": 'rlineto', 380 "ct": 'rrcurveto', 381 "cp": '', 382 "ed": 'endchar' 383} 384 385 386kHintArgsNoOverlap = 0 387kHintArgsOverLap = 1 388kHintArgsMatch = 2 389 390 391def checkStem3ArgsOverlap(arg_list, hint_list): 392 status = kHintArgsNoOverlap 393 for x0, x1 in arg_list: 394 x1 = x0 + x1 395 for y0, y1 in hint_list: 396 y1 = y0 + y1 397 if x0 == y0: 398 if x1 == y1: 399 status = kHintArgsMatch 400 else: 401 return kHintArgsOverLap 402 elif x1 == y1: 403 return kHintArgsOverLap 404 else: 405 if (x0 > y0) and (x0 < y1): 406 return kHintArgsOverLap 407 if (x1 > y0) and (x1 < y1): 408 return kHintArgsOverLap 409 return status 410 411 412def _add_cntr_maskHints(counter_mask_list, src_hints, is_h): 413 for arg_list in src_hints: 414 for mask in counter_mask_list: 415 dst_hints = mask.h_list if is_h else mask.v_list 416 if not dst_hints: 417 dst_hints.extend(arg_list) 418 overlap_status = kHintArgsMatch 419 break 420 overlap_status = checkStem3ArgsOverlap(arg_list, dst_hints) 421 # The args match args in this control mask. 422 if overlap_status == kHintArgsMatch: 423 break 424 if overlap_status != kHintArgsMatch: 425 mask = HintMask(0) 426 counter_mask_list.append(mask) 427 dst_hints.extend(arg_list) 428 429 430def build_counter_mask_list(h_stem3_list, v_stem3_list): 431 432 v_counter_mask = HintMask(0) 433 h_counter_mask = v_counter_mask 434 counter_mask_list = [h_counter_mask] 435 _add_cntr_maskHints(counter_mask_list, h_stem3_list, is_h=True) 436 _add_cntr_maskHints(counter_mask_list, v_stem3_list, is_h=False) 437 438 return counter_mask_list 439 440 441def makeRelativeCTArgs(arg_list, curX, curY): 442 newCurX = arg_list[4] 443 newCurY = arg_list[5] 444 arg_list[5] -= arg_list[3] 445 arg_list[4] -= arg_list[2] 446 447 arg_list[3] -= arg_list[1] 448 arg_list[2] -= arg_list[0] 449 450 arg_list[0] -= curX 451 arg_list[1] -= curY 452 return arg_list, newCurX, newCurY 453 454 455def build_hint_order(hints): 456 # MM hints have duplicate hints. We want to return a list of indices into 457 # the original unsorted and unfiltered list. The list should be sorted, and 458 # should filter out duplicates 459 460 num_hints = len(hints) 461 index_list = list(range(num_hints)) 462 hint_list = list(zip(hints, index_list)) 463 hint_list.sort() 464 new_hints = [hint_list[i] for i in range(1, num_hints) 465 if hint_list[i][0] != hint_list[i - 1][0]] 466 new_hints = [hint_list[0]] + new_hints 467 hints, hint_order = list(zip(*new_hints)) 468 # hints is now a list of hint pairs, sorted by increasing bottom edge. 469 # hint_order is now a list of the hint indices from the bez file, but 470 # sorted in the order of the hint pairs. 471 return hints, hint_order 472 473 474def make_abs(hint_pair): 475 bottom_edge, delta = hint_pair 476 new_hint_pair = [bottom_edge, delta] 477 if delta in [-20, -21]: # It is a ghost hint! 478 # We use this only in comparing overlap and order: 479 # pretend the delta is 0, as it isn't a real value. 480 new_hint_pair[1] = bottom_edge 481 else: 482 new_hint_pair[1] = bottom_edge + delta 483 return new_hint_pair 484 485 486def check_hint_overlap(hint_list, last_idx, bad_hint_idxs): 487 # return True if there is an overlap. 488 prev = hint_list[0] 489 for i, hint_pair in enumerate(hint_list[1:], 1): 490 if prev[1] >= hint_pair[0]: 491 bad_hint_idxs.add(i + last_idx - 1) 492 prev = hint_pair 493 494 495def check_hint_pairs(hint_pairs, mm_hint_info, last_idx=0): 496 # pairs must be in ascending order by bottom (or left) edge, 497 # and pairs in a hint group must not overlap. 498 499 # check order first 500 bad_hint_idxs = set() 501 prev = hint_pairs[0] 502 for i, hint_pair in enumerate(hint_pairs[1:], 1): 503 if prev[0] > hint_pair[0]: 504 # If there is a conflict, we drop the previous hint 505 bad_hint_idxs.add(i + last_idx - 1) 506 prev = hint_pair 507 508 # check for overlap in hint groups. 509 if mm_hint_info.hint_masks: 510 for hint_mask in mm_hint_info.hint_masks: 511 if last_idx == 0: 512 hint_list = hint_mask.h_list 513 else: 514 hint_list = hint_mask.v_list 515 hint_list = [make_abs(hint_pair) for hint_pair in hint_list] 516 check_hint_overlap(hint_list, last_idx, bad_hint_idxs) 517 else: 518 hint_list = [make_abs(hint_pair) for hint_pair in hint_pairs] 519 check_hint_overlap(hint_list, last_idx, bad_hint_idxs) 520 521 if bad_hint_idxs: 522 mm_hint_info.bad_hint_idxs |= bad_hint_idxs 523 524 525def update_hints(in_mm_hints, arg_list, hints, hint_mask, is_v=False): 526 if in_mm_hints: 527 hints.append(arg_list) 528 i = len(hints) - 1 529 else: 530 try: 531 i = hints.index(arg_list) 532 except ValueError: 533 i = len(hints) 534 hints.append(arg_list) 535 if hint_mask: 536 hint_list = hint_mask.v_list if is_v else hint_mask.h_list 537 if hints[i] not in hint_list: 538 hint_list.append(hints[i]) 539 return i 540 541 542def convertBezToT2(bezString, mm_hint_info=None): 543 # convert bez data to a T2 outline program, a list of operator tokens. 544 # 545 # Convert all bez ops to simplest T2 equivalent. 546 # Add all hints to vertical and horizontal hint lists as encountered. 547 # Insert a HintMask class whenever a new set of hints is encountered. 548 # Add all hints as prefix to t2Program 549 # After all operators have been processed, convert HintMask items into 550 # hintmask ops and hintmask bytes. 551 # Review operator list to optimize T2 operators. 552 # 553 # If doing MM-hinting, extra work is needed to maintain merge 554 # compatibility between the reference font and the region fonts. 555 # Although hints are generated for exactly the same outline features 556 # in all fonts, they will have different values. Consequently, the 557 # hints in a region font may not sort to the same order as in the 558 # reference font. In addition, they may be filtered differently. Only 559 # unique hints are added from the bez file to the hint list. Two hint 560 # pairs may differ in one font, but not in another. 561 # We work around these problems by first not filtering the hint 562 # pairs for uniqueness when accumulating the hint lists. For the 563 # reference font, once we have collected all the hints, we remove any 564 # duplicate pairs, but keep a list of the retained hint pair indices 565 # into the unfiltered hint pair list. For the region fonts, we 566 # select hints from the unfiltered hint pair lists by using the selected 567 # index list from the reference font. 568 # Note that this breaks the CFF spec for snapshotted instances of the 569 # CFF2 VF variable font, as hints may not be in ascending order, and the 570 # hint list may contain duplicate hints. 571 572 in_mm_hints = mm_hint_info is not None 573 bezString = re.sub(r"%.+?\n", "", bezString) # suppress comments 574 bezList = re.findall(r"(\S+)", bezString) 575 if not bezList: 576 return "" 577 hhints = [] 578 vhints = [] 579 # Always assume a hint mask exists until proven 580 # otherwise - make an initial HintMask. 581 hint_mask = HintMask(0) 582 hintMaskList = [hint_mask] 583 vStem3Args = [] 584 hStem3Args = [] 585 v_stem3_list = [] 586 h_stem3_list = [] 587 arg_list = [] 588 t2List = [] 589 590 lastPathOp = None 591 curX = 0 592 curY = 0 593 for token in bezList: 594 try: 595 val1 = round(float(token), 2) 596 try: 597 val2 = int(token) 598 if int(val1) == val2: 599 arg_list.append(val2) 600 else: 601 arg_list.append("%s 100 div" % int(val1 * 100)) 602 except ValueError: 603 arg_list.append(val1) 604 continue 605 except ValueError: 606 pass 607 608 if token == "newcolors": 609 lastPathOp = token 610 elif token in ["beginsubr", "endsubr"]: 611 lastPathOp = token 612 elif token == "snc": 613 lastPathOp = token 614 # The index into the t2list is kept 615 # so we can quickly find them later. 616 hint_mask = HintMask(len(t2List)) 617 t2List.append([hint_mask]) 618 hintMaskList.append(hint_mask) 619 elif token == "enc": 620 lastPathOp = token 621 elif token == "rb": 622 update_hints(in_mm_hints, arg_list, hhints, hint_mask, False) 623 arg_list = [] 624 lastPathOp = token 625 elif token == "ry": 626 update_hints(in_mm_hints, arg_list, vhints, hint_mask, True) 627 arg_list = [] 628 lastPathOp = token 629 elif token == "rm": # vstem3 hints are vhints 630 update_hints(in_mm_hints, arg_list, vhints, hint_mask, True) 631 if (lastPathOp != token) and vStem3Args: 632 # first rm, must be start of a new vstem3 633 # if we already have a set of vstems in vStem3Args, save them, 634 # and then clear the vStem3Args so we can add the new set. 635 v_stem3_list.append(vStem3Args) 636 vStem3Args = [] 637 638 vStem3Args.append(arg_list) 639 arg_list = [] 640 lastPathOp = token 641 elif token == "rv": # hstem3 are hhints 642 update_hints(in_mm_hints, arg_list, hhints, hint_mask, False) 643 644 if (lastPathOp != token) and hStem3Args: 645 # first rv, must be start of a new h countermask 646 h_stem3_list.append(hStem3Args) 647 hStem3Args = [] 648 649 hStem3Args.append(arg_list) 650 arg_list = [] 651 lastPathOp = token 652 elif token == "preflx1": 653 # The preflx1/preflx2a sequence provides the same 'i' as the flex 654 # sequence. The difference is that the preflx1/preflx2a sequence 655 # provides the argument values needed for building a Type1 string 656 # while the flex sequence is simply the 6 rrcurveto points. 657 # Both sequences are always provided. 658 lastPathOp = token 659 arg_list = [] 660 elif token == "preflx2a": 661 lastPathOp = token 662 del t2List[-1] 663 arg_list = [] 664 elif token == "flxa": 665 lastPathOp = token 666 argList1, curX, curY = makeRelativeCTArgs(arg_list[:6], curX, curY) 667 argList2, curX, curY = makeRelativeCTArgs(arg_list[6:], curX, curY) 668 arg_list = argList1 + argList2 669 t2List.append([arg_list[:12] + [50], "flex"]) 670 arg_list = [] 671 elif token == "sc": 672 lastPathOp = token 673 else: 674 if token in ["rmt", "mt", "dt", "ct"]: 675 lastPathOp = token 676 t2Op = bezToT2.get(token, None) 677 if token in ["mt", "dt"]: 678 newList = [arg_list[0] - curX, arg_list[1] - curY] 679 curX = arg_list[0] 680 curY = arg_list[1] 681 arg_list = newList 682 elif token == "ct": 683 arg_list, curX, curY = makeRelativeCTArgs(arg_list, curX, curY) 684 if t2Op: 685 t2List.append([arg_list, t2Op]) 686 elif t2Op is None: 687 raise KeyError("Unhandled operation %s %s" % (arg_list, token)) 688 arg_list = [] 689 690 # Add hints, if any. Must be done at the end of op processing to make sure 691 # we have seen all the hints in the bez string. Note that the hintmask are 692 # identified in the t2List by an index into the list; be careful NOT to 693 # change the t2List length until the hintmasks have been converted. 694 need_hint_masks = len(hintMaskList) > 1 695 if vStem3Args: 696 v_stem3_list.append(vStem3Args) 697 if hStem3Args: 698 h_stem3_list.append(hStem3Args) 699 700 t2Program = [] 701 702 if hhints or vhints: 703 if mm_hint_info is None: 704 hhints.sort() 705 vhints.sort() 706 elif mm_hint_info.defined: 707 # Apply hint order from reference font in MM hinting 708 hhints = [hhints[j] for j in mm_hint_info.h_order] 709 vhints = [vhints[j] for j in mm_hint_info.v_order] 710 else: 711 # Define hint order from reference font in MM hinting 712 hhints, mm_hint_info.h_order = build_hint_order(hhints) 713 vhints, mm_hint_info.v_order = build_hint_order(vhints) 714 715 num_hhints = len(hhints) 716 num_vhints = len(vhints) 717 hint_limit = int((kStackLimit - 2) / 2) 718 if num_hhints >= hint_limit: 719 hhints = hhints[:hint_limit] 720 if num_vhints >= hint_limit: 721 vhints = vhints[:hint_limit] 722 723 if mm_hint_info and mm_hint_info.defined: 724 check_hint_pairs(hhints, mm_hint_info) 725 last_idx = len(hhints) 726 check_hint_pairs(vhints, mm_hint_info, last_idx) 727 728 if hhints: 729 t2Program = make_hint_list(hhints, need_hint_masks, is_h=True) 730 if vhints: 731 t2Program += make_hint_list(vhints, need_hint_masks, is_h=False) 732 733 cntrmask_progam = None 734 if mm_hint_info is None: 735 if v_stem3_list or h_stem3_list: 736 counter_mask_list = build_counter_mask_list(h_stem3_list, 737 v_stem3_list) 738 cntrmask_progam = [['cntrmask', cMask.maskByte(hhints, 739 vhints)] for 740 cMask in counter_mask_list] 741 elif (not mm_hint_info.defined): 742 if v_stem3_list or h_stem3_list: 743 # this is the reference font - we need to build the list. 744 counter_mask_list = build_counter_mask_list(h_stem3_list, 745 v_stem3_list) 746 cntrmask_progam = [['cntrmask', cMask.maskByte(hhints, 747 vhints)] for 748 cMask in counter_mask_list] 749 mm_hint_info.cntr_masks = counter_mask_list 750 else: 751 # This is a region font - we need to used the reference font list. 752 counter_mask_list = mm_hint_info.cntr_masks 753 cntrmask_progam = [['cntrmask', cMask.mask] for 754 cMask in counter_mask_list] 755 756 if cntrmask_progam: 757 cntrmask_progam = itertools.chain(*cntrmask_progam) 758 t2Program.extend(cntrmask_progam) 759 760 if need_hint_masks: 761 # If there is not a hintsub before any drawing operators, then 762 # add an initial first hint mask to the t2Program. 763 if (mm_hint_info is None) or (not mm_hint_info.defined): 764 # a single font and a reference font for mm hinting are 765 # processed the same way 766 if hintMaskList[1].listPos != 0: 767 hBytes = hintMaskList[0].maskByte(hhints, vhints) 768 t2Program.extend(["hintmask", hBytes]) 769 if in_mm_hints: 770 mm_hint_info.hint_masks.append(hintMaskList[0]) 771 772 # Convert the rest of the hint masks 773 # to a hintmask op and hintmask bytes. 774 for hint_mask in hintMaskList[1:]: 775 pos = hint_mask.listPos 776 hBytes = hint_mask.maskByte(hhints, vhints) 777 t2List[pos] = [["hintmask"], hBytes] 778 if in_mm_hints: 779 mm_hint_info.hint_masks.append(hint_mask) 780 elif (mm_hint_info is not None): 781 # This is a MM region font: 782 # apply hint masks from reference font. 783 try: 784 hm0_mask = mm_hint_info.hint_masks[0].mask 785 except IndexError: 786 import pdb 787 pdb.set_trace() 788 if isinstance(t2List[0][0], HintMask): 789 t2List[0] = [["hintmask"], hm0_mask] 790 else: 791 t2Program.extend(["hintmask", hm0_mask]) 792 793 for hm in mm_hint_info.hint_masks[1:]: 794 t2List[hm.listPos] = [["hintmask"], hm.mask] 795 796 for entry in t2List: 797 try: 798 t2Program.extend(entry[0]) 799 t2Program.append(entry[1]) 800 except Exception: 801 raise KeyError("Failed to extend t2Program with entry %s" % entry) 802 803 if in_mm_hints: 804 mm_hint_info.defined = True 805 return t2Program 806 807 808def _run_tx(args): 809 try: 810 subprocess.check_call(["tx"] + args) 811 except (subprocess.CalledProcessError, OSError) as e: 812 raise FontParseError(e) 813 814 815class FixHintWidthDecompiler(SimpleT2Decompiler): 816 # If we are using this class, we know the charstring has hints. 817 def __init__(self, localSubrs, globalSubrs, private=None): 818 self.hintMaskBytes = 0 # to silence false Codacy error. 819 SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private) 820 self.has_explicit_width = None 821 self.h_hint_args = self.v_hint_args = None 822 self.last_stem_index = None 823 824 def op_hstem(self, index): 825 self.countHints(is_vert=False) 826 self.last_stem_index = index 827 op_hstemhm = op_hstem 828 829 def op_vstem(self, index): 830 self.countHints(is_vert=True) 831 self.last_stem_index = index 832 op_vstemhm = op_vstem 833 834 def op_hintmask(self, index): 835 if not self.hintMaskBytes: 836 # Note that I am assuming that there is never an op_vstemhm 837 # followed by an op_hintmask. Since this is applied after saving 838 # the font with fontTools, this is safe. 839 self.countHints(is_vert=True) 840 self.hintMaskBytes = (self.hintCount + 7) // 8 841 cs = self.callingStack[-1] 842 hintMaskBytes, index = cs.getBytes(index, self.hintMaskBytes) 843 return hintMaskBytes, index 844 op_cntrmask = op_hintmask 845 846 def countHints(self, is_vert): 847 args = self.popall() 848 if self.has_explicit_width is None: 849 if (len(args) % 2) == 0: 850 self.has_explicit_width = False 851 else: 852 self.has_explicit_width = True 853 self.width_arg = args[0] 854 args = args[1:] 855 self.hintCount = self.hintCount + len(args) // 2 856 if is_vert: 857 self.v_hint_args = args 858 else: 859 self.h_hint_args = args 860 861 862class CFFFontData: 863 def __init__(self, path, font_format): 864 self.inputPath = path 865 self.font_format = font_format 866 self.mm_hint_info_dict = {} 867 self.t2_widths = {} 868 self.is_cff2 = False 869 self.is_vf = False 870 self.vs_data_models = None 871 if font_format == "OTF": 872 # It is an OTF font, we can process it directly. 873 font = TTFont(path) 874 if "CFF "in font: 875 cff_format = "CFF " 876 elif "CFF2" in font: 877 cff_format = "CFF2" 878 self.is_cff2 = True 879 else: 880 raise FontParseError("OTF font has no CFF table <%s>." % path) 881 else: 882 # Else, package it in an OTF font. 883 cff_format = "CFF " 884 if font_format == "CFF": 885 with open(path, "rb") as fp: 886 data = fp.read() 887 else: 888 fd, temp_path = tempfile.mkstemp() 889 os.close(fd) 890 try: 891 _run_tx(["-cff", "+b", "-std", path, temp_path]) 892 with open(temp_path, "rb") as fp: 893 data = fp.read() 894 finally: 895 os.remove(temp_path) 896 897 font = TTFont() 898 font['CFF '] = newTable('CFF ') 899 font['CFF '].decompile(data, font) 900 901 self.ttFont = font 902 self.cffTable = font[cff_format] 903 904 # for identifier in glyph-list: 905 # Get charstring. 906 self.topDict = self.cffTable.cff.topDictIndex[0] 907 self.charStrings = self.topDict.CharStrings 908 if 'fvar' in self.ttFont: 909 self.is_vf = True 910 fvar = self.ttFont['fvar'] 911 CFF2 = self.cffTable 912 CFF2.desubroutinize() 913 topDict = CFF2.cff.topDictIndex[0] 914 self.temp_cs = copy.deepcopy(self.charStrings['.notdef']) 915 self.vs_data_models = self.get_vs_data_models(topDict, 916 fvar) 917 918 def getGlyphList(self): 919 return self.ttFont.getGlyphOrder() 920 921 def getPSName(self): 922 if self.is_cff2 and 'name' in self.ttFont: 923 psName = next((name_rec.string for name_rec in self.ttFont[ 924 'name'].names if (name_rec.nameID == 6) and ( 925 name_rec.platformID == 3))) 926 psName = psName.decode('utf-16be') 927 else: 928 psName = self.cffTable.cff.fontNames[0] 929 return psName 930 931 def get_min_max(self, pTopDict, upm): 932 if self.is_cff2 and 'hhea' in self.ttFont: 933 font_max = self.ttFont['hhea'].ascent 934 font_min = self.ttFont['hhea'].descent 935 elif hasattr(pTopDict, 'FontBBox'): 936 font_max = pTopDict.FontBBox[3] 937 font_min = pTopDict.FontBBox[1] 938 else: 939 font_max = upm * 1.25 940 font_min = -upm * 0.25 941 alignment_min = min(-upm * 0.25, font_min) 942 alignment_max = max(upm * 1.25, font_max) 943 return alignment_min, alignment_max 944 945 def convertToBez(self, glyphName, read_hints, round_coords, doAll=False): 946 t2Wdth = None 947 t2CharString = self.charStrings[glyphName] 948 try: 949 bezString, t2Wdth = convertT2GlyphToBez(t2CharString, 950 read_hints, round_coords) 951 # Note: the glyph name is important, as it is used by autohintexe 952 # for various heuristics, including [hv]stem3 derivation. 953 bezString = "% " + glyphName + "\n" + bezString 954 except SEACError: 955 log.warning("Skipping %s: can't process SEAC composite glyphs.", 956 glyphName) 957 bezString = None 958 self.t2_widths[glyphName] = t2Wdth 959 return bezString 960 961 def updateFromBez(self, bezData, glyphName, mm_hint_info=None): 962 t2Program = convertBezToT2(bezData, mm_hint_info) 963 if not self.is_cff2: 964 t2_width_arg = self.t2_widths[glyphName] 965 if t2_width_arg is not None: 966 t2Program = [t2_width_arg] + t2Program 967 if self.vs_data_models is not None: 968 # It is a variable font. Accumulate the charstrings. 969 self.glyph_programs.append(t2Program) 970 else: 971 # This is an MM source font. Update the font's charstring directly. 972 t2CharString = self.charStrings[glyphName] 973 t2CharString.program = t2Program 974 975 def save(self, path): 976 if path is None: 977 path = self.inputPath 978 979 if self.font_format == "OTF": 980 self.ttFont.save(path) 981 self.ttFont.close() 982 else: 983 data = self.ttFont["CFF "].compile(self.ttFont) 984 if self.font_format == "CFF": 985 with open(path, "wb") as fp: 986 fp.write(data) 987 else: 988 fd, temp_path = tempfile.mkstemp() 989 os.write(fd, data) 990 os.close(fd) 991 992 try: 993 args = ["-t1", "-std"] 994 if self.font_format == "PFB": 995 args.append("-pfb") 996 _run_tx(args + [temp_path, path]) 997 finally: 998 os.remove(temp_path) 999 1000 def close(self): 1001 self.ttFont.close() 1002 1003 def isCID(self): 1004 return hasattr(self.topDict, "FDSelect") 1005 1006 def hasFDArray(self): 1007 return self.is_cff2 or hasattr(self.topDict, "FDSelect") 1008 1009 def flattenBlends(self, blendList): 1010 if type(blendList[0]) is list: 1011 flatList = [blendList[i][0] for i in range(len(blendList))] 1012 else: 1013 flatList = blendList 1014 return flatList 1015 1016 def getFontInfo(self, allow_no_blues, noFlex, 1017 vCounterGlyphs, hCounterGlyphs, fdIndex=0): 1018 # The psautohint library needs the global font hint zones 1019 # and standard stem widths. 1020 # Format them into a single text string. 1021 # The text format is arbitrary, inherited from very old software, 1022 # but there is no real need to change it. 1023 pTopDict = self.topDict 1024 if hasattr(pTopDict, "FDArray"): 1025 pDict = pTopDict.FDArray[fdIndex] 1026 else: 1027 pDict = pTopDict 1028 privateDict = pDict.Private 1029 1030 fdDict = fdTools.FDDict() 1031 fdDict.LanguageGroup = getattr(privateDict, "LanguageGroup", "0") 1032 1033 if hasattr(pDict, "FontMatrix"): 1034 fdDict.FontMatrix = pDict.FontMatrix 1035 else: 1036 fdDict.FontMatrix = pTopDict.FontMatrix 1037 upm = int(1 / fdDict.FontMatrix[0]) 1038 fdDict.OrigEmSqUnits = str(upm) 1039 1040 fdDict.FontName = getattr(pTopDict, "FontName", self.getPSName()) 1041 1042 blueValues = getattr(privateDict, "BlueValues", [])[:] 1043 numBlueValues = len(blueValues) 1044 if numBlueValues < 4: 1045 low, high = self.get_min_max(pTopDict, upm) 1046 # Make a set of inactive alignment zones: zones outside of the 1047 # font BBox so as not to affect hinting. Used when source font has 1048 # no BlueValues or has invalid BlueValues. Some fonts have bad BBox 1049 # values, so I don't let this be smaller than -upm*0.25, upm*1.25. 1050 inactiveAlignmentValues = [low, low, high, high] 1051 if allow_no_blues: 1052 blueValues = inactiveAlignmentValues 1053 numBlueValues = len(blueValues) 1054 else: 1055 raise FontParseError("Font must have at least four values in " 1056 "its BlueValues array for PSAutoHint to " 1057 "work!") 1058 blueValues.sort() 1059 1060 # The first pair only is a bottom zone, where the first value is the 1061 # overshoot position. The rest are top zones, and second value of the 1062 # pair is the overshoot position. 1063 blueValues = self.flattenBlends(blueValues) 1064 blueValues[0] = blueValues[0] - blueValues[1] 1065 for i in range(3, numBlueValues, 2): 1066 blueValues[i] = blueValues[i] - blueValues[i - 1] 1067 1068 blueValues = [str(v) for v in blueValues] 1069 numBlueValues = min(numBlueValues, len(fdTools.kBlueValueKeys)) 1070 for i in range(numBlueValues): 1071 key = fdTools.kBlueValueKeys[i] 1072 value = blueValues[i] 1073 setattr(fdDict, key, value) 1074 1075 if hasattr(privateDict, "OtherBlues"): 1076 # For all OtherBlues, the pairs are bottom zones, and 1077 # the first value of each pair is the overshoot position. 1078 i = 0 1079 numBlueValues = len(privateDict.OtherBlues) 1080 blueValues = privateDict.OtherBlues[:] 1081 blueValues.sort() 1082 blueValues = self.flattenBlends(blueValues) 1083 for i in range(0, numBlueValues, 2): 1084 blueValues[i] = blueValues[i] - blueValues[i + 1] 1085 blueValues = [str(v) for v in blueValues] 1086 numBlueValues = min(numBlueValues, 1087 len(fdTools.kOtherBlueValueKeys)) 1088 for i in range(numBlueValues): 1089 key = fdTools.kOtherBlueValueKeys[i] 1090 value = blueValues[i] 1091 setattr(fdDict, key, value) 1092 1093 if hasattr(privateDict, "StemSnapV"): 1094 vstems = privateDict.StemSnapV 1095 elif hasattr(privateDict, "StdVW"): 1096 vstems = [privateDict.StdVW] 1097 else: 1098 if allow_no_blues: 1099 # dummy value. Needs to be larger than any hint will likely be, 1100 # as the autohint program strips out any hint wider than twice 1101 # the largest global stem width. 1102 vstems = [upm] 1103 else: 1104 raise FontParseError("Font has neither StemSnapV nor StdVW!") 1105 vstems.sort() 1106 vstems = self.flattenBlends(vstems) 1107 if (len(vstems) == 0) or ((len(vstems) == 1) and (vstems[0] < 1)): 1108 vstems = [upm] # dummy value that will allow PyAC to run 1109 log.warning("There is no value or 0 value for DominantV.") 1110 fdDict.DominantV = "[" + " ".join([str(v) for v in vstems]) + "]" 1111 1112 if hasattr(privateDict, "StemSnapH"): 1113 hstems = privateDict.StemSnapH 1114 elif hasattr(privateDict, "StdHW"): 1115 hstems = [privateDict.StdHW] 1116 else: 1117 if allow_no_blues: 1118 # dummy value. Needs to be larger than any hint will likely be, 1119 # as the autohint program strips out any hint wider than twice 1120 # the largest global stem width. 1121 hstems = [upm] 1122 else: 1123 raise FontParseError("Font has neither StemSnapH nor StdHW!") 1124 hstems.sort() 1125 hstems = self.flattenBlends(hstems) 1126 if (len(hstems) == 0) or ((len(hstems) == 1) and (hstems[0] < 1)): 1127 hstems = [upm] # dummy value that will allow PyAC to run 1128 log.warning("There is no value or 0 value for DominantH.") 1129 fdDict.DominantH = "[" + " ".join([str(v) for v in hstems]) + "]" 1130 1131 if noFlex: 1132 fdDict.FlexOK = "false" 1133 else: 1134 fdDict.FlexOK = "true" 1135 1136 # Add candidate lists for counter hints, if any. 1137 if vCounterGlyphs: 1138 temp = " ".join(vCounterGlyphs) 1139 fdDict.VCounterChars = "( %s )" % (temp) 1140 if hCounterGlyphs: 1141 temp = " ".join(hCounterGlyphs) 1142 fdDict.HCounterChars = "( %s )" % (temp) 1143 1144 fdDict.BlueFuzz = getattr(privateDict, "BlueFuzz", 1) 1145 1146 return fdDict 1147 1148 def getfdIndex(self, name): 1149 gid = self.ttFont.getGlyphID(name) 1150 if hasattr(self.topDict, "FDSelect"): 1151 fdIndex = self.topDict.FDSelect[gid] 1152 else: 1153 fdIndex = 0 1154 return fdIndex 1155 1156 def getfdInfo(self, allow_no_blues, noFlex, vCounterGlyphs, hCounterGlyphs, 1157 glyphList, fdIndex=0): 1158 topDict = self.topDict 1159 fdGlyphDict = None 1160 1161 # Get the default fontinfo from the font's top dict. 1162 fdDict = self.getFontInfo( 1163 allow_no_blues, noFlex, vCounterGlyphs, hCounterGlyphs, fdIndex) 1164 fontDictList = [fdDict] 1165 1166 # Check the fontinfo file, and add any other font dicts 1167 srcFontInfo = os.path.dirname(self.inputPath) 1168 srcFontInfo = os.path.join(srcFontInfo, "fontinfo") 1169 if os.path.exists(srcFontInfo): 1170 with open(srcFontInfo, "r", encoding="utf-8") as fi: 1171 fontInfoData = fi.read() 1172 fontInfoData = re.sub(r"#[^\r\n]+", "", fontInfoData) 1173 else: 1174 return fdGlyphDict, fontDictList 1175 1176 if "FDDict" in fontInfoData: 1177 maxY = topDict.FontBBox[3] 1178 minY = topDict.FontBBox[1] 1179 fdGlyphDict, fontDictList, finalFDict = fdTools.parseFontInfoFile( 1180 fontDictList, fontInfoData, glyphList, maxY, minY, 1181 self.getPSName()) 1182 if hasattr(topDict, "FDArray"): 1183 private = topDict.FDArray[fdIndex].Private 1184 else: 1185 private = topDict.Private 1186 if finalFDict is None: 1187 # If a font dict was not explicitly specified for the 1188 # output font, use the first user-specified font dict. 1189 fdTools.mergeFDDicts(fontDictList[1:], private) 1190 else: 1191 fdTools.mergeFDDicts([finalFDict], private) 1192 return fdGlyphDict, fontDictList 1193 1194 @staticmethod 1195 def args_to_hints(hint_args): 1196 hints = [hint_args[0:2]] 1197 prev = hints[0] 1198 for i in range(2, len(hint_args), 2): 1199 bottom = hint_args[i] + prev[0] + prev[1] 1200 hints.append([bottom, hint_args[i + 1]]) 1201 prev = hints[-1] 1202 return hints 1203 1204 @staticmethod 1205 def extract_hint_args(program): 1206 width = None 1207 h_hint_args = [] 1208 v_hint_args = [] 1209 for i, token in enumerate(program): 1210 if type(token) is str: 1211 if i % 2 != 0: 1212 width = program[0] 1213 del program[0] 1214 idx = i - 1 1215 else: 1216 idx = i 1217 1218 if (token[:4] == 'vstem') or token[-3:] == 'mask': 1219 h_hint_args = [] 1220 v_hint_args = program[:idx] 1221 1222 elif token[:5] == 'hstem': 1223 h_hint_args = program[:idx] 1224 v_program = program[idx+1:] 1225 1226 for j, vtoken in enumerate(v_program): 1227 if type(vtoken) is str: 1228 if (vtoken[:5] == 'vstem') or vtoken[-4:] == \ 1229 'mask': 1230 v_hint_args = v_program[:j] 1231 break 1232 break 1233 1234 return width, h_hint_args, v_hint_args 1235 1236 def fix_t2_program_hints(self, program, mm_hint_info, is_reference_font): 1237 1238 width_arg, h_hint_args, v_hint_args = self.extract_hint_args(program) 1239 1240 # 1. Build list of good [vh]hints. 1241 bad_hint_idxs = list(mm_hint_info.bad_hint_idxs) 1242 bad_hint_idxs.sort() 1243 num_hhint_pairs = len(h_hint_args) // 2 1244 for idx in reversed(bad_hint_idxs): 1245 if idx < num_hhint_pairs: 1246 hint_args = h_hint_args 1247 bottom_idx = idx * 2 1248 else: 1249 hint_args = v_hint_args 1250 bottom_idx = (idx - num_hhint_pairs) * 2 1251 delta = hint_args[bottom_idx] + hint_args[bottom_idx + 1] 1252 del hint_args[bottom_idx:bottom_idx + 2] 1253 if len(hint_args) > bottom_idx: 1254 hint_args[bottom_idx] += delta 1255 1256 # delete old hints from program 1257 if mm_hint_info.cntr_masks: 1258 last_hint_idx = program.index('cntrmask') 1259 elif mm_hint_info.hint_masks: 1260 last_hint_idx = program.index('hintmask') 1261 else: 1262 for op in ['vstem', 'hstem']: 1263 try: 1264 last_hint_idx = program.index(op) 1265 break 1266 except IndexError: 1267 last_hint_idx = None 1268 if last_hint_idx is not None: 1269 del program[:last_hint_idx] 1270 1271 # If there were v_hint_args, but they have now all been 1272 # deleted, the first token will still be 'vstem[hm]'. Delete it. 1273 if ((not v_hint_args) and program[0].startswith('vstem')): 1274 del program[0] 1275 1276 # Add width and updated hints back. 1277 if width_arg is not None: 1278 hint_program = [width_arg] 1279 else: 1280 hint_program = [] 1281 if h_hint_args: 1282 op_hstem = 'hstemhm' if mm_hint_info.hint_masks else 'hstem' 1283 hint_program.extend(h_hint_args) 1284 hint_program.append(op_hstem) 1285 if v_hint_args: 1286 hint_program.extend(v_hint_args) 1287 # Don't need to append op_vstem, as this is still in hint_program. 1288 program = hint_program + program 1289 1290 # Re-calculate the hint masks. 1291 if is_reference_font: 1292 hhints = self.args_to_hints(h_hint_args) 1293 vhints = self.args_to_hints(v_hint_args) 1294 for hm in mm_hint_info.hint_masks: 1295 hm.maskByte(hhints, vhints) 1296 1297 # Apply fixed hint masks 1298 if mm_hint_info.hint_masks: 1299 hm_pos_list = [i for i, token in enumerate(program) 1300 if token == 'hintmask'] 1301 for i, hm in enumerate(mm_hint_info.hint_masks): 1302 pos = hm_pos_list[i] 1303 program[pos + 1] = hm.mask 1304 1305 # Now fix the control masks. We will weed out a control mask 1306 # if it ends up with fewer than 3 hints. 1307 cntr_masks = mm_hint_info.cntr_masks 1308 if is_reference_font and cntr_masks: 1309 # Update mask bytes, 1310 # and remove control masks with fewer than 3 bits. 1311 mask_byte_list = [cm.mask for cm in cntr_masks] 1312 for cm in cntr_masks: 1313 cm.maskByte(hhints, vhints) 1314 new_cm_list = [cm for cm in cntr_masks if cm.num_bits >= 3] 1315 new_mask_byte_list = [cm.mask for cm in new_cm_list] 1316 if new_mask_byte_list != mask_byte_list: 1317 mm_hint_info.new_cntr_masks = new_cm_list 1318 if mm_hint_info.new_cntr_masks: 1319 # Remove all the old cntrmask ops 1320 num_old_cm = len(cntr_masks) 1321 idx = program.index('cntrmask') 1322 del program[idx:idx + num_old_cm * 2] 1323 cm_progam = [['cntrmask', cm.mask] for cm in 1324 mm_hint_info.new_cntr_masks] 1325 cm_progam = list(itertools.chain(*cm_progam)) 1326 program[idx:idx] = cm_progam 1327 return program 1328 1329 def fix_glyph_hints(self, glyph_name, mm_hint_info, 1330 is_reference_font=None): 1331 # 1. Delete any bad hints. 1332 # 2. If reference font, recalculate the hint mask byte strings 1333 # 3. Replace hint masks. 1334 # 3. Fix cntr masks. 1335 if self.is_vf: 1336 # We get called once, and fix all the charstring programs. 1337 for i, t2_program in enumerate(self.glyph_programs): 1338 self.glyph_programs[i] = self.fix_t2_program_hints( 1339 t2_program, mm_hint_info, is_reference_font=(i == 0)) 1340 else: 1341 # we are called for each font in turn 1342 try: 1343 t2CharString = self.charStrings[glyph_name] 1344 except KeyError: 1345 return # Happens with sparse sources - just skip the glyph. 1346 1347 program = self.fix_t2_program_hints(t2CharString.program, 1348 mm_hint_info, 1349 is_reference_font) 1350 t2CharString.program = program 1351 1352 def get_vf_bez_glyphs(self, glyph_name): 1353 1354 charstring = self.charStrings[glyph_name] 1355 if 'fvar' in self.ttFont: 1356 # have not yet collected VF global data. 1357 self.is_vf = True 1358 fvar = self.ttFont['fvar'] 1359 CFF2 = self.cffTable 1360 CFF2.desubroutinize() 1361 topDict = CFF2.cff.topDictIndex[0] 1362 # We need a new charstring object into which we can save the 1363 # hinted CFF2 program data. Copying an existing charstring is a 1364 # little easier than creating a new one and making sure that all 1365 # properties are set correctly. 1366 self.temp_cs = copy.deepcopy(self.charStrings['.notdef']) 1367 self.vs_data_models = self.get_vs_data_models(topDict, 1368 fvar) 1369 1370 if 'vsindex' in charstring.program: 1371 op_index = charstring.program.index('vsindex') 1372 vsindex = charstring.program[op_index - 1] 1373 else: 1374 vsindex = 0 1375 self.vsindex = vsindex 1376 self.glyph_programs = [] 1377 vs_data_model = self.vs_data_model = self.vs_data_models[vsindex] 1378 1379 bez_list = [] 1380 for vsi in vs_data_model.master_vsi_list: 1381 t2_program = interpolate_cff2_charstring(charstring, glyph_name, 1382 vsi.interpolateFromDeltas, 1383 vsindex) 1384 self.temp_cs.program = t2_program 1385 bezString, _ = convertT2GlyphToBez(self.temp_cs, True, True) 1386 # DBG Adding glyph name is useful only for debugging. 1387 bezString = "% {}\n".format(glyph_name) + bezString 1388 bez_list.append(bezString) 1389 return bez_list 1390 1391 @staticmethod 1392 def get_vs_data_models(topDict, fvar): 1393 otvs = topDict.VarStore.otVarStore 1394 region_list = otvs.VarRegionList.Region 1395 axis_tags = [axis_entry.axisTag for axis_entry in fvar.axes] 1396 vs_data_models = [] 1397 for vsindex, var_data in enumerate(otvs.VarData): 1398 vsi = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, {}) 1399 master_vsi_list = [vsi] 1400 for region_idx in var_data.VarRegionIndex: 1401 region = region_list[region_idx] 1402 loc = {} 1403 for i, axis in enumerate(region.VarRegionAxis): 1404 loc[axis_tags[i]] = axis.PeakCoord 1405 vsi = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, 1406 loc) 1407 master_vsi_list.append(vsi) 1408 vdm = VarDataModel(var_data, vsindex, master_vsi_list) 1409 vs_data_models.append(vdm) 1410 return vs_data_models 1411 1412 def merge_hinted_glyphs(self, name): 1413 new_t2cs = merge_hinted_programs(self.temp_cs, self.glyph_programs, 1414 name, self.vs_data_model) 1415 if self.vsindex: 1416 new_t2cs.program = [self.vsindex, 'vsindex'] + new_t2cs.program 1417 self.charStrings[name] = new_t2cs 1418 1419 1420def interpolate_cff2_charstring(charstring, gname, interpolateFromDeltas, 1421 vsindex): 1422 # Interpolate charstring 1423 # e.g replace blend op args with regular args, 1424 # and discard vsindex op. 1425 new_program = [] 1426 last_i = 0 1427 program = charstring.program 1428 for i, token in enumerate(program): 1429 if token == 'vsindex': 1430 if last_i != 0: 1431 new_program.extend(program[last_i:i - 1]) 1432 last_i = i + 1 1433 elif token == 'blend': 1434 num_regions = charstring.getNumRegions(vsindex) 1435 numMasters = 1 + num_regions 1436 num_args = program[i - 1] 1437 # The program list starting at program[i] is now: 1438 # ..args for following operations 1439 # num_args values from the default font 1440 # num_args tuples, each with numMasters-1 delta values 1441 # num_blend_args 1442 # 'blend' 1443 argi = i - (num_args * numMasters + 1) 1444 if last_i != argi: 1445 new_program.extend(program[last_i:argi]) 1446 end_args = tuplei = argi + num_args 1447 master_args = [] 1448 while argi < end_args: 1449 next_ti = tuplei + num_regions 1450 deltas = program[tuplei:next_ti] 1451 val = interpolateFromDeltas(vsindex, deltas) 1452 master_val = program[argi] 1453 master_val += otRound(val) 1454 master_args.append(master_val) 1455 tuplei = next_ti 1456 argi += 1 1457 new_program.extend(master_args) 1458 last_i = i + 1 1459 if last_i != 0: 1460 new_program.extend(program[last_i:]) 1461 return new_program 1462 1463 1464def merge_hinted_programs(charstring, t2_programs, gname, vs_data_model): 1465 num_masters = vs_data_model.num_masters 1466 var_pen = CFF2CharStringMergePen([], gname, num_masters, 0) 1467 charstring.outlineExtractor = MergeOutlineExtractor 1468 1469 for i, t2_program in enumerate(t2_programs): 1470 var_pen.restart(i) 1471 charstring.program = t2_program 1472 charstring.draw(var_pen) 1473 1474 new_charstring = var_pen.getCharString( 1475 private=charstring.private, 1476 globalSubrs=charstring.globalSubrs, 1477 var_model=vs_data_model, optimize=True) 1478 return new_charstring 1479 1480 1481@_add_method(VarStoreInstancer) 1482def get_scalars(self, vsindex, region_idx): 1483 varData = self._varData 1484 # The index key needs to be the master value index, which includes 1485 # the default font value. VarRegionIndex provides the region indices. 1486 scalars = {0: 1.0} # The default font always has a weight of 1.0 1487 region_index = varData[vsindex].VarRegionIndex 1488 for idx in range(region_idx): # omit the scalar for the region. 1489 scalar = self._getScalar(region_index[idx]) 1490 if scalar: 1491 scalars[idx+1] = scalar 1492 return scalars 1493 1494 1495class VarDataModel(object): 1496 1497 def __init__(self, var_data, vsindex, master_vsi_list): 1498 self.master_vsi_list = master_vsi_list 1499 self.var_data = var_data 1500 self._num_masters = len(master_vsi_list) 1501 self.delta_weights = [{}] # for default font value 1502 for region_idx, vsi in enumerate(master_vsi_list[1:]): 1503 scalars = vsi.get_scalars(vsindex, region_idx) 1504 self.delta_weights.append(scalars) 1505 1506 @property 1507 def num_masters(self): 1508 return self._num_masters 1509 1510 def getDeltas(self, master_values): 1511 assert len(master_values) == len(self.delta_weights) 1512 out = [] 1513 for i, scalars in enumerate(self.delta_weights): 1514 delta = master_values[i] 1515 for j, scalar in scalars.items(): 1516 if scalar: 1517 delta -= out[j] * scalar 1518 out.append(delta) 1519 return out 1520