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