1916 lines
66 KiB
Python
1916 lines
66 KiB
Python
from fontTools.misc.py23 import *
|
|
from fontTools.misc.fixedTools import floatToFixedToFloat
|
|
from fontTools import ttLib
|
|
from fontTools import designspaceLib
|
|
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
|
|
from fontTools.ttLib.tables import _f_v_a_r, _g_l_y_f
|
|
from fontTools.ttLib.tables import otTables
|
|
from fontTools.ttLib.tables.TupleVariation import TupleVariation
|
|
from fontTools import varLib
|
|
from fontTools.varLib import instancer
|
|
from fontTools.varLib.mvar import MVAR_ENTRIES
|
|
from fontTools.varLib import builder
|
|
from fontTools.varLib import featureVars
|
|
from fontTools.varLib import models
|
|
import collections
|
|
from copy import deepcopy
|
|
import logging
|
|
import os
|
|
import re
|
|
import pytest
|
|
|
|
|
|
TESTDATA = os.path.join(os.path.dirname(__file__), "data")
|
|
|
|
|
|
@pytest.fixture
|
|
def varfont():
|
|
f = ttLib.TTFont()
|
|
f.importXML(os.path.join(TESTDATA, "PartialInstancerTest-VF.ttx"))
|
|
return f
|
|
|
|
|
|
@pytest.fixture(params=[True, False], ids=["optimize", "no-optimize"])
|
|
def optimize(request):
|
|
return request.param
|
|
|
|
|
|
@pytest.fixture
|
|
def fvarAxes():
|
|
wght = _f_v_a_r.Axis()
|
|
wght.axisTag = Tag("wght")
|
|
wght.minValue = 100
|
|
wght.defaultValue = 400
|
|
wght.maxValue = 900
|
|
wdth = _f_v_a_r.Axis()
|
|
wdth.axisTag = Tag("wdth")
|
|
wdth.minValue = 70
|
|
wdth.defaultValue = 100
|
|
wdth.maxValue = 100
|
|
return [wght, wdth]
|
|
|
|
|
|
def _get_coordinates(varfont, glyphname):
|
|
# converts GlyphCoordinates to a list of (x, y) tuples, so that pytest's
|
|
# assert will give us a nicer diff
|
|
return list(varfont["glyf"].getCoordinatesAndControls(glyphname, varfont)[0])
|
|
|
|
|
|
class InstantiateGvarTest(object):
|
|
@pytest.mark.parametrize("glyph_name", ["hyphen"])
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
pytest.param(
|
|
{"wdth": -1.0},
|
|
{
|
|
"hyphen": [
|
|
(27, 229),
|
|
(27, 310),
|
|
(247, 310),
|
|
(247, 229),
|
|
(0, 0),
|
|
(274, 0),
|
|
(0, 536),
|
|
(0, 0),
|
|
]
|
|
},
|
|
id="wdth=-1.0",
|
|
),
|
|
pytest.param(
|
|
{"wdth": -0.5},
|
|
{
|
|
"hyphen": [
|
|
(33.5, 229),
|
|
(33.5, 308.5),
|
|
(264.5, 308.5),
|
|
(264.5, 229),
|
|
(0, 0),
|
|
(298, 0),
|
|
(0, 536),
|
|
(0, 0),
|
|
]
|
|
},
|
|
id="wdth=-0.5",
|
|
),
|
|
# an axis pinned at the default normalized location (0.0) means
|
|
# the default glyf outline stays the same
|
|
pytest.param(
|
|
{"wdth": 0.0},
|
|
{
|
|
"hyphen": [
|
|
(40, 229),
|
|
(40, 307),
|
|
(282, 307),
|
|
(282, 229),
|
|
(0, 0),
|
|
(322, 0),
|
|
(0, 536),
|
|
(0, 0),
|
|
]
|
|
},
|
|
id="wdth=0.0",
|
|
),
|
|
],
|
|
)
|
|
def test_pin_and_drop_axis(self, varfont, glyph_name, location, expected, optimize):
|
|
instancer.instantiateGvar(varfont, location, optimize=optimize)
|
|
|
|
assert _get_coordinates(varfont, glyph_name) == expected[glyph_name]
|
|
|
|
# check that the pinned axis has been dropped from gvar
|
|
assert not any(
|
|
"wdth" in t.axes
|
|
for tuples in varfont["gvar"].variations.values()
|
|
for t in tuples
|
|
)
|
|
|
|
def test_full_instance(self, varfont, optimize):
|
|
instancer.instantiateGvar(
|
|
varfont, {"wght": 0.0, "wdth": -0.5}, optimize=optimize
|
|
)
|
|
|
|
assert _get_coordinates(varfont, "hyphen") == [
|
|
(33.5, 229),
|
|
(33.5, 308.5),
|
|
(264.5, 308.5),
|
|
(264.5, 229),
|
|
(0, 0),
|
|
(298, 0),
|
|
(0, 536),
|
|
(0, 0),
|
|
]
|
|
|
|
assert "gvar" not in varfont
|
|
|
|
def test_composite_glyph_not_in_gvar(self, varfont):
|
|
""" The 'minus' glyph is a composite glyph, which references 'hyphen' as a
|
|
component, but has no tuple variations in gvar table, so the component offset
|
|
and the phantom points do not change; however the sidebearings and bounding box
|
|
do change as a result of the parent glyph 'hyphen' changing.
|
|
"""
|
|
hmtx = varfont["hmtx"]
|
|
vmtx = varfont["vmtx"]
|
|
|
|
hyphenCoords = _get_coordinates(varfont, "hyphen")
|
|
assert hyphenCoords == [
|
|
(40, 229),
|
|
(40, 307),
|
|
(282, 307),
|
|
(282, 229),
|
|
(0, 0),
|
|
(322, 0),
|
|
(0, 536),
|
|
(0, 0),
|
|
]
|
|
assert hmtx["hyphen"] == (322, 40)
|
|
assert vmtx["hyphen"] == (536, 229)
|
|
|
|
minusCoords = _get_coordinates(varfont, "minus")
|
|
assert minusCoords == [(0, 0), (0, 0), (422, 0), (0, 536), (0, 0)]
|
|
assert hmtx["minus"] == (422, 40)
|
|
assert vmtx["minus"] == (536, 229)
|
|
|
|
location = {"wght": -1.0, "wdth": -1.0}
|
|
|
|
instancer.instantiateGvar(varfont, location)
|
|
|
|
# check 'hyphen' coordinates changed
|
|
assert _get_coordinates(varfont, "hyphen") == [
|
|
(26, 259),
|
|
(26, 286),
|
|
(237, 286),
|
|
(237, 259),
|
|
(0, 0),
|
|
(263, 0),
|
|
(0, 536),
|
|
(0, 0),
|
|
]
|
|
# check 'minus' coordinates (i.e. component offset and phantom points)
|
|
# did _not_ change
|
|
assert _get_coordinates(varfont, "minus") == minusCoords
|
|
|
|
assert hmtx["hyphen"] == (263, 26)
|
|
assert vmtx["hyphen"] == (536, 250)
|
|
|
|
assert hmtx["minus"] == (422, 26) # 'minus' left sidebearing changed
|
|
assert vmtx["minus"] == (536, 250) # 'minus' top sidebearing too
|
|
|
|
|
|
class InstantiateCvarTest(object):
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
pytest.param({"wght": -1.0}, [500, -400, 150, 250], id="wght=-1.0"),
|
|
pytest.param({"wdth": -1.0}, [500, -400, 180, 200], id="wdth=-1.0"),
|
|
pytest.param({"wght": -0.5}, [500, -400, 165, 250], id="wght=-0.5"),
|
|
pytest.param({"wdth": -0.3}, [500, -400, 180, 235], id="wdth=-0.3"),
|
|
],
|
|
)
|
|
def test_pin_and_drop_axis(self, varfont, location, expected):
|
|
instancer.instantiateCvar(varfont, location)
|
|
|
|
assert list(varfont["cvt "].values) == expected
|
|
|
|
# check that the pinned axis has been dropped from cvar
|
|
pinned_axes = location.keys()
|
|
assert not any(
|
|
axis in t.axes for t in varfont["cvar"].variations for axis in pinned_axes
|
|
)
|
|
|
|
def test_full_instance(self, varfont):
|
|
instancer.instantiateCvar(varfont, {"wght": -0.5, "wdth": -0.5})
|
|
|
|
assert list(varfont["cvt "].values) == [500, -400, 165, 225]
|
|
|
|
assert "cvar" not in varfont
|
|
|
|
|
|
class InstantiateMVARTest(object):
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
pytest.param(
|
|
{"wght": 1.0},
|
|
{"strs": 100, "undo": -200, "unds": 150, "xhgt": 530},
|
|
id="wght=1.0",
|
|
),
|
|
pytest.param(
|
|
{"wght": 0.5},
|
|
{"strs": 75, "undo": -150, "unds": 100, "xhgt": 515},
|
|
id="wght=0.5",
|
|
),
|
|
pytest.param(
|
|
{"wght": 0.0},
|
|
{"strs": 50, "undo": -100, "unds": 50, "xhgt": 500},
|
|
id="wght=0.0",
|
|
),
|
|
pytest.param(
|
|
{"wdth": -1.0},
|
|
{"strs": 20, "undo": -100, "unds": 50, "xhgt": 500},
|
|
id="wdth=-1.0",
|
|
),
|
|
pytest.param(
|
|
{"wdth": -0.5},
|
|
{"strs": 35, "undo": -100, "unds": 50, "xhgt": 500},
|
|
id="wdth=-0.5",
|
|
),
|
|
pytest.param(
|
|
{"wdth": 0.0},
|
|
{"strs": 50, "undo": -100, "unds": 50, "xhgt": 500},
|
|
id="wdth=0.0",
|
|
),
|
|
],
|
|
)
|
|
def test_pin_and_drop_axis(self, varfont, location, expected):
|
|
mvar = varfont["MVAR"].table
|
|
# initially we have two VarData: the first contains deltas associated with 3
|
|
# regions: 1 with only wght, 1 with only wdth, and 1 with both wght and wdth
|
|
assert len(mvar.VarStore.VarData) == 2
|
|
assert mvar.VarStore.VarRegionList.RegionCount == 3
|
|
assert mvar.VarStore.VarData[0].VarRegionCount == 3
|
|
assert all(len(item) == 3 for item in mvar.VarStore.VarData[0].Item)
|
|
# The second VarData has deltas associated only with 1 region (wght only).
|
|
assert mvar.VarStore.VarData[1].VarRegionCount == 1
|
|
assert all(len(item) == 1 for item in mvar.VarStore.VarData[1].Item)
|
|
|
|
instancer.instantiateMVAR(varfont, location)
|
|
|
|
for mvar_tag, expected_value in expected.items():
|
|
table_tag, item_name = MVAR_ENTRIES[mvar_tag]
|
|
assert getattr(varfont[table_tag], item_name) == expected_value
|
|
|
|
# check that regions and accompanying deltas have been dropped
|
|
num_regions_left = len(mvar.VarStore.VarRegionList.Region)
|
|
assert num_regions_left < 3
|
|
assert mvar.VarStore.VarRegionList.RegionCount == num_regions_left
|
|
assert mvar.VarStore.VarData[0].VarRegionCount == num_regions_left
|
|
# VarData subtables have been merged
|
|
assert len(mvar.VarStore.VarData) == 1
|
|
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
pytest.param(
|
|
{"wght": 1.0, "wdth": 0.0},
|
|
{"strs": 100, "undo": -200, "unds": 150},
|
|
id="wght=1.0,wdth=0.0",
|
|
),
|
|
pytest.param(
|
|
{"wght": 0.0, "wdth": -1.0},
|
|
{"strs": 20, "undo": -100, "unds": 50},
|
|
id="wght=0.0,wdth=-1.0",
|
|
),
|
|
pytest.param(
|
|
{"wght": 0.5, "wdth": -0.5},
|
|
{"strs": 55, "undo": -145, "unds": 95},
|
|
id="wght=0.5,wdth=-0.5",
|
|
),
|
|
pytest.param(
|
|
{"wght": 1.0, "wdth": -1.0},
|
|
{"strs": 50, "undo": -180, "unds": 130},
|
|
id="wght=0.5,wdth=-0.5",
|
|
),
|
|
],
|
|
)
|
|
def test_full_instance(self, varfont, location, expected):
|
|
instancer.instantiateMVAR(varfont, location)
|
|
|
|
for mvar_tag, expected_value in expected.items():
|
|
table_tag, item_name = MVAR_ENTRIES[mvar_tag]
|
|
assert getattr(varfont[table_tag], item_name) == expected_value
|
|
|
|
assert "MVAR" not in varfont
|
|
|
|
|
|
class InstantiateHVARTest(object):
|
|
# the 'expectedDeltas' below refer to the VarData item deltas for the "hyphen"
|
|
# glyph in the PartialInstancerTest-VF.ttx test font, that are left after
|
|
# partial instancing
|
|
@pytest.mark.parametrize(
|
|
"location, expectedRegions, expectedDeltas",
|
|
[
|
|
({"wght": -1.0}, [{"wdth": (-1.0, -1.0, 0)}], [-59]),
|
|
({"wght": 0}, [{"wdth": (-1.0, -1.0, 0)}], [-48]),
|
|
({"wght": 1.0}, [{"wdth": (-1.0, -1.0, 0)}], [7]),
|
|
(
|
|
{"wdth": -1.0},
|
|
[
|
|
{"wght": (-1.0, -1.0, 0.0)},
|
|
{"wght": (0.0, 0.6099854, 1.0)},
|
|
{"wght": (0.6099854, 1.0, 1.0)},
|
|
],
|
|
[-11, 31, 51],
|
|
),
|
|
({"wdth": 0}, [{"wght": (0.6099854, 1.0, 1.0)}], [-4]),
|
|
],
|
|
)
|
|
def test_partial_instance(self, varfont, location, expectedRegions, expectedDeltas):
|
|
instancer.instantiateHVAR(varfont, location)
|
|
|
|
assert "HVAR" in varfont
|
|
hvar = varfont["HVAR"].table
|
|
varStore = hvar.VarStore
|
|
|
|
regions = varStore.VarRegionList.Region
|
|
fvarAxes = [a for a in varfont["fvar"].axes if a.axisTag not in location]
|
|
regionDicts = [reg.get_support(fvarAxes) for reg in regions]
|
|
assert len(regionDicts) == len(expectedRegions)
|
|
for region, expectedRegion in zip(regionDicts, expectedRegions):
|
|
assert region.keys() == expectedRegion.keys()
|
|
for axisTag, support in region.items():
|
|
assert support == pytest.approx(expectedRegion[axisTag])
|
|
|
|
assert len(varStore.VarData) == 1
|
|
assert varStore.VarData[0].ItemCount == 2
|
|
|
|
assert hvar.AdvWidthMap is not None
|
|
advWithMap = hvar.AdvWidthMap.mapping
|
|
|
|
assert advWithMap[".notdef"] == advWithMap["space"]
|
|
varIdx = advWithMap[".notdef"]
|
|
# these glyphs have no metrics variations in the test font
|
|
assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == (
|
|
[0] * varStore.VarData[0].VarRegionCount
|
|
)
|
|
|
|
varIdx = advWithMap["hyphen"]
|
|
assert varStore.VarData[varIdx >> 16].Item[varIdx & 0xFFFF] == expectedDeltas
|
|
|
|
def test_full_instance(self, varfont):
|
|
instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0})
|
|
|
|
assert "HVAR" not in varfont
|
|
|
|
def test_partial_instance_keep_empty_table(self, varfont):
|
|
# Append an additional dummy axis to fvar, for which the current HVAR table
|
|
# in our test 'varfont' contains no variation data.
|
|
# Instancing the other two wght and wdth axes should leave HVAR table empty,
|
|
# to signal there are variations to the glyph's advance widths.
|
|
fvar = varfont["fvar"]
|
|
axis = _f_v_a_r.Axis()
|
|
axis.axisTag = "TEST"
|
|
fvar.axes.append(axis)
|
|
|
|
instancer.instantiateHVAR(varfont, {"wght": 0, "wdth": 0})
|
|
|
|
assert "HVAR" in varfont
|
|
|
|
varStore = varfont["HVAR"].table.VarStore
|
|
|
|
assert varStore.VarRegionList.RegionCount == 0
|
|
assert not varStore.VarRegionList.Region
|
|
assert varStore.VarRegionList.RegionAxisCount == 1
|
|
|
|
|
|
class InstantiateItemVariationStoreTest(object):
|
|
def test_VarRegion_get_support(self):
|
|
axisOrder = ["wght", "wdth", "opsz"]
|
|
regionAxes = {"wdth": (-1.0, -1.0, 0.0), "wght": (0.0, 1.0, 1.0)}
|
|
region = builder.buildVarRegion(regionAxes, axisOrder)
|
|
|
|
assert len(region.VarRegionAxis) == 3
|
|
assert region.VarRegionAxis[2].PeakCoord == 0
|
|
|
|
fvarAxes = [SimpleNamespace(axisTag=axisTag) for axisTag in axisOrder]
|
|
|
|
assert region.get_support(fvarAxes) == regionAxes
|
|
|
|
@pytest.fixture
|
|
def varStore(self):
|
|
return builder.buildVarStore(
|
|
builder.buildVarRegionList(
|
|
[
|
|
{"wght": (-1.0, -1.0, 0)},
|
|
{"wght": (0, 0.5, 1.0)},
|
|
{"wght": (0.5, 1.0, 1.0)},
|
|
{"wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
|
|
],
|
|
["wght", "wdth"],
|
|
),
|
|
[
|
|
builder.buildVarData([0, 1, 2], [[100, 100, 100], [100, 100, 100]]),
|
|
builder.buildVarData(
|
|
[3, 4, 5, 6], [[100, 100, 100, 100], [100, 100, 100, 100]]
|
|
),
|
|
],
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"location, expected_deltas, num_regions",
|
|
[
|
|
({"wght": 0}, [[0, 0], [0, 0]], 1),
|
|
({"wght": 0.25}, [[50, 50], [0, 0]], 1),
|
|
({"wdth": 0}, [[0, 0], [0, 0]], 3),
|
|
({"wdth": -0.75}, [[0, 0], [75, 75]], 3),
|
|
({"wght": 0, "wdth": 0}, [[0, 0], [0, 0]], 0),
|
|
({"wght": 0.25, "wdth": 0}, [[50, 50], [0, 0]], 0),
|
|
({"wght": 0, "wdth": -0.75}, [[0, 0], [75, 75]], 0),
|
|
],
|
|
)
|
|
def test_instantiate_default_deltas(
|
|
self, varStore, fvarAxes, location, expected_deltas, num_regions
|
|
):
|
|
defaultDeltas = instancer.instantiateItemVariationStore(
|
|
varStore, fvarAxes, location
|
|
)
|
|
|
|
defaultDeltaArray = []
|
|
for varidx, delta in sorted(defaultDeltas.items()):
|
|
major, minor = varidx >> 16, varidx & 0xFFFF
|
|
if major == len(defaultDeltaArray):
|
|
defaultDeltaArray.append([])
|
|
assert len(defaultDeltaArray[major]) == minor
|
|
defaultDeltaArray[major].append(delta)
|
|
|
|
assert defaultDeltaArray == expected_deltas
|
|
assert varStore.VarRegionList.RegionCount == num_regions
|
|
|
|
|
|
class TupleVarStoreAdapterTest(object):
|
|
def test_instantiate(self):
|
|
regions = [
|
|
{"wght": (-1.0, -1.0, 0)},
|
|
{"wght": (0.0, 1.0, 1.0)},
|
|
{"wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
|
|
]
|
|
axisOrder = ["wght", "wdth"]
|
|
tupleVarData = [
|
|
[
|
|
TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]),
|
|
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [30, 90]),
|
|
TupleVariation(
|
|
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100]
|
|
),
|
|
TupleVariation(
|
|
{"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120]
|
|
),
|
|
],
|
|
[
|
|
TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]),
|
|
TupleVariation(
|
|
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55]
|
|
),
|
|
TupleVariation(
|
|
{"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75]
|
|
),
|
|
],
|
|
]
|
|
adapter = instancer._TupleVarStoreAdapter(
|
|
regions, axisOrder, tupleVarData, itemCounts=[2, 2]
|
|
)
|
|
|
|
defaultDeltaArray = adapter.instantiate({"wght": 0.5})
|
|
|
|
assert defaultDeltaArray == [[15, 45], [0, 0]]
|
|
assert adapter.regions == [{"wdth": (-1.0, -1.0, 0)}]
|
|
assert adapter.tupleVarData == [
|
|
[TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-30, -60])],
|
|
[TupleVariation({"wdth": (-1.0, -1.0, 0)}, [-12, 8])],
|
|
]
|
|
|
|
def test_rebuildRegions(self):
|
|
regions = [
|
|
{"wght": (-1.0, -1.0, 0)},
|
|
{"wght": (0.0, 1.0, 1.0)},
|
|
{"wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (0, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
|
|
]
|
|
axisOrder = ["wght", "wdth"]
|
|
variations = []
|
|
for region in regions:
|
|
variations.append(TupleVariation(region, [100]))
|
|
tupleVarData = [variations[:3], variations[3:]]
|
|
adapter = instancer._TupleVarStoreAdapter(
|
|
regions, axisOrder, tupleVarData, itemCounts=[1, 1]
|
|
)
|
|
|
|
adapter.rebuildRegions()
|
|
|
|
assert adapter.regions == regions
|
|
|
|
del tupleVarData[0][2]
|
|
tupleVarData[1][0].axes = {"wght": (-1.0, -0.5, 0)}
|
|
tupleVarData[1][1].axes = {"wght": (0, 0.5, 1.0)}
|
|
|
|
adapter.rebuildRegions()
|
|
|
|
assert adapter.regions == [
|
|
{"wght": (-1.0, -1.0, 0)},
|
|
{"wght": (0.0, 1.0, 1.0)},
|
|
{"wght": (-1.0, -0.5, 0)},
|
|
{"wght": (0, 0.5, 1.0)},
|
|
]
|
|
|
|
def test_roundtrip(self, fvarAxes):
|
|
regions = [
|
|
{"wght": (-1.0, -1.0, 0)},
|
|
{"wght": (0, 0.5, 1.0)},
|
|
{"wght": (0.5, 1.0, 1.0)},
|
|
{"wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)},
|
|
{"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)},
|
|
]
|
|
axisOrder = [axis.axisTag for axis in fvarAxes]
|
|
|
|
itemVarStore = builder.buildVarStore(
|
|
builder.buildVarRegionList(regions, axisOrder),
|
|
[
|
|
builder.buildVarData(
|
|
[0, 1, 2, 4, 5, 6],
|
|
[[10, -20, 30, -40, 50, -60], [70, -80, 90, -100, 110, -120]],
|
|
),
|
|
builder.buildVarData(
|
|
[3, 4, 5, 6], [[5, -15, 25, -35], [45, -55, 65, -75]]
|
|
),
|
|
],
|
|
)
|
|
|
|
adapter = instancer._TupleVarStoreAdapter.fromItemVarStore(
|
|
itemVarStore, fvarAxes
|
|
)
|
|
|
|
assert adapter.tupleVarData == [
|
|
[
|
|
TupleVariation({"wght": (-1.0, -1.0, 0)}, [10, 70]),
|
|
TupleVariation({"wght": (0, 0.5, 1.0)}, [-20, -80]),
|
|
TupleVariation({"wght": (0.5, 1.0, 1.0)}, [30, 90]),
|
|
TupleVariation(
|
|
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-40, -100]
|
|
),
|
|
TupleVariation(
|
|
{"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [50, 110]
|
|
),
|
|
TupleVariation(
|
|
{"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-60, -120]
|
|
),
|
|
],
|
|
[
|
|
TupleVariation({"wdth": (-1.0, -1.0, 0)}, [5, 45]),
|
|
TupleVariation(
|
|
{"wght": (-1.0, -1.0, 0), "wdth": (-1.0, -1.0, 0)}, [-15, -55]
|
|
),
|
|
TupleVariation(
|
|
{"wght": (0, 0.5, 1.0), "wdth": (-1.0, -1.0, 0)}, [25, 65]
|
|
),
|
|
TupleVariation(
|
|
{"wght": (0.5, 1.0, 1.0), "wdth": (-1.0, -1.0, 0)}, [-35, -75]
|
|
),
|
|
],
|
|
]
|
|
assert adapter.itemCounts == [data.ItemCount for data in itemVarStore.VarData]
|
|
assert adapter.regions == regions
|
|
assert adapter.axisOrder == axisOrder
|
|
|
|
itemVarStore2 = adapter.asItemVarStore()
|
|
|
|
assert [
|
|
reg.get_support(fvarAxes) for reg in itemVarStore2.VarRegionList.Region
|
|
] == regions
|
|
|
|
assert itemVarStore2.VarDataCount == 2
|
|
assert itemVarStore2.VarData[0].VarRegionIndex == [0, 1, 2, 4, 5, 6]
|
|
assert itemVarStore2.VarData[0].Item == [
|
|
[10, -20, 30, -40, 50, -60],
|
|
[70, -80, 90, -100, 110, -120],
|
|
]
|
|
assert itemVarStore2.VarData[1].VarRegionIndex == [3, 4, 5, 6]
|
|
assert itemVarStore2.VarData[1].Item == [[5, -15, 25, -35], [45, -55, 65, -75]]
|
|
|
|
|
|
def makeTTFont(glyphOrder, features):
|
|
font = ttLib.TTFont()
|
|
font.setGlyphOrder(glyphOrder)
|
|
addOpenTypeFeaturesFromString(font, features)
|
|
font["name"] = ttLib.newTable("name")
|
|
return font
|
|
|
|
|
|
def _makeDSAxesDict(axes):
|
|
dsAxes = collections.OrderedDict()
|
|
for axisTag, axisValues in axes:
|
|
axis = designspaceLib.AxisDescriptor()
|
|
axis.name = axis.tag = axis.labelNames["en"] = axisTag
|
|
axis.minimum, axis.default, axis.maximum = axisValues
|
|
dsAxes[axis.tag] = axis
|
|
return dsAxes
|
|
|
|
|
|
def makeVariableFont(masters, baseIndex, axes, masterLocations):
|
|
vf = deepcopy(masters[baseIndex])
|
|
dsAxes = _makeDSAxesDict(axes)
|
|
fvar = varLib._add_fvar(vf, dsAxes, instances=())
|
|
axisTags = [axis.axisTag for axis in fvar.axes]
|
|
normalizedLocs = [models.normalizeLocation(m, dict(axes)) for m in masterLocations]
|
|
model = models.VariationModel(normalizedLocs, axisOrder=axisTags)
|
|
varLib._merge_OTL(vf, model, masters, axisTags)
|
|
return vf
|
|
|
|
|
|
def makeParametrizedVF(glyphOrder, features, values, increments):
|
|
# Create a test VF with given glyphs and parametrized OTL features.
|
|
# The VF is built from 9 masters (3 x 3 along wght and wdth), with
|
|
# locations hard-coded and base master at wght=400 and wdth=100.
|
|
# 'values' is a list of initial values that are interpolated in the
|
|
# 'features' string, and incremented for each subsequent master by the
|
|
# given 'increments' (list of 2-tuple) along the two axes.
|
|
assert values and len(values) == len(increments)
|
|
assert all(len(i) == 2 for i in increments)
|
|
masterLocations = [
|
|
{"wght": 100, "wdth": 50},
|
|
{"wght": 100, "wdth": 100},
|
|
{"wght": 100, "wdth": 150},
|
|
{"wght": 400, "wdth": 50},
|
|
{"wght": 400, "wdth": 100}, # base master
|
|
{"wght": 400, "wdth": 150},
|
|
{"wght": 700, "wdth": 50},
|
|
{"wght": 700, "wdth": 100},
|
|
{"wght": 700, "wdth": 150},
|
|
]
|
|
n = len(values)
|
|
values = list(values)
|
|
masters = []
|
|
for _ in range(3):
|
|
for _ in range(3):
|
|
master = makeTTFont(glyphOrder, features=features % tuple(values))
|
|
masters.append(master)
|
|
for i in range(n):
|
|
values[i] += increments[i][1]
|
|
for i in range(n):
|
|
values[i] += increments[i][0]
|
|
baseIndex = 4
|
|
axes = [("wght", (100, 400, 700)), ("wdth", (50, 100, 150))]
|
|
vf = makeVariableFont(masters, baseIndex, axes, masterLocations)
|
|
return vf
|
|
|
|
|
|
@pytest.fixture
|
|
def varfontGDEF():
|
|
glyphOrder = [".notdef", "f", "i", "f_i"]
|
|
features = (
|
|
"feature liga { sub f i by f_i;} liga;"
|
|
"table GDEF { LigatureCaretByPos f_i %d; } GDEF;"
|
|
)
|
|
values = [100]
|
|
increments = [(+30, +10)]
|
|
return makeParametrizedVF(glyphOrder, features, values, increments)
|
|
|
|
|
|
@pytest.fixture
|
|
def varfontGPOS():
|
|
glyphOrder = [".notdef", "V", "A"]
|
|
features = "feature kern { pos V A %d; } kern;"
|
|
values = [-80]
|
|
increments = [(-10, -5)]
|
|
return makeParametrizedVF(glyphOrder, features, values, increments)
|
|
|
|
|
|
@pytest.fixture
|
|
def varfontGPOS2():
|
|
glyphOrder = [".notdef", "V", "A", "acutecomb"]
|
|
features = (
|
|
"markClass [acutecomb] <anchor 150 -10> @TOP_MARKS;"
|
|
"feature mark {"
|
|
" pos base A <anchor %d 450> mark @TOP_MARKS;"
|
|
"} mark;"
|
|
"feature kern {"
|
|
" pos V A %d;"
|
|
"} kern;"
|
|
)
|
|
values = [200, -80]
|
|
increments = [(+30, +10), (-10, -5)]
|
|
return makeParametrizedVF(glyphOrder, features, values, increments)
|
|
|
|
|
|
class InstantiateOTLTest(object):
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
({"wght": -1.0}, 110), # -60
|
|
({"wght": 0}, 170),
|
|
({"wght": 0.5}, 200), # +30
|
|
({"wght": 1.0}, 230), # +60
|
|
({"wdth": -1.0}, 160), # -10
|
|
({"wdth": -0.3}, 167), # -3
|
|
({"wdth": 0}, 170),
|
|
({"wdth": 1.0}, 180), # +10
|
|
],
|
|
)
|
|
def test_pin_and_drop_axis_GDEF(self, varfontGDEF, location, expected):
|
|
vf = varfontGDEF
|
|
assert "GDEF" in vf
|
|
|
|
instancer.instantiateOTL(vf, location)
|
|
|
|
assert "GDEF" in vf
|
|
gdef = vf["GDEF"].table
|
|
assert gdef.Version == 0x00010003
|
|
assert gdef.VarStore
|
|
assert gdef.LigCaretList
|
|
caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0]
|
|
assert caretValue.Format == 3
|
|
assert hasattr(caretValue, "DeviceTable")
|
|
assert caretValue.DeviceTable.DeltaFormat == 0x8000
|
|
assert caretValue.Coordinate == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
({"wght": -1.0, "wdth": -1.0}, 100), # -60 - 10
|
|
({"wght": -1.0, "wdth": 0.0}, 110), # -60
|
|
({"wght": -1.0, "wdth": 1.0}, 120), # -60 + 10
|
|
({"wght": 0.0, "wdth": -1.0}, 160), # -10
|
|
({"wght": 0.0, "wdth": 0.0}, 170),
|
|
({"wght": 0.0, "wdth": 1.0}, 180), # +10
|
|
({"wght": 1.0, "wdth": -1.0}, 220), # +60 - 10
|
|
({"wght": 1.0, "wdth": 0.0}, 230), # +60
|
|
({"wght": 1.0, "wdth": 1.0}, 240), # +60 + 10
|
|
],
|
|
)
|
|
def test_full_instance_GDEF(self, varfontGDEF, location, expected):
|
|
vf = varfontGDEF
|
|
assert "GDEF" in vf
|
|
|
|
instancer.instantiateOTL(vf, location)
|
|
|
|
assert "GDEF" in vf
|
|
gdef = vf["GDEF"].table
|
|
assert gdef.Version == 0x00010000
|
|
assert not hasattr(gdef, "VarStore")
|
|
assert gdef.LigCaretList
|
|
caretValue = gdef.LigCaretList.LigGlyph[0].CaretValue[0]
|
|
assert caretValue.Format == 1
|
|
assert not hasattr(caretValue, "DeviceTable")
|
|
assert caretValue.Coordinate == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
({"wght": -1.0}, -85), # +25
|
|
({"wght": 0}, -110),
|
|
({"wght": 1.0}, -135), # -25
|
|
({"wdth": -1.0}, -105), # +5
|
|
({"wdth": 0}, -110),
|
|
({"wdth": 1.0}, -115), # -5
|
|
],
|
|
)
|
|
def test_pin_and_drop_axis_GPOS_kern(self, varfontGPOS, location, expected):
|
|
vf = varfontGPOS
|
|
assert "GDEF" in vf
|
|
assert "GPOS" in vf
|
|
|
|
instancer.instantiateOTL(vf, location)
|
|
|
|
gdef = vf["GDEF"].table
|
|
gpos = vf["GPOS"].table
|
|
assert gdef.Version == 0x00010003
|
|
assert gdef.VarStore
|
|
|
|
assert gpos.LookupList.Lookup[0].LookupType == 2 # PairPos
|
|
pairPos = gpos.LookupList.Lookup[0].SubTable[0]
|
|
valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
|
|
assert valueRec1.XAdvDevice
|
|
assert valueRec1.XAdvDevice.DeltaFormat == 0x8000
|
|
assert valueRec1.XAdvance == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
({"wght": -1.0, "wdth": -1.0}, -80), # +25 + 5
|
|
({"wght": -1.0, "wdth": 0.0}, -85), # +25
|
|
({"wght": -1.0, "wdth": 1.0}, -90), # +25 - 5
|
|
({"wght": 0.0, "wdth": -1.0}, -105), # +5
|
|
({"wght": 0.0, "wdth": 0.0}, -110),
|
|
({"wght": 0.0, "wdth": 1.0}, -115), # -5
|
|
({"wght": 1.0, "wdth": -1.0}, -130), # -25 + 5
|
|
({"wght": 1.0, "wdth": 0.0}, -135), # -25
|
|
({"wght": 1.0, "wdth": 1.0}, -140), # -25 - 5
|
|
],
|
|
)
|
|
def test_full_instance_GPOS_kern(self, varfontGPOS, location, expected):
|
|
vf = varfontGPOS
|
|
assert "GDEF" in vf
|
|
assert "GPOS" in vf
|
|
|
|
instancer.instantiateOTL(vf, location)
|
|
|
|
assert "GDEF" not in vf
|
|
gpos = vf["GPOS"].table
|
|
|
|
assert gpos.LookupList.Lookup[0].LookupType == 2 # PairPos
|
|
pairPos = gpos.LookupList.Lookup[0].SubTable[0]
|
|
valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
|
|
assert not hasattr(valueRec1, "XAdvDevice")
|
|
assert valueRec1.XAdvance == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
({"wght": -1.0}, (210, -85)), # -60, +25
|
|
({"wght": 0}, (270, -110)),
|
|
({"wght": 0.5}, (300, -122)), # +30, -12
|
|
({"wght": 1.0}, (330, -135)), # +60, -25
|
|
({"wdth": -1.0}, (260, -105)), # -10, +5
|
|
({"wdth": -0.3}, (267, -108)), # -3, +2
|
|
({"wdth": 0}, (270, -110)),
|
|
({"wdth": 1.0}, (280, -115)), # +10, -5
|
|
],
|
|
)
|
|
def test_pin_and_drop_axis_GPOS_mark_and_kern(
|
|
self, varfontGPOS2, location, expected
|
|
):
|
|
vf = varfontGPOS2
|
|
assert "GDEF" in vf
|
|
assert "GPOS" in vf
|
|
|
|
instancer.instantiateOTL(vf, location)
|
|
|
|
v1, v2 = expected
|
|
gdef = vf["GDEF"].table
|
|
gpos = vf["GPOS"].table
|
|
assert gdef.Version == 0x00010003
|
|
assert gdef.VarStore
|
|
assert gdef.GlyphClassDef
|
|
|
|
assert gpos.LookupList.Lookup[0].LookupType == 4 # MarkBasePos
|
|
markBasePos = gpos.LookupList.Lookup[0].SubTable[0]
|
|
baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0]
|
|
assert baseAnchor.Format == 3
|
|
assert baseAnchor.XDeviceTable
|
|
assert baseAnchor.XDeviceTable.DeltaFormat == 0x8000
|
|
assert not baseAnchor.YDeviceTable
|
|
assert baseAnchor.XCoordinate == v1
|
|
assert baseAnchor.YCoordinate == 450
|
|
|
|
assert gpos.LookupList.Lookup[1].LookupType == 2 # PairPos
|
|
pairPos = gpos.LookupList.Lookup[1].SubTable[0]
|
|
valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
|
|
assert valueRec1.XAdvDevice
|
|
assert valueRec1.XAdvDevice.DeltaFormat == 0x8000
|
|
assert valueRec1.XAdvance == v2
|
|
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
({"wght": -1.0, "wdth": -1.0}, (200, -80)), # -60 - 10, +25 + 5
|
|
({"wght": -1.0, "wdth": 0.0}, (210, -85)), # -60, +25
|
|
({"wght": -1.0, "wdth": 1.0}, (220, -90)), # -60 + 10, +25 - 5
|
|
({"wght": 0.0, "wdth": -1.0}, (260, -105)), # -10, +5
|
|
({"wght": 0.0, "wdth": 0.0}, (270, -110)),
|
|
({"wght": 0.0, "wdth": 1.0}, (280, -115)), # +10, -5
|
|
({"wght": 1.0, "wdth": -1.0}, (320, -130)), # +60 - 10, -25 + 5
|
|
({"wght": 1.0, "wdth": 0.0}, (330, -135)), # +60, -25
|
|
({"wght": 1.0, "wdth": 1.0}, (340, -140)), # +60 + 10, -25 - 5
|
|
],
|
|
)
|
|
def test_full_instance_GPOS_mark_and_kern(self, varfontGPOS2, location, expected):
|
|
vf = varfontGPOS2
|
|
assert "GDEF" in vf
|
|
assert "GPOS" in vf
|
|
|
|
instancer.instantiateOTL(vf, location)
|
|
|
|
v1, v2 = expected
|
|
gdef = vf["GDEF"].table
|
|
gpos = vf["GPOS"].table
|
|
assert gdef.Version == 0x00010000
|
|
assert not hasattr(gdef, "VarStore")
|
|
assert gdef.GlyphClassDef
|
|
|
|
assert gpos.LookupList.Lookup[0].LookupType == 4 # MarkBasePos
|
|
markBasePos = gpos.LookupList.Lookup[0].SubTable[0]
|
|
baseAnchor = markBasePos.BaseArray.BaseRecord[0].BaseAnchor[0]
|
|
assert baseAnchor.Format == 1
|
|
assert not hasattr(baseAnchor, "XDeviceTable")
|
|
assert not hasattr(baseAnchor, "YDeviceTable")
|
|
assert baseAnchor.XCoordinate == v1
|
|
assert baseAnchor.YCoordinate == 450
|
|
|
|
assert gpos.LookupList.Lookup[1].LookupType == 2 # PairPos
|
|
pairPos = gpos.LookupList.Lookup[1].SubTable[0]
|
|
valueRec1 = pairPos.PairSet[0].PairValueRecord[0].Value1
|
|
assert not hasattr(valueRec1, "XAdvDevice")
|
|
assert valueRec1.XAdvance == v2
|
|
|
|
|
|
class InstantiateAvarTest(object):
|
|
@pytest.mark.parametrize("location", [{"wght": 0.0}, {"wdth": 0.0}])
|
|
def test_pin_and_drop_axis(self, varfont, location):
|
|
instancer.instantiateAvar(varfont, location)
|
|
|
|
assert set(varfont["avar"].segments).isdisjoint(location)
|
|
|
|
def test_full_instance(self, varfont):
|
|
instancer.instantiateAvar(varfont, {"wght": 0.0, "wdth": 0.0})
|
|
|
|
assert "avar" not in varfont
|
|
|
|
@staticmethod
|
|
def quantizeF2Dot14Floats(mapping):
|
|
return {
|
|
floatToFixedToFloat(k, 14): floatToFixedToFloat(v, 14)
|
|
for k, v in mapping.items()
|
|
}
|
|
|
|
# the following values come from NotoSans-VF.ttf
|
|
DFLT_WGHT_MAPPING = {
|
|
-1.0: -1.0,
|
|
-0.6667: -0.7969,
|
|
-0.3333: -0.5,
|
|
0: 0,
|
|
0.2: 0.18,
|
|
0.4: 0.38,
|
|
0.6: 0.61,
|
|
0.8: 0.79,
|
|
1.0: 1.0,
|
|
}
|
|
|
|
DFLT_WDTH_MAPPING = {-1.0: -1.0, -0.6667: -0.7, -0.3333: -0.36664, 0: 0, 1.0: 1.0}
|
|
|
|
@pytest.fixture
|
|
def varfont(self):
|
|
fvarAxes = ("wght", (100, 400, 900)), ("wdth", (62.5, 100, 100))
|
|
avarSegments = {
|
|
"wght": self.quantizeF2Dot14Floats(self.DFLT_WGHT_MAPPING),
|
|
"wdth": self.quantizeF2Dot14Floats(self.DFLT_WDTH_MAPPING),
|
|
}
|
|
varfont = ttLib.TTFont()
|
|
varfont["name"] = ttLib.newTable("name")
|
|
varLib._add_fvar(varfont, _makeDSAxesDict(fvarAxes), instances=())
|
|
avar = varfont["avar"] = ttLib.newTable("avar")
|
|
avar.segments = avarSegments
|
|
return varfont
|
|
|
|
@pytest.mark.parametrize(
|
|
"axisLimits, expectedSegments",
|
|
[
|
|
pytest.param(
|
|
{"wght": (100, 900)},
|
|
{"wght": DFLT_WGHT_MAPPING, "wdth": DFLT_WDTH_MAPPING},
|
|
id="wght=100:900",
|
|
),
|
|
pytest.param(
|
|
{"wght": (400, 900)},
|
|
{
|
|
"wght": {
|
|
-1.0: -1.0,
|
|
0: 0,
|
|
0.2: 0.18,
|
|
0.4: 0.38,
|
|
0.6: 0.61,
|
|
0.8: 0.79,
|
|
1.0: 1.0,
|
|
},
|
|
"wdth": DFLT_WDTH_MAPPING,
|
|
},
|
|
id="wght=400:900",
|
|
),
|
|
pytest.param(
|
|
{"wght": (100, 400)},
|
|
{
|
|
"wght": {
|
|
-1.0: -1.0,
|
|
-0.6667: -0.7969,
|
|
-0.3333: -0.5,
|
|
0: 0,
|
|
1.0: 1.0,
|
|
},
|
|
"wdth": DFLT_WDTH_MAPPING,
|
|
},
|
|
id="wght=100:400",
|
|
),
|
|
pytest.param(
|
|
{"wght": (400, 800)},
|
|
{
|
|
"wght": {
|
|
-1.0: -1.0,
|
|
0: 0,
|
|
0.25: 0.22784,
|
|
0.50006: 0.48103,
|
|
0.75: 0.77214,
|
|
1.0: 1.0,
|
|
},
|
|
"wdth": DFLT_WDTH_MAPPING,
|
|
},
|
|
id="wght=400:800",
|
|
),
|
|
pytest.param(
|
|
{"wght": (400, 700)},
|
|
{
|
|
"wght": {
|
|
-1.0: -1.0,
|
|
0: 0,
|
|
0.3334: 0.2951,
|
|
0.66675: 0.623,
|
|
1.0: 1.0,
|
|
},
|
|
"wdth": DFLT_WDTH_MAPPING,
|
|
},
|
|
id="wght=400:700",
|
|
),
|
|
pytest.param(
|
|
{"wght": (400, 600)},
|
|
{
|
|
"wght": {-1.0: -1.0, 0: 0, 0.5: 0.47363, 1.0: 1.0},
|
|
"wdth": DFLT_WDTH_MAPPING,
|
|
},
|
|
id="wght=400:600",
|
|
),
|
|
pytest.param(
|
|
{"wdth": (62.5, 100)},
|
|
{
|
|
"wght": DFLT_WGHT_MAPPING,
|
|
"wdth": {
|
|
-1.0: -1.0,
|
|
-0.6667: -0.7,
|
|
-0.3333: -0.36664,
|
|
0: 0,
|
|
1.0: 1.0,
|
|
},
|
|
},
|
|
id="wdth=62.5:100",
|
|
),
|
|
pytest.param(
|
|
{"wdth": (70, 100)},
|
|
{
|
|
"wght": DFLT_WGHT_MAPPING,
|
|
"wdth": {
|
|
-1.0: -1.0,
|
|
-0.8334: -0.85364,
|
|
-0.4166: -0.44714,
|
|
0: 0,
|
|
1.0: 1.0,
|
|
},
|
|
},
|
|
id="wdth=70:100",
|
|
),
|
|
pytest.param(
|
|
{"wdth": (75, 100)},
|
|
{
|
|
"wght": DFLT_WGHT_MAPPING,
|
|
"wdth": {-1.0: -1.0, -0.49994: -0.52374, 0: 0, 1.0: 1.0},
|
|
},
|
|
id="wdth=75:100",
|
|
),
|
|
pytest.param(
|
|
{"wdth": (77, 100)},
|
|
{
|
|
"wght": DFLT_WGHT_MAPPING,
|
|
"wdth": {-1.0: -1.0, -0.54346: -0.56696, 0: 0, 1.0: 1.0},
|
|
},
|
|
id="wdth=77:100",
|
|
),
|
|
pytest.param(
|
|
{"wdth": (87.5, 100)},
|
|
{"wght": DFLT_WGHT_MAPPING, "wdth": {-1.0: -1.0, 0: 0, 1.0: 1.0}},
|
|
id="wdth=87.5:100",
|
|
),
|
|
],
|
|
)
|
|
def test_limit_axes(self, varfont, axisLimits, expectedSegments):
|
|
instancer.instantiateAvar(varfont, axisLimits)
|
|
|
|
newSegments = varfont["avar"].segments
|
|
expectedSegments = {
|
|
axisTag: self.quantizeF2Dot14Floats(mapping)
|
|
for axisTag, mapping in expectedSegments.items()
|
|
}
|
|
assert newSegments == expectedSegments
|
|
|
|
@pytest.mark.parametrize(
|
|
"invalidSegmentMap",
|
|
[
|
|
pytest.param({0.5: 0.5}, id="missing-required-maps-1"),
|
|
pytest.param({-1.0: -1.0, 1.0: 1.0}, id="missing-required-maps-2"),
|
|
pytest.param(
|
|
{-1.0: -1.0, 0: 0, 0.5: 0.5, 0.6: 0.4, 1.0: 1.0},
|
|
id="retrograde-value-maps",
|
|
),
|
|
],
|
|
)
|
|
def test_drop_invalid_segment_map(self, varfont, invalidSegmentMap, caplog):
|
|
varfont["avar"].segments["wght"] = invalidSegmentMap
|
|
|
|
with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
|
|
instancer.instantiateAvar(varfont, {"wght": (100, 400)})
|
|
|
|
assert "Invalid avar" in caplog.text
|
|
assert "wght" not in varfont["avar"].segments
|
|
|
|
def test_isValidAvarSegmentMap(self):
|
|
assert instancer._isValidAvarSegmentMap("FOOO", {})
|
|
assert instancer._isValidAvarSegmentMap("FOOO", {-1.0: -1.0, 0: 0, 1.0: 1.0})
|
|
assert instancer._isValidAvarSegmentMap(
|
|
"FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 1.0: 1.0}
|
|
)
|
|
assert instancer._isValidAvarSegmentMap(
|
|
"FOOO", {-1.0: -1.0, 0: 0, 0.5: 0.5, 0.7: 0.5, 1.0: 1.0}
|
|
)
|
|
|
|
|
|
class InstantiateFvarTest(object):
|
|
@pytest.mark.parametrize(
|
|
"location, instancesLeft",
|
|
[
|
|
(
|
|
{"wght": 400.0},
|
|
["Regular", "SemiCondensed", "Condensed", "ExtraCondensed"],
|
|
),
|
|
(
|
|
{"wght": 100.0},
|
|
["Thin", "SemiCondensed Thin", "Condensed Thin", "ExtraCondensed Thin"],
|
|
),
|
|
(
|
|
{"wdth": 100.0},
|
|
[
|
|
"Thin",
|
|
"ExtraLight",
|
|
"Light",
|
|
"Regular",
|
|
"Medium",
|
|
"SemiBold",
|
|
"Bold",
|
|
"ExtraBold",
|
|
"Black",
|
|
],
|
|
),
|
|
# no named instance at pinned location
|
|
({"wdth": 90.0}, []),
|
|
],
|
|
)
|
|
def test_pin_and_drop_axis(self, varfont, location, instancesLeft):
|
|
instancer.instantiateFvar(varfont, location)
|
|
|
|
fvar = varfont["fvar"]
|
|
assert {a.axisTag for a in fvar.axes}.isdisjoint(location)
|
|
|
|
for instance in fvar.instances:
|
|
assert set(instance.coordinates).isdisjoint(location)
|
|
|
|
name = varfont["name"]
|
|
assert [
|
|
name.getDebugName(instance.subfamilyNameID) for instance in fvar.instances
|
|
] == instancesLeft
|
|
|
|
def test_full_instance(self, varfont):
|
|
instancer.instantiateFvar(varfont, {"wght": 0.0, "wdth": 0.0})
|
|
|
|
assert "fvar" not in varfont
|
|
|
|
|
|
class InstantiateSTATTest(object):
|
|
@pytest.mark.parametrize(
|
|
"location, expected",
|
|
[
|
|
({"wght": 400}, ["Regular", "Condensed", "Upright"]),
|
|
({"wdth": 100}, ["Thin", "Regular", "Black", "Upright"]),
|
|
],
|
|
)
|
|
def test_pin_and_drop_axis(self, varfont, location, expected):
|
|
instancer.instantiateSTAT(varfont, location)
|
|
|
|
stat = varfont["STAT"].table
|
|
designAxes = {a.AxisTag for a in stat.DesignAxisRecord.Axis}
|
|
|
|
assert designAxes == {"wght", "wdth", "ital"}
|
|
|
|
name = varfont["name"]
|
|
valueNames = []
|
|
for axisValueTable in stat.AxisValueArray.AxisValue:
|
|
valueName = name.getDebugName(axisValueTable.ValueNameID)
|
|
valueNames.append(valueName)
|
|
|
|
assert valueNames == expected
|
|
|
|
def test_skip_table_no_axis_value_array(self, varfont):
|
|
varfont["STAT"].table.AxisValueArray = None
|
|
|
|
instancer.instantiateSTAT(varfont, {"wght": 100})
|
|
|
|
assert len(varfont["STAT"].table.DesignAxisRecord.Axis) == 3
|
|
assert varfont["STAT"].table.AxisValueArray is None
|
|
|
|
def test_skip_table_axis_value_array_empty(self, varfont):
|
|
varfont["STAT"].table.AxisValueArray.AxisValue = []
|
|
|
|
instancer.instantiateSTAT(varfont, {"wght": 100})
|
|
|
|
assert len(varfont["STAT"].table.DesignAxisRecord.Axis) == 3
|
|
assert not varfont["STAT"].table.AxisValueArray.AxisValue
|
|
|
|
def test_skip_table_no_design_axes(self, varfont):
|
|
stat = otTables.STAT()
|
|
stat.Version = 0x00010001
|
|
stat.populateDefaults()
|
|
assert not stat.DesignAxisRecord
|
|
assert not stat.AxisValueArray
|
|
varfont["STAT"].table = stat
|
|
|
|
instancer.instantiateSTAT(varfont, {"wght": 100})
|
|
|
|
assert not varfont["STAT"].table.DesignAxisRecord
|
|
|
|
@staticmethod
|
|
def get_STAT_axis_values(stat):
|
|
axes = stat.DesignAxisRecord.Axis
|
|
result = []
|
|
for axisValue in stat.AxisValueArray.AxisValue:
|
|
if axisValue.Format == 1:
|
|
result.append((axes[axisValue.AxisIndex].AxisTag, axisValue.Value))
|
|
elif axisValue.Format == 3:
|
|
result.append(
|
|
(
|
|
axes[axisValue.AxisIndex].AxisTag,
|
|
(axisValue.Value, axisValue.LinkedValue),
|
|
)
|
|
)
|
|
elif axisValue.Format == 2:
|
|
result.append(
|
|
(
|
|
axes[axisValue.AxisIndex].AxisTag,
|
|
(
|
|
axisValue.RangeMinValue,
|
|
axisValue.NominalValue,
|
|
axisValue.RangeMaxValue,
|
|
),
|
|
)
|
|
)
|
|
elif axisValue.Format == 4:
|
|
result.append(
|
|
tuple(
|
|
(axes[rec.AxisIndex].AxisTag, rec.Value)
|
|
for rec in axisValue.AxisValueRecord
|
|
)
|
|
)
|
|
else:
|
|
raise AssertionError(axisValue.Format)
|
|
return result
|
|
|
|
def test_limit_axes(self, varfont2):
|
|
instancer.instantiateSTAT(varfont2, {"wght": (400, 500), "wdth": (75, 100)})
|
|
|
|
assert len(varfont2["STAT"].table.AxisValueArray.AxisValue) == 5
|
|
assert self.get_STAT_axis_values(varfont2["STAT"].table) == [
|
|
("wght", (400.0, 700.0)),
|
|
("wght", 500.0),
|
|
("wdth", (93.75, 100.0, 100.0)),
|
|
("wdth", (81.25, 87.5, 93.75)),
|
|
("wdth", (68.75, 75.0, 81.25)),
|
|
]
|
|
|
|
def test_limit_axis_value_format_4(self, varfont2):
|
|
stat = varfont2["STAT"].table
|
|
|
|
axisValue = otTables.AxisValue()
|
|
axisValue.Format = 4
|
|
axisValue.AxisValueRecord = []
|
|
for tag, value in (("wght", 575), ("wdth", 90)):
|
|
rec = otTables.AxisValueRecord()
|
|
rec.AxisIndex = next(
|
|
i for i, a in enumerate(stat.DesignAxisRecord.Axis) if a.AxisTag == tag
|
|
)
|
|
rec.Value = value
|
|
axisValue.AxisValueRecord.append(rec)
|
|
stat.AxisValueArray.AxisValue.append(axisValue)
|
|
|
|
instancer.instantiateSTAT(varfont2, {"wght": (100, 600)})
|
|
|
|
assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue
|
|
|
|
instancer.instantiateSTAT(varfont2, {"wdth": (62.5, 87.5)})
|
|
|
|
assert axisValue not in varfont2["STAT"].table.AxisValueArray.AxisValue
|
|
|
|
def test_unknown_axis_value_format(self, varfont2, caplog):
|
|
stat = varfont2["STAT"].table
|
|
axisValue = otTables.AxisValue()
|
|
axisValue.Format = 5
|
|
stat.AxisValueArray.AxisValue.append(axisValue)
|
|
|
|
with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
|
|
instancer.instantiateSTAT(varfont2, {"wght": 400})
|
|
|
|
assert "Unknown AxisValue table format (5)" in caplog.text
|
|
assert axisValue in varfont2["STAT"].table.AxisValueArray.AxisValue
|
|
|
|
|
|
def test_pruningUnusedNames(varfont):
|
|
varNameIDs = instancer.getVariationNameIDs(varfont)
|
|
|
|
assert varNameIDs == set(range(256, 296 + 1))
|
|
|
|
fvar = varfont["fvar"]
|
|
stat = varfont["STAT"].table
|
|
|
|
with instancer.pruningUnusedNames(varfont):
|
|
del fvar.axes[0] # Weight (nameID=256)
|
|
del fvar.instances[0] # Thin (nameID=258)
|
|
del stat.DesignAxisRecord.Axis[0] # Weight (nameID=256)
|
|
del stat.AxisValueArray.AxisValue[0] # Thin (nameID=258)
|
|
|
|
assert not any(n for n in varfont["name"].names if n.nameID in {256, 258})
|
|
|
|
with instancer.pruningUnusedNames(varfont):
|
|
del varfont["fvar"]
|
|
del varfont["STAT"]
|
|
|
|
assert not any(n for n in varfont["name"].names if n.nameID in varNameIDs)
|
|
assert "ltag" not in varfont
|
|
|
|
|
|
def test_setMacOverlapFlags():
|
|
flagOverlapCompound = _g_l_y_f.OVERLAP_COMPOUND
|
|
flagOverlapSimple = _g_l_y_f.flagOverlapSimple
|
|
|
|
glyf = ttLib.newTable("glyf")
|
|
glyf.glyphOrder = ["a", "b", "c"]
|
|
a = _g_l_y_f.Glyph()
|
|
a.numberOfContours = 1
|
|
a.flags = [0]
|
|
b = _g_l_y_f.Glyph()
|
|
b.numberOfContours = -1
|
|
comp = _g_l_y_f.GlyphComponent()
|
|
comp.flags = 0
|
|
b.components = [comp]
|
|
c = _g_l_y_f.Glyph()
|
|
c.numberOfContours = 0
|
|
glyf.glyphs = {"a": a, "b": b, "c": c}
|
|
|
|
instancer.setMacOverlapFlags(glyf)
|
|
|
|
assert a.flags[0] & flagOverlapSimple != 0
|
|
assert b.components[0].flags & flagOverlapCompound != 0
|
|
|
|
|
|
def _strip_ttLibVersion(string):
|
|
return re.sub(' ttLibVersion=".*"', "", string)
|
|
|
|
|
|
@pytest.fixture
|
|
def varfont2():
|
|
f = ttLib.TTFont(recalcTimestamp=False)
|
|
f.importXML(os.path.join(TESTDATA, "PartialInstancerTest2-VF.ttx"))
|
|
return f
|
|
|
|
|
|
def _dump_ttx(ttFont):
|
|
# compile to temporary bytes stream, reload and dump to XML
|
|
tmp = BytesIO()
|
|
ttFont.save(tmp)
|
|
tmp.seek(0)
|
|
ttFont2 = ttLib.TTFont(tmp, recalcBBoxes=False, recalcTimestamp=False)
|
|
s = StringIO()
|
|
ttFont2.saveXML(s, newlinestr="\n")
|
|
return _strip_ttLibVersion(s.getvalue())
|
|
|
|
|
|
def _get_expected_instance_ttx(wght, wdth):
|
|
with open(
|
|
os.path.join(
|
|
TESTDATA,
|
|
"test_results",
|
|
"PartialInstancerTest2-VF-instance-{0},{1}.ttx".format(wght, wdth),
|
|
),
|
|
"r",
|
|
encoding="utf-8",
|
|
) as fp:
|
|
return _strip_ttLibVersion(fp.read())
|
|
|
|
|
|
class InstantiateVariableFontTest(object):
|
|
@pytest.mark.parametrize(
|
|
"wght, wdth",
|
|
[(100, 100), (400, 100), (900, 100), (100, 62.5), (400, 62.5), (900, 62.5)],
|
|
)
|
|
def test_multiple_instancing(self, varfont2, wght, wdth):
|
|
partial = instancer.instantiateVariableFont(varfont2, {"wght": wght})
|
|
instance = instancer.instantiateVariableFont(partial, {"wdth": wdth})
|
|
|
|
expected = _get_expected_instance_ttx(wght, wdth)
|
|
|
|
assert _dump_ttx(instance) == expected
|
|
|
|
def test_default_instance(self, varfont2):
|
|
instance = instancer.instantiateVariableFont(
|
|
varfont2, {"wght": None, "wdth": None}
|
|
)
|
|
|
|
expected = _get_expected_instance_ttx(400, 100)
|
|
|
|
assert _dump_ttx(instance) == expected
|
|
|
|
|
|
def _conditionSetAsDict(conditionSet, axisOrder):
|
|
result = {}
|
|
for cond in conditionSet.ConditionTable:
|
|
assert cond.Format == 1
|
|
axisTag = axisOrder[cond.AxisIndex]
|
|
result[axisTag] = (cond.FilterRangeMinValue, cond.FilterRangeMaxValue)
|
|
return result
|
|
|
|
|
|
def _getSubstitutions(gsub, lookupIndices):
|
|
subs = {}
|
|
for index, lookup in enumerate(gsub.LookupList.Lookup):
|
|
if index in lookupIndices:
|
|
for subtable in lookup.SubTable:
|
|
subs.update(subtable.mapping)
|
|
return subs
|
|
|
|
|
|
def makeFeatureVarsFont(conditionalSubstitutions):
|
|
axes = set()
|
|
glyphs = set()
|
|
for region, substitutions in conditionalSubstitutions:
|
|
for box in region:
|
|
axes.update(box.keys())
|
|
glyphs.update(*substitutions.items())
|
|
|
|
varfont = ttLib.TTFont()
|
|
varfont.setGlyphOrder(sorted(glyphs))
|
|
|
|
fvar = varfont["fvar"] = ttLib.newTable("fvar")
|
|
fvar.axes = []
|
|
for axisTag in sorted(axes):
|
|
axis = _f_v_a_r.Axis()
|
|
axis.axisTag = Tag(axisTag)
|
|
fvar.axes.append(axis)
|
|
|
|
featureVars.addFeatureVariations(varfont, conditionalSubstitutions)
|
|
|
|
return varfont
|
|
|
|
|
|
class InstantiateFeatureVariationsTest(object):
|
|
@pytest.mark.parametrize(
|
|
"location, appliedSubs, expectedRecords",
|
|
[
|
|
({"wght": 0}, {}, [({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"})]),
|
|
(
|
|
{"wght": -1.0},
|
|
{},
|
|
[
|
|
({"cntr": (0, 0.25)}, {"uni0061": "uni0041"}),
|
|
({"cntr": (0.75, 1.0)}, {"uni0041": "uni0061"}),
|
|
],
|
|
),
|
|
(
|
|
{"wght": 1.0},
|
|
{"uni0024": "uni0024.nostroke"},
|
|
[
|
|
(
|
|
{"cntr": (0.75, 1.0)},
|
|
{"uni0024": "uni0024.nostroke", "uni0041": "uni0061"},
|
|
)
|
|
],
|
|
),
|
|
(
|
|
{"cntr": 0},
|
|
{},
|
|
[
|
|
({"wght": (-1.0, -0.45654)}, {"uni0061": "uni0041"}),
|
|
({"wght": (0.20886, 1.0)}, {"uni0024": "uni0024.nostroke"}),
|
|
],
|
|
),
|
|
(
|
|
{"cntr": 1.0},
|
|
{"uni0041": "uni0061"},
|
|
[
|
|
(
|
|
{"wght": (0.20886, 1.0)},
|
|
{"uni0024": "uni0024.nostroke", "uni0041": "uni0061"},
|
|
)
|
|
],
|
|
),
|
|
],
|
|
)
|
|
def test_partial_instance(self, location, appliedSubs, expectedRecords):
|
|
font = makeFeatureVarsFont(
|
|
[
|
|
([{"wght": (0.20886, 1.0)}], {"uni0024": "uni0024.nostroke"}),
|
|
([{"cntr": (0.75, 1.0)}], {"uni0041": "uni0061"}),
|
|
(
|
|
[{"wght": (-1.0, -0.45654), "cntr": (0, 0.25)}],
|
|
{"uni0061": "uni0041"},
|
|
),
|
|
]
|
|
)
|
|
|
|
instancer.instantiateFeatureVariations(font, location)
|
|
|
|
gsub = font["GSUB"].table
|
|
featureVariations = gsub.FeatureVariations
|
|
|
|
assert featureVariations.FeatureVariationCount == len(expectedRecords)
|
|
|
|
axisOrder = [a.axisTag for a in font["fvar"].axes if a.axisTag not in location]
|
|
for i, (expectedConditionSet, expectedSubs) in enumerate(expectedRecords):
|
|
rec = featureVariations.FeatureVariationRecord[i]
|
|
conditionSet = _conditionSetAsDict(rec.ConditionSet, axisOrder)
|
|
|
|
assert conditionSet == expectedConditionSet
|
|
|
|
subsRecord = rec.FeatureTableSubstitution.SubstitutionRecord[0]
|
|
lookupIndices = subsRecord.Feature.LookupListIndex
|
|
substitutions = _getSubstitutions(gsub, lookupIndices)
|
|
|
|
assert substitutions == expectedSubs
|
|
|
|
appliedLookupIndices = gsub.FeatureList.FeatureRecord[0].Feature.LookupListIndex
|
|
|
|
assert _getSubstitutions(gsub, appliedLookupIndices) == appliedSubs
|
|
|
|
@pytest.mark.parametrize(
|
|
"location, appliedSubs",
|
|
[
|
|
({"wght": 0, "cntr": 0}, None),
|
|
({"wght": -1.0, "cntr": 0}, {"uni0061": "uni0041"}),
|
|
({"wght": 1.0, "cntr": 0}, {"uni0024": "uni0024.nostroke"}),
|
|
({"wght": 0.0, "cntr": 1.0}, {"uni0041": "uni0061"}),
|
|
(
|
|
{"wght": 1.0, "cntr": 1.0},
|
|
{"uni0041": "uni0061", "uni0024": "uni0024.nostroke"},
|
|
),
|
|
({"wght": -1.0, "cntr": 0.3}, None),
|
|
],
|
|
)
|
|
def test_full_instance(self, location, appliedSubs):
|
|
font = makeFeatureVarsFont(
|
|
[
|
|
([{"wght": (0.20886, 1.0)}], {"uni0024": "uni0024.nostroke"}),
|
|
([{"cntr": (0.75, 1.0)}], {"uni0041": "uni0061"}),
|
|
(
|
|
[{"wght": (-1.0, -0.45654), "cntr": (0, 0.25)}],
|
|
{"uni0061": "uni0041"},
|
|
),
|
|
]
|
|
)
|
|
|
|
instancer.instantiateFeatureVariations(font, location)
|
|
|
|
gsub = font["GSUB"].table
|
|
assert not hasattr(gsub, "FeatureVariations")
|
|
|
|
if appliedSubs:
|
|
lookupIndices = gsub.FeatureList.FeatureRecord[0].Feature.LookupListIndex
|
|
assert _getSubstitutions(gsub, lookupIndices) == appliedSubs
|
|
else:
|
|
assert not gsub.FeatureList.FeatureRecord
|
|
|
|
def test_unsupported_condition_format(self, caplog):
|
|
font = makeFeatureVarsFont(
|
|
[
|
|
(
|
|
[{"wdth": (-1.0, -0.5), "wght": (0.5, 1.0)}],
|
|
{"dollar": "dollar.nostroke"},
|
|
)
|
|
]
|
|
)
|
|
featureVariations = font["GSUB"].table.FeatureVariations
|
|
rec1 = featureVariations.FeatureVariationRecord[0]
|
|
assert len(rec1.ConditionSet.ConditionTable) == 2
|
|
rec1.ConditionSet.ConditionTable[0].Format = 2
|
|
|
|
with caplog.at_level(logging.WARNING, logger="fontTools.varLib.instancer"):
|
|
instancer.instantiateFeatureVariations(font, {"wdth": 0})
|
|
|
|
assert (
|
|
"Condition table 0 of FeatureVariationRecord 0 "
|
|
"has unsupported format (2); ignored"
|
|
) in caplog.text
|
|
|
|
# check that record with unsupported condition format (but whose other
|
|
# conditions do not reference pinned axes) is kept as is
|
|
featureVariations = font["GSUB"].table.FeatureVariations
|
|
assert featureVariations.FeatureVariationRecord[0] is rec1
|
|
assert len(rec1.ConditionSet.ConditionTable) == 2
|
|
assert rec1.ConditionSet.ConditionTable[0].Format == 2
|
|
|
|
|
|
class LimitTupleVariationAxisRangesTest:
|
|
def check_limit_single_var_axis_range(self, var, axisTag, axisRange, expected):
|
|
result = instancer.limitTupleVariationAxisRange(var, axisTag, axisRange)
|
|
print(result)
|
|
|
|
assert len(result) == len(expected)
|
|
for v1, v2 in zip(result, expected):
|
|
assert v1.coordinates == pytest.approx(v2.coordinates)
|
|
assert v1.axes.keys() == v2.axes.keys()
|
|
for k in v1.axes:
|
|
p, q = v1.axes[k], v2.axes[k]
|
|
assert p == pytest.approx(q)
|
|
|
|
@pytest.mark.parametrize(
|
|
"var, axisTag, newMax, expected",
|
|
[
|
|
(
|
|
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
|
|
"wdth",
|
|
0.5,
|
|
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
|
|
"wght",
|
|
0.5,
|
|
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [50, 50])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
|
|
"wght",
|
|
0.8,
|
|
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]),
|
|
"wght",
|
|
1.0,
|
|
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])],
|
|
),
|
|
(TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100]), "wght", 0.0, []),
|
|
(TupleVariation({"wght": (0.5, 1.0, 1.0)}, [100, 100]), "wght", 0.4, []),
|
|
(
|
|
TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]),
|
|
"wght",
|
|
0.5,
|
|
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [100, 100])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]),
|
|
"wght",
|
|
0.4,
|
|
[TupleVariation({"wght": (0.0, 1.0, 1.0)}, [80, 80])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (0.0, 0.5, 1.0)}, [100, 100]),
|
|
"wght",
|
|
0.6,
|
|
[TupleVariation({"wght": (0.0, 0.833334, 1.666667)}, [100, 100])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]),
|
|
"wght",
|
|
0.4,
|
|
[
|
|
TupleVariation({"wght": (0.0, 0.5, 1.99994)}, [100, 100]),
|
|
TupleVariation({"wght": (0.5, 1.0, 1.0)}, [8.33333, 8.33333]),
|
|
],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (0.0, 0.2, 1.0)}, [100, 100]),
|
|
"wght",
|
|
0.5,
|
|
[TupleVariation({"wght": (0.0, 0.4, 1.99994)}, [100, 100])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (0.5, 0.5, 1.0)}, [100, 100]),
|
|
"wght",
|
|
0.5,
|
|
[TupleVariation({"wght": (1.0, 1.0, 1.0)}, [100, 100])],
|
|
),
|
|
],
|
|
)
|
|
def test_positive_var(self, var, axisTag, newMax, expected):
|
|
axisRange = instancer.NormalizedAxisRange(0, newMax)
|
|
self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)
|
|
|
|
@pytest.mark.parametrize(
|
|
"var, axisTag, newMin, expected",
|
|
[
|
|
(
|
|
TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
|
|
"wdth",
|
|
-0.5,
|
|
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
|
|
"wght",
|
|
-0.5,
|
|
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [50, 50])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
|
|
"wght",
|
|
-0.8,
|
|
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]),
|
|
"wght",
|
|
-1.0,
|
|
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])],
|
|
),
|
|
(TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100]), "wght", 0.0, []),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [100, 100]),
|
|
"wght",
|
|
-0.4,
|
|
[],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]),
|
|
"wght",
|
|
-0.5,
|
|
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [100, 100])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]),
|
|
"wght",
|
|
-0.4,
|
|
[TupleVariation({"wght": (-1.0, -1.0, 0.0)}, [80, 80])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -0.5, 0.0)}, [100, 100]),
|
|
"wght",
|
|
-0.6,
|
|
[TupleVariation({"wght": (-1.666667, -0.833334, 0.0)}, [100, 100])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]),
|
|
"wght",
|
|
-0.4,
|
|
[
|
|
TupleVariation({"wght": (-2.0, -0.5, -0.0)}, [100, 100]),
|
|
TupleVariation({"wght": (-1.0, -1.0, -0.5)}, [8.33333, 8.33333]),
|
|
],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -0.2, 0.0)}, [100, 100]),
|
|
"wght",
|
|
-0.5,
|
|
[TupleVariation({"wght": (-2.0, -0.4, 0.0)}, [100, 100])],
|
|
),
|
|
(
|
|
TupleVariation({"wght": (-1.0, -0.5, -0.5)}, [100, 100]),
|
|
"wght",
|
|
-0.5,
|
|
[TupleVariation({"wght": (-1.0, -1.0, -1.0)}, [100, 100])],
|
|
),
|
|
],
|
|
)
|
|
def test_negative_var(self, var, axisTag, newMin, expected):
|
|
axisRange = instancer.NormalizedAxisRange(newMin, 0)
|
|
self.check_limit_single_var_axis_range(var, axisTag, axisRange, expected)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"oldRange, newRange, expected",
|
|
[
|
|
((1.0, -1.0), (-1.0, 1.0), None), # invalid oldRange min > max
|
|
((0.6, 1.0), (0, 0.5), None),
|
|
((-1.0, -0.6), (-0.5, 0), None),
|
|
((0.4, 1.0), (0, 0.5), (0.8, 1.0)),
|
|
((-1.0, -0.4), (-0.5, 0), (-1.0, -0.8)),
|
|
((0.4, 1.0), (0, 0.4), (1.0, 1.0)),
|
|
((-1.0, -0.4), (-0.4, 0), (-1.0, -1.0)),
|
|
((-0.5, 0.5), (-0.4, 0.4), (-1.0, 1.0)),
|
|
((0, 1.0), (-1.0, 0), (0, 0)), # or None?
|
|
((-1.0, 0), (0, 1.0), (0, 0)), # or None?
|
|
],
|
|
)
|
|
def test_limitFeatureVariationConditionRange(oldRange, newRange, expected):
|
|
condition = featureVars.buildConditionTable(0, *oldRange)
|
|
|
|
result = instancer._limitFeatureVariationConditionRange(
|
|
condition, instancer.NormalizedAxisRange(*newRange)
|
|
)
|
|
|
|
assert result == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"limits, expected",
|
|
[
|
|
(["wght=400", "wdth=100"], {"wght": 400, "wdth": 100}),
|
|
(["wght=400:900"], {"wght": (400, 900)}),
|
|
(["slnt=11.4"], {"slnt": pytest.approx(11.399994)}),
|
|
(["ABCD=drop"], {"ABCD": None}),
|
|
],
|
|
)
|
|
def test_parseLimits(limits, expected):
|
|
assert instancer.parseLimits(limits) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"limits", [["abcde=123", "=0", "wght=:", "wght=1:", "wght=abcd", "wght=x:y"]]
|
|
)
|
|
def test_parseLimits_invalid(limits):
|
|
with pytest.raises(ValueError, match="invalid location format"):
|
|
instancer.parseLimits(limits)
|
|
|
|
|
|
def test_normalizeAxisLimits_tuple(varfont):
|
|
normalized = instancer.normalizeAxisLimits(varfont, {"wght": (100, 400)})
|
|
assert normalized == {"wght": (-1.0, 0)}
|
|
|
|
|
|
def test_normalizeAxisLimits_no_avar(varfont):
|
|
del varfont["avar"]
|
|
|
|
normalized = instancer.normalizeAxisLimits(varfont, {"wght": (400, 500)})
|
|
|
|
assert normalized["wght"] == pytest.approx((0, 0.2), 1e-4)
|
|
|
|
|
|
def test_normalizeAxisLimits_missing_from_fvar(varfont):
|
|
with pytest.raises(ValueError, match="not present in fvar"):
|
|
instancer.normalizeAxisLimits(varfont, {"ZZZZ": 1000})
|
|
|
|
|
|
def test_sanityCheckVariableTables(varfont):
|
|
font = ttLib.TTFont()
|
|
with pytest.raises(ValueError, match="Missing required table fvar"):
|
|
instancer.sanityCheckVariableTables(font)
|
|
|
|
del varfont["glyf"]
|
|
|
|
with pytest.raises(ValueError, match="Can't have gvar without glyf"):
|
|
instancer.sanityCheckVariableTables(varfont)
|
|
|
|
|
|
def test_main(varfont, tmpdir):
|
|
fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf")
|
|
varfont.save(fontfile)
|
|
args = [fontfile, "wght=400"]
|
|
|
|
# exits without errors
|
|
assert instancer.main(args) is None
|
|
|
|
|
|
def test_main_exit_nonexistent_file(capsys):
|
|
with pytest.raises(SystemExit):
|
|
instancer.main([""])
|
|
captured = capsys.readouterr()
|
|
|
|
assert "No such file ''" in captured.err
|
|
|
|
|
|
def test_main_exit_invalid_location(varfont, tmpdir, capsys):
|
|
fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf")
|
|
varfont.save(fontfile)
|
|
|
|
with pytest.raises(SystemExit):
|
|
instancer.main([fontfile, "wght:100"])
|
|
captured = capsys.readouterr()
|
|
|
|
assert "invalid location format" in captured.err
|
|
|
|
|
|
def test_main_exit_multiple_limits(varfont, tmpdir, capsys):
|
|
fontfile = str(tmpdir / "PartialInstancerTest-VF.ttf")
|
|
varfont.save(fontfile)
|
|
|
|
with pytest.raises(SystemExit):
|
|
instancer.main([fontfile, "wght=400", "wght=90"])
|
|
captured = capsys.readouterr()
|
|
|
|
assert "Specified multiple limits for the same axis" in captured.err
|