2023-12-02 11:18:31 +00:00
|
|
|
from collections import OrderedDict
|
|
|
|
from fontTools.designspaceLib import AxisDescriptor
|
|
|
|
from fontTools.ttLib import TTFont, newTable
|
|
|
|
from fontTools import varLib
|
|
|
|
from fontTools.varLib.featureVars import (
|
|
|
|
addFeatureVariations,
|
|
|
|
overlayFeatureVariations,
|
|
|
|
overlayBox,
|
|
|
|
)
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
def makeVariableFont(glyphOrder, axes):
|
|
|
|
font = TTFont()
|
|
|
|
font.setGlyphOrder(glyphOrder)
|
|
|
|
font["name"] = newTable("name")
|
|
|
|
ds_axes = OrderedDict()
|
|
|
|
for axisTag, (minimum, default, maximum) in axes.items():
|
|
|
|
axis = AxisDescriptor()
|
|
|
|
axis.name = axis.tag = axis.labelNames["en"] = axisTag
|
|
|
|
axis.minimum, axis.default, axis.maximum = minimum, default, maximum
|
|
|
|
ds_axes[axisTag] = axis
|
|
|
|
varLib._add_fvar(font, ds_axes, instances=())
|
|
|
|
return font
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def varfont():
|
|
|
|
return makeVariableFont(
|
|
|
|
[".notdef", "space", "A", "B", "A.alt", "B.alt"],
|
|
|
|
{"wght": (100, 400, 900)},
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_addFeatureVariations(varfont):
|
|
|
|
assert "GSUB" not in varfont
|
|
|
|
|
|
|
|
addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})])
|
|
|
|
|
|
|
|
assert "GSUB" in varfont
|
|
|
|
gsub = varfont["GSUB"].table
|
|
|
|
|
|
|
|
assert len(gsub.ScriptList.ScriptRecord) == 1
|
|
|
|
assert gsub.ScriptList.ScriptRecord[0].ScriptTag == "DFLT"
|
|
|
|
|
|
|
|
assert len(gsub.FeatureList.FeatureRecord) == 1
|
|
|
|
assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rvrn"
|
|
|
|
|
|
|
|
assert len(gsub.LookupList.Lookup) == 1
|
|
|
|
assert gsub.LookupList.Lookup[0].LookupType == 1
|
|
|
|
assert len(gsub.LookupList.Lookup[0].SubTable) == 1
|
|
|
|
assert gsub.LookupList.Lookup[0].SubTable[0].mapping == {"A": "A.alt"}
|
|
|
|
|
|
|
|
assert gsub.FeatureVariations is not None
|
|
|
|
assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
|
|
|
|
fvr = gsub.FeatureVariations.FeatureVariationRecord[0]
|
|
|
|
assert len(fvr.ConditionSet.ConditionTable) == 1
|
|
|
|
cst = fvr.ConditionSet.ConditionTable[0]
|
|
|
|
assert cst.AxisIndex == 0
|
|
|
|
assert cst.FilterRangeMinValue == 0.5
|
|
|
|
assert cst.FilterRangeMaxValue == 1.0
|
|
|
|
assert len(fvr.FeatureTableSubstitution.SubstitutionRecord) == 1
|
|
|
|
ftsr = fvr.FeatureTableSubstitution.SubstitutionRecord[0]
|
|
|
|
assert ftsr.FeatureIndex == 0
|
|
|
|
assert ftsr.Feature.LookupListIndex == [0]
|
|
|
|
|
|
|
|
|
|
|
|
def _substitution_features(gsub, rec_index):
|
|
|
|
fea_tags = [feature.FeatureTag for feature in gsub.FeatureList.FeatureRecord]
|
|
|
|
fea_indices = [
|
|
|
|
gsub.FeatureVariations.FeatureVariationRecord[rec_index]
|
|
|
|
.FeatureTableSubstitution.SubstitutionRecord[i]
|
|
|
|
.FeatureIndex
|
|
|
|
for i in range(
|
|
|
|
len(
|
|
|
|
gsub.FeatureVariations.FeatureVariationRecord[
|
|
|
|
rec_index
|
|
|
|
].FeatureTableSubstitution.SubstitutionRecord
|
|
|
|
)
|
|
|
|
)
|
|
|
|
]
|
|
|
|
return [(i, fea_tags[i]) for i in fea_indices]
|
|
|
|
|
|
|
|
|
|
|
|
def test_addFeatureVariations_existing_variable_feature(varfont):
|
|
|
|
assert "GSUB" not in varfont
|
|
|
|
|
|
|
|
addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})])
|
|
|
|
|
|
|
|
gsub = varfont["GSUB"].table
|
|
|
|
assert len(gsub.FeatureList.FeatureRecord) == 1
|
|
|
|
assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rvrn"
|
|
|
|
assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
|
|
|
|
assert _substitution_features(gsub, rec_index=0) == [(0, "rvrn")]
|
|
|
|
|
|
|
|
# can't add feature variations for an existing feature tag that already has some,
|
|
|
|
# in this case the default 'rvrn'
|
|
|
|
with pytest.raises(
|
|
|
|
varLib.VarLibError,
|
|
|
|
match=r"FeatureVariations already exist for feature tag\(s\): {'rvrn'}",
|
|
|
|
):
|
|
|
|
addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})])
|
|
|
|
|
|
|
|
|
|
|
|
def test_addFeatureVariations_new_feature(varfont):
|
|
|
|
assert "GSUB" not in varfont
|
|
|
|
|
|
|
|
addFeatureVariations(varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})])
|
|
|
|
|
|
|
|
gsub = varfont["GSUB"].table
|
|
|
|
assert len(gsub.FeatureList.FeatureRecord) == 1
|
|
|
|
assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rvrn"
|
|
|
|
assert len(gsub.LookupList.Lookup) == 1
|
|
|
|
assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
|
|
|
|
assert _substitution_features(gsub, rec_index=0) == [(0, "rvrn")]
|
|
|
|
|
|
|
|
# we can add feature variations for a feature tag that does not have
|
|
|
|
# any feature variations yet
|
|
|
|
addFeatureVariations(
|
|
|
|
varfont, [([{"wght": (-1.0, 0.0)}], {"B": "B.alt"})], featureTag="rclt"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert len(gsub.FeatureList.FeatureRecord) == 2
|
|
|
|
# Note 'rclt' is now first (index=0) in the feature list sorted by tag, and
|
|
|
|
# 'rvrn' is second (index=1)
|
|
|
|
assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "rclt"
|
|
|
|
assert gsub.FeatureList.FeatureRecord[1].FeatureTag == "rvrn"
|
|
|
|
assert len(gsub.LookupList.Lookup) == 2
|
|
|
|
assert len(gsub.FeatureVariations.FeatureVariationRecord) == 2
|
|
|
|
# The new 'rclt' feature variation record is appended to the end;
|
|
|
|
# the feature index for 'rvrn' feature table substitution record is now 1
|
|
|
|
assert _substitution_features(gsub, rec_index=0) == [(1, "rvrn")]
|
|
|
|
assert _substitution_features(gsub, rec_index=1) == [(0, "rclt")]
|
2018-11-09 11:47:49 -05:00
|
|
|
|
|
|
|
|
2023-02-08 14:18:49 -07:00
|
|
|
def _test_linear(n):
|
2018-11-09 11:47:49 -05:00
|
|
|
conds = []
|
|
|
|
for i in range(n):
|
|
|
|
end = i / n
|
2022-12-13 11:26:36 +00:00
|
|
|
start = end - 1.0
|
|
|
|
region = [{"X": (start, end)}]
|
|
|
|
subst = {"g%.2g" % start: "g%.2g" % end}
|
2018-11-09 11:47:49 -05:00
|
|
|
conds.append((region, subst))
|
|
|
|
overlaps = overlayFeatureVariations(conds)
|
2018-11-09 16:04:21 -05:00
|
|
|
assert len(overlaps) == 2 * n - 1, overlaps
|
2018-11-09 11:47:49 -05:00
|
|
|
return conds, overlaps
|
|
|
|
|
2023-02-08 14:27:48 -07:00
|
|
|
|
2023-02-08 14:18:49 -07:00
|
|
|
def test_linear():
|
|
|
|
_test_linear(10)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2023-02-08 14:27:48 -07:00
|
|
|
|
2023-02-08 14:18:49 -07:00
|
|
|
def _test_quadratic(n):
|
2018-11-09 18:59:28 -05:00
|
|
|
conds = []
|
|
|
|
for i in range(1, n + 1):
|
2022-12-13 11:26:36 +00:00
|
|
|
region = [{"X": (0, i / n), "Y": (0, (n + 1 - i) / n)}]
|
2018-11-09 18:59:28 -05:00
|
|
|
subst = {str(i): str(n + 1 - i)}
|
|
|
|
conds.append((region, subst))
|
|
|
|
overlaps = overlayFeatureVariations(conds)
|
|
|
|
assert len(overlaps) == n * (n + 1) // 2, overlaps
|
|
|
|
return conds, overlaps
|
|
|
|
|
2023-02-08 14:27:48 -07:00
|
|
|
|
2023-02-08 14:18:49 -07:00
|
|
|
def test_quadratic():
|
|
|
|
_test_quadratic(10)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2023-02-08 14:27:48 -07:00
|
|
|
|
2018-12-05 19:54:14 +01:00
|
|
|
def _merge_substitutions(substitutions):
|
|
|
|
merged = {}
|
|
|
|
for subst in substitutions:
|
|
|
|
merged.update(subst)
|
|
|
|
return merged
|
|
|
|
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-12-05 19:54:14 +01:00
|
|
|
def _match_condition(location, overlaps):
|
|
|
|
for box, substitutions in overlaps:
|
|
|
|
for tag, coord in location.items():
|
|
|
|
start, end = box[tag]
|
|
|
|
if start <= coord <= end:
|
|
|
|
return _merge_substitutions(substitutions)
|
|
|
|
return {} # no match
|
|
|
|
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-12-05 19:54:14 +01:00
|
|
|
def test_overlaps_1():
|
|
|
|
# https://github.com/fonttools/fonttools/issues/1400
|
|
|
|
conds = [
|
2022-12-13 11:26:36 +00:00
|
|
|
([{"abcd": (4, 9)}], {0: 0}),
|
|
|
|
([{"abcd": (5, 10)}], {1: 1}),
|
|
|
|
([{"abcd": (0, 8)}], {2: 2}),
|
|
|
|
([{"abcd": (3, 7)}], {3: 3}),
|
2018-12-05 19:54:14 +01:00
|
|
|
]
|
|
|
|
overlaps = overlayFeatureVariations(conds)
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 0}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {2: 2}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 1}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {2: 2}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 3}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {2: 2, 3: 3}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 4}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {0: 0, 2: 2, 3: 3}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 5}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {0: 0, 1: 1, 2: 2, 3: 3}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 7}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {0: 0, 1: 1, 2: 2, 3: 3}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 8}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {0: 0, 1: 1, 2: 2}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 9}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {0: 0, 1: 1}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 10}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {1: 1}
|
|
|
|
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-12-05 19:54:14 +01:00
|
|
|
def test_overlaps_2():
|
|
|
|
# https://github.com/fonttools/fonttools/issues/1400
|
|
|
|
conds = [
|
2022-12-13 11:26:36 +00:00
|
|
|
([{"abcd": (1, 9)}], {0: 0}),
|
|
|
|
([{"abcd": (8, 10)}], {1: 1}),
|
|
|
|
([{"abcd": (3, 4)}], {2: 2}),
|
|
|
|
([{"abcd": (1, 10)}], {3: 3}),
|
2018-12-05 19:54:14 +01:00
|
|
|
]
|
|
|
|
overlaps = overlayFeatureVariations(conds)
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 0}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 1}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {0: 0, 3: 3}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 2}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {0: 0, 3: 3}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 3}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {0: 0, 2: 2, 3: 3}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 5}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {0: 0, 3: 3}
|
2022-12-13 11:26:36 +00:00
|
|
|
subst = _match_condition({"abcd": 10}, overlaps)
|
2018-12-05 19:54:14 +01:00
|
|
|
assert subst == {1: 1, 3: 3}
|
|
|
|
|
2018-11-09 18:59:28 -05:00
|
|
|
|
2023-02-23 19:01:12 -07:00
|
|
|
def test_overlayBox():
|
|
|
|
# https://github.com/fonttools/fonttools/issues/3003
|
|
|
|
top = {"opsz": (0.75, 1.0), "wght": (0.5, 1.0)}
|
|
|
|
bot = {"wght": (0.25, 1.0)}
|
|
|
|
intersection, remainder = overlayBox(top, bot)
|
|
|
|
assert intersection == {"opsz": (0.75, 1.0), "wght": (0.5, 1.0)}
|
|
|
|
assert remainder == {"wght": (0.25, 1.0)}
|
|
|
|
|
|
|
|
|
2018-11-09 18:59:28 -05:00
|
|
|
def run(test, n, quiet):
|
|
|
|
print()
|
|
|
|
print("%s:" % test.__name__)
|
|
|
|
input, output = test(n)
|
2018-11-09 11:54:40 -05:00
|
|
|
if quiet:
|
|
|
|
print(len(output))
|
|
|
|
else:
|
2018-11-09 18:59:28 -05:00
|
|
|
print()
|
2018-11-09 11:54:40 -05:00
|
|
|
print("Input:")
|
|
|
|
pprint(input)
|
|
|
|
print()
|
|
|
|
print("Output:")
|
|
|
|
pprint(output)
|
2018-11-09 18:59:28 -05:00
|
|
|
print()
|
|
|
|
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-09 18:59:28 -05:00
|
|
|
if __name__ == "__main__":
|
|
|
|
import sys
|
|
|
|
from pprint import pprint
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-09 18:59:28 -05:00
|
|
|
quiet = False
|
|
|
|
n = 3
|
2022-12-13 11:26:36 +00:00
|
|
|
if len(sys.argv) > 1 and sys.argv[1] == "-q":
|
2018-11-09 18:59:28 -05:00
|
|
|
quiet = True
|
|
|
|
del sys.argv[1]
|
|
|
|
if len(sys.argv) > 1:
|
|
|
|
n = int(sys.argv[1])
|
|
|
|
|
2023-02-08 14:18:49 -07:00
|
|
|
run(_test_linear, n=n, quiet=quiet)
|
|
|
|
run(_test_quadratic, n=n, quiet=quiet)
|