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