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