• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.misc.testTools import parseXML
2from fontTools.misc.textTools import deHexStr
3from fontTools.misc.xmlWriter import XMLWriter
4from fontTools.ttLib import TTLibError
5from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis, NamedInstance
6from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e, NameRecord
7from io import BytesIO
8import unittest
9
10
11FVAR_DATA = deHexStr(
12    "00 01 00 00 00 10 00 02 00 02 00 14 00 02 00 0C "
13    "77 67 68 74 00 64 00 00 01 90 00 00 03 84 00 00 00 00 01 01 "
14    "77 64 74 68 00 32 00 00 00 64 00 00 00 c8 00 00 00 00 01 02 "
15    "01 03 00 00 01 2c 00 00 00 64 00 00 "
16    "01 04 00 00 01 2c 00 00 00 4b 00 00"
17)
18
19FVAR_AXIS_DATA = deHexStr("6F 70 73 7a ff ff 80 00 00 01 4c cd 00 01 80 00 00 00 01 59")
20
21FVAR_INSTANCE_DATA_WITHOUT_PSNAME = deHexStr("01 59 00 00 00 00 b3 33 00 00 80 00")
22
23FVAR_INSTANCE_DATA_WITH_PSNAME = FVAR_INSTANCE_DATA_WITHOUT_PSNAME + deHexStr("02 34")
24
25
26def xml_lines(writer):
27    content = writer.file.getvalue().decode("utf-8")
28    return [line.strip() for line in content.splitlines()][1:]
29
30
31def AddName(font, name):
32    nameTable = font.get("name")
33    if nameTable is None:
34        nameTable = font["name"] = table__n_a_m_e()
35        nameTable.names = []
36    namerec = NameRecord()
37    namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256])
38    namerec.string = name.encode("mac_roman")
39    namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0)
40    nameTable.names.append(namerec)
41    return namerec
42
43
44def MakeFont():
45    axes = [("wght", "Weight", 100, 400, 900), ("wdth", "Width", 50, 100, 200)]
46    instances = [("Light", 300, 100), ("Light Condensed", 300, 75)]
47    fvarTable = table__f_v_a_r()
48    font = {"fvar": fvarTable}
49    for tag, name, minValue, defaultValue, maxValue in axes:
50        axis = Axis()
51        axis.axisTag = tag
52        axis.defaultValue = defaultValue
53        axis.minValue, axis.maxValue = minValue, maxValue
54        axis.axisNameID = AddName(font, name).nameID
55        fvarTable.axes.append(axis)
56    for name, weight, width in instances:
57        inst = NamedInstance()
58        inst.subfamilyNameID = AddName(font, name).nameID
59        inst.coordinates = {"wght": weight, "wdth": width}
60        fvarTable.instances.append(inst)
61    return font
62
63
64class FontVariationTableTest(unittest.TestCase):
65    def test_compile(self):
66        font = MakeFont()
67        h = font["fvar"].compile(font)
68        self.assertEqual(FVAR_DATA, font["fvar"].compile(font))
69
70    def test_decompile(self):
71        fvar = table__f_v_a_r()
72        fvar.decompile(FVAR_DATA, ttFont={"fvar": fvar})
73        self.assertEqual(["wght", "wdth"], [a.axisTag for a in fvar.axes])
74        self.assertEqual([259, 260], [i.subfamilyNameID for i in fvar.instances])
75
76    def test_toXML(self):
77        font = MakeFont()
78        writer = XMLWriter(BytesIO())
79        font["fvar"].toXML(writer, font)
80        xml = writer.file.getvalue().decode("utf-8")
81        self.assertEqual(2, xml.count("<Axis>"))
82        self.assertTrue("<AxisTag>wght</AxisTag>" in xml)
83        self.assertTrue("<AxisTag>wdth</AxisTag>" in xml)
84        self.assertEqual(2, xml.count("<NamedInstance "))
85        self.assertTrue("<!-- Light -->" in xml)
86        self.assertTrue("<!-- Light Condensed -->" in xml)
87
88    def test_fromXML(self):
89        fvar = table__f_v_a_r()
90        for name, attrs, content in parseXML(
91            "<Axis>"
92            "    <AxisTag>opsz</AxisTag>"
93            "</Axis>"
94            "<Axis>"
95            "    <AxisTag>slnt</AxisTag>"
96            "    <Flags>0x123</Flags>"
97            "</Axis>"
98            '<NamedInstance subfamilyNameID="765"/>'
99            '<NamedInstance subfamilyNameID="234"/>'
100        ):
101            fvar.fromXML(name, attrs, content, ttFont=None)
102        self.assertEqual(["opsz", "slnt"], [a.axisTag for a in fvar.axes])
103        self.assertEqual([0, 0x123], [a.flags for a in fvar.axes])
104        self.assertEqual([765, 234], [i.subfamilyNameID for i in fvar.instances])
105
106
107class AxisTest(unittest.TestCase):
108    def test_compile(self):
109        axis = Axis()
110        axis.axisTag, axis.axisNameID = ("opsz", 345)
111        axis.minValue, axis.defaultValue, axis.maxValue = (-0.5, 1.3, 1.5)
112        self.assertEqual(FVAR_AXIS_DATA, axis.compile())
113
114    def test_decompile(self):
115        axis = Axis()
116        axis.decompile(FVAR_AXIS_DATA)
117        self.assertEqual("opsz", axis.axisTag)
118        self.assertEqual(345, axis.axisNameID)
119        self.assertEqual(-0.5, axis.minValue)
120        self.assertAlmostEqual(1.3000031, axis.defaultValue)
121        self.assertEqual(1.5, axis.maxValue)
122
123    def test_toXML(self):
124        font = MakeFont()
125        axis = Axis()
126        axis.decompile(FVAR_AXIS_DATA)
127        AddName(font, "Optical Size").nameID = 256
128        axis.axisNameID = 256
129        axis.flags = 0xABC
130        writer = XMLWriter(BytesIO())
131        axis.toXML(writer, font)
132        self.assertEqual(
133            [
134                "",
135                "<!-- Optical Size -->",
136                "<Axis>",
137                "<AxisTag>opsz</AxisTag>",
138                "<Flags>0xABC</Flags>",
139                "<MinValue>-0.5</MinValue>",
140                "<DefaultValue>1.3</DefaultValue>",
141                "<MaxValue>1.5</MaxValue>",
142                "<AxisNameID>256</AxisNameID>",
143                "</Axis>",
144            ],
145            xml_lines(writer),
146        )
147
148    def test_fromXML(self):
149        axis = Axis()
150        for name, attrs, content in parseXML(
151            "<Axis>"
152            "    <AxisTag>wght</AxisTag>"
153            "    <Flags>0x123ABC</Flags>"
154            "    <MinValue>100</MinValue>"
155            "    <DefaultValue>400</DefaultValue>"
156            "    <MaxValue>900</MaxValue>"
157            "    <AxisNameID>256</AxisNameID>"
158            "</Axis>"
159        ):
160            axis.fromXML(name, attrs, content, ttFont=None)
161        self.assertEqual("wght", axis.axisTag)
162        self.assertEqual(0x123ABC, axis.flags)
163        self.assertEqual(100, axis.minValue)
164        self.assertEqual(400, axis.defaultValue)
165        self.assertEqual(900, axis.maxValue)
166        self.assertEqual(256, axis.axisNameID)
167
168
169class NamedInstanceTest(unittest.TestCase):
170    def assertDictAlmostEqual(self, dict1, dict2):
171        self.assertEqual(set(dict1.keys()), set(dict2.keys()))
172        for key in dict1:
173            self.assertAlmostEqual(dict1[key], dict2[key])
174
175    def test_compile_withPostScriptName(self):
176        inst = NamedInstance()
177        inst.subfamilyNameID = 345
178        inst.postscriptNameID = 564
179        inst.coordinates = {"wght": 0.7, "wdth": 0.5}
180        self.assertEqual(
181            FVAR_INSTANCE_DATA_WITH_PSNAME, inst.compile(["wght", "wdth"], True)
182        )
183
184    def test_compile_withoutPostScriptName(self):
185        inst = NamedInstance()
186        inst.subfamilyNameID = 345
187        inst.postscriptNameID = 564
188        inst.coordinates = {"wght": 0.7, "wdth": 0.5}
189        self.assertEqual(
190            FVAR_INSTANCE_DATA_WITHOUT_PSNAME, inst.compile(["wght", "wdth"], False)
191        )
192
193    def test_decompile_withPostScriptName(self):
194        inst = NamedInstance()
195        inst.decompile(FVAR_INSTANCE_DATA_WITH_PSNAME, ["wght", "wdth"])
196        self.assertEqual(564, inst.postscriptNameID)
197        self.assertEqual(345, inst.subfamilyNameID)
198        self.assertDictAlmostEqual({"wght": 0.6999969, "wdth": 0.5}, inst.coordinates)
199
200    def test_decompile_withoutPostScriptName(self):
201        inst = NamedInstance()
202        inst.decompile(FVAR_INSTANCE_DATA_WITHOUT_PSNAME, ["wght", "wdth"])
203        self.assertEqual(0xFFFF, inst.postscriptNameID)
204        self.assertEqual(345, inst.subfamilyNameID)
205        self.assertDictAlmostEqual({"wght": 0.6999969, "wdth": 0.5}, inst.coordinates)
206
207    def test_toXML_withPostScriptName(self):
208        font = MakeFont()
209        inst = NamedInstance()
210        inst.flags = 0xE9
211        inst.subfamilyNameID = AddName(font, "Light Condensed").nameID
212        inst.postscriptNameID = AddName(font, "Test-LightCondensed").nameID
213        inst.coordinates = {"wght": 0.7, "wdth": 0.5}
214        writer = XMLWriter(BytesIO())
215        inst.toXML(writer, font)
216        self.assertEqual(
217            [
218                "",
219                "<!-- Light Condensed -->",
220                "<!-- PostScript: Test-LightCondensed -->",
221                '<NamedInstance flags="0xE9" postscriptNameID="%s" subfamilyNameID="%s">'
222                % (inst.postscriptNameID, inst.subfamilyNameID),
223                '<coord axis="wght" value="0.7"/>',
224                '<coord axis="wdth" value="0.5"/>',
225                "</NamedInstance>",
226            ],
227            xml_lines(writer),
228        )
229
230    def test_toXML_withoutPostScriptName(self):
231        font = MakeFont()
232        inst = NamedInstance()
233        inst.flags = 0xABC
234        inst.subfamilyNameID = AddName(font, "Light Condensed").nameID
235        inst.coordinates = {"wght": 0.7, "wdth": 0.5}
236        writer = XMLWriter(BytesIO())
237        inst.toXML(writer, font)
238        self.assertEqual(
239            [
240                "",
241                "<!-- Light Condensed -->",
242                '<NamedInstance flags="0xABC" subfamilyNameID="%s">'
243                % inst.subfamilyNameID,
244                '<coord axis="wght" value="0.7"/>',
245                '<coord axis="wdth" value="0.5"/>',
246                "</NamedInstance>",
247            ],
248            xml_lines(writer),
249        )
250
251    def test_fromXML_withPostScriptName(self):
252        inst = NamedInstance()
253        for name, attrs, content in parseXML(
254            '<NamedInstance flags="0x0" postscriptNameID="257" subfamilyNameID="345">'
255            '    <coord axis="wght" value="0.7"/>'
256            '    <coord axis="wdth" value="0.5"/>'
257            "</NamedInstance>"
258        ):
259            inst.fromXML(name, attrs, content, ttFont=MakeFont())
260        self.assertEqual(257, inst.postscriptNameID)
261        self.assertEqual(345, inst.subfamilyNameID)
262        self.assertDictAlmostEqual({"wght": 0.6999969, "wdth": 0.5}, inst.coordinates)
263
264    def test_fromXML_withoutPostScriptName(self):
265        inst = NamedInstance()
266        for name, attrs, content in parseXML(
267            '<NamedInstance flags="0x123ABC" subfamilyNameID="345">'
268            '    <coord axis="wght" value="0.7"/>'
269            '    <coord axis="wdth" value="0.5"/>'
270            "</NamedInstance>"
271        ):
272            inst.fromXML(name, attrs, content, ttFont=MakeFont())
273        self.assertEqual(0x123ABC, inst.flags)
274        self.assertEqual(345, inst.subfamilyNameID)
275        self.assertDictAlmostEqual({"wght": 0.6999969, "wdth": 0.5}, inst.coordinates)
276
277
278if __name__ == "__main__":
279    import sys
280
281    sys.exit(unittest.main())
282