diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py index e61ad26f8..5fddcb67d 100644 --- a/Lib/fontTools/cffLib/specializer.py +++ b/Lib/fontTools/cffLib/specializer.py @@ -80,8 +80,9 @@ def programToCommands(program, getNumRegions=None): 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) + lenStack = len(stack) + lenBlendStack += numBlends + lenStack - 1 + lastBlendIndex = lenStack # if a blend op exists, this is or will be a CFF2 charstring. continue @@ -153,9 +154,10 @@ def commandsToProgram(commands): def _everyN(el, 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) - for i in range(0, len(el), n): + for i in range(0, l, n): yield el[i : i + n] @@ -218,9 +220,10 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod 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) - if len(args) % 2 == 1: + if l % 2 == 1: yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0]) args = args[5:] for args in _everyN(args, 4): @@ -228,9 +231,10 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod 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) - if len(args) % 2 == 1: + if l % 2 == 1: yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]]) args = args[5:] for args in _everyN(args, 4): @@ -238,11 +242,12 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod 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) last_args = None - if len(args) % 2 == 1: - lastStraight = len(args) % 8 == 5 + if l % 2 == 1: + lastStraight = l % 8 == 5 args, last_args = args[:-5], args[-5:] it = _everyN(args, 4) try: @@ -262,11 +267,12 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod 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) last_args = None - if len(args) % 2 == 1: - lastStraight = len(args) % 8 == 5 + if l % 2 == 1: + lastStraight = l % 8 == 5 args, last_args = args[:-5], args[-5:] it = _everyN(args, 4) try: @@ -286,7 +292,8 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod 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) args, last_args = args[:-2], args[-2:] for args in _everyN(args, 6): @@ -295,7 +302,8 @@ class _GeneralizerDecombinerCommandsMap(object): @staticmethod 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) args, last_args = args[:-6], args[-6:] for args in _everyN(args, 2): @@ -330,8 +338,9 @@ def _convertBlendOpToArgs(blendList): # comprehension. See calling context args = args[:-1] - numRegions = len(args) // numBlends - 1 - if not (numBlends * (numRegions + 1) == len(args)): + l = len(args) + numRegions = l // numBlends - 1 + if not (numBlends * (numRegions + 1) == l): raise ValueError(blendList) defaultArgs = [[arg] for arg in args[:numBlends]] @@ -368,7 +377,7 @@ def generalizeCommands(commands, ignoreErrors=False): raise func = getattr(mapping, op, None) - if not func: + if func is None: result.append((op, args)) continue try: @@ -715,6 +724,7 @@ def specializeCommands( continue # 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): op1, args1 = commands[i - 1] op2, args2 = commands[i] @@ -725,9 +735,10 @@ def specializeCommands( if op1 == op2: new_op = op1 else: - if op2 == "rrcurveto" and len(args2) == 6: + l = len(args2) + if op2 == "rrcurveto" and l == 6: new_op = "rlinecurve" - elif len(args2) == 2: + elif l == 2: new_op = "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 # 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) del commands[i] + stackUse = combinedStackUse + else: + stackUse = args1StackUse # 6. Resolve any remaining made-up operators into real operators. for i in range(len(commands)): @@ -777,9 +793,11 @@ def specializeCommands( continue if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}: + l = len(args) + op0, op1 = op[:2] if (op0 == "r") ^ (op1 == "r"): - assert len(args) % 2 == 1 + assert l % 2 == 1 if op0 == "0": op0 = "h" if op1 == "0": @@ -790,9 +808,9 @@ def specializeCommands( op1 = _negateCategory(op0) assert {op0, op1} <= {"h", "v"}, (op0, op1) - if len(args) % 2: + if l % 2: if op0 != op1: # vhcurveto / hvcurveto - if (op0 == "h") ^ (len(args) % 8 == 1): + if (op0 == "h") ^ (l % 8 == 1): # Swap last two args order args = args[:-2] + args[-1:] + args[-2:-1] else: # hhcurveto / vvcurveto diff --git a/Tests/cffLib/specializer_test.py b/Tests/cffLib/specializer_test.py index 08c137ada..ebe702fee 100644 --- a/Tests/cffLib/specializer_test.py +++ b/Tests/cffLib/specializer_test.py @@ -23,9 +23,7 @@ def charstr_specialize(charstr, **kwargs): return programToString(specializeProgram(stringToProgram(charstr), **kwargs)) -def charstr_stack_use(charstr, getNumRegions=None): - program = stringToProgram(charstr) - +def program_stack_use(program, getNumRegions=None): vsindex = None maxStack = 0 stack = [] @@ -586,19 +584,38 @@ class CFFSpecializeProgramTest: # maxstack CFF2=513, specializer uses up to 512 def test_maxstack_blends(self): numRegions = 15 - numOps = 2000 + numOps = 600 getNumRegions = lambda iv: numRegions - blend_one = " ".join([str(i) for i in range(1 + numRegions)] + ["1", "blend"]) - operands = " ".join([blend_one] * 6) + blend_one = [i for i in range(1 + numRegions)] + [1, "blend"] + operands = blend_one * 6 operator = "rrcurveto" - charstr = " ".join([operands, operator] * numOps) - expected = charstr - specialized = charstr_specialize( - charstr, getNumRegions=getNumRegions, maxstack=maxStack + program = (operands + [operator]) * numOps + specialized = specializeProgram( + program, + 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 + 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): def test_blend_round_trip(self):