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