aboutsummaryrefslogtreecommitdiff
path: root/Tests/otlLib
diff options
context:
space:
mode:
Diffstat (limited to 'Tests/otlLib')
-rw-r--r--Tests/otlLib/builder_test.py662
-rw-r--r--Tests/otlLib/optimize_test.py175
2 files changed, 576 insertions, 261 deletions
diff --git a/Tests/otlLib/builder_test.py b/Tests/otlLib/builder_test.py
index 1c2c324d..548a31e9 100644
--- a/Tests/otlLib/builder_test.py
+++ b/Tests/otlLib/builder_test.py
@@ -1,6 +1,6 @@
import io
import struct
-from fontTools.misc.fixedTools import floatToFixed
+from fontTools.misc.fixedTools import floatToFixed, fixedToFloat
from fontTools.misc.testTools import getXML
from fontTools.otlLib import builder, error
from fontTools import ttLib
@@ -204,7 +204,7 @@ class BuilderTest(object):
assert builder.buildComponentRecord(None) is None
def test_buildCoverage(self):
- cov = builder.buildCoverage({"two", "four"}, {"two": 2, "four": 4})
+ cov = builder.buildCoverage(("two", "four", "two"), {"two": 2, "four": 4})
assert getXML(cov.toXML) == [
"<Coverage>",
' <Glyph value="two"/>',
@@ -918,7 +918,6 @@ class BuilderTest(object):
("A", "zero"): (d0, d50),
("A", "one"): (None, d20),
("B", "five"): (d8020, d50),
-
},
self.GLYPHMAP,
)
@@ -1120,262 +1119,309 @@ class ClassDefBuilderTest(object):
buildStatTable_test_data = [
- ([
- dict(
- tag="wght",
- name="Weight",
- values=[
- dict(value=100, name='Thin'),
- dict(value=400, name='Regular', flags=0x2),
- dict(value=900, name='Black')])], None, "Regular", [
- ' <STAT>',
- ' <Version value="0x00010001"/>',
- ' <DesignAxisRecordSize value="8"/>',
- ' <!-- DesignAxisCount=1 -->',
- ' <DesignAxisRecord>',
- ' <Axis index="0">',
- ' <AxisTag value="wght"/>',
- ' <AxisNameID value="257"/> <!-- Weight -->',
- ' <AxisOrdering value="0"/>',
- ' </Axis>',
- ' </DesignAxisRecord>',
- ' <!-- AxisValueCount=3 -->',
- ' <AxisValueArray>',
- ' <AxisValue index="0" Format="1">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="0"/>',
- ' <ValueNameID value="258"/> <!-- Thin -->',
- ' <Value value="100.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="1" Format="1">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
- ' <ValueNameID value="256"/> <!-- Regular -->',
- ' <Value value="400.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="2" Format="1">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="0"/>',
- ' <ValueNameID value="259"/> <!-- Black -->',
- ' <Value value="900.0"/>',
- ' </AxisValue>',
- ' </AxisValueArray>',
- ' <ElidedFallbackNameID value="256"/> <!-- Regular -->',
- ' </STAT>']),
- ([
- dict(
- tag="wght",
- name=dict(en="Weight", nl="Gewicht"),
- values=[
- dict(value=100, name=dict(en='Thin', nl='Dun')),
- dict(value=400, name='Regular', flags=0x2),
- dict(value=900, name='Black'),
- ]),
- dict(
- tag="wdth",
- name="Width",
- values=[
- dict(value=50, name='Condensed'),
- dict(value=100, name='Regular', flags=0x2),
- dict(value=200, name='Extended')])], None, 2, [
- ' <STAT>',
- ' <Version value="0x00010001"/>',
- ' <DesignAxisRecordSize value="8"/>',
- ' <!-- DesignAxisCount=2 -->',
- ' <DesignAxisRecord>',
- ' <Axis index="0">',
- ' <AxisTag value="wght"/>',
- ' <AxisNameID value="256"/> <!-- Weight -->',
- ' <AxisOrdering value="0"/>',
- ' </Axis>',
- ' <Axis index="1">',
- ' <AxisTag value="wdth"/>',
- ' <AxisNameID value="260"/> <!-- Width -->',
- ' <AxisOrdering value="1"/>',
- ' </Axis>',
- ' </DesignAxisRecord>',
- ' <!-- AxisValueCount=6 -->',
- ' <AxisValueArray>',
- ' <AxisValue index="0" Format="1">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="0"/>',
- ' <ValueNameID value="257"/> <!-- Thin -->',
- ' <Value value="100.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="1" Format="1">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
- ' <ValueNameID value="258"/> <!-- Regular -->',
- ' <Value value="400.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="2" Format="1">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="0"/>',
- ' <ValueNameID value="259"/> <!-- Black -->',
- ' <Value value="900.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="3" Format="1">',
- ' <AxisIndex value="1"/>',
- ' <Flags value="0"/>',
- ' <ValueNameID value="261"/> <!-- Condensed -->',
- ' <Value value="50.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="4" Format="1">',
- ' <AxisIndex value="1"/>',
- ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
- ' <ValueNameID value="258"/> <!-- Regular -->',
- ' <Value value="100.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="5" Format="1">',
- ' <AxisIndex value="1"/>',
- ' <Flags value="0"/>',
- ' <ValueNameID value="262"/> <!-- Extended -->',
- ' <Value value="200.0"/>',
- ' </AxisValue>',
- ' </AxisValueArray>',
- ' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
- ' </STAT>']),
- ([
- dict(
- tag="wght",
- name="Weight",
- values=[
- dict(value=400, name='Regular', flags=0x2),
- dict(value=600, linkedValue=650, name='Bold')])], None, 18, [
- ' <STAT>',
- ' <Version value="0x00010001"/>',
- ' <DesignAxisRecordSize value="8"/>',
- ' <!-- DesignAxisCount=1 -->',
- ' <DesignAxisRecord>',
- ' <Axis index="0">',
- ' <AxisTag value="wght"/>',
- ' <AxisNameID value="256"/> <!-- Weight -->',
- ' <AxisOrdering value="0"/>',
- ' </Axis>',
- ' </DesignAxisRecord>',
- ' <!-- AxisValueCount=2 -->',
- ' <AxisValueArray>',
- ' <AxisValue index="0" Format="1">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
- ' <ValueNameID value="257"/> <!-- Regular -->',
- ' <Value value="400.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="1" Format="3">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="0"/>',
- ' <ValueNameID value="258"/> <!-- Bold -->',
- ' <Value value="600.0"/>',
- ' <LinkedValue value="650.0"/>',
- ' </AxisValue>',
- ' </AxisValueArray>',
- ' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
- ' </STAT>']),
- ([
- dict(
- tag="opsz",
- name="Optical Size",
- values=[
- dict(nominalValue=6, rangeMaxValue=10, name='Small'),
- dict(rangeMinValue=10, nominalValue=14, rangeMaxValue=24, name='Text', flags=0x2),
- dict(rangeMinValue=24, nominalValue=600, name='Display')])], None, 2, [
- ' <STAT>',
- ' <Version value="0x00010001"/>',
- ' <DesignAxisRecordSize value="8"/>',
- ' <!-- DesignAxisCount=1 -->',
- ' <DesignAxisRecord>',
- ' <Axis index="0">',
- ' <AxisTag value="opsz"/>',
- ' <AxisNameID value="256"/> <!-- Optical Size -->',
- ' <AxisOrdering value="0"/>',
- ' </Axis>',
- ' </DesignAxisRecord>',
- ' <!-- AxisValueCount=3 -->',
- ' <AxisValueArray>',
- ' <AxisValue index="0" Format="2">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="0"/>',
- ' <ValueNameID value="257"/> <!-- Small -->',
- ' <NominalValue value="6.0"/>',
- ' <RangeMinValue value="-32768.0"/>',
- ' <RangeMaxValue value="10.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="1" Format="2">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
- ' <ValueNameID value="258"/> <!-- Text -->',
- ' <NominalValue value="14.0"/>',
- ' <RangeMinValue value="10.0"/>',
- ' <RangeMaxValue value="24.0"/>',
- ' </AxisValue>',
- ' <AxisValue index="2" Format="2">',
- ' <AxisIndex value="0"/>',
- ' <Flags value="0"/>',
- ' <ValueNameID value="259"/> <!-- Display -->',
- ' <NominalValue value="600.0"/>',
- ' <RangeMinValue value="24.0"/>',
- ' <RangeMaxValue value="32767.99998"/>',
- ' </AxisValue>',
- ' </AxisValueArray>',
- ' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
- ' </STAT>']),
- ([
- dict(
- tag="wght",
- name="Weight",
- ordering=1,
- values=[]),
- dict(
- tag="ABCD",
- name="ABCDTest",
- ordering=0,
- values=[
- dict(value=100, name="Regular", flags=0x2)])],
- [dict(location=dict(wght=300, ABCD=100), name='Regular ABCD')], 18, [
- ' <STAT>',
- ' <Version value="0x00010002"/>',
- ' <DesignAxisRecordSize value="8"/>',
- ' <!-- DesignAxisCount=2 -->',
- ' <DesignAxisRecord>',
- ' <Axis index="0">',
- ' <AxisTag value="wght"/>',
- ' <AxisNameID value="256"/> <!-- Weight -->',
- ' <AxisOrdering value="1"/>',
- ' </Axis>',
- ' <Axis index="1">',
- ' <AxisTag value="ABCD"/>',
- ' <AxisNameID value="257"/> <!-- ABCDTest -->',
- ' <AxisOrdering value="0"/>',
- ' </Axis>',
- ' </DesignAxisRecord>',
- ' <!-- AxisValueCount=2 -->',
- ' <AxisValueArray>',
- ' <AxisValue index="0" Format="4">',
- ' <!-- AxisCount=2 -->',
- ' <Flags value="0"/>',
- ' <ValueNameID value="259"/> <!-- Regular ABCD -->',
- ' <AxisValueRecord index="0">',
- ' <AxisIndex value="0"/>',
- ' <Value value="300.0"/>',
- ' </AxisValueRecord>',
- ' <AxisValueRecord index="1">',
- ' <AxisIndex value="1"/>',
- ' <Value value="100.0"/>',
- ' </AxisValueRecord>',
- ' </AxisValue>',
- ' <AxisValue index="1" Format="1">',
- ' <AxisIndex value="1"/>',
- ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
- ' <ValueNameID value="258"/> <!-- Regular -->',
- ' <Value value="100.0"/>',
- ' </AxisValue>',
- ' </AxisValueArray>',
- ' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
- ' </STAT>']),
+ (
+ [
+ dict(
+ tag="wght",
+ name="Weight",
+ values=[
+ dict(value=100, name="Thin"),
+ dict(value=400, name="Regular", flags=0x2),
+ dict(value=900, name="Black"),
+ ],
+ )
+ ],
+ None,
+ "Regular",
+ [
+ " <STAT>",
+ ' <Version value="0x00010001"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ " <!-- DesignAxisCount=1 -->",
+ " <DesignAxisRecord>",
+ ' <Axis index="0">',
+ ' <AxisTag value="wght"/>',
+ ' <AxisNameID value="257"/> <!-- Weight -->',
+ ' <AxisOrdering value="0"/>',
+ " </Axis>",
+ " </DesignAxisRecord>",
+ " <!-- AxisValueCount=3 -->",
+ " <AxisValueArray>",
+ ' <AxisValue index="0" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="258"/> <!-- Thin -->',
+ ' <Value value="100.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="1" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
+ ' <ValueNameID value="256"/> <!-- Regular -->',
+ ' <Value value="400.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="2" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="259"/> <!-- Black -->',
+ ' <Value value="900.0"/>',
+ " </AxisValue>",
+ " </AxisValueArray>",
+ ' <ElidedFallbackNameID value="256"/> <!-- Regular -->',
+ " </STAT>",
+ ],
+ ),
+ (
+ [
+ dict(
+ tag="wght",
+ name=dict(en="Weight", nl="Gewicht"),
+ values=[
+ dict(value=100, name=dict(en="Thin", nl="Dun")),
+ dict(value=400, name="Regular", flags=0x2),
+ dict(value=900, name="Black"),
+ ],
+ ),
+ dict(
+ tag="wdth",
+ name="Width",
+ values=[
+ dict(value=50, name="Condensed"),
+ dict(value=100, name="Regular", flags=0x2),
+ dict(value=200, name="Extended"),
+ ],
+ ),
+ ],
+ None,
+ 2,
+ [
+ " <STAT>",
+ ' <Version value="0x00010001"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ " <!-- DesignAxisCount=2 -->",
+ " <DesignAxisRecord>",
+ ' <Axis index="0">',
+ ' <AxisTag value="wght"/>',
+ ' <AxisNameID value="256"/> <!-- Weight -->',
+ ' <AxisOrdering value="0"/>',
+ " </Axis>",
+ ' <Axis index="1">',
+ ' <AxisTag value="wdth"/>',
+ ' <AxisNameID value="260"/> <!-- Width -->',
+ ' <AxisOrdering value="1"/>',
+ " </Axis>",
+ " </DesignAxisRecord>",
+ " <!-- AxisValueCount=6 -->",
+ " <AxisValueArray>",
+ ' <AxisValue index="0" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="257"/> <!-- Thin -->',
+ ' <Value value="100.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="1" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
+ ' <ValueNameID value="258"/> <!-- Regular -->',
+ ' <Value value="400.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="2" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="259"/> <!-- Black -->',
+ ' <Value value="900.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="3" Format="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="261"/> <!-- Condensed -->',
+ ' <Value value="50.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="4" Format="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
+ ' <ValueNameID value="258"/> <!-- Regular -->',
+ ' <Value value="100.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="5" Format="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="262"/> <!-- Extended -->',
+ ' <Value value="200.0"/>',
+ " </AxisValue>",
+ " </AxisValueArray>",
+ ' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
+ " </STAT>",
+ ],
+ ),
+ (
+ [
+ dict(
+ tag="wght",
+ name="Weight",
+ values=[
+ dict(value=400, name="Regular", flags=0x2),
+ dict(value=600, linkedValue=650, name="Bold"),
+ ],
+ )
+ ],
+ None,
+ 18,
+ [
+ " <STAT>",
+ ' <Version value="0x00010001"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ " <!-- DesignAxisCount=1 -->",
+ " <DesignAxisRecord>",
+ ' <Axis index="0">',
+ ' <AxisTag value="wght"/>',
+ ' <AxisNameID value="256"/> <!-- Weight -->',
+ ' <AxisOrdering value="0"/>',
+ " </Axis>",
+ " </DesignAxisRecord>",
+ " <!-- AxisValueCount=2 -->",
+ " <AxisValueArray>",
+ ' <AxisValue index="0" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
+ ' <ValueNameID value="257"/> <!-- Regular -->',
+ ' <Value value="400.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="1" Format="3">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="258"/> <!-- Bold -->',
+ ' <Value value="600.0"/>',
+ ' <LinkedValue value="650.0"/>',
+ " </AxisValue>",
+ " </AxisValueArray>",
+ ' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
+ " </STAT>",
+ ],
+ ),
+ (
+ [
+ dict(
+ tag="opsz",
+ name="Optical Size",
+ values=[
+ dict(nominalValue=6, rangeMaxValue=10, name="Small"),
+ dict(
+ rangeMinValue=10,
+ nominalValue=14,
+ rangeMaxValue=24,
+ name="Text",
+ flags=0x2,
+ ),
+ dict(rangeMinValue=24, nominalValue=600, name="Display"),
+ ],
+ )
+ ],
+ None,
+ 2,
+ [
+ " <STAT>",
+ ' <Version value="0x00010001"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ " <!-- DesignAxisCount=1 -->",
+ " <DesignAxisRecord>",
+ ' <Axis index="0">',
+ ' <AxisTag value="opsz"/>',
+ ' <AxisNameID value="256"/> <!-- Optical Size -->',
+ ' <AxisOrdering value="0"/>',
+ " </Axis>",
+ " </DesignAxisRecord>",
+ " <!-- AxisValueCount=3 -->",
+ " <AxisValueArray>",
+ ' <AxisValue index="0" Format="2">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="257"/> <!-- Small -->',
+ ' <NominalValue value="6.0"/>',
+ ' <RangeMinValue value="-32768.0"/>',
+ ' <RangeMaxValue value="10.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="1" Format="2">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
+ ' <ValueNameID value="258"/> <!-- Text -->',
+ ' <NominalValue value="14.0"/>',
+ ' <RangeMinValue value="10.0"/>',
+ ' <RangeMaxValue value="24.0"/>',
+ " </AxisValue>",
+ ' <AxisValue index="2" Format="2">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="259"/> <!-- Display -->',
+ ' <NominalValue value="600.0"/>',
+ ' <RangeMinValue value="24.0"/>',
+ ' <RangeMaxValue value="32767.99998"/>',
+ " </AxisValue>",
+ " </AxisValueArray>",
+ ' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
+ " </STAT>",
+ ],
+ ),
+ (
+ [
+ dict(tag="wght", name="Weight", ordering=1, values=[]),
+ dict(
+ tag="ABCD",
+ name="ABCDTest",
+ ordering=0,
+ values=[dict(value=100, name="Regular", flags=0x2)],
+ ),
+ ],
+ [dict(location=dict(wght=300, ABCD=100), name="Regular ABCD")],
+ 18,
+ [
+ " <STAT>",
+ ' <Version value="0x00010002"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ " <!-- DesignAxisCount=2 -->",
+ " <DesignAxisRecord>",
+ ' <Axis index="0">',
+ ' <AxisTag value="wght"/>',
+ ' <AxisNameID value="256"/> <!-- Weight -->',
+ ' <AxisOrdering value="1"/>',
+ " </Axis>",
+ ' <Axis index="1">',
+ ' <AxisTag value="ABCD"/>',
+ ' <AxisNameID value="257"/> <!-- ABCDTest -->',
+ ' <AxisOrdering value="0"/>',
+ " </Axis>",
+ " </DesignAxisRecord>",
+ " <!-- AxisValueCount=2 -->",
+ " <AxisValueArray>",
+ ' <AxisValue index="0" Format="4">',
+ " <!-- AxisCount=2 -->",
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="259"/> <!-- Regular ABCD -->',
+ ' <AxisValueRecord index="0">',
+ ' <AxisIndex value="0"/>',
+ ' <Value value="300.0"/>',
+ " </AxisValueRecord>",
+ ' <AxisValueRecord index="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Value value="100.0"/>',
+ " </AxisValueRecord>",
+ " </AxisValue>",
+ ' <AxisValue index="1" Format="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Flags value="2"/> <!-- ElidableAxisValueName -->',
+ ' <ValueNameID value="258"/> <!-- Regular -->',
+ ' <Value value="100.0"/>',
+ " </AxisValue>",
+ " </AxisValueArray>",
+ ' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
+ " </STAT>",
+ ],
+ ),
]
-@pytest.mark.parametrize("axes, axisValues, elidedFallbackName, expected_ttx", buildStatTable_test_data)
+@pytest.mark.parametrize(
+ "axes, axisValues, elidedFallbackName, expected_ttx", buildStatTable_test_data
+)
def test_buildStatTable(axes, axisValues, elidedFallbackName, expected_ttx):
font = ttLib.TTFont()
font["name"] = ttLib.newTable("name")
@@ -1402,6 +1448,100 @@ def test_buildStatTable(axes, axisValues, elidedFallbackName, expected_ttx):
assert expected_ttx == ttx
+def test_buildStatTable_platform_specific_names():
+ # PR: https://github.com/fonttools/fonttools/pull/2528
+ # Introduce new 'platform' feature for creating a STAT table.
+ # Set windowsNames and or macNames to create name table entries
+ # in the specified platforms
+ font_obj = ttLib.TTFont()
+ font_obj["name"] = ttLib.newTable("name")
+ font_obj["name"].names = []
+
+ wght_values = [
+ dict(nominalValue=200, rangeMinValue=200, rangeMaxValue=250, name="ExtraLight"),
+ dict(nominalValue=300, rangeMinValue=250, rangeMaxValue=350, name="Light"),
+ dict(
+ nominalValue=400,
+ rangeMinValue=350,
+ rangeMaxValue=450,
+ name="Regular",
+ flags=0x2,
+ ),
+ dict(nominalValue=500, rangeMinValue=450, rangeMaxValue=650, name="Medium"),
+ dict(nominalValue=700, rangeMinValue=650, rangeMaxValue=750, name="Bold"),
+ dict(nominalValue=800, rangeMinValue=750, rangeMaxValue=850, name="ExtraBold"),
+ dict(nominalValue=900, rangeMinValue=850, rangeMaxValue=900, name="Black"),
+ ]
+
+ AXES = [
+ dict(
+ tag="wght",
+ name="Weight",
+ ordering=1,
+ values=wght_values,
+ ),
+ ]
+
+ font_obj["name"].setName("ExtraLight", 260, 3, 1, 0x409)
+ font_obj["name"].setName("Light", 261, 3, 1, 0x409)
+ font_obj["name"].setName("Regular", 262, 3, 1, 0x409)
+ font_obj["name"].setName("Medium", 263, 3, 1, 0x409)
+ font_obj["name"].setName("Bold", 264, 3, 1, 0x409)
+ font_obj["name"].setName("ExtraBold", 265, 3, 1, 0x409)
+ font_obj["name"].setName("Black", 266, 3, 1, 0x409)
+
+ font_obj["name"].setName("Weight", 270, 3, 1, 0x409)
+
+ expected_names = [x.string for x in font_obj["name"].names]
+
+ builder.buildStatTable(font_obj, AXES, windowsNames=True, macNames=False)
+ actual_names = [x.string for x in font_obj["name"].names]
+
+ # no new name records were added by buildStatTable
+ # because windows-only names with the same strings were already present
+ assert expected_names == actual_names
+
+ font_obj["name"].removeNames(nameID=270)
+ expected_names = [x.string for x in font_obj["name"].names] + ["Weight"]
+
+ builder.buildStatTable(font_obj, AXES, windowsNames=True, macNames=False)
+ actual_names = [x.string for x in font_obj["name"].names]
+ # One new name records 'Weight' were added by buildStatTable
+ assert expected_names == actual_names
+
+ builder.buildStatTable(font_obj, AXES, windowsNames=True, macNames=True)
+ actual_names = [x.string for x in font_obj["name"].names]
+ expected_names = [
+ "Weight",
+ "Weight",
+ "Weight",
+ "ExtraLight",
+ "ExtraLight",
+ "ExtraLight",
+ "Light",
+ "Light",
+ "Light",
+ "Regular",
+ "Regular",
+ "Regular",
+ "Medium",
+ "Medium",
+ "Medium",
+ "Bold",
+ "Bold",
+ "Bold",
+ "ExtraBold",
+ "ExtraBold",
+ "ExtraBold",
+ "Black",
+ "Black",
+ "Black",
+ ]
+ # Because there is an inconsistency in the names add new name IDs
+ # for each platform -> windowsNames=True, macNames=True
+ assert sorted(expected_names) == sorted(actual_names)
+
+
def test_stat_infinities():
negInf = floatToFixed(builder.AXIS_VALUE_NEGATIVE_INFINITY, 16)
assert struct.pack(">l", negInf) == b"\x80\x00\x00\x00"
@@ -1412,7 +1552,7 @@ def test_stat_infinities():
class ChainContextualRulesetTest(object):
def test_makeRulesets(self):
font = ttLib.TTFont()
- font.setGlyphOrder(["a","b","c","d","A","B","C","D","E"])
+ font.setGlyphOrder(["a", "b", "c", "d", "A", "B", "C", "D", "E"])
sb = builder.ChainContextSubstBuilder(font, None)
prefix, input_, suffix, lookups = [["a"], ["b"]], [["c"]], [], [None]
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
@@ -1425,7 +1565,7 @@ class ChainContextualRulesetTest(object):
# Second subtable has some glyph classes
prefix, input_, suffix, lookups = [["A"]], [["E"]], [], [None]
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
- prefix, input_, suffix, lookups = [["A"]], [["C","D"]], [], [None]
+ prefix, input_, suffix, lookups = [["A"]], [["C", "D"]], [], [None]
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
prefix, input_, suffix, lookups = [["A", "B"]], [["E"]], [], [None]
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
@@ -1435,7 +1575,7 @@ class ChainContextualRulesetTest(object):
# Third subtable has no pre/post context
prefix, input_, suffix, lookups = [], [["E"]], [], [None]
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
- prefix, input_, suffix, lookups = [], [["C","D"]], [], [None]
+ prefix, input_, suffix, lookups = [], [["C", "D"]], [], [None]
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
rulesets = sb.rulesets()
@@ -1443,7 +1583,7 @@ class ChainContextualRulesetTest(object):
assert rulesets[0].hasPrefixOrSuffix
assert not rulesets[0].hasAnyGlyphClasses
cd = rulesets[0].format2ClassDefs()
- assert set(cd[0].classes()[1:]) == set([("d",),("b",),("a",)])
+ assert set(cd[0].classes()[1:]) == set([("d",), ("b",), ("a",)])
assert set(cd[1].classes()[1:]) == set([("c",)])
assert set(cd[2].classes()[1:]) == set()
@@ -1456,7 +1596,7 @@ class ChainContextualRulesetTest(object):
assert rulesets[2].format2ClassDefs()
cd = rulesets[2].format2ClassDefs()
assert set(cd[0].classes()[1:]) == set()
- assert set(cd[1].classes()[1:]) == set([("C","D"), ("E",)])
+ assert set(cd[1].classes()[1:]) == set([("C", "D"), ("E",)])
assert set(cd[2].classes()[1:]) == set()
diff --git a/Tests/otlLib/optimize_test.py b/Tests/otlLib/optimize_test.py
new file mode 100644
index 00000000..40cf389e
--- /dev/null
+++ b/Tests/otlLib/optimize_test.py
@@ -0,0 +1,175 @@
+import logging
+from pathlib import Path
+from subprocess import run
+import contextlib
+import os
+from typing import List, Optional, Tuple
+from fontTools.ttLib import TTFont
+
+import pytest
+
+from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
+from fontTools.fontBuilder import FontBuilder
+
+from fontTools.ttLib.tables.otBase import OTTableWriter, ValueRecord
+
+
+def test_main(tmpdir: Path):
+ """Check that calling the main function on an input TTF works."""
+ glyphs = ".notdef space A Aacute B D".split()
+ features = """
+ @A = [A Aacute];
+ @B = [B D];
+ feature kern {
+ pos @A @B -50;
+ } kern;
+ """
+ fb = FontBuilder(1000)
+ fb.setupGlyphOrder(glyphs)
+ addOpenTypeFeaturesFromString(fb.font, features)
+ input = tmpdir / "in.ttf"
+ fb.save(str(input))
+ output = tmpdir / "out.ttf"
+ run(
+ [
+ "fonttools",
+ "otlLib.optimize",
+ "--gpos-compact-mode",
+ "5",
+ str(input),
+ "-o",
+ str(output),
+ ],
+ check=True,
+ )
+ assert output.exists()
+
+
+# Copy-pasted from https://stackoverflow.com/questions/2059482/python-temporarily-modify-the-current-processs-environment
+# TODO: remove when moving to the Config class
+@contextlib.contextmanager
+def set_env(**environ):
+ """
+ Temporarily set the process environment variables.
+
+ >>> with set_env(PLUGINS_DIR=u'test/plugins'):
+ ... "PLUGINS_DIR" in os.environ
+ True
+
+ >>> "PLUGINS_DIR" in os.environ
+ False
+
+ :type environ: dict[str, unicode]
+ :param environ: Environment variables to set
+ """
+ old_environ = dict(os.environ)
+ os.environ.update(environ)
+ try:
+ yield
+ finally:
+ os.environ.clear()
+ os.environ.update(old_environ)
+
+
+def count_pairpos_subtables(font: TTFont) -> int:
+ subtables = 0
+ for lookup in font["GPOS"].table.LookupList.Lookup:
+ if lookup.LookupType == 2:
+ subtables += len(lookup.SubTable)
+ elif lookup.LookupType == 9:
+ for subtable in lookup.SubTable:
+ if subtable.ExtensionLookupType == 2:
+ subtables += 1
+ return subtables
+
+
+def count_pairpos_bytes(font: TTFont) -> int:
+ bytes = 0
+ gpos = font["GPOS"]
+ for lookup in font["GPOS"].table.LookupList.Lookup:
+ if lookup.LookupType == 2:
+ w = OTTableWriter(tableTag=gpos.tableTag)
+ lookup.compile(w, font)
+ bytes += len(w.getAllData())
+ elif lookup.LookupType == 9:
+ if any(subtable.ExtensionLookupType == 2 for subtable in lookup.SubTable):
+ w = OTTableWriter(tableTag=gpos.tableTag)
+ lookup.compile(w, font)
+ bytes += len(w.getAllData())
+ return bytes
+
+
+def get_kerning_by_blocks(blocks: List[Tuple[int, int]]) -> Tuple[List[str], str]:
+ """Generate a highly compressible font by generating a bunch of rectangular
+ blocks on the diagonal that can easily be sliced into subtables.
+
+ Returns the list of glyphs and feature code of the font.
+ """
+ value = 0
+ glyphs: List[str] = []
+ rules = []
+ # Each block is like a script in a multi-script font
+ for script, (width, height) in enumerate(blocks):
+ glyphs.extend(f"g_{script}_{i}" for i in range(max(width, height)))
+ for l in range(height):
+ for r in range(width):
+ value += 1
+ rules.append((f"g_{script}_{l}", f"g_{script}_{r}", value))
+ classes = "\n".join([f"@{g} = [{g}];" for g in glyphs])
+ statements = "\n".join([f"pos @{l} @{r} {v};" for (l, r, v) in rules])
+ features = f"""
+ {classes}
+ feature kern {{
+ {statements}
+ }} kern;
+ """
+ return glyphs, features
+
+
+@pytest.mark.parametrize(
+ ("blocks", "mode", "expected_subtables", "expected_bytes"),
+ [
+ # Mode = 0 = no optimization leads to 650 bytes of GPOS
+ ([(15, 3), (2, 10)], None, 1, 602),
+ # Optimization level 1 recognizes the 2 blocks and splits into 2
+ # subtables = adds 1 subtable leading to a size reduction of
+ # (602-298)/602 = 50%
+ ([(15, 3), (2, 10)], 1, 2, 298),
+ # On a bigger block configuration, we see that mode=5 doesn't create
+ # as many subtables as it could, because of the stop criteria
+ ([(4, 4) for _ in range(20)], 5, 14, 2042),
+ # while level=9 creates as many subtables as there were blocks on the
+ # diagonal and yields a better saving
+ ([(4, 4) for _ in range(20)], 9, 20, 1886),
+ # On a fully occupied kerning matrix, even the strategy 9 doesn't
+ # split anything.
+ ([(10, 10)], 9, 1, 304)
+ ],
+)
+def test_optimization_mode(
+ caplog,
+ blocks: List[Tuple[int, int]],
+ mode: Optional[int],
+ expected_subtables: int,
+ expected_bytes: int,
+):
+ """Check that the optimizations are off by default, and that increasing
+ the optimization level creates more subtables and a smaller byte size.
+ """
+ caplog.set_level(logging.DEBUG)
+
+ glyphs, features = get_kerning_by_blocks(blocks)
+ glyphs = [".notdef space"] + glyphs
+
+ env = {}
+ if mode is not None:
+ # NOTE: activating this optimization via the environment variable is
+ # experimental and may not be supported once an alternative mechanism
+ # is in place. See: https://github.com/fonttools/fonttools/issues/2349
+ env["FONTTOOLS_GPOS_COMPACT_MODE"] = str(mode)
+ with set_env(**env):
+ fb = FontBuilder(1000)
+ fb.setupGlyphOrder(glyphs)
+ addOpenTypeFeaturesFromString(fb.font, features)
+ assert expected_subtables == count_pairpos_subtables(fb.font)
+ assert expected_bytes == count_pairpos_bytes(fb.font)