1# -*- coding: utf-8 -*- 2 3"""T2CharString operator specializer and generalizer. 4 5PostScript glyph drawing operations can be expressed in multiple different 6ways. For example, as well as the ``lineto`` operator, there is also a 7``hlineto`` operator which draws a horizontal line, removing the need to 8specify a ``dx`` coordinate, and a ``vlineto`` operator which draws a 9vertical line, removing the need to specify a ``dy`` coordinate. As well 10as decompiling :class:`fontTools.misc.psCharStrings.T2CharString` objects 11into lists of operations, this module allows for conversion between general 12and specific forms of the operation. 13 14""" 15 16from fontTools.cffLib import maxStackLimit 17 18 19def stringToProgram(string): 20 if isinstance(string, str): 21 string = string.split() 22 program = [] 23 for token in string: 24 try: 25 token = int(token) 26 except ValueError: 27 try: 28 token = float(token) 29 except ValueError: 30 pass 31 program.append(token) 32 return program 33 34 35def programToString(program): 36 return " ".join(str(x) for x in program) 37 38 39def programToCommands(program, getNumRegions=None): 40 """Takes a T2CharString program list and returns list of commands. 41 Each command is a two-tuple of commandname,arg-list. The commandname might 42 be empty string if no commandname shall be emitted (used for glyph width, 43 hintmask/cntrmask argument, as well as stray arguments at the end of the 44 program (). 45 'getNumRegions' may be None, or a callable object. It must return the 46 number of regions. 'getNumRegions' takes a single argument, vsindex. If 47 the vsindex argument is None, getNumRegions returns the default number 48 of regions for the charstring, else it returns the numRegions for 49 the vsindex. 50 The Charstring may or may not start with a width value. If the first 51 non-blend operator has an odd number of arguments, then the first argument is 52 a width, and is popped off. This is complicated with blend operators, as 53 there may be more than one before the first hint or moveto operator, and each 54 one reduces several arguments to just one list argument. We have to sum the 55 number of arguments that are not part of the blend arguments, and all the 56 'numBlends' values. We could instead have said that by definition, if there 57 is a blend operator, there is no width value, since CFF2 Charstrings don't 58 have width values. I discussed this with Behdad, and we are allowing for an 59 initial width value in this case because developers may assemble a CFF2 60 charstring from CFF Charstrings, which could have width values. 61 """ 62 63 seenWidthOp = False 64 vsIndex = None 65 lenBlendStack = 0 66 lastBlendIndex = 0 67 commands = [] 68 stack = [] 69 it = iter(program) 70 71 for token in it: 72 if not isinstance(token, str): 73 stack.append(token) 74 continue 75 76 if token == "blend": 77 assert getNumRegions is not None 78 numSourceFonts = 1 + getNumRegions(vsIndex) 79 # replace the blend op args on the stack with a single list 80 # containing all the blend op args. 81 numBlends = stack[-1] 82 numBlendArgs = numBlends * numSourceFonts + 1 83 # replace first blend op by a list of the blend ops. 84 stack[-numBlendArgs:] = [stack[-numBlendArgs:]] 85 lenBlendStack += numBlends + len(stack) - 1 86 lastBlendIndex = len(stack) 87 # if a blend op exists, this is or will be a CFF2 charstring. 88 continue 89 90 elif token == "vsindex": 91 vsIndex = stack[-1] 92 assert type(vsIndex) is int 93 94 elif (not seenWidthOp) and token in { 95 "hstem", 96 "hstemhm", 97 "vstem", 98 "vstemhm", 99 "cntrmask", 100 "hintmask", 101 "hmoveto", 102 "vmoveto", 103 "rmoveto", 104 "endchar", 105 }: 106 seenWidthOp = True 107 parity = token in {"hmoveto", "vmoveto"} 108 if lenBlendStack: 109 # lenBlendStack has the number of args represented by the last blend 110 # arg and all the preceding args. We need to now add the number of 111 # args following the last blend arg. 112 numArgs = lenBlendStack + len(stack[lastBlendIndex:]) 113 else: 114 numArgs = len(stack) 115 if numArgs and (numArgs % 2) ^ parity: 116 width = stack.pop(0) 117 commands.append(("", [width])) 118 119 if token in {"hintmask", "cntrmask"}: 120 if stack: 121 commands.append(("", stack)) 122 commands.append((token, [])) 123 commands.append(("", [next(it)])) 124 else: 125 commands.append((token, stack)) 126 stack = [] 127 if stack: 128 commands.append(("", stack)) 129 return commands 130 131 132def _flattenBlendArgs(args): 133 token_list = [] 134 for arg in args: 135 if isinstance(arg, list): 136 token_list.extend(arg) 137 token_list.append("blend") 138 else: 139 token_list.append(arg) 140 return token_list 141 142 143def commandsToProgram(commands): 144 """Takes a commands list as returned by programToCommands() and converts 145 it back to a T2CharString program list.""" 146 program = [] 147 for op, args in commands: 148 if any(isinstance(arg, list) for arg in args): 149 args = _flattenBlendArgs(args) 150 program.extend(args) 151 if op: 152 program.append(op) 153 return program 154 155 156def _everyN(el, n): 157 """Group the list el into groups of size n""" 158 if len(el) % n != 0: 159 raise ValueError(el) 160 for i in range(0, len(el), n): 161 yield el[i : i + n] 162 163 164class _GeneralizerDecombinerCommandsMap(object): 165 @staticmethod 166 def rmoveto(args): 167 if len(args) != 2: 168 raise ValueError(args) 169 yield ("rmoveto", args) 170 171 @staticmethod 172 def hmoveto(args): 173 if len(args) != 1: 174 raise ValueError(args) 175 yield ("rmoveto", [args[0], 0]) 176 177 @staticmethod 178 def vmoveto(args): 179 if len(args) != 1: 180 raise ValueError(args) 181 yield ("rmoveto", [0, args[0]]) 182 183 @staticmethod 184 def rlineto(args): 185 if not args: 186 raise ValueError(args) 187 for args in _everyN(args, 2): 188 yield ("rlineto", args) 189 190 @staticmethod 191 def hlineto(args): 192 if not args: 193 raise ValueError(args) 194 it = iter(args) 195 try: 196 while True: 197 yield ("rlineto", [next(it), 0]) 198 yield ("rlineto", [0, next(it)]) 199 except StopIteration: 200 pass 201 202 @staticmethod 203 def vlineto(args): 204 if not args: 205 raise ValueError(args) 206 it = iter(args) 207 try: 208 while True: 209 yield ("rlineto", [0, next(it)]) 210 yield ("rlineto", [next(it), 0]) 211 except StopIteration: 212 pass 213 214 @staticmethod 215 def rrcurveto(args): 216 if not args: 217 raise ValueError(args) 218 for args in _everyN(args, 6): 219 yield ("rrcurveto", args) 220 221 @staticmethod 222 def hhcurveto(args): 223 if len(args) < 4 or len(args) % 4 > 1: 224 raise ValueError(args) 225 if len(args) % 2 == 1: 226 yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0]) 227 args = args[5:] 228 for args in _everyN(args, 4): 229 yield ("rrcurveto", [args[0], 0, args[1], args[2], args[3], 0]) 230 231 @staticmethod 232 def vvcurveto(args): 233 if len(args) < 4 or len(args) % 4 > 1: 234 raise ValueError(args) 235 if len(args) % 2 == 1: 236 yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]]) 237 args = args[5:] 238 for args in _everyN(args, 4): 239 yield ("rrcurveto", [0, args[0], args[1], args[2], 0, args[3]]) 240 241 @staticmethod 242 def hvcurveto(args): 243 if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}: 244 raise ValueError(args) 245 last_args = None 246 if len(args) % 2 == 1: 247 lastStraight = len(args) % 8 == 5 248 args, last_args = args[:-5], args[-5:] 249 it = _everyN(args, 4) 250 try: 251 while True: 252 args = next(it) 253 yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]]) 254 args = next(it) 255 yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0]) 256 except StopIteration: 257 pass 258 if last_args: 259 args = last_args 260 if lastStraight: 261 yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]]) 262 else: 263 yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]]) 264 265 @staticmethod 266 def vhcurveto(args): 267 if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}: 268 raise ValueError(args) 269 last_args = None 270 if len(args) % 2 == 1: 271 lastStraight = len(args) % 8 == 5 272 args, last_args = args[:-5], args[-5:] 273 it = _everyN(args, 4) 274 try: 275 while True: 276 args = next(it) 277 yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0]) 278 args = next(it) 279 yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]]) 280 except StopIteration: 281 pass 282 if last_args: 283 args = last_args 284 if lastStraight: 285 yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]]) 286 else: 287 yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]]) 288 289 @staticmethod 290 def rcurveline(args): 291 if len(args) < 8 or len(args) % 6 != 2: 292 raise ValueError(args) 293 args, last_args = args[:-2], args[-2:] 294 for args in _everyN(args, 6): 295 yield ("rrcurveto", args) 296 yield ("rlineto", last_args) 297 298 @staticmethod 299 def rlinecurve(args): 300 if len(args) < 8 or len(args) % 2 != 0: 301 raise ValueError(args) 302 args, last_args = args[:-6], args[-6:] 303 for args in _everyN(args, 2): 304 yield ("rlineto", args) 305 yield ("rrcurveto", last_args) 306 307 308def _convertBlendOpToArgs(blendList): 309 # args is list of blend op args. Since we are supporting 310 # recursive blend op calls, some of these args may also 311 # be a list of blend op args, and need to be converted before 312 # we convert the current list. 313 if any([isinstance(arg, list) for arg in blendList]): 314 args = [ 315 i 316 for e in blendList 317 for i in (_convertBlendOpToArgs(e) if isinstance(e, list) else [e]) 318 ] 319 else: 320 args = blendList 321 322 # We now know that blendList contains a blend op argument list, even if 323 # some of the args are lists that each contain a blend op argument list. 324 # Convert from: 325 # [default font arg sequence x0,...,xn] + [delta tuple for x0] + ... + [delta tuple for xn] 326 # to: 327 # [ [x0] + [delta tuple for x0], 328 # ..., 329 # [xn] + [delta tuple for xn] ] 330 numBlends = args[-1] 331 # Can't use args.pop() when the args are being used in a nested list 332 # comprehension. See calling context 333 args = args[:-1] 334 335 numRegions = len(args) // numBlends - 1 336 if not (numBlends * (numRegions + 1) == len(args)): 337 raise ValueError(blendList) 338 339 defaultArgs = [[arg] for arg in args[:numBlends]] 340 deltaArgs = args[numBlends:] 341 numDeltaValues = len(deltaArgs) 342 deltaList = [ 343 deltaArgs[i : i + numRegions] for i in range(0, numDeltaValues, numRegions) 344 ] 345 blend_args = [a + b + [1] for a, b in zip(defaultArgs, deltaList)] 346 return blend_args 347 348 349def generalizeCommands(commands, ignoreErrors=False): 350 result = [] 351 mapping = _GeneralizerDecombinerCommandsMap 352 for op, args in commands: 353 # First, generalize any blend args in the arg list. 354 if any([isinstance(arg, list) for arg in args]): 355 try: 356 args = [ 357 n 358 for arg in args 359 for n in ( 360 _convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg] 361 ) 362 ] 363 except ValueError: 364 if ignoreErrors: 365 # Store op as data, such that consumers of commands do not have to 366 # deal with incorrect number of arguments. 367 result.append(("", args)) 368 result.append(("", [op])) 369 else: 370 raise 371 372 func = getattr(mapping, op, None) 373 if not func: 374 result.append((op, args)) 375 continue 376 try: 377 for command in func(args): 378 result.append(command) 379 except ValueError: 380 if ignoreErrors: 381 # Store op as data, such that consumers of commands do not have to 382 # deal with incorrect number of arguments. 383 result.append(("", args)) 384 result.append(("", [op])) 385 else: 386 raise 387 return result 388 389 390def generalizeProgram(program, getNumRegions=None, **kwargs): 391 return commandsToProgram( 392 generalizeCommands(programToCommands(program, getNumRegions), **kwargs) 393 ) 394 395 396def _categorizeVector(v): 397 """ 398 Takes X,Y vector v and returns one of r, h, v, or 0 depending on which 399 of X and/or Y are zero, plus tuple of nonzero ones. If both are zero, 400 it returns a single zero still. 401 402 >>> _categorizeVector((0,0)) 403 ('0', (0,)) 404 >>> _categorizeVector((1,0)) 405 ('h', (1,)) 406 >>> _categorizeVector((0,2)) 407 ('v', (2,)) 408 >>> _categorizeVector((1,2)) 409 ('r', (1, 2)) 410 """ 411 if not v[0]: 412 if not v[1]: 413 return "0", v[:1] 414 else: 415 return "v", v[1:] 416 else: 417 if not v[1]: 418 return "h", v[:1] 419 else: 420 return "r", v 421 422 423def _mergeCategories(a, b): 424 if a == "0": 425 return b 426 if b == "0": 427 return a 428 if a == b: 429 return a 430 return None 431 432 433def _negateCategory(a): 434 if a == "h": 435 return "v" 436 if a == "v": 437 return "h" 438 assert a in "0r" 439 return a 440 441 442def _convertToBlendCmds(args): 443 # return a list of blend commands, and 444 # the remaining non-blended args, if any. 445 num_args = len(args) 446 stack_use = 0 447 new_args = [] 448 i = 0 449 while i < num_args: 450 arg = args[i] 451 if not isinstance(arg, list): 452 new_args.append(arg) 453 i += 1 454 stack_use += 1 455 else: 456 prev_stack_use = stack_use 457 # The arg is a tuple of blend values. 458 # These are each (master 0,delta 1..delta n, 1) 459 # Combine as many successive tuples as we can, 460 # up to the max stack limit. 461 num_sources = len(arg) - 1 462 blendlist = [arg] 463 i += 1 464 stack_use += 1 + num_sources # 1 for the num_blends arg 465 while (i < num_args) and isinstance(args[i], list): 466 blendlist.append(args[i]) 467 i += 1 468 stack_use += num_sources 469 if stack_use + num_sources > maxStackLimit: 470 # if we are here, max stack is the CFF2 max stack. 471 # I use the CFF2 max stack limit here rather than 472 # the 'maxstack' chosen by the client, as the default 473 # maxstack may have been used unintentionally. For all 474 # the other operators, this just produces a little less 475 # optimization, but here it puts a hard (and low) limit 476 # on the number of source fonts that can be used. 477 break 478 # blendList now contains as many single blend tuples as can be 479 # combined without exceeding the CFF2 stack limit. 480 num_blends = len(blendlist) 481 # append the 'num_blends' default font values 482 blend_args = [] 483 for arg in blendlist: 484 blend_args.append(arg[0]) 485 for arg in blendlist: 486 assert arg[-1] == 1 487 blend_args.extend(arg[1:-1]) 488 blend_args.append(num_blends) 489 new_args.append(blend_args) 490 stack_use = prev_stack_use + num_blends 491 492 return new_args 493 494 495def _addArgs(a, b): 496 if isinstance(b, list): 497 if isinstance(a, list): 498 if len(a) != len(b) or a[-1] != b[-1]: 499 raise ValueError() 500 return [_addArgs(va, vb) for va, vb in zip(a[:-1], b[:-1])] + [a[-1]] 501 else: 502 a, b = b, a 503 if isinstance(a, list): 504 assert a[-1] == 1 505 return [_addArgs(a[0], b)] + a[1:] 506 return a + b 507 508 509def specializeCommands( 510 commands, 511 ignoreErrors=False, 512 generalizeFirst=True, 513 preserveTopology=False, 514 maxstack=48, 515): 516 # We perform several rounds of optimizations. They are carefully ordered and are: 517 # 518 # 0. Generalize commands. 519 # This ensures that they are in our expected simple form, with each line/curve only 520 # having arguments for one segment, and using the generic form (rlineto/rrcurveto). 521 # If caller is sure the input is in this form, they can turn off generalization to 522 # save time. 523 # 524 # 1. Combine successive rmoveto operations. 525 # 526 # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants. 527 # We specialize into some, made-up, variants as well, which simplifies following 528 # passes. 529 # 530 # 3. Merge or delete redundant operations, to the extent requested. 531 # OpenType spec declares point numbers in CFF undefined. As such, we happily 532 # change topology. If client relies on point numbers (in GPOS anchors, or for 533 # hinting purposes(what?)) they can turn this off. 534 # 535 # 4. Peephole optimization to revert back some of the h/v variants back into their 536 # original "relative" operator (rline/rrcurveto) if that saves a byte. 537 # 538 # 5. Combine adjacent operators when possible, minding not to go over max stack size. 539 # 540 # 6. Resolve any remaining made-up operators into real operators. 541 # 542 # I have convinced myself that this produces optimal bytecode (except for, possibly 543 # one byte each time maxstack size prohibits combining.) YMMV, but you'd be wrong. :-) 544 # A dynamic-programming approach can do the same but would be significantly slower. 545 # 546 # 7. For any args which are blend lists, convert them to a blend command. 547 548 # 0. Generalize commands. 549 if generalizeFirst: 550 commands = generalizeCommands(commands, ignoreErrors=ignoreErrors) 551 else: 552 commands = list(commands) # Make copy since we modify in-place later. 553 554 # 1. Combine successive rmoveto operations. 555 for i in range(len(commands) - 1, 0, -1): 556 if "rmoveto" == commands[i][0] == commands[i - 1][0]: 557 v1, v2 = commands[i - 1][1], commands[i][1] 558 commands[i - 1] = ("rmoveto", [v1[0] + v2[0], v1[1] + v2[1]]) 559 del commands[i] 560 561 # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants. 562 # 563 # We, in fact, specialize into more, made-up, variants that special-case when both 564 # X and Y components are zero. This simplifies the following optimization passes. 565 # This case is rare, but OCD does not let me skip it. 566 # 567 # After this round, we will have four variants that use the following mnemonics: 568 # 569 # - 'r' for relative, ie. non-zero X and non-zero Y, 570 # - 'h' for horizontal, ie. zero X and non-zero Y, 571 # - 'v' for vertical, ie. non-zero X and zero Y, 572 # - '0' for zeros, ie. zero X and zero Y. 573 # 574 # The '0' pseudo-operators are not part of the spec, but help simplify the following 575 # optimization rounds. We resolve them at the end. So, after this, we will have four 576 # moveto and four lineto variants: 577 # 578 # - 0moveto, 0lineto 579 # - hmoveto, hlineto 580 # - vmoveto, vlineto 581 # - rmoveto, rlineto 582 # 583 # and sixteen curveto variants. For example, a '0hcurveto' operator means a curve 584 # dx0,dy0,dx1,dy1,dx2,dy2,dx3,dy3 where dx0, dx1, and dy3 are zero but not dx3. 585 # An 'rvcurveto' means dx3 is zero but not dx0,dy0,dy3. 586 # 587 # There are nine different variants of curves without the '0'. Those nine map exactly 588 # to the existing curve variants in the spec: rrcurveto, and the four variants hhcurveto, 589 # vvcurveto, hvcurveto, and vhcurveto each cover two cases, one with an odd number of 590 # arguments and one without. Eg. an hhcurveto with an extra argument (odd number of 591 # arguments) is in fact an rhcurveto. The operators in the spec are designed such that 592 # all four of rhcurveto, rvcurveto, hrcurveto, and vrcurveto are encodable for one curve. 593 # 594 # Of the curve types with '0', the 00curveto is equivalent to a lineto variant. The rest 595 # of the curve types with a 0 need to be encoded as a h or v variant. Ie. a '0' can be 596 # thought of a "don't care" and can be used as either an 'h' or a 'v'. As such, we always 597 # encode a number 0 as argument when we use a '0' variant. Later on, we can just substitute 598 # the '0' with either 'h' or 'v' and it works. 599 # 600 # When we get to curve splines however, things become more complicated... XXX finish this. 601 # There's one more complexity with splines. If one side of the spline is not horizontal or 602 # vertical (or zero), ie. if it's 'r', then it limits which spline types we can encode. 603 # Only hhcurveto and vvcurveto operators can encode a spline starting with 'r', and 604 # only hvcurveto and vhcurveto operators can encode a spline ending with 'r'. 605 # This limits our merge opportunities later. 606 # 607 for i in range(len(commands)): 608 op, args = commands[i] 609 610 if op in {"rmoveto", "rlineto"}: 611 c, args = _categorizeVector(args) 612 commands[i] = c + op[1:], args 613 continue 614 615 if op == "rrcurveto": 616 c1, args1 = _categorizeVector(args[:2]) 617 c2, args2 = _categorizeVector(args[-2:]) 618 commands[i] = c1 + c2 + "curveto", args1 + args[2:4] + args2 619 continue 620 621 # 3. Merge or delete redundant operations, to the extent requested. 622 # 623 # TODO 624 # A 0moveto that comes before all other path operations can be removed. 625 # though I find conflicting evidence for this. 626 # 627 # TODO 628 # "If hstem and vstem hints are both declared at the beginning of a 629 # CharString, and this sequence is followed directly by the hintmask or 630 # cntrmask operators, then the vstem hint operator (or, if applicable, 631 # the vstemhm operator) need not be included." 632 # 633 # "The sequence and form of a CFF2 CharString program may be represented as: 634 # {hs* vs* cm* hm* mt subpath}? {mt subpath}*" 635 # 636 # https://www.microsoft.com/typography/otspec/cff2charstr.htm#section3.1 637 # 638 # For Type2 CharStrings the sequence is: 639 # w? {hs* vs* cm* hm* mt subpath}? {mt subpath}* endchar" 640 641 # Some other redundancies change topology (point numbers). 642 if not preserveTopology: 643 for i in range(len(commands) - 1, -1, -1): 644 op, args = commands[i] 645 646 # A 00curveto is demoted to a (specialized) lineto. 647 if op == "00curveto": 648 assert len(args) == 4 649 c, args = _categorizeVector(args[1:3]) 650 op = c + "lineto" 651 commands[i] = op, args 652 # and then... 653 654 # A 0lineto can be deleted. 655 if op == "0lineto": 656 del commands[i] 657 continue 658 659 # Merge adjacent hlineto's and vlineto's. 660 # In CFF2 charstrings from variable fonts, each 661 # arg item may be a list of blendable values, one from 662 # each source font. 663 if i and op in {"hlineto", "vlineto"} and (op == commands[i - 1][0]): 664 _, other_args = commands[i - 1] 665 assert len(args) == 1 and len(other_args) == 1 666 try: 667 new_args = [_addArgs(args[0], other_args[0])] 668 except ValueError: 669 continue 670 commands[i - 1] = (op, new_args) 671 del commands[i] 672 continue 673 674 # 4. Peephole optimization to revert back some of the h/v variants back into their 675 # original "relative" operator (rline/rrcurveto) if that saves a byte. 676 for i in range(1, len(commands) - 1): 677 op, args = commands[i] 678 prv, nxt = commands[i - 1][0], commands[i + 1][0] 679 680 if op in {"0lineto", "hlineto", "vlineto"} and prv == nxt == "rlineto": 681 assert len(args) == 1 682 args = [0, args[0]] if op[0] == "v" else [args[0], 0] 683 commands[i] = ("rlineto", args) 684 continue 685 686 if op[2:] == "curveto" and len(args) == 5 and prv == nxt == "rrcurveto": 687 assert (op[0] == "r") ^ (op[1] == "r") 688 if op[0] == "v": 689 pos = 0 690 elif op[0] != "r": 691 pos = 1 692 elif op[1] == "v": 693 pos = 4 694 else: 695 pos = 5 696 # Insert, while maintaining the type of args (can be tuple or list). 697 args = args[:pos] + type(args)((0,)) + args[pos:] 698 commands[i] = ("rrcurveto", args) 699 continue 700 701 # 5. Combine adjacent operators when possible, minding not to go over max stack size. 702 for i in range(len(commands) - 1, 0, -1): 703 op1, args1 = commands[i - 1] 704 op2, args2 = commands[i] 705 new_op = None 706 707 # Merge logic... 708 if {op1, op2} <= {"rlineto", "rrcurveto"}: 709 if op1 == op2: 710 new_op = op1 711 else: 712 if op2 == "rrcurveto" and len(args2) == 6: 713 new_op = "rlinecurve" 714 elif len(args2) == 2: 715 new_op = "rcurveline" 716 717 elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}: 718 new_op = op2 719 720 elif {op1, op2} == {"vlineto", "hlineto"}: 721 new_op = op1 722 723 elif "curveto" == op1[2:] == op2[2:]: 724 d0, d1 = op1[:2] 725 d2, d3 = op2[:2] 726 727 if d1 == "r" or d2 == "r" or d0 == d3 == "r": 728 continue 729 730 d = _mergeCategories(d1, d2) 731 if d is None: 732 continue 733 if d0 == "r": 734 d = _mergeCategories(d, d3) 735 if d is None: 736 continue 737 new_op = "r" + d + "curveto" 738 elif d3 == "r": 739 d0 = _mergeCategories(d0, _negateCategory(d)) 740 if d0 is None: 741 continue 742 new_op = d0 + "r" + "curveto" 743 else: 744 d0 = _mergeCategories(d0, d3) 745 if d0 is None: 746 continue 747 new_op = d0 + d + "curveto" 748 749 # Make sure the stack depth does not exceed (maxstack - 1), so 750 # that subroutinizer can insert subroutine calls at any point. 751 if new_op and len(args1) + len(args2) < maxstack: 752 commands[i - 1] = (new_op, args1 + args2) 753 del commands[i] 754 755 # 6. Resolve any remaining made-up operators into real operators. 756 for i in range(len(commands)): 757 op, args = commands[i] 758 759 if op in {"0moveto", "0lineto"}: 760 commands[i] = "h" + op[1:], args 761 continue 762 763 if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}: 764 op0, op1 = op[:2] 765 if (op0 == "r") ^ (op1 == "r"): 766 assert len(args) % 2 == 1 767 if op0 == "0": 768 op0 = "h" 769 if op1 == "0": 770 op1 = "h" 771 if op0 == "r": 772 op0 = op1 773 if op1 == "r": 774 op1 = _negateCategory(op0) 775 assert {op0, op1} <= {"h", "v"}, (op0, op1) 776 777 if len(args) % 2: 778 if op0 != op1: # vhcurveto / hvcurveto 779 if (op0 == "h") ^ (len(args) % 8 == 1): 780 # Swap last two args order 781 args = args[:-2] + args[-1:] + args[-2:-1] 782 else: # hhcurveto / vvcurveto 783 if op0 == "h": # hhcurveto 784 # Swap first two args order 785 args = args[1:2] + args[:1] + args[2:] 786 787 commands[i] = op0 + op1 + "curveto", args 788 continue 789 790 # 7. For any series of args which are blend lists, convert the series to a single blend arg. 791 for i in range(len(commands)): 792 op, args = commands[i] 793 if any(isinstance(arg, list) for arg in args): 794 commands[i] = op, _convertToBlendCmds(args) 795 796 return commands 797 798 799def specializeProgram(program, getNumRegions=None, **kwargs): 800 return commandsToProgram( 801 specializeCommands(programToCommands(program, getNumRegions), **kwargs) 802 ) 803 804 805if __name__ == "__main__": 806 import sys 807 808 if len(sys.argv) == 1: 809 import doctest 810 811 sys.exit(doctest.testmod().failed) 812 813 import argparse 814 815 parser = argparse.ArgumentParser( 816 "fonttools cffLib.specialer", 817 description="CFF CharString generalizer/specializer", 818 ) 819 parser.add_argument("program", metavar="command", nargs="*", help="Commands.") 820 parser.add_argument( 821 "--num-regions", 822 metavar="NumRegions", 823 nargs="*", 824 default=None, 825 help="Number of variable-font regions for blend opertaions.", 826 ) 827 828 options = parser.parse_args(sys.argv[1:]) 829 830 getNumRegions = ( 831 None 832 if options.num_regions is None 833 else lambda vsIndex: int(options.num_regions[0 if vsIndex is None else vsIndex]) 834 ) 835 836 program = stringToProgram(options.program) 837 print("Program:") 838 print(programToString(program)) 839 commands = programToCommands(program, getNumRegions) 840 print("Commands:") 841 print(commands) 842 program2 = commandsToProgram(commands) 843 print("Program from commands:") 844 print(programToString(program2)) 845 assert program == program2 846 print("Generalized program:") 847 print(programToString(generalizeProgram(program, getNumRegions))) 848 print("Specialized program:") 849 print(programToString(specializeProgram(program, getNumRegions))) 850