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