|
|
|
|
|
"""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. 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 = 0 |
|
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) |
|
|
|
|
|
numBlends = stack[-1] |
|
numBlendArgs = numBlends * numSourceFonts + 1 |
|
|
|
stack[-numBlendArgs:] = [stack[-numBlendArgs:]] |
|
lenBlendStack += numBlends + len(stack) - 1 |
|
lastBlendIndex = len(stack) |
|
|
|
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: |
|
|
|
|
|
|
|
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): |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
numBlends = args[-1] |
|
|
|
|
|
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: |
|
|
|
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: |
|
|
|
|
|
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: |
|
|
|
|
|
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): |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
num_sources = len(arg) - 1 |
|
blendlist = [arg] |
|
i += 1 |
|
stack_use += 1 + num_sources |
|
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: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
break |
|
|
|
|
|
num_blends = len(blendlist) |
|
|
|
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, |
|
): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if generalizeFirst: |
|
commands = generalizeCommands(commands, ignoreErrors=ignoreErrors) |
|
else: |
|
commands = list(commands) |
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not preserveTopology: |
|
for i in range(len(commands) - 1, -1, -1): |
|
op, args = commands[i] |
|
|
|
|
|
if op == "00curveto": |
|
assert len(args) == 4 |
|
c, args = _categorizeVector(args[1:3]) |
|
op = c + "lineto" |
|
commands[i] = op, args |
|
|
|
|
|
|
|
if op == "0lineto": |
|
del commands[i] |
|
continue |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
|
|
args = args[:pos] + type(args)((0,)) + args[pos:] |
|
commands[i] = ("rrcurveto", args) |
|
continue |
|
|
|
|
|
for i in range(len(commands) - 1, 0, -1): |
|
op1, args1 = commands[i - 1] |
|
op2, args2 = commands[i] |
|
new_op = None |
|
|
|
|
|
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" |
|
|
|
|
|
|
|
if new_op and len(args1) + len(args2) < maxstack: |
|
commands[i - 1] = (new_op, args1 + args2) |
|
del commands[i] |
|
|
|
|
|
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: |
|
if (op0 == "h") ^ (len(args) % 8 == 1): |
|
|
|
args = args[:-2] + args[-1:] + args[-2:-1] |
|
else: |
|
if op0 == "h": |
|
|
|
args = args[1:2] + args[:1] + args[2:] |
|
|
|
commands[i] = op0 + op1 + "curveto", args |
|
continue |
|
|
|
|
|
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.specializer", |
|
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))) |
|
|