blob: 6716178c62fc8d0619d3666759de50020a7bb5cc [file] [edit]
#!/usr/bin/env python3
"""Generate CFF2 instancing regressions for fonttools/fonttools#4338.
Run from the repository root. Each ring contour closes explicitly in the
default and in every variation region, so relative rounding must not open it.
The other glyphs exercise compact path operators, flexes, and hint masks.
"""
import math
from pathlib import Path
import subprocess
import sys
from fontTools.fontBuilder import FontBuilder
from fontTools.misc.psCharStrings import T2CharString
def ring(radius, wobble):
commands = []
previous = (0, 0)
for cx, cy, direction in ((400, 400, 1), (1100, 400, -1)):
points = [
(
round(cx + (radius + wobble * (i % 3)) * math.cos(i * math.pi / 8)),
round(cy + (radius - wobble * (i % 2)) * math.sin(i * math.pi / 8)),
)
for i in range(16)
][::direction]
commands.append(("rmoveto", [points[0][j] - previous[j] for j in (0, 1)]))
previous = points[0]
for start, end in zip(points, points[1:] + points[:1]):
controls = [
tuple(start[j] + (end[j] - start[j]) // 3 for j in (0, 1)),
tuple(end[j] - (end[j] - start[j]) // 3 for j in (0, 1)),
end,
]
args = []
for point in controls:
args.extend(point[j] - previous[j] for j in (0, 1))
previous = point
commands.append(("rrcurveto", args))
return commands
def blended(default, deltas):
return list(default) + [d[i] for i in range(len(default)) for d in deltas] + [len(default), "blend"]
program = []
masters = [ring(r, w) for r, w in ((300, 7), (337, 23), (305, 11), (343, 29), (321, 13))]
for i, (op, args) in enumerate(masters[0]):
# Independent wght, slnt, interaction, and intermediate-wght regions.
deltas = [
[masters[1][i][1][j] - v for j, v in enumerate(args)],
[masters[2][i][1][j] - v for j, v in enumerate(args)],
[masters[3][i][1][j] - masters[1][i][1][j] - masters[2][i][1][j] + v for j, v in enumerate(args)],
[masters[4][i][1][j] - (masters[1][i][1][j] + v) / 2 for j, v in enumerate(args)],
]
program.extend(blended(args, deltas) + [op])
programs = {".notdef": [], "ring": program}
operators = {
"hlineto": [23, 31, -19, 41, 37],
"vlineto": [23, 31, -19, 41, 37],
"rcurveline": [17, 23, 31, -19, 41, 37, 29, -11],
"rlinecurve": [17, 23, 31, -19, 41, 37, 29, -11],
"hhcurveto": [13, 17, 23, 31, -19, 41, 37, 29, -11],
"vvcurveto": [13, 17, 23, 31, -19, 41, 37, 29, -11],
"hvcurveto": [17, 23, 31, -19, 41, 37, 29, -11, 13],
"vhcurveto": [17, 23, 31, -19, 41, 37, 29, -11, 13],
"flex": [17, 23, 31, -19, 41, 37, 29, -11, 13, 47, 19, -23, 50],
"hflex": [17, 23, 31, -19, 41, 37, 29],
"hflex1": [17, 23, 31, -19, 41, 37, 29, -11, 13],
# Its direction changes over wght: exercise the conservative fallback.
"flex1": [10, 20] * 5 + [17],
}
for op in ("hhcurveto", "vvcurveto", "hvcurveto", "vhcurveto"):
operators[op + ".one"] = [17, 23, 31, -19]
operators[op + ".two"] = [17, 23, 31, -19, 41, 37, 29, -11]
for op, args in operators.items():
deltas = [[(j % 7) - 3 for j in range(len(args))]] + [[0] * len(args)] * 3
if op == "flex":
deltas[0][-1] = 0
if op == "flex1":
deltas[0] = [30, -10] * 5 + [0]
p = blended([101, 103], [[1, -1], [0, 0], [0, 0], [0, 0]]) + ["rmoveto"]
# Repeat to expose cumulative errors, including flex's implicit y reset.
for _ in range(3):
p += blended(args, deltas) + [op.split(".")[0]]
p += [23, 29, "rlineto", 31, "hmoveto", 37, "vmoveto", 41, 43, "rlineto"]
programs[op] = p
stem = blended([101, 23, 29, 31], [[1, 1, 1, 1], [0] * 4, [0] * 4, [0] * 4])
programs["hinted"] = stem + ["hstemhm"] + stem + ["hintmask", b"\xf0"] + program
programs["fractional"] = [10.25, 20.75, "rmoveto", 100.5, 0, 0, 100.5, -100.5, 0, "rlineto"]
order = list(programs)
fb = FontBuilder(1000, isTTF=False)
fb.setupGlyphOrder(order)
fb.setupCharacterMap({0x41 + i: name for i, name in enumerate(order[1:])})
fb.setupHorizontalMetrics({name: (2000, 0) for name in order})
fb.setupHorizontalHeader(ascent=1600, descent=-400)
fb.setupNameTable({"familyName": "CFF2 Rounding", "styleName": "Regular"})
fb.setupOS2()
fb.setupFvar([("wght", 300, 300, 900, "Weight"), ("slnt", -11, 0, 0, "Slant")], [])
fb.setupCFF2(
{name: T2CharString(program=p) for name, p in programs.items()},
regions=[
{"wght": (0, 1, 1)},
{"slnt": (-1, -1, 0)},
{"wght": (0, 1, 1), "slnt": (-1, -1, 0)},
{"wght": (0, 0.5, 1)},
],
)
fb.setupPost(keepGlyphNames=True)
fb.font["head"].created = fb.font["head"].modified = 2082844800
fb.font.recalcTimestamp = False
fb.save(Path(__file__).parent / "fonts" / "cff2-rounding.otf")
# The existing API test also compares this table's serialized bytes.
subprocess.run(
[
sys.argv[1] if len(sys.argv) > 1 else "build/util/hb-subset",
str(Path(__file__).parent / "fonts" / "AdobeVFPrototype.abc.otf"),
"--unicodes=61,62,63",
"--variations=wght=250,CNTR=0",
"--output-file=" + str(Path(__file__).parent / "fonts" / "AdobeVFPrototype.abc.static.otf"),
],
check=True,
)