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
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