aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/cffLib/specializer.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/cffLib/specializer.py')
-rw-r--r--Lib/fontTools/cffLib/specializer.py201
1 files changed, 186 insertions, 15 deletions
diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py
index caf8c3b3..db6e5f3d 100644
--- a/Lib/fontTools/cffLib/specializer.py
+++ b/Lib/fontTools/cffLib/specializer.py
@@ -4,6 +4,7 @@
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
+from fontTools.cffLib import maxStackLimit
def stringToProgram(string):
@@ -26,14 +27,21 @@ def programToString(program):
return ' '.join(str(x) for x in program)
-def programToCommands(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 (¯\_(ツ)_/¯)."""
+ 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."""
width = None
+ seenWidthOp = False
+ vsIndex = None
commands = []
stack = []
it = iter(program)
@@ -42,10 +50,37 @@ def programToCommands(program):
stack.append(token)
continue
- if width is None and token in {'hstem', 'hstemhm', 'vstem', 'vstemhm',
- 'cntrmask', 'hintmask',
- 'hmoveto', 'vmoveto', 'rmoveto',
- 'endchar'}:
+ 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.
+ numBlendOps = stack[-1] * numSourceFonts + 1
+ # replace first blend op by a list of the blend ops.
+ stack[-numBlendOps:] = [stack[-numBlendOps:]]
+
+ # Check for width.
+ if not seenWidthOp:
+ seenWidthOp = True
+ widthLen = len(stack) - numBlendOps
+ if widthLen and (widthLen % 2):
+ stack.pop(0)
+ elif width is not None:
+ commands.pop(0)
+ width = None
+ # We do NOT add the width to the command list if a blend is seen:
+ # 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 stack and (len(stack) % 2) ^ parity:
width = stack.pop(0)
@@ -64,11 +99,23 @@ def programToCommands(program):
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)
@@ -203,11 +250,58 @@ class _GeneralizerDecombinerCommandsMap(object):
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 for a, b in zip(defaultArgs,deltaList)]
+ return blend_args
def generalizeCommands(commands, ignoreErrors=False):
result = []
mapping = _GeneralizerDecombinerCommandsMap
- for op,args in commands:
+ 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))
@@ -225,8 +319,8 @@ def generalizeCommands(commands, ignoreErrors=False):
raise
return result
-def generalizeProgram(program, **kwargs):
- return commandsToProgram(generalizeCommands(programToCommands(program), **kwargs))
+def generalizeProgram(program, getNumRegions=None, **kwargs):
+ return commandsToProgram(generalizeCommands(programToCommands(program, getNumRegions), **kwargs))
def _categorizeVector(v):
@@ -267,6 +361,70 @@ def _negateCategory(a):
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)
+ # Combine as many successive tuples as we can,
+ # up to the max stack limit.
+ num_sources = len(arg)
+ 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:
+ blend_args.extend(arg[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):
+ raise ValueError()
+ return [_addArgs(va, vb) for va,vb in zip(a, b)]
+ else:
+ a, b = b, a
+ if isinstance(a, list):
+ return [_addArgs(a[0], b)] + a[1:]
+ return a + b
+
+
def specializeCommands(commands,
ignoreErrors=False,
generalizeFirst=True,
@@ -302,6 +460,8 @@ def specializeCommands(commands,
# 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.
@@ -417,12 +577,18 @@ def specializeCommands(commands,
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]) and
- (not isinstance(args[0], list))):
+ (op == commands[i-1][0])):
_, other_args = commands[i-1]
assert len(args) == 1 and len(other_args) == 1
- commands[i-1] = (op, [other_args[0]+args[0]])
+ try:
+ new_args = [_addArgs(args[0], other_args[0])]
+ except ValueError:
+ continue
+ commands[i-1] = (op, new_args)
del commands[i]
continue
@@ -534,10 +700,16 @@ def specializeCommands(commands,
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, **kwargs):
- return commandsToProgram(specializeCommands(programToCommands(program), **kwargs))
+def specializeProgram(program, getNumRegions=None, **kwargs):
+ return commandsToProgram(specializeCommands(programToCommands(program, getNumRegions), **kwargs))
if __name__ == '__main__':
@@ -554,4 +726,3 @@ if __name__ == '__main__':
assert program == program2
print("Generalized program:"); print(programToString(generalizeProgram(program)))
print("Specialized program:"); print(programToString(specializeProgram(program)))
-