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