123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849 |
- # -*- coding: utf-8 -*-
- """T2CharString operator specializer and generalizer.
- PostScript glyph drawing operations can be expressed in multiple different
- ways. For example, as well as the ``lineto`` operator, there is also a
- ``hlineto`` operator which draws a horizontal line, removing the need to
- specify a ``dx`` coordinate, and a ``vlineto`` operator which draws a
- vertical line, removing the need to specify a ``dy`` coordinate. As well
- as decompiling :class:`fontTools.misc.psCharStrings.T2CharString` objects
- into lists of operations, this module allows for conversion between general
- and specific forms of the operation.
- """
- from fontTools.cffLib import maxStackLimit
- def stringToProgram(string):
- if isinstance(string, str):
- string = string.split()
- program = []
- for token in string:
- try:
- token = int(token)
- except ValueError:
- try:
- token = float(token)
- except ValueError:
- pass
- program.append(token)
- return program
- def programToString(program):
- return " ".join(str(x) for x in program)
- def programToCommands(program, getNumRegions=None):
- """Takes a T2CharString program list and returns list of commands.
- Each command is a two-tuple of commandname,arg-list. The commandname might
- be empty string if no commandname shall be emitted (used for glyph width,
- hintmask/cntrmask argument, as well as stray arguments at the end of the
- program (🤷).
- 'getNumRegions' may be None, or a callable object. It must return the
- number of regions. 'getNumRegions' takes a single argument, vsindex. If
- the vsindex argument is None, getNumRegions returns the default number
- of regions for the charstring, else it returns the numRegions for
- the vsindex.
- The Charstring may or may not start with a width value. If the first
- non-blend operator has an odd number of arguments, then the first argument is
- a width, and is popped off. This is complicated with blend operators, as
- there may be more than one before the first hint or moveto operator, and each
- one reduces several arguments to just one list argument. We have to sum the
- number of arguments that are not part of the blend arguments, and all the
- 'numBlends' values. We could instead have said that by definition, if there
- is a blend operator, there is no width value, since CFF2 Charstrings don't
- have width values. I discussed this with Behdad, and we are allowing for an
- initial width value in this case because developers may assemble a CFF2
- charstring from CFF Charstrings, which could have width values.
- """
- seenWidthOp = False
- vsIndex = None
- lenBlendStack = 0
- lastBlendIndex = 0
- commands = []
- stack = []
- it = iter(program)
- for token in it:
- if not isinstance(token, str):
- stack.append(token)
- continue
- if token == "blend":
- assert getNumRegions is not None
- numSourceFonts = 1 + getNumRegions(vsIndex)
- # replace the blend op args on the stack with a single list
- # containing all the blend op args.
- numBlends = stack[-1]
- numBlendArgs = numBlends * numSourceFonts + 1
- # replace first blend op by a list of the blend ops.
- stack[-numBlendArgs:] = [stack[-numBlendArgs:]]
- lenBlendStack += numBlends + len(stack) - 1
- lastBlendIndex = len(stack)
- # if a blend op exists, this is or will be a CFF2 charstring.
- continue
- elif token == "vsindex":
- vsIndex = stack[-1]
- assert type(vsIndex) is int
- elif (not seenWidthOp) and token in {
- "hstem",
- "hstemhm",
- "vstem",
- "vstemhm",
- "cntrmask",
- "hintmask",
- "hmoveto",
- "vmoveto",
- "rmoveto",
- "endchar",
- }:
- seenWidthOp = True
- parity = token in {"hmoveto", "vmoveto"}
- if lenBlendStack:
- # lenBlendStack has the number of args represented by the last blend
- # arg and all the preceding args. We need to now add the number of
- # args following the last blend arg.
- numArgs = lenBlendStack + len(stack[lastBlendIndex:])
- else:
- numArgs = len(stack)
- if numArgs and (numArgs % 2) ^ parity:
- width = stack.pop(0)
- commands.append(("", [width]))
- if token in {"hintmask", "cntrmask"}:
- if stack:
- commands.append(("", stack))
- commands.append((token, []))
- commands.append(("", [next(it)]))
- else:
- commands.append((token, stack))
- stack = []
- if stack:
- commands.append(("", stack))
- return commands
- def _flattenBlendArgs(args):
- token_list = []
- for arg in args:
- if isinstance(arg, list):
- token_list.extend(arg)
- token_list.append("blend")
- else:
- token_list.append(arg)
- return token_list
- def commandsToProgram(commands):
- """Takes a commands list as returned by programToCommands() and converts
- it back to a T2CharString program list."""
- program = []
- for op, args in commands:
- if any(isinstance(arg, list) for arg in args):
- args = _flattenBlendArgs(args)
- program.extend(args)
- if op:
- program.append(op)
- return program
- def _everyN(el, n):
- """Group the list el into groups of size n"""
- if len(el) % n != 0:
- raise ValueError(el)
- for i in range(0, len(el), n):
- yield el[i : i + n]
- class _GeneralizerDecombinerCommandsMap(object):
- @staticmethod
- def rmoveto(args):
- if len(args) != 2:
- raise ValueError(args)
- yield ("rmoveto", args)
- @staticmethod
- def hmoveto(args):
- if len(args) != 1:
- raise ValueError(args)
- yield ("rmoveto", [args[0], 0])
- @staticmethod
- def vmoveto(args):
- if len(args) != 1:
- raise ValueError(args)
- yield ("rmoveto", [0, args[0]])
- @staticmethod
- def rlineto(args):
- if not args:
- raise ValueError(args)
- for args in _everyN(args, 2):
- yield ("rlineto", args)
- @staticmethod
- def hlineto(args):
- if not args:
- raise ValueError(args)
- it = iter(args)
- try:
- while True:
- yield ("rlineto", [next(it), 0])
- yield ("rlineto", [0, next(it)])
- except StopIteration:
- pass
- @staticmethod
- def vlineto(args):
- if not args:
- raise ValueError(args)
- it = iter(args)
- try:
- while True:
- yield ("rlineto", [0, next(it)])
- yield ("rlineto", [next(it), 0])
- except StopIteration:
- pass
- @staticmethod
- def rrcurveto(args):
- if not args:
- raise ValueError(args)
- for args in _everyN(args, 6):
- yield ("rrcurveto", args)
- @staticmethod
- def hhcurveto(args):
- if len(args) < 4 or len(args) % 4 > 1:
- raise ValueError(args)
- if len(args) % 2 == 1:
- yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0])
- args = args[5:]
- for args in _everyN(args, 4):
- yield ("rrcurveto", [args[0], 0, args[1], args[2], args[3], 0])
- @staticmethod
- def vvcurveto(args):
- if len(args) < 4 or len(args) % 4 > 1:
- raise ValueError(args)
- if len(args) % 2 == 1:
- yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]])
- args = args[5:]
- for args in _everyN(args, 4):
- yield ("rrcurveto", [0, args[0], args[1], args[2], 0, args[3]])
- @staticmethod
- def hvcurveto(args):
- if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}:
- raise ValueError(args)
- last_args = None
- if len(args) % 2 == 1:
- lastStraight = len(args) % 8 == 5
- args, last_args = args[:-5], args[-5:]
- it = _everyN(args, 4)
- try:
- while True:
- args = next(it)
- yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]])
- args = next(it)
- yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0])
- except StopIteration:
- pass
- if last_args:
- args = last_args
- if lastStraight:
- yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]])
- else:
- yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]])
- @staticmethod
- def vhcurveto(args):
- if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}:
- raise ValueError(args)
- last_args = None
- if len(args) % 2 == 1:
- lastStraight = len(args) % 8 == 5
- args, last_args = args[:-5], args[-5:]
- it = _everyN(args, 4)
- try:
- while True:
- args = next(it)
- yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0])
- args = next(it)
- yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]])
- except StopIteration:
- pass
- if last_args:
- args = last_args
- if lastStraight:
- yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]])
- else:
- yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]])
- @staticmethod
- def rcurveline(args):
- if len(args) < 8 or len(args) % 6 != 2:
- raise ValueError(args)
- args, last_args = args[:-2], args[-2:]
- for args in _everyN(args, 6):
- yield ("rrcurveto", args)
- yield ("rlineto", last_args)
- @staticmethod
- def rlinecurve(args):
- if len(args) < 8 or len(args) % 2 != 0:
- raise ValueError(args)
- args, last_args = args[:-6], args[-6:]
- for args in _everyN(args, 2):
- yield ("rlineto", args)
- yield ("rrcurveto", last_args)
- def _convertBlendOpToArgs(blendList):
- # args is list of blend op args. Since we are supporting
- # recursive blend op calls, some of these args may also
- # be a list of blend op args, and need to be converted before
- # we convert the current list.
- if any([isinstance(arg, list) for arg in blendList]):
- args = [
- i
- for e in blendList
- for i in (_convertBlendOpToArgs(e) if isinstance(e, list) else [e])
- ]
- else:
- args = blendList
- # We now know that blendList contains a blend op argument list, even if
- # some of the args are lists that each contain a blend op argument list.
- # Convert from:
- # [default font arg sequence x0,...,xn] + [delta tuple for x0] + ... + [delta tuple for xn]
- # to:
- # [ [x0] + [delta tuple for x0],
- # ...,
- # [xn] + [delta tuple for xn] ]
- numBlends = args[-1]
- # Can't use args.pop() when the args are being used in a nested list
- # comprehension. See calling context
- args = args[:-1]
- numRegions = len(args) // numBlends - 1
- if not (numBlends * (numRegions + 1) == len(args)):
- raise ValueError(blendList)
- defaultArgs = [[arg] for arg in args[:numBlends]]
- deltaArgs = args[numBlends:]
- numDeltaValues = len(deltaArgs)
- deltaList = [
- deltaArgs[i : i + numRegions] for i in range(0, numDeltaValues, numRegions)
- ]
- blend_args = [a + b + [1] for a, b in zip(defaultArgs, deltaList)]
- return blend_args
- def generalizeCommands(commands, ignoreErrors=False):
- result = []
- mapping = _GeneralizerDecombinerCommandsMap
- for op, args in commands:
- # First, generalize any blend args in the arg list.
- if any([isinstance(arg, list) for arg in args]):
- try:
- args = [
- n
- for arg in args
- for n in (
- _convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg]
- )
- ]
- except ValueError:
- if ignoreErrors:
- # Store op as data, such that consumers of commands do not have to
- # deal with incorrect number of arguments.
- result.append(("", args))
- result.append(("", [op]))
- else:
- raise
- func = getattr(mapping, op, None)
- if not func:
- result.append((op, args))
- continue
- try:
- for command in func(args):
- result.append(command)
- except ValueError:
- if ignoreErrors:
- # Store op as data, such that consumers of commands do not have to
- # deal with incorrect number of arguments.
- result.append(("", args))
- result.append(("", [op]))
- else:
- raise
- return result
- def generalizeProgram(program, getNumRegions=None, **kwargs):
- return commandsToProgram(
- generalizeCommands(programToCommands(program, getNumRegions), **kwargs)
- )
- def _categorizeVector(v):
- """
- Takes X,Y vector v and returns one of r, h, v, or 0 depending on which
- of X and/or Y are zero, plus tuple of nonzero ones. If both are zero,
- it returns a single zero still.
- >>> _categorizeVector((0,0))
- ('0', (0,))
- >>> _categorizeVector((1,0))
- ('h', (1,))
- >>> _categorizeVector((0,2))
- ('v', (2,))
- >>> _categorizeVector((1,2))
- ('r', (1, 2))
- """
- if not v[0]:
- if not v[1]:
- return "0", v[:1]
- else:
- return "v", v[1:]
- else:
- if not v[1]:
- return "h", v[:1]
- else:
- return "r", v
- def _mergeCategories(a, b):
- if a == "0":
- return b
- if b == "0":
- return a
- if a == b:
- return a
- return None
- def _negateCategory(a):
- if a == "h":
- return "v"
- if a == "v":
- return "h"
- assert a in "0r"
- return a
- def _convertToBlendCmds(args):
- # return a list of blend commands, and
- # the remaining non-blended args, if any.
- num_args = len(args)
- stack_use = 0
- new_args = []
- i = 0
- while i < num_args:
- arg = args[i]
- if not isinstance(arg, list):
- new_args.append(arg)
- i += 1
- stack_use += 1
- else:
- prev_stack_use = stack_use
- # The arg is a tuple of blend values.
- # These are each (master 0,delta 1..delta n, 1)
- # Combine as many successive tuples as we can,
- # up to the max stack limit.
- num_sources = len(arg) - 1
- blendlist = [arg]
- i += 1
- stack_use += 1 + num_sources # 1 for the num_blends arg
- while (i < num_args) and isinstance(args[i], list):
- blendlist.append(args[i])
- i += 1
- stack_use += num_sources
- if stack_use + num_sources > maxStackLimit:
- # if we are here, max stack is the CFF2 max stack.
- # I use the CFF2 max stack limit here rather than
- # the 'maxstack' chosen by the client, as the default
- # maxstack may have been used unintentionally. For all
- # the other operators, this just produces a little less
- # optimization, but here it puts a hard (and low) limit
- # on the number of source fonts that can be used.
- break
- # blendList now contains as many single blend tuples as can be
- # combined without exceeding the CFF2 stack limit.
- num_blends = len(blendlist)
- # append the 'num_blends' default font values
- blend_args = []
- for arg in blendlist:
- blend_args.append(arg[0])
- for arg in blendlist:
- assert arg[-1] == 1
- blend_args.extend(arg[1:-1])
- blend_args.append(num_blends)
- new_args.append(blend_args)
- stack_use = prev_stack_use + num_blends
- return new_args
- def _addArgs(a, b):
- if isinstance(b, list):
- if isinstance(a, list):
- if len(a) != len(b) or a[-1] != b[-1]:
- raise ValueError()
- return [_addArgs(va, vb) for va, vb in zip(a[:-1], b[:-1])] + [a[-1]]
- else:
- a, b = b, a
- if isinstance(a, list):
- assert a[-1] == 1
- return [_addArgs(a[0], b)] + a[1:]
- return a + b
- def specializeCommands(
- commands,
- ignoreErrors=False,
- generalizeFirst=True,
- preserveTopology=False,
- maxstack=48,
- ):
- # We perform several rounds of optimizations. They are carefully ordered and are:
- #
- # 0. Generalize commands.
- # This ensures that they are in our expected simple form, with each line/curve only
- # having arguments for one segment, and using the generic form (rlineto/rrcurveto).
- # If caller is sure the input is in this form, they can turn off generalization to
- # save time.
- #
- # 1. Combine successive rmoveto operations.
- #
- # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
- # We specialize into some, made-up, variants as well, which simplifies following
- # passes.
- #
- # 3. Merge or delete redundant operations, to the extent requested.
- # OpenType spec declares point numbers in CFF undefined. As such, we happily
- # change topology. If client relies on point numbers (in GPOS anchors, or for
- # hinting purposes(what?)) they can turn this off.
- #
- # 4. Peephole optimization to revert back some of the h/v variants back into their
- # original "relative" operator (rline/rrcurveto) if that saves a byte.
- #
- # 5. Combine adjacent operators when possible, minding not to go over max stack size.
- #
- # 6. Resolve any remaining made-up operators into real operators.
- #
- # I have convinced myself that this produces optimal bytecode (except for, possibly
- # one byte each time maxstack size prohibits combining.) YMMV, but you'd be wrong. :-)
- # A dynamic-programming approach can do the same but would be significantly slower.
- #
- # 7. For any args which are blend lists, convert them to a blend command.
- # 0. Generalize commands.
- if generalizeFirst:
- commands = generalizeCommands(commands, ignoreErrors=ignoreErrors)
- else:
- commands = list(commands) # Make copy since we modify in-place later.
- # 1. Combine successive rmoveto operations.
- for i in range(len(commands) - 1, 0, -1):
- if "rmoveto" == commands[i][0] == commands[i - 1][0]:
- v1, v2 = commands[i - 1][1], commands[i][1]
- commands[i - 1] = ("rmoveto", [v1[0] + v2[0], v1[1] + v2[1]])
- del commands[i]
- # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants.
- #
- # We, in fact, specialize into more, made-up, variants that special-case when both
- # X and Y components are zero. This simplifies the following optimization passes.
- # This case is rare, but OCD does not let me skip it.
- #
- # After this round, we will have four variants that use the following mnemonics:
- #
- # - 'r' for relative, ie. non-zero X and non-zero Y,
- # - 'h' for horizontal, ie. zero X and non-zero Y,
- # - 'v' for vertical, ie. non-zero X and zero Y,
- # - '0' for zeros, ie. zero X and zero Y.
- #
- # The '0' pseudo-operators are not part of the spec, but help simplify the following
- # optimization rounds. We resolve them at the end. So, after this, we will have four
- # moveto and four lineto variants:
- #
- # - 0moveto, 0lineto
- # - hmoveto, hlineto
- # - vmoveto, vlineto
- # - rmoveto, rlineto
- #
- # and sixteen curveto variants. For example, a '0hcurveto' operator means a curve
- # dx0,dy0,dx1,dy1,dx2,dy2,dx3,dy3 where dx0, dx1, and dy3 are zero but not dx3.
- # An 'rvcurveto' means dx3 is zero but not dx0,dy0,dy3.
- #
- # There are nine different variants of curves without the '0'. Those nine map exactly
- # to the existing curve variants in the spec: rrcurveto, and the four variants hhcurveto,
- # vvcurveto, hvcurveto, and vhcurveto each cover two cases, one with an odd number of
- # arguments and one without. Eg. an hhcurveto with an extra argument (odd number of
- # arguments) is in fact an rhcurveto. The operators in the spec are designed such that
- # all four of rhcurveto, rvcurveto, hrcurveto, and vrcurveto are encodable for one curve.
- #
- # Of the curve types with '0', the 00curveto is equivalent to a lineto variant. The rest
- # of the curve types with a 0 need to be encoded as a h or v variant. Ie. a '0' can be
- # thought of a "don't care" and can be used as either an 'h' or a 'v'. As such, we always
- # encode a number 0 as argument when we use a '0' variant. Later on, we can just substitute
- # the '0' with either 'h' or 'v' and it works.
- #
- # When we get to curve splines however, things become more complicated... XXX finish this.
- # There's one more complexity with splines. If one side of the spline is not horizontal or
- # vertical (or zero), ie. if it's 'r', then it limits which spline types we can encode.
- # Only hhcurveto and vvcurveto operators can encode a spline starting with 'r', and
- # only hvcurveto and vhcurveto operators can encode a spline ending with 'r'.
- # This limits our merge opportunities later.
- #
- for i in range(len(commands)):
- op, args = commands[i]
- if op in {"rmoveto", "rlineto"}:
- c, args = _categorizeVector(args)
- commands[i] = c + op[1:], args
- continue
- if op == "rrcurveto":
- c1, args1 = _categorizeVector(args[:2])
- c2, args2 = _categorizeVector(args[-2:])
- commands[i] = c1 + c2 + "curveto", args1 + args[2:4] + args2
- continue
- # 3. Merge or delete redundant operations, to the extent requested.
- #
- # TODO
- # A 0moveto that comes before all other path operations can be removed.
- # though I find conflicting evidence for this.
- #
- # TODO
- # "If hstem and vstem hints are both declared at the beginning of a
- # CharString, and this sequence is followed directly by the hintmask or
- # cntrmask operators, then the vstem hint operator (or, if applicable,
- # the vstemhm operator) need not be included."
- #
- # "The sequence and form of a CFF2 CharString program may be represented as:
- # {hs* vs* cm* hm* mt subpath}? {mt subpath}*"
- #
- # https://www.microsoft.com/typography/otspec/cff2charstr.htm#section3.1
- #
- # For Type2 CharStrings the sequence is:
- # w? {hs* vs* cm* hm* mt subpath}? {mt subpath}* endchar"
- # Some other redundancies change topology (point numbers).
- if not preserveTopology:
- for i in range(len(commands) - 1, -1, -1):
- op, args = commands[i]
- # A 00curveto is demoted to a (specialized) lineto.
- if op == "00curveto":
- assert len(args) == 4
- c, args = _categorizeVector(args[1:3])
- op = c + "lineto"
- commands[i] = op, args
- # and then...
- # A 0lineto can be deleted.
- if op == "0lineto":
- del commands[i]
- continue
- # Merge adjacent hlineto's and vlineto's.
- # In CFF2 charstrings from variable fonts, each
- # arg item may be a list of blendable values, one from
- # each source font.
- if i and op in {"hlineto", "vlineto"} and (op == commands[i - 1][0]):
- _, other_args = commands[i - 1]
- assert len(args) == 1 and len(other_args) == 1
- try:
- new_args = [_addArgs(args[0], other_args[0])]
- except ValueError:
- continue
- commands[i - 1] = (op, new_args)
- del commands[i]
- continue
- # 4. Peephole optimization to revert back some of the h/v variants back into their
- # original "relative" operator (rline/rrcurveto) if that saves a byte.
- for i in range(1, len(commands) - 1):
- op, args = commands[i]
- prv, nxt = commands[i - 1][0], commands[i + 1][0]
- if op in {"0lineto", "hlineto", "vlineto"} and prv == nxt == "rlineto":
- assert len(args) == 1
- args = [0, args[0]] if op[0] == "v" else [args[0], 0]
- commands[i] = ("rlineto", args)
- continue
- if op[2:] == "curveto" and len(args) == 5 and prv == nxt == "rrcurveto":
- assert (op[0] == "r") ^ (op[1] == "r")
- if op[0] == "v":
- pos = 0
- elif op[0] != "r":
- pos = 1
- elif op[1] == "v":
- pos = 4
- else:
- pos = 5
- # Insert, while maintaining the type of args (can be tuple or list).
- args = args[:pos] + type(args)((0,)) + args[pos:]
- commands[i] = ("rrcurveto", args)
- continue
- # 5. Combine adjacent operators when possible, minding not to go over max stack size.
- for i in range(len(commands) - 1, 0, -1):
- op1, args1 = commands[i - 1]
- op2, args2 = commands[i]
- new_op = None
- # Merge logic...
- if {op1, op2} <= {"rlineto", "rrcurveto"}:
- if op1 == op2:
- new_op = op1
- else:
- if op2 == "rrcurveto" and len(args2) == 6:
- new_op = "rlinecurve"
- elif len(args2) == 2:
- new_op = "rcurveline"
- elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}:
- new_op = op2
- elif {op1, op2} == {"vlineto", "hlineto"}:
- new_op = op1
- elif "curveto" == op1[2:] == op2[2:]:
- d0, d1 = op1[:2]
- d2, d3 = op2[:2]
- if d1 == "r" or d2 == "r" or d0 == d3 == "r":
- continue
- d = _mergeCategories(d1, d2)
- if d is None:
- continue
- if d0 == "r":
- d = _mergeCategories(d, d3)
- if d is None:
- continue
- new_op = "r" + d + "curveto"
- elif d3 == "r":
- d0 = _mergeCategories(d0, _negateCategory(d))
- if d0 is None:
- continue
- new_op = d0 + "r" + "curveto"
- else:
- d0 = _mergeCategories(d0, d3)
- if d0 is None:
- continue
- new_op = d0 + d + "curveto"
- # Make sure the stack depth does not exceed (maxstack - 1), so
- # that subroutinizer can insert subroutine calls at any point.
- if new_op and len(args1) + len(args2) < maxstack:
- commands[i - 1] = (new_op, args1 + args2)
- del commands[i]
- # 6. Resolve any remaining made-up operators into real operators.
- for i in range(len(commands)):
- op, args = commands[i]
- if op in {"0moveto", "0lineto"}:
- commands[i] = "h" + op[1:], args
- continue
- if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}:
- op0, op1 = op[:2]
- if (op0 == "r") ^ (op1 == "r"):
- assert len(args) % 2 == 1
- if op0 == "0":
- op0 = "h"
- if op1 == "0":
- op1 = "h"
- if op0 == "r":
- op0 = op1
- if op1 == "r":
- op1 = _negateCategory(op0)
- assert {op0, op1} <= {"h", "v"}, (op0, op1)
- if len(args) % 2:
- if op0 != op1: # vhcurveto / hvcurveto
- if (op0 == "h") ^ (len(args) % 8 == 1):
- # Swap last two args order
- args = args[:-2] + args[-1:] + args[-2:-1]
- else: # hhcurveto / vvcurveto
- if op0 == "h": # hhcurveto
- # Swap first two args order
- args = args[1:2] + args[:1] + args[2:]
- commands[i] = op0 + op1 + "curveto", args
- continue
- # 7. For any series of args which are blend lists, convert the series to a single blend arg.
- for i in range(len(commands)):
- op, args = commands[i]
- if any(isinstance(arg, list) for arg in args):
- commands[i] = op, _convertToBlendCmds(args)
- return commands
- def specializeProgram(program, getNumRegions=None, **kwargs):
- return commandsToProgram(
- specializeCommands(programToCommands(program, getNumRegions), **kwargs)
- )
- if __name__ == "__main__":
- import sys
- if len(sys.argv) == 1:
- import doctest
- sys.exit(doctest.testmod().failed)
- import argparse
- parser = argparse.ArgumentParser(
- "fonttools cffLib.specialer",
- description="CFF CharString generalizer/specializer",
- )
- parser.add_argument("program", metavar="command", nargs="*", help="Commands.")
- parser.add_argument(
- "--num-regions",
- metavar="NumRegions",
- nargs="*",
- default=None,
- help="Number of variable-font regions for blend opertaions.",
- )
- options = parser.parse_args(sys.argv[1:])
- getNumRegions = (
- None
- if options.num_regions is None
- else lambda vsIndex: int(options.num_regions[0 if vsIndex is None else vsIndex])
- )
- program = stringToProgram(options.program)
- print("Program:")
- print(programToString(program))
- commands = programToCommands(program, getNumRegions)
- print("Commands:")
- print(commands)
- program2 = commandsToProgram(commands)
- print("Program from commands:")
- print(programToString(program2))
- assert program == program2
- print("Generalized program:")
- print(programToString(generalizeProgram(program, getNumRegions)))
- print("Specialized program:")
- print(programToString(specializeProgram(program, getNumRegions)))
|