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