Merge pull request #3689 from fonttools/specializer-argsStackUse

[cffLib.specializer] Adjust stack use calculation
This commit is contained in:
Behdad Esfahbod 2024-11-13 08:37:23 -07:00 committed by GitHub
commit 081d6a27ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 71 additions and 36 deletions

View File

@ -80,8 +80,9 @@ def programToCommands(program, getNumRegions=None):
numBlendArgs = numBlends * numSourceFonts + 1 numBlendArgs = numBlends * numSourceFonts + 1
# replace first blend op by a list of the blend ops. # replace first blend op by a list of the blend ops.
stack[-numBlendArgs:] = [stack[-numBlendArgs:]] stack[-numBlendArgs:] = [stack[-numBlendArgs:]]
lenBlendStack += numBlends + len(stack) - 1 lenStack = len(stack)
lastBlendIndex = len(stack) lenBlendStack += numBlends + lenStack - 1
lastBlendIndex = lenStack
# if a blend op exists, this is or will be a CFF2 charstring. # if a blend op exists, this is or will be a CFF2 charstring.
continue continue
@ -153,9 +154,10 @@ def commandsToProgram(commands):
def _everyN(el, n): def _everyN(el, n):
"""Group the list el into groups of size n""" """Group the list el into groups of size n"""
if len(el) % n != 0: l = len(el)
if l % n != 0:
raise ValueError(el) raise ValueError(el)
for i in range(0, len(el), n): for i in range(0, l, n):
yield el[i : i + n] yield el[i : i + n]
@ -218,9 +220,10 @@ class _GeneralizerDecombinerCommandsMap(object):
@staticmethod @staticmethod
def hhcurveto(args): def hhcurveto(args):
if len(args) < 4 or len(args) % 4 > 1: l = len(args)
if l < 4 or l % 4 > 1:
raise ValueError(args) raise ValueError(args)
if len(args) % 2 == 1: if l % 2 == 1:
yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0]) yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0])
args = args[5:] args = args[5:]
for args in _everyN(args, 4): for args in _everyN(args, 4):
@ -228,9 +231,10 @@ class _GeneralizerDecombinerCommandsMap(object):
@staticmethod @staticmethod
def vvcurveto(args): def vvcurveto(args):
if len(args) < 4 or len(args) % 4 > 1: l = len(args)
if l < 4 or l % 4 > 1:
raise ValueError(args) raise ValueError(args)
if len(args) % 2 == 1: if l % 2 == 1:
yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]]) yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]])
args = args[5:] args = args[5:]
for args in _everyN(args, 4): for args in _everyN(args, 4):
@ -238,11 +242,12 @@ class _GeneralizerDecombinerCommandsMap(object):
@staticmethod @staticmethod
def hvcurveto(args): def hvcurveto(args):
if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}: l = len(args)
if l < 4 or l % 8 not in {0, 1, 4, 5}:
raise ValueError(args) raise ValueError(args)
last_args = None last_args = None
if len(args) % 2 == 1: if l % 2 == 1:
lastStraight = len(args) % 8 == 5 lastStraight = l % 8 == 5
args, last_args = args[:-5], args[-5:] args, last_args = args[:-5], args[-5:]
it = _everyN(args, 4) it = _everyN(args, 4)
try: try:
@ -262,11 +267,12 @@ class _GeneralizerDecombinerCommandsMap(object):
@staticmethod @staticmethod
def vhcurveto(args): def vhcurveto(args):
if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}: l = len(args)
if l < 4 or l % 8 not in {0, 1, 4, 5}:
raise ValueError(args) raise ValueError(args)
last_args = None last_args = None
if len(args) % 2 == 1: if l % 2 == 1:
lastStraight = len(args) % 8 == 5 lastStraight = l % 8 == 5
args, last_args = args[:-5], args[-5:] args, last_args = args[:-5], args[-5:]
it = _everyN(args, 4) it = _everyN(args, 4)
try: try:
@ -286,7 +292,8 @@ class _GeneralizerDecombinerCommandsMap(object):
@staticmethod @staticmethod
def rcurveline(args): def rcurveline(args):
if len(args) < 8 or len(args) % 6 != 2: l = len(args)
if l < 8 or l % 6 != 2:
raise ValueError(args) raise ValueError(args)
args, last_args = args[:-2], args[-2:] args, last_args = args[:-2], args[-2:]
for args in _everyN(args, 6): for args in _everyN(args, 6):
@ -295,7 +302,8 @@ class _GeneralizerDecombinerCommandsMap(object):
@staticmethod @staticmethod
def rlinecurve(args): def rlinecurve(args):
if len(args) < 8 or len(args) % 2 != 0: l = len(args)
if l < 8 or l % 2 != 0:
raise ValueError(args) raise ValueError(args)
args, last_args = args[:-6], args[-6:] args, last_args = args[:-6], args[-6:]
for args in _everyN(args, 2): for args in _everyN(args, 2):
@ -330,8 +338,9 @@ def _convertBlendOpToArgs(blendList):
# comprehension. See calling context # comprehension. See calling context
args = args[:-1] args = args[:-1]
numRegions = len(args) // numBlends - 1 l = len(args)
if not (numBlends * (numRegions + 1) == len(args)): numRegions = l // numBlends - 1
if not (numBlends * (numRegions + 1) == l):
raise ValueError(blendList) raise ValueError(blendList)
defaultArgs = [[arg] for arg in args[:numBlends]] defaultArgs = [[arg] for arg in args[:numBlends]]
@ -368,7 +377,7 @@ def generalizeCommands(commands, ignoreErrors=False):
raise raise
func = getattr(mapping, op, None) func = getattr(mapping, op, None)
if not func: if func is None:
result.append((op, args)) result.append((op, args))
continue continue
try: try:
@ -715,6 +724,7 @@ def specializeCommands(
continue continue
# 5. Combine adjacent operators when possible, minding not to go over max stack size. # 5. Combine adjacent operators when possible, minding not to go over max stack size.
stackUse = _argsStackUse(commands[-1][1]) if commands else 0
for i in range(len(commands) - 1, 0, -1): for i in range(len(commands) - 1, 0, -1):
op1, args1 = commands[i - 1] op1, args1 = commands[i - 1]
op2, args2 = commands[i] op2, args2 = commands[i]
@ -725,9 +735,10 @@ def specializeCommands(
if op1 == op2: if op1 == op2:
new_op = op1 new_op = op1
else: else:
if op2 == "rrcurveto" and len(args2) == 6: l = len(args2)
if op2 == "rrcurveto" and l == 6:
new_op = "rlinecurve" new_op = "rlinecurve"
elif len(args2) == 2: elif l == 2:
new_op = "rcurveline" new_op = "rcurveline"
elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}: elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}:
@ -764,9 +775,14 @@ def specializeCommands(
# Make sure the stack depth does not exceed (maxstack - 1), so # Make sure the stack depth does not exceed (maxstack - 1), so
# that subroutinizer can insert subroutine calls at any point. # that subroutinizer can insert subroutine calls at any point.
if new_op and _argsStackUse(args1) + _argsStackUse(args2) < maxstack: args1StackUse = _argsStackUse(args1)
combinedStackUse = max(args1StackUse, len(args1) + stackUse)
if new_op and combinedStackUse < maxstack:
commands[i - 1] = (new_op, args1 + args2) commands[i - 1] = (new_op, args1 + args2)
del commands[i] del commands[i]
stackUse = combinedStackUse
else:
stackUse = args1StackUse
# 6. Resolve any remaining made-up operators into real operators. # 6. Resolve any remaining made-up operators into real operators.
for i in range(len(commands)): for i in range(len(commands)):
@ -777,9 +793,11 @@ def specializeCommands(
continue continue
if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}: if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}:
l = len(args)
op0, op1 = op[:2] op0, op1 = op[:2]
if (op0 == "r") ^ (op1 == "r"): if (op0 == "r") ^ (op1 == "r"):
assert len(args) % 2 == 1 assert l % 2 == 1
if op0 == "0": if op0 == "0":
op0 = "h" op0 = "h"
if op1 == "0": if op1 == "0":
@ -790,9 +808,9 @@ def specializeCommands(
op1 = _negateCategory(op0) op1 = _negateCategory(op0)
assert {op0, op1} <= {"h", "v"}, (op0, op1) assert {op0, op1} <= {"h", "v"}, (op0, op1)
if len(args) % 2: if l % 2:
if op0 != op1: # vhcurveto / hvcurveto if op0 != op1: # vhcurveto / hvcurveto
if (op0 == "h") ^ (len(args) % 8 == 1): if (op0 == "h") ^ (l % 8 == 1):
# Swap last two args order # Swap last two args order
args = args[:-2] + args[-1:] + args[-2:-1] args = args[:-2] + args[-1:] + args[-2:-1]
else: # hhcurveto / vvcurveto else: # hhcurveto / vvcurveto

View File

@ -23,9 +23,7 @@ def charstr_specialize(charstr, **kwargs):
return programToString(specializeProgram(stringToProgram(charstr), **kwargs)) return programToString(specializeProgram(stringToProgram(charstr), **kwargs))
def charstr_stack_use(charstr, getNumRegions=None): def program_stack_use(program, getNumRegions=None):
program = stringToProgram(charstr)
vsindex = None vsindex = None
maxStack = 0 maxStack = 0
stack = [] stack = []
@ -586,19 +584,38 @@ class CFFSpecializeProgramTest:
# maxstack CFF2=513, specializer uses up to 512 # maxstack CFF2=513, specializer uses up to 512
def test_maxstack_blends(self): def test_maxstack_blends(self):
numRegions = 15 numRegions = 15
numOps = 2000 numOps = 600
getNumRegions = lambda iv: numRegions getNumRegions = lambda iv: numRegions
blend_one = " ".join([str(i) for i in range(1 + numRegions)] + ["1", "blend"]) blend_one = [i for i in range(1 + numRegions)] + [1, "blend"]
operands = " ".join([blend_one] * 6) operands = blend_one * 6
operator = "rrcurveto" operator = "rrcurveto"
charstr = " ".join([operands, operator] * numOps) program = (operands + [operator]) * numOps
expected = charstr specialized = specializeProgram(
specialized = charstr_specialize( program,
charstr, getNumRegions=getNumRegions, maxstack=maxStack getNumRegions=getNumRegions,
maxstack=maxStack,
generalizeFirst=False,
) )
stack_use = charstr_stack_use(specialized, getNumRegions=getNumRegions) stack_use = program_stack_use(specialized, getNumRegions=getNumRegions)
assert maxStack - numRegions < stack_use < maxStack assert maxStack - numRegions < stack_use < maxStack
def test_maxstack_commands(self):
# See if two commands with deep blends are merged into one
numRegions = 400
numOps = 2
getNumRegions = lambda iv: numRegions
blend_one = [i for i in range(1 + numRegions)] + [1, "blend"]
operands = blend_one * 6
operator = "rrcurveto"
program = (operands + [operator]) * numOps
specialized = specializeProgram(
program,
getNumRegions=getNumRegions,
maxstack=maxStack,
generalizeFirst=False,
)
assert specialized.index("rrcurveto") == len(specialized) - 1
class CFF2VFTestSpecialize(DataFilesHandler): class CFF2VFTestSpecialize(DataFilesHandler):
def test_blend_round_trip(self): def test_blend_round_trip(self):