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