1from fontTools.misc.loggingTools import CapturingLogHandler 2from fontTools.misc.testTools import parseXML 3from fontTools.misc.textTools import deHexStr, hexStr 4from fontTools.misc.xmlWriter import XMLWriter 5from fontTools.ttLib.tables.TupleVariation import \ 6 log, TupleVariation, compileSharedTuples, decompileSharedTuples, \ 7 compileTupleVariationStore, decompileTupleVariationStore, inferRegion_ 8from io import BytesIO 9import random 10import unittest 11 12 13def hexencode(s): 14 h = hexStr(s).upper() 15 return ' '.join([h[i:i+2] for i in range(0, len(h), 2)]) 16 17 18AXES = { 19 "wdth": (0.25, 0.375, 0.5), 20 "wght": (0.0, 1.0, 1.0), 21 "opsz": (-0.75, -0.75, 0.0) 22} 23 24 25# Shared tuples in the 'gvar' table of the Skia font, as printed 26# in Apple's TrueType specification. 27# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html 28SKIA_GVAR_SHARED_TUPLES_DATA = deHexStr( 29 "40 00 00 00 C0 00 00 00 00 00 40 00 00 00 C0 00 " 30 "C0 00 C0 00 40 00 C0 00 40 00 40 00 C0 00 40 00") 31 32SKIA_GVAR_SHARED_TUPLES = [ 33 {"wght": 1.0, "wdth": 0.0}, 34 {"wght": -1.0, "wdth": 0.0}, 35 {"wght": 0.0, "wdth": 1.0}, 36 {"wght": 0.0, "wdth": -1.0}, 37 {"wght": -1.0, "wdth": -1.0}, 38 {"wght": 1.0, "wdth": -1.0}, 39 {"wght": 1.0, "wdth": 1.0}, 40 {"wght": -1.0, "wdth": 1.0} 41] 42 43 44# Tuple Variation Store of uppercase I in the Skia font, as printed in Apple's 45# TrueType spec. The actual Skia font uses a different table for uppercase I 46# than what is printed in Apple's spec, but we still want to make sure that 47# we can parse the data as it appears in the specification. 48# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html 49SKIA_GVAR_I_DATA = deHexStr( 50 "00 08 00 24 00 33 20 00 00 15 20 01 00 1B 20 02 " 51 "00 24 20 03 00 15 20 04 00 26 20 07 00 0D 20 06 " 52 "00 1A 20 05 00 40 01 01 01 81 80 43 FF 7E FF 7E " 53 "FF 7E FF 7E 00 81 45 01 01 01 03 01 04 01 04 01 " 54 "04 01 02 80 40 00 82 81 81 04 3A 5A 3E 43 20 81 " 55 "04 0E 40 15 45 7C 83 00 0D 9E F3 F2 F0 F0 F0 F0 " 56 "F3 9E A0 A1 A1 A1 9F 80 00 91 81 91 00 0D 0A 0A " 57 "09 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0B 80 00 15 81 " 58 "81 00 C4 89 00 C4 83 00 0D 80 99 98 96 96 96 96 " 59 "99 80 82 83 83 83 81 80 40 FF 18 81 81 04 E6 F9 " 60 "10 21 02 81 04 E8 E5 EB 4D DA 83 00 0D CE D3 D4 " 61 "D3 D3 D3 D5 D2 CE CC CD CD CD CD 80 00 A1 81 91 " 62 "00 0D 07 03 04 02 02 02 03 03 07 07 08 08 08 07 " 63 "80 00 09 81 81 00 28 40 00 A4 02 24 24 66 81 04 " 64 "08 FA FA FA 28 83 00 82 02 FF FF FF 83 02 01 01 " 65 "01 84 91 00 80 06 07 08 08 08 08 0A 07 80 03 FE " 66 "FF FF FF 81 00 08 81 82 02 EE EE EE 8B 6D 00") 67 68 69class TupleVariationTest(unittest.TestCase): 70 def __init__(self, methodName): 71 unittest.TestCase.__init__(self, methodName) 72 # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 73 # and fires deprecation warnings if a program uses the old name. 74 if not hasattr(self, "assertRaisesRegex"): 75 self.assertRaisesRegex = self.assertRaisesRegexp 76 77 def test_equal(self): 78 var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) 79 var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) 80 self.assertEqual(var1, var2) 81 82 def test_equal_differentAxes(self): 83 var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) 84 var2 = TupleVariation({"wght":(0.7, 0.8, 0.9)}, [(0,0), (9,8), (7,6)]) 85 self.assertNotEqual(var1, var2) 86 87 def test_equal_differentCoordinates(self): 88 var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) 89 var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8)]) 90 self.assertNotEqual(var1, var2) 91 92 def test_hasImpact_someDeltasNotZero(self): 93 axes = {"wght":(0.0, 1.0, 1.0)} 94 var = TupleVariation(axes, [(0,0), (9,8), (7,6)]) 95 self.assertTrue(var.hasImpact()) 96 97 def test_hasImpact_allDeltasZero(self): 98 axes = {"wght":(0.0, 1.0, 1.0)} 99 var = TupleVariation(axes, [(0,0), (0,0), (0,0)]) 100 self.assertTrue(var.hasImpact()) 101 102 def test_hasImpact_allDeltasNone(self): 103 axes = {"wght":(0.0, 1.0, 1.0)} 104 var = TupleVariation(axes, [None, None, None]) 105 self.assertFalse(var.hasImpact()) 106 107 def test_toXML_badDeltaFormat(self): 108 writer = XMLWriter(BytesIO()) 109 g = TupleVariation(AXES, ["String"]) 110 with CapturingLogHandler(log, "ERROR") as captor: 111 g.toXML(writer, ["wdth"]) 112 self.assertIn("bad delta format", [r.msg for r in captor.records]) 113 self.assertEqual([ 114 '<tuple>', 115 '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>', 116 '<!-- bad delta #0 -->', 117 '</tuple>', 118 ], TupleVariationTest.xml_lines(writer)) 119 120 def test_toXML_constants(self): 121 writer = XMLWriter(BytesIO()) 122 g = TupleVariation(AXES, [42, None, 23, 0, -17, None]) 123 g.toXML(writer, ["wdth", "wght", "opsz"]) 124 self.assertEqual([ 125 '<tuple>', 126 '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>', 127 '<coord axis="wght" value="1.0"/>', 128 '<coord axis="opsz" value="-0.75"/>', 129 '<delta cvt="0" value="42"/>', 130 '<delta cvt="2" value="23"/>', 131 '<delta cvt="3" value="0"/>', 132 '<delta cvt="4" value="-17"/>', 133 '</tuple>' 134 ], TupleVariationTest.xml_lines(writer)) 135 136 def test_toXML_points(self): 137 writer = XMLWriter(BytesIO()) 138 g = TupleVariation(AXES, [(9,8), None, (7,6), (0,0), (-1,-2), None]) 139 g.toXML(writer, ["wdth", "wght", "opsz"]) 140 self.assertEqual([ 141 '<tuple>', 142 '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>', 143 '<coord axis="wght" value="1.0"/>', 144 '<coord axis="opsz" value="-0.75"/>', 145 '<delta pt="0" x="9" y="8"/>', 146 '<delta pt="2" x="7" y="6"/>', 147 '<delta pt="3" x="0" y="0"/>', 148 '<delta pt="4" x="-1" y="-2"/>', 149 '</tuple>' 150 ], TupleVariationTest.xml_lines(writer)) 151 152 def test_toXML_allDeltasNone(self): 153 writer = XMLWriter(BytesIO()) 154 axes = {"wght":(0.0, 1.0, 1.0)} 155 g = TupleVariation(axes, [None] * 5) 156 g.toXML(writer, ["wght", "wdth"]) 157 self.assertEqual([ 158 '<tuple>', 159 '<coord axis="wght" value="1.0"/>', 160 '<!-- no deltas -->', 161 '</tuple>' 162 ], TupleVariationTest.xml_lines(writer)) 163 164 def test_toXML_axes_floats(self): 165 writer = XMLWriter(BytesIO()) 166 axes = { 167 "wght": (0.0, 0.2999878, 0.7000122), 168 "wdth": (0.0, 0.4000244, 0.4000244), 169 } 170 g = TupleVariation(axes, [None] * 5) 171 g.toXML(writer, ["wght", "wdth"]) 172 self.assertEqual( 173 [ 174 '<coord axis="wght" min="0.0" value="0.3" max="0.7"/>', 175 '<coord axis="wdth" value="0.4"/>', 176 ], 177 TupleVariationTest.xml_lines(writer)[1:3] 178 ) 179 180 def test_fromXML_badDeltaFormat(self): 181 g = TupleVariation({}, []) 182 with CapturingLogHandler(log, "WARNING") as captor: 183 for name, attrs, content in parseXML('<delta a="1" b="2"/>'): 184 g.fromXML(name, attrs, content) 185 self.assertIn("bad delta format: a, b", 186 [r.msg for r in captor.records]) 187 188 def test_fromXML_constants(self): 189 g = TupleVariation({}, [None] * 4) 190 for name, attrs, content in parseXML( 191 '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>' 192 '<coord axis="wght" value="1.0"/>' 193 '<coord axis="opsz" value="-0.75"/>' 194 '<delta cvt="1" value="42"/>' 195 '<delta cvt="2" value="-23"/>'): 196 g.fromXML(name, attrs, content) 197 self.assertEqual(AXES, g.axes) 198 self.assertEqual([None, 42, -23, None], g.coordinates) 199 200 def test_fromXML_points(self): 201 g = TupleVariation({}, [None] * 4) 202 for name, attrs, content in parseXML( 203 '<coord axis="wdth" min="0.25" value="0.375" max="0.5"/>' 204 '<coord axis="wght" value="1.0"/>' 205 '<coord axis="opsz" value="-0.75"/>' 206 '<delta pt="1" x="33" y="44"/>' 207 '<delta pt="2" x="-2" y="170"/>'): 208 g.fromXML(name, attrs, content) 209 self.assertEqual(AXES, g.axes) 210 self.assertEqual([None, (33, 44), (-2, 170), None], g.coordinates) 211 212 def test_fromXML_axes_floats(self): 213 g = TupleVariation({}, [None] * 4) 214 for name, attrs, content in parseXML( 215 '<coord axis="wght" min="0.0" value="0.3" max="0.7"/>' 216 '<coord axis="wdth" value="0.4"/>' 217 ): 218 g.fromXML(name, attrs, content) 219 220 self.assertEqual(g.axes["wght"][0], 0) 221 self.assertAlmostEqual(g.axes["wght"][1], 0.2999878) 222 self.assertAlmostEqual(g.axes["wght"][2], 0.7000122) 223 224 self.assertEqual(g.axes["wdth"][0], 0) 225 self.assertAlmostEqual(g.axes["wdth"][1], 0.4000244) 226 self.assertAlmostEqual(g.axes["wdth"][2], 0.4000244) 227 228 def test_compile_sharedPeaks_nonIntermediate_sharedPoints(self): 229 var = TupleVariation( 230 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 231 [(7,4), (8,5), (9,6)]) 232 axisTags = ["wght", "wdth"] 233 sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } 234 tup, deltas = var.compile(axisTags, sharedPeakIndices, pointData=b'') 235 # len(deltas)=8; flags=None; tupleIndex=0x77 236 # embeddedPeaks=[]; intermediateCoord=[] 237 self.assertEqual("00 08 00 77", hexencode(tup)) 238 self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] 239 "02 04 05 06", # deltaY: [4, 5, 6] 240 hexencode(deltas)) 241 242 def test_compile_sharedPeaks_intermediate_sharedPoints(self): 243 var = TupleVariation( 244 {"wght": (0.3, 0.5, 0.7), "wdth": (0.1, 0.8, 0.9)}, 245 [(7,4), (8,5), (9,6)]) 246 axisTags = ["wght", "wdth"] 247 sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } 248 tup, deltas = var.compile(axisTags, sharedPeakIndices, pointData=b'') 249 # len(deltas)=8; flags=INTERMEDIATE_REGION; tupleIndex=0x77 250 # embeddedPeak=[]; intermediateCoord=[(0.3, 0.1), (0.7, 0.9)] 251 self.assertEqual("00 08 40 77 13 33 06 66 2C CD 39 9A", hexencode(tup)) 252 self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] 253 "02 04 05 06", # deltaY: [4, 5, 6] 254 hexencode(deltas)) 255 256 def test_compile_sharedPeaks_nonIntermediate_privatePoints(self): 257 var = TupleVariation( 258 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 259 [(7,4), (8,5), (9,6)]) 260 axisTags = ["wght", "wdth"] 261 sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } 262 tup, deltas = var.compile(axisTags, sharedPeakIndices) 263 # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77 264 # embeddedPeak=[]; intermediateCoord=[] 265 self.assertEqual("00 09 20 77", hexencode(tup)) 266 self.assertEqual("00 " # all points in glyph 267 "02 07 08 09 " # deltaX: [7, 8, 9] 268 "02 04 05 06", # deltaY: [4, 5, 6] 269 hexencode(deltas)) 270 271 def test_compile_sharedPeaks_intermediate_privatePoints(self): 272 var = TupleVariation( 273 {"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 1.0)}, 274 [(7,4), (8,5), (9,6)]) 275 axisTags = ["wght", "wdth"] 276 sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } 277 tuple, deltas = var.compile(axisTags, sharedPeakIndices) 278 # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77 279 # embeddedPeak=[]; intermediateCoord=[(0.0, 0.0), (1.0, 1.0)] 280 self.assertEqual("00 09 60 77 00 00 00 00 40 00 40 00", 281 hexencode(tuple)) 282 self.assertEqual("00 " # all points in glyph 283 "02 07 08 09 " # deltaX: [7, 8, 9] 284 "02 04 05 06", # deltaY: [4, 5, 6] 285 hexencode(deltas)) 286 287 def test_compile_embeddedPeak_nonIntermediate_sharedPoints(self): 288 var = TupleVariation( 289 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 290 [(7,4), (8,5), (9,6)]) 291 tup, deltas = var.compile(axisTags=["wght", "wdth"], pointData=b'') 292 # len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE 293 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] 294 self.assertEqual("00 08 80 00 20 00 33 33", hexencode(tup)) 295 self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] 296 "02 04 05 06", # deltaY: [4, 5, 6] 297 hexencode(deltas)) 298 299 def test_compile_embeddedPeak_nonIntermediate_sharedConstants(self): 300 var = TupleVariation( 301 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 302 [3, 1, 4]) 303 tup, deltas = var.compile(axisTags=["wght", "wdth"], pointData=b'') 304 # len(deltas)=4; flags=EMBEDDED_PEAK_TUPLE 305 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] 306 self.assertEqual("00 04 80 00 20 00 33 33", hexencode(tup)) 307 self.assertEqual("02 03 01 04", # delta: [3, 1, 4] 308 hexencode(deltas)) 309 310 def test_compile_embeddedPeak_intermediate_sharedPoints(self): 311 var = TupleVariation( 312 {"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 0.8)}, 313 [(7,4), (8,5), (9,6)]) 314 tup, deltas = var.compile(axisTags=["wght", "wdth"], pointData=b'') 315 # len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE 316 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[(0.0, 0.0), (1.0, 0.8)] 317 self.assertEqual("00 08 C0 00 20 00 33 33 00 00 00 00 40 00 33 33", 318 hexencode(tup)) 319 self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] 320 "02 04 05 06", # deltaY: [4, 5, 6] 321 hexencode(deltas)) 322 323 def test_compile_embeddedPeak_nonIntermediate_privatePoints(self): 324 var = TupleVariation( 325 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 326 [(7,4), (8,5), (9,6)]) 327 tup, deltas = var.compile(axisTags=["wght", "wdth"]) 328 # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE 329 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] 330 self.assertEqual("00 09 A0 00 20 00 33 33", hexencode(tup)) 331 self.assertEqual("00 " # all points in glyph 332 "02 07 08 09 " # deltaX: [7, 8, 9] 333 "02 04 05 06", # deltaY: [4, 5, 6] 334 hexencode(deltas)) 335 336 def test_compile_embeddedPeak_nonIntermediate_privateConstants(self): 337 var = TupleVariation( 338 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 339 [7, 8, 9]) 340 tup, deltas = var.compile(axisTags=["wght", "wdth"]) 341 # len(deltas)=5; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE 342 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] 343 self.assertEqual("00 05 A0 00 20 00 33 33", hexencode(tup)) 344 self.assertEqual("00 " # all points in glyph 345 "02 07 08 09", # delta: [7, 8, 9] 346 hexencode(deltas)) 347 348 def test_compile_embeddedPeak_intermediate_privatePoints(self): 349 var = TupleVariation( 350 {"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)}, 351 [(7,4), (8,5), (9,6)]) 352 tup, deltas = var.compile(axisTags = ["wght", "wdth"]) 353 # len(deltas)=9; 354 # flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE 355 # embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)] 356 self.assertEqual("00 09 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A", 357 hexencode(tup)) 358 self.assertEqual("00 " # all points in glyph 359 "02 07 08 09 " # deltaX: [7, 8, 9] 360 "02 04 05 06", # deltaY: [4, 5, 6] 361 hexencode(deltas)) 362 363 def test_compile_embeddedPeak_intermediate_privateConstants(self): 364 var = TupleVariation( 365 {"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)}, 366 [7, 8, 9]) 367 tup, deltas = var.compile(axisTags = ["wght", "wdth"]) 368 # len(deltas)=5; 369 # flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE 370 # embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)] 371 self.assertEqual("00 05 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A", 372 hexencode(tup)) 373 self.assertEqual("00 " # all points in glyph 374 "02 07 08 09", # delta: [7, 8, 9] 375 hexencode(deltas)) 376 377 def test_compileCoord(self): 378 var = TupleVariation({"wght": (-1.0, -1.0, -1.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4) 379 self.assertEqual("C0 00 20 00", hexencode(var.compileCoord(["wght", "wdth"]))) 380 self.assertEqual("20 00 C0 00", hexencode(var.compileCoord(["wdth", "wght"]))) 381 self.assertEqual("C0 00", hexencode(var.compileCoord(["wght"]))) 382 383 def test_compileIntermediateCoord(self): 384 var = TupleVariation({"wght": (-1.0, -1.0, 0.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4) 385 self.assertEqual("C0 00 19 9A 00 00 26 66", hexencode(var.compileIntermediateCoord(["wght", "wdth"]))) 386 self.assertEqual("19 9A C0 00 26 66 00 00", hexencode(var.compileIntermediateCoord(["wdth", "wght"]))) 387 self.assertEqual(None, var.compileIntermediateCoord(["wght"])) 388 self.assertEqual("19 9A 26 66", hexencode(var.compileIntermediateCoord(["wdth"]))) 389 390 def test_decompileCoord(self): 391 decompileCoord = TupleVariation.decompileCoord_ 392 data = deHexStr("DE AD C0 00 20 00 DE AD") 393 self.assertEqual(({"wght": -1.0, "wdth": 0.5}, 6), decompileCoord(["wght", "wdth"], data, 2)) 394 395 def test_decompileCoord_roundTrip(self): 396 # Make sure we are not affected by https://github.com/fonttools/fonttools/issues/286 397 data = deHexStr("7F B9 80 35") 398 values, _ = TupleVariation.decompileCoord_(["wght", "wdth"], data, 0) 399 axisValues = {axis:(val, val, val) for axis, val in values.items()} 400 var = TupleVariation(axisValues, [None] * 4) 401 self.assertEqual("7F B9 80 35", hexencode(var.compileCoord(["wght", "wdth"]))) 402 403 def test_compilePoints(self): 404 compilePoints = lambda p: TupleVariation.compilePoints(set(p)) 405 self.assertEqual("00", hexencode(compilePoints(set()))) # all points in glyph 406 self.assertEqual("01 00 07", hexencode(compilePoints([7]))) 407 self.assertEqual("01 80 FF FF", hexencode(compilePoints([65535]))) 408 self.assertEqual("02 01 09 06", hexencode(compilePoints([9, 15]))) 409 self.assertEqual("06 05 07 01 F7 02 01 F2", hexencode(compilePoints([7, 8, 255, 257, 258, 500]))) 410 self.assertEqual("03 01 07 01 80 01 EC", hexencode(compilePoints([7, 8, 500]))) 411 self.assertEqual("04 01 07 01 81 BE E7 0C 0F", hexencode(compilePoints([7, 8, 0xBEEF, 0xCAFE]))) 412 self.maxDiff = None 413 self.assertEqual("81 2C" + # 300 points (0x12c) in total 414 " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] 415 " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] 416 " 2B" + (44 * " 01"), # third run, contains 44 points: [256 .. 299] 417 hexencode(compilePoints(range(300)))) 418 self.assertEqual("81 8F" + # 399 points (0x18f) in total 419 " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] 420 " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] 421 " 7F" + (128 * " 01") + # third run, contains 128 points: [256 .. 383] 422 " 0E" + (15 * " 01"), # fourth run, contains 15 points: [384 .. 398] 423 hexencode(compilePoints(range(399)))) 424 425 def test_decompilePoints(self): 426 numPointsInGlyph = 65536 427 allPoints = list(range(numPointsInGlyph)) 428 def decompilePoints(data, offset): 429 points, offset = TupleVariation.decompilePoints_(numPointsInGlyph, deHexStr(data), offset, "gvar") 430 # Conversion to list needed for Python 3. 431 return (list(points), offset) 432 # all points in glyph 433 self.assertEqual((allPoints, 1), decompilePoints("00", 0)) 434 # all points in glyph (in overly verbose encoding, not explicitly prohibited by spec) 435 self.assertEqual((allPoints, 2), decompilePoints("80 00", 0)) 436 # 2 points; first run: [9, 9+6] 437 self.assertEqual(([9, 15], 4), decompilePoints("02 01 09 06", 0)) 438 # 2 points; first run: [0xBEEF, 0xCAFE]. (0x0C0F = 0xCAFE - 0xBEEF) 439 self.assertEqual(([0xBEEF, 0xCAFE], 6), decompilePoints("02 81 BE EF 0C 0F", 0)) 440 # 1 point; first run: [7] 441 self.assertEqual(([7], 3), decompilePoints("01 00 07", 0)) 442 # 1 point; first run: [7] in overly verbose encoding 443 self.assertEqual(([7], 4), decompilePoints("01 80 00 07", 0)) 444 # 1 point; first run: [65535]; requires words to be treated as unsigned numbers 445 self.assertEqual(([65535], 4), decompilePoints("01 80 FF FF", 0)) 446 # 4 points; first run: [7, 8]; second run: [255, 257]. 257 is stored in delta-encoded bytes (0xFF + 2). 447 self.assertEqual(([7, 8, 263, 265], 7), decompilePoints("04 01 07 01 01 FF 02", 0)) 448 # combination of all encodings, preceded and followed by 4 bytes of unused data 449 data = "DE AD DE AD 04 01 07 01 81 BE E7 0C 0F DE AD DE AD" 450 self.assertEqual(([7, 8, 0xBEEF, 0xCAFE], 13), decompilePoints(data, 4)) 451 self.assertSetEqual(set(range(300)), set(decompilePoints( 452 "81 2C" + # 300 points (0x12c) in total 453 " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] 454 " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] 455 " AB" + (44 * " 00 01"), # third run, contains 44 points: [256 .. 299] 456 0)[0])) 457 self.assertSetEqual(set(range(399)), set(decompilePoints( 458 "81 8F" + # 399 points (0x18f) in total 459 " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] 460 " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] 461 " FF" + (128 * " 00 01") + # third run, contains 128 points: [256 .. 383] 462 " 8E" + (15 * " 00 01"), # fourth run, contains 15 points: [384 .. 398] 463 0)[0])) 464 465 def test_decompilePoints_shouldAcceptBadPointNumbers(self): 466 decompilePoints = TupleVariation.decompilePoints_ 467 # 2 points; first run: [3, 9]. 468 numPointsInGlyph = 8 469 with CapturingLogHandler(log, "WARNING") as captor: 470 decompilePoints(numPointsInGlyph, 471 deHexStr("02 01 03 06"), 0, "cvar") 472 self.assertIn("point 9 out of range in 'cvar' table", 473 [r.msg for r in captor.records]) 474 475 def test_decompilePoints_roundTrip(self): 476 numPointsInGlyph = 500 # greater than 255, so we also exercise code path for 16-bit encoding 477 compile = lambda points: TupleVariation.compilePoints(points) 478 decompile = lambda data: set(TupleVariation.decompilePoints_(numPointsInGlyph, data, 0, "gvar")[0]) 479 for i in range(50): 480 points = set(random.sample(range(numPointsInGlyph), 30)) 481 self.assertSetEqual(points, decompile(compile(points)), 482 "failed round-trip decompile/compilePoints; points=%s" % points) 483 allPoints = set(range(numPointsInGlyph)) 484 self.assertSetEqual(allPoints, decompile(compile(allPoints))) 485 self.assertSetEqual(allPoints, decompile(compile(set()))) 486 487 def test_compileDeltas_points(self): 488 var = TupleVariation({}, [None, (1, 0), (2, 0), None, (4, 0), None]) 489 # deltaX for points: [1, 2, 4]; deltaY for points: [0, 0, 0] 490 self.assertEqual("02 01 02 04 82", hexencode(var.compileDeltas())) 491 492 def test_compileDeltas_constants(self): 493 var = TupleVariation({}, [None, 1, 2, None, 4, None]) 494 # delta for cvts: [1, 2, 4] 495 self.assertEqual("02 01 02 04", hexencode(var.compileDeltas())) 496 497 def test_compileDeltaValues(self): 498 compileDeltaValues = lambda values: hexencode(TupleVariation.compileDeltaValues_(values)) 499 # zeroes 500 self.assertEqual("80", compileDeltaValues([0])) 501 self.assertEqual("BF", compileDeltaValues([0] * 64)) 502 self.assertEqual("BF 80", compileDeltaValues([0] * 65)) 503 self.assertEqual("BF A3", compileDeltaValues([0] * 100)) 504 self.assertEqual("BF BF BF BF", compileDeltaValues([0] * 256)) 505 # bytes 506 self.assertEqual("00 01", compileDeltaValues([1])) 507 self.assertEqual("06 01 02 03 7F 80 FF FE", compileDeltaValues([1, 2, 3, 127, -128, -1, -2])) 508 self.assertEqual("3F" + (64 * " 7F"), compileDeltaValues([127] * 64)) 509 self.assertEqual("3F" + (64 * " 7F") + " 00 7F", compileDeltaValues([127] * 65)) 510 # words 511 self.assertEqual("40 66 66", compileDeltaValues([0x6666])) 512 self.assertEqual("43 66 66 7F FF FF FF 80 00", compileDeltaValues([0x6666, 32767, -1, -32768])) 513 self.assertEqual("7F" + (64 * " 11 22"), compileDeltaValues([0x1122] * 64)) 514 self.assertEqual("7F" + (64 * " 11 22") + " 40 11 22", compileDeltaValues([0x1122] * 65)) 515 # bytes, zeroes, bytes: a single zero is more compact when encoded as part of the bytes run 516 self.assertEqual("04 7F 7F 00 7F 7F", compileDeltaValues([127, 127, 0, 127, 127])) 517 self.assertEqual("01 7F 7F 81 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 127, 127])) 518 self.assertEqual("01 7F 7F 82 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 127, 127])) 519 self.assertEqual("01 7F 7F 83 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 0, 127, 127])) 520 # bytes, zeroes 521 self.assertEqual("01 01 00", compileDeltaValues([1, 0])) 522 self.assertEqual("00 01 81", compileDeltaValues([1, 0, 0])) 523 # words, bytes, words: a single byte is more compact when encoded as part of the words run 524 self.assertEqual("42 66 66 00 02 77 77", compileDeltaValues([0x6666, 2, 0x7777])) 525 self.assertEqual("40 66 66 01 02 02 40 77 77", compileDeltaValues([0x6666, 2, 2, 0x7777])) 526 # words, zeroes, words 527 self.assertEqual("40 66 66 80 40 77 77", compileDeltaValues([0x6666, 0, 0x7777])) 528 self.assertEqual("40 66 66 81 40 77 77", compileDeltaValues([0x6666, 0, 0, 0x7777])) 529 self.assertEqual("40 66 66 82 40 77 77", compileDeltaValues([0x6666, 0, 0, 0, 0x7777])) 530 # words, zeroes, bytes 531 self.assertEqual("40 66 66 80 02 01 02 03", compileDeltaValues([0x6666, 0, 1, 2, 3])) 532 self.assertEqual("40 66 66 81 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 1, 2, 3])) 533 self.assertEqual("40 66 66 82 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 0, 1, 2, 3])) 534 # words, zeroes 535 self.assertEqual("40 66 66 80", compileDeltaValues([0x6666, 0])) 536 self.assertEqual("40 66 66 81", compileDeltaValues([0x6666, 0, 0])) 537 538 def test_decompileDeltas(self): 539 decompileDeltas = TupleVariation.decompileDeltas_ 540 # 83 = zero values (0x80), count = 4 (1 + 0x83 & 0x3F) 541 self.assertEqual(([0, 0, 0, 0], 1), decompileDeltas(4, deHexStr("83"), 0)) 542 # 41 01 02 FF FF = signed 16-bit values (0x40), count = 2 (1 + 0x41 & 0x3F) 543 self.assertEqual(([258, -1], 5), decompileDeltas(2, deHexStr("41 01 02 FF FF"), 0)) 544 # 01 81 07 = signed 8-bit values, count = 2 (1 + 0x01 & 0x3F) 545 self.assertEqual(([-127, 7], 3), decompileDeltas(2, deHexStr("01 81 07"), 0)) 546 # combination of all three encodings, preceded and followed by 4 bytes of unused data 547 data = deHexStr("DE AD BE EF 83 40 01 02 01 81 80 DE AD BE EF") 548 self.assertEqual(([0, 0, 0, 0, 258, -127, -128], 11), decompileDeltas(7, data, 4)) 549 550 def test_decompileDeltas_roundTrip(self): 551 numDeltas = 30 552 compile = TupleVariation.compileDeltaValues_ 553 decompile = lambda data: TupleVariation.decompileDeltas_(numDeltas, data, 0)[0] 554 for i in range(50): 555 deltas = random.sample(range(-128, 127), 10) 556 deltas.extend(random.sample(range(-32768, 32767), 10)) 557 deltas.extend([0] * 10) 558 random.shuffle(deltas) 559 self.assertListEqual(deltas, decompile(compile(deltas))) 560 561 def test_compileSharedTuples(self): 562 # Below, the peak coordinate {"wght": 1.0, "wdth": 0.8} appears 563 # three times (most frequent sorted first); {"wght": 1.0, "wdth": 0.5} 564 # and {"wght": 1.0, "wdth": 0.7} both appears two times (tie) and 565 # are sorted alphanumerically to ensure determinism. 566 # The peak coordinate {"wght": 1.0, "wdth": 0.9} appears only once 567 # and is thus ignored. 568 # Because the start and end of variation ranges is not encoded 569 # into the shared pool, they should get ignored. 570 deltas = [None] * 4 571 variations = [ 572 TupleVariation({ 573 "wght": (1.0, 1.0, 1.0), 574 "wdth": (0.5, 0.7, 1.0) 575 }, deltas), 576 TupleVariation({ 577 "wght": (1.0, 1.0, 1.0), 578 "wdth": (0.2, 0.7, 1.0) 579 }, deltas), 580 TupleVariation({ 581 "wght": (1.0, 1.0, 1.0), 582 "wdth": (0.2, 0.8, 1.0) 583 }, deltas), 584 TupleVariation({ 585 "wght": (1.0, 1.0, 1.0), 586 "wdth": (0.3, 0.5, 1.0) 587 }, deltas), 588 TupleVariation({ 589 "wght": (1.0, 1.0, 1.0), 590 "wdth": (0.3, 0.8, 1.0) 591 }, deltas), 592 TupleVariation({ 593 "wght": (1.0, 1.0, 1.0), 594 "wdth": (0.3, 0.9, 1.0) 595 }, deltas), 596 TupleVariation({ 597 "wght": (1.0, 1.0, 1.0), 598 "wdth": (0.4, 0.8, 1.0) 599 }, deltas), 600 TupleVariation({ 601 "wght": (1.0, 1.0, 1.0), 602 "wdth": (0.5, 0.5, 1.0) 603 }, deltas), 604 ] 605 result = compileSharedTuples(["wght", "wdth"], variations) 606 self.assertEqual([hexencode(c) for c in result], 607 ["40 00 33 33", "40 00 20 00", "40 00 2C CD"]) 608 609 def test_decompileSharedTuples_Skia(self): 610 sharedTuples = decompileSharedTuples( 611 axisTags=["wght", "wdth"], sharedTupleCount=8, 612 data=SKIA_GVAR_SHARED_TUPLES_DATA, offset=0) 613 self.assertEqual(sharedTuples, SKIA_GVAR_SHARED_TUPLES) 614 615 def test_decompileSharedTuples_empty(self): 616 self.assertEqual(decompileSharedTuples(["wght"], 0, b"", 0), []) 617 618 def test_compileTupleVariationStore_allVariationsRedundant(self): 619 axes = {"wght": (0.3, 0.4, 0.5), "opsz": (0.7, 0.8, 0.9)} 620 variations = [ 621 TupleVariation(axes, [None] * 4), 622 TupleVariation(axes, [None] * 4), 623 TupleVariation(axes, [None] * 4) 624 ] 625 self.assertEqual( 626 compileTupleVariationStore(variations, pointCount=8, 627 axisTags=["wght", "opsz"], 628 sharedTupleIndices={}), 629 (0, b"", b"")) 630 631 def test_compileTupleVariationStore_noVariations(self): 632 self.assertEqual( 633 compileTupleVariationStore(variations=[], pointCount=8, 634 axisTags=["wght", "opsz"], 635 sharedTupleIndices={}), 636 (0, b"", b"")) 637 638 def test_compileTupleVariationStore_roundTrip_cvar(self): 639 deltas = [1, 2, 3, 4] 640 variations = [ 641 TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, 642 deltas), 643 TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, 644 deltas) 645 ] 646 tupleVariationCount, tuples, data = compileTupleVariationStore( 647 variations, pointCount=4, axisTags=["wght", "wdth"], 648 sharedTupleIndices={}) 649 self.assertEqual( 650 decompileTupleVariationStore("cvar", ["wght", "wdth"], 651 tupleVariationCount, pointCount=4, 652 sharedTuples={}, data=(tuples + data), 653 pos=0, dataPos=len(tuples)), 654 variations) 655 656 def test_compileTupleVariationStore_roundTrip_gvar(self): 657 deltas = [(1,1), (2,2), (3,3), (4,4)] 658 variations = [ 659 TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, 660 deltas), 661 TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, 662 deltas) 663 ] 664 tupleVariationCount, tuples, data = compileTupleVariationStore( 665 variations, pointCount=4, axisTags=["wght", "wdth"], 666 sharedTupleIndices={}) 667 self.assertEqual( 668 decompileTupleVariationStore("gvar", ["wght", "wdth"], 669 tupleVariationCount, pointCount=4, 670 sharedTuples={}, data=(tuples + data), 671 pos=0, dataPos=len(tuples)), 672 variations) 673 674 def test_decompileTupleVariationStore_Skia_I(self): 675 tvar = decompileTupleVariationStore( 676 tableTag="gvar", axisTags=["wght", "wdth"], 677 tupleVariationCount=8, pointCount=18, 678 sharedTuples=SKIA_GVAR_SHARED_TUPLES, 679 data=SKIA_GVAR_I_DATA, pos=4, dataPos=36) 680 self.assertEqual(len(tvar), 8) 681 self.assertEqual(tvar[0].axes, {"wght": (0.0, 1.0, 1.0)}) 682 self.assertEqual( 683 " ".join(["%d,%d" % c for c in tvar[0].coordinates]), 684 "257,0 -127,0 -128,58 -130,90 -130,62 -130,67 -130,32 -127,0 " 685 "257,0 259,14 260,64 260,21 260,69 258,124 0,0 130,0 0,0 0,0") 686 687 def test_decompileTupleVariationStore_empty(self): 688 self.assertEqual( 689 decompileTupleVariationStore(tableTag="gvar", axisTags=[], 690 tupleVariationCount=0, pointCount=5, 691 sharedTuples=[], 692 data=b"", pos=4, dataPos=4), 693 []) 694 695 def test_getTupleSize(self): 696 getTupleSize = TupleVariation.getTupleSize_ 697 numAxes = 3 698 self.assertEqual(4 + numAxes * 2, getTupleSize(0x8042, numAxes)) 699 self.assertEqual(4 + numAxes * 4, getTupleSize(0x4077, numAxes)) 700 self.assertEqual(4, getTupleSize(0x2077, numAxes)) 701 self.assertEqual(4, getTupleSize(11, numAxes)) 702 703 def test_inferRegion(self): 704 start, end = inferRegion_({"wght": -0.3, "wdth": 0.7}) 705 self.assertEqual(start, {"wght": -0.3, "wdth": 0.0}) 706 self.assertEqual(end, {"wght": 0.0, "wdth": 0.7}) 707 708 @staticmethod 709 def xml_lines(writer): 710 content = writer.file.getvalue().decode("utf-8") 711 return [line.strip() for line in content.splitlines()][1:] 712 713 def test_getCoordWidth(self): 714 empty = TupleVariation({}, []) 715 self.assertEqual(empty.getCoordWidth(), 0) 716 717 empty = TupleVariation({}, [None]) 718 self.assertEqual(empty.getCoordWidth(), 0) 719 720 gvarTuple = TupleVariation({}, [None, (0, 0)]) 721 self.assertEqual(gvarTuple.getCoordWidth(), 2) 722 723 cvarTuple = TupleVariation({}, [None, 0]) 724 self.assertEqual(cvarTuple.getCoordWidth(), 1) 725 726 cvarTuple.coordinates[1] *= 1.0 727 self.assertEqual(cvarTuple.getCoordWidth(), 1) 728 729 with self.assertRaises(TypeError): 730 TupleVariation({}, [None, "a"]).getCoordWidth() 731 732 def test_scaleDeltas_cvar(self): 733 var = TupleVariation({}, [100, None]) 734 735 var.scaleDeltas(1.0) 736 self.assertEqual(var.coordinates, [100, None]) 737 738 var.scaleDeltas(0.333) 739 self.assertAlmostEqual(var.coordinates[0], 33.3) 740 self.assertIsNone(var.coordinates[1]) 741 742 var.scaleDeltas(0.0) 743 self.assertEqual(var.coordinates, [0, None]) 744 745 def test_scaleDeltas_gvar(self): 746 var = TupleVariation({}, [(100, 200), None]) 747 748 var.scaleDeltas(1.0) 749 self.assertEqual(var.coordinates, [(100, 200), None]) 750 751 var.scaleDeltas(0.333) 752 self.assertAlmostEqual(var.coordinates[0][0], 33.3) 753 self.assertAlmostEqual(var.coordinates[0][1], 66.6) 754 self.assertIsNone(var.coordinates[1]) 755 756 var.scaleDeltas(0.0) 757 self.assertEqual(var.coordinates, [(0, 0), None]) 758 759 def test_roundDeltas_cvar(self): 760 var = TupleVariation({}, [55.5, None, 99.9]) 761 var.roundDeltas() 762 self.assertEqual(var.coordinates, [56, None, 100]) 763 764 def test_roundDeltas_gvar(self): 765 var = TupleVariation({}, [(55.5, 100.0), None, (99.9, 100.0)]) 766 var.roundDeltas() 767 self.assertEqual(var.coordinates, [(56, 100), None, (100, 100)]) 768 769 def test_calcInferredDeltas(self): 770 var = TupleVariation({}, [(0, 0), None, None, None]) 771 coords = [(1, 1), (1, 1), (1, 1), (1, 1)] 772 773 var.calcInferredDeltas(coords, []) 774 775 self.assertEqual( 776 var.coordinates, 777 [(0, 0), (0, 0), (0, 0), (0, 0)] 778 ) 779 780 def test_calcInferredDeltas_invalid(self): 781 # cvar tuples can't have inferred deltas 782 with self.assertRaises(TypeError): 783 TupleVariation({}, [0]).calcInferredDeltas([], []) 784 785 # origCoords must have same length as self.coordinates 786 with self.assertRaises(ValueError): 787 TupleVariation({}, [(0, 0), None]).calcInferredDeltas([], []) 788 789 # at least 4 phantom points required 790 with self.assertRaises(AssertionError): 791 TupleVariation({}, [(0, 0), None]).calcInferredDeltas([(0, 0), (0, 0)], []) 792 793 with self.assertRaises(AssertionError): 794 TupleVariation({}, [(0, 0)] + [None]*5).calcInferredDeltas( 795 [(0, 0)]*6, 796 [1, 0] # endPts not in increasing order 797 ) 798 799 def test_optimize(self): 800 var = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0)]*5) 801 802 var.optimize([(0, 0)]*5, [0]) 803 804 self.assertEqual(var.coordinates, [None, None, None, None, None]) 805 806 def test_optimize_isComposite(self): 807 # when a composite glyph's deltas are all (0, 0), we still want 808 # to write out an entry in gvar, else macOS doesn't apply any 809 # variations to the composite glyph (even if its individual components 810 # do vary). 811 # https://github.com/fonttools/fonttools/issues/1381 812 var = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0)]*5) 813 var.optimize([(0, 0)]*5, [0], isComposite=True) 814 self.assertEqual(var.coordinates, [(0, 0)]*5) 815 816 # it takes more than 128 (0, 0) deltas before the optimized tuple with 817 # (None) inferred deltas (except for the first) becomes smaller than 818 # the un-optimized one that has all deltas explicitly set to (0, 0). 819 var = TupleVariation({"wght": (0.0, 1.0, 1.0)}, [(0, 0)]*129) 820 var.optimize([(0, 0)]*129, list(range(129-4)), isComposite=True) 821 self.assertEqual(var.coordinates, [(0, 0)] + [None]*128) 822 823 def test_sum_deltas_gvar(self): 824 var1 = TupleVariation( 825 {}, 826 [ 827 (-20, 0), (-20, 0), (20, 0), (20, 0), 828 (0, 0), (0, 0), (0, 0), (0, 0), 829 ] 830 ) 831 var2 = TupleVariation( 832 {}, 833 [ 834 (-10, 0), (-10, 0), (10, 0), (10, 0), 835 (0, 0), (20, 0), (0, 0), (0, 0), 836 ] 837 ) 838 839 var1 += var2 840 841 self.assertEqual( 842 var1.coordinates, 843 [ 844 (-30, 0), (-30, 0), (30, 0), (30, 0), 845 (0, 0), (20, 0), (0, 0), (0, 0), 846 ] 847 ) 848 849 def test_sum_deltas_gvar_invalid_length(self): 850 var1 = TupleVariation({}, [(1, 2)]) 851 var2 = TupleVariation({}, [(1, 2), (3, 4)]) 852 853 with self.assertRaisesRegex(ValueError, "deltas with different lengths"): 854 var1 += var2 855 856 def test_sum_deltas_gvar_with_inferred_points(self): 857 var1 = TupleVariation({}, [(1, 2), None]) 858 var2 = TupleVariation({}, [(2, 3), None]) 859 860 with self.assertRaisesRegex(ValueError, "deltas with inferred points"): 861 var1 += var2 862 863 def test_sum_deltas_cvar(self): 864 axes = {"wght": (0.0, 1.0, 1.0)} 865 var1 = TupleVariation(axes, [0, 1, None, None]) 866 var2 = TupleVariation(axes, [None, 2, None, 3]) 867 var3 = TupleVariation(axes, [None, None, None, 4]) 868 869 var1 += var2 870 var1 += var3 871 872 self.assertEqual(var1.coordinates, [0, 3, None, 7]) 873 874 875if __name__ == "__main__": 876 import sys 877 sys.exit(unittest.main()) 878