If a FeatureVariationRecord with the same ConditionTable exists re-use it and append FeatureTableSubstitutionRecord’s. Without this, in the following feature code only the first lookup will be applied since there will be two FeatureVariationRecord with the same ConditionTable, so the first will be matched and the other will be skipped: conditionset test { wght 600 1000; wdth 150 200; } test; variation ccmp test { sub e by a; } ccmp; variation rlig test { sub b by c; } rlig; With this change only one FeatureVariationRecord will be created with two FeatureTableSubstitutionRecord’s.
307 lines
10 KiB
Python
307 lines
10 KiB
Python
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")]
|
|
|
|
|
|
def test_addFeatureVariations_existing_condition(varfont):
|
|
assert "GSUB" not in varfont
|
|
|
|
# Add a feature variation for 'ccmp' feature tag with a condition
|
|
addFeatureVariations(
|
|
varfont, [([{"wght": (0.5, 1.0)}], {"A": "A.alt"})], featureTag="ccmp"
|
|
)
|
|
|
|
gsub = varfont["GSUB"].table
|
|
|
|
# Should now have one feature record, one lookup, and one feature variation record
|
|
assert len(gsub.FeatureList.FeatureRecord) == 1
|
|
assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "ccmp"
|
|
assert len(gsub.LookupList.Lookup) == 1
|
|
assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
|
|
assert _substitution_features(gsub, rec_index=0) == [(0, "ccmp")]
|
|
|
|
# Add a feature variation for 'rlig' feature tag with the same condition
|
|
addFeatureVariations(
|
|
varfont, [([{"wght": (0.5, 1.0)}], {"B": "B.alt"})], featureTag="rlig"
|
|
)
|
|
|
|
# Should now have two feature records, two lookups, and one feature variation
|
|
# record, since the condition is the same for both feature variations
|
|
assert len(gsub.FeatureList.FeatureRecord) == 2
|
|
assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "ccmp"
|
|
assert gsub.FeatureList.FeatureRecord[1].FeatureTag == "rlig"
|
|
assert len(gsub.LookupList.Lookup) == 2
|
|
assert len(gsub.FeatureVariations.FeatureVariationRecord) == 1
|
|
assert _substitution_features(gsub, rec_index=0) == [(0, "ccmp"), (1, "rlig")]
|
|
|
|
|
|
def _test_linear(n):
|
|
conds = []
|
|
for i in range(n):
|
|
end = i / n
|
|
start = end - 1.0
|
|
region = [{"X": (start, end)}]
|
|
subst = {"g%.2g" % start: "g%.2g" % end}
|
|
conds.append((region, subst))
|
|
overlaps = overlayFeatureVariations(conds)
|
|
assert len(overlaps) == 2 * n - 1, overlaps
|
|
return conds, overlaps
|
|
|
|
|
|
def test_linear():
|
|
_test_linear(10)
|
|
|
|
|
|
def _test_quadratic(n):
|
|
conds = []
|
|
for i in range(1, n + 1):
|
|
region = [{"X": (0, i / n), "Y": (0, (n + 1 - i) / n)}]
|
|
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
|
|
|
|
|
|
def test_quadratic():
|
|
_test_quadratic(10)
|
|
|
|
|
|
def _merge_substitutions(substitutions):
|
|
merged = {}
|
|
for subst in substitutions:
|
|
merged.update(subst)
|
|
return merged
|
|
|
|
|
|
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
|
|
|
|
|
|
def test_overlaps_1():
|
|
# https://github.com/fonttools/fonttools/issues/1400
|
|
conds = [
|
|
([{"abcd": (4, 9)}], {0: 0}),
|
|
([{"abcd": (5, 10)}], {1: 1}),
|
|
([{"abcd": (0, 8)}], {2: 2}),
|
|
([{"abcd": (3, 7)}], {3: 3}),
|
|
]
|
|
overlaps = overlayFeatureVariations(conds)
|
|
subst = _match_condition({"abcd": 0}, overlaps)
|
|
assert subst == {2: 2}
|
|
subst = _match_condition({"abcd": 1}, overlaps)
|
|
assert subst == {2: 2}
|
|
subst = _match_condition({"abcd": 3}, overlaps)
|
|
assert subst == {2: 2, 3: 3}
|
|
subst = _match_condition({"abcd": 4}, overlaps)
|
|
assert subst == {0: 0, 2: 2, 3: 3}
|
|
subst = _match_condition({"abcd": 5}, overlaps)
|
|
assert subst == {0: 0, 1: 1, 2: 2, 3: 3}
|
|
subst = _match_condition({"abcd": 7}, overlaps)
|
|
assert subst == {0: 0, 1: 1, 2: 2, 3: 3}
|
|
subst = _match_condition({"abcd": 8}, overlaps)
|
|
assert subst == {0: 0, 1: 1, 2: 2}
|
|
subst = _match_condition({"abcd": 9}, overlaps)
|
|
assert subst == {0: 0, 1: 1}
|
|
subst = _match_condition({"abcd": 10}, overlaps)
|
|
assert subst == {1: 1}
|
|
|
|
|
|
def test_overlaps_2():
|
|
# https://github.com/fonttools/fonttools/issues/1400
|
|
conds = [
|
|
([{"abcd": (1, 9)}], {0: 0}),
|
|
([{"abcd": (8, 10)}], {1: 1}),
|
|
([{"abcd": (3, 4)}], {2: 2}),
|
|
([{"abcd": (1, 10)}], {3: 3}),
|
|
]
|
|
overlaps = overlayFeatureVariations(conds)
|
|
subst = _match_condition({"abcd": 0}, overlaps)
|
|
assert subst == {}
|
|
subst = _match_condition({"abcd": 1}, overlaps)
|
|
assert subst == {0: 0, 3: 3}
|
|
subst = _match_condition({"abcd": 2}, overlaps)
|
|
assert subst == {0: 0, 3: 3}
|
|
subst = _match_condition({"abcd": 3}, overlaps)
|
|
assert subst == {0: 0, 2: 2, 3: 3}
|
|
subst = _match_condition({"abcd": 5}, overlaps)
|
|
assert subst == {0: 0, 3: 3}
|
|
subst = _match_condition({"abcd": 10}, overlaps)
|
|
assert subst == {1: 1, 3: 3}
|
|
|
|
|
|
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)}
|
|
|
|
|
|
def run(test, n, quiet):
|
|
print()
|
|
print("%s:" % test.__name__)
|
|
input, output = test(n)
|
|
if quiet:
|
|
print(len(output))
|
|
else:
|
|
print()
|
|
print("Input:")
|
|
pprint(input)
|
|
print()
|
|
print("Output:")
|
|
pprint(output)
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
from pprint import pprint
|
|
|
|
quiet = False
|
|
n = 3
|
|
if len(sys.argv) > 1 and sys.argv[1] == "-q":
|
|
quiet = True
|
|
del sys.argv[1]
|
|
if len(sys.argv) > 1:
|
|
n = int(sys.argv[1])
|
|
|
|
run(_test_linear, n=n, quiet=quiet)
|
|
run(_test_quadratic, n=n, quiet=quiet)
|