850 lines
30 KiB
Python
850 lines
30 KiB
Python
# -*- 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)))
|