xref: /aosp_15_r20/external/fonttools/Lib/fontTools/cffLib/specializer.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1 # -*- coding: utf-8 -*-
2 
3 """T2CharString operator specializer and generalizer.
4 
5 PostScript glyph drawing operations can be expressed in multiple different
6 ways. For example, as well as the ``lineto`` operator, there is also a
7 ``hlineto`` operator which draws a horizontal line, removing the need to
8 specify a ``dx`` coordinate, and a ``vlineto`` operator which draws a
9 vertical line, removing the need to specify a ``dy`` coordinate. As well
10 as decompiling :class:`fontTools.misc.psCharStrings.T2CharString` objects
11 into lists of operations, this module allows for conversion between general
12 and specific forms of the operation.
13 
14 """
15 
16 from fontTools.cffLib import maxStackLimit
17 
18 
19 def 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 
35 def programToString(program):
36     return " ".join(str(x) for x in program)
37 
38 
39 def 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 
132 def _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 
143 def 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 
156 def _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 
164 class _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 
308 def _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 
349 def 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 
390 def generalizeProgram(program, getNumRegions=None, **kwargs):
391     return commandsToProgram(
392         generalizeCommands(programToCommands(program, getNumRegions), **kwargs)
393     )
394 
395 
396 def _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 
423 def _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 
433 def _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 
442 def _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 
495 def _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 
509 def 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 
799 def specializeProgram(program, getNumRegions=None, **kwargs):
800     return commandsToProgram(
801         specializeCommands(programToCommands(program, getNumRegions), **kwargs)
802     )
803 
804 
805 if __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