1 from __future__ import \ 2 print_function, division, absolute_import, unicode_literals 3 from fontTools.misc.py23 import * 4 from fontTools.misc.loggingTools import CapturingLogHandler 5 from fontTools.misc.testTools import parseXML 6 from fontTools.misc.textTools import deHexStr, hexStr 7 from fontTools.misc.xmlWriter import XMLWriter 8 from fontTools.ttLib.tables.TupleVariation import \ 9 log, TupleVariation, compileSharedTuples, decompileSharedTuples, \ 10 compileTupleVariationStore, decompileTupleVariationStore, inferRegion_ 11 import random 12 import unittest 13 14 15 def hexencode(s): 16 h = hexStr(s).upper() 17 return ' '.join([h[i:i+2] for i in range(0, len(h), 2)]) 18 19 20 AXES = { 21 "wdth": (0.3, 0.4, 0.5), 22 "wght": (0.0, 1.0, 1.0), 23 "opsz": (-0.7, -0.7, 0.0) 24 } 25 26 27 # Shared tuples in the 'gvar' table of the Skia font, as printed 28 # in Apple's TrueType specification. 29 # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html 30 SKIA_GVAR_SHARED_TUPLES_DATA = deHexStr( 31 "40 00 00 00 C0 00 00 00 00 00 40 00 00 00 C0 00 " 32 "C0 00 C0 00 40 00 C0 00 40 00 40 00 C0 00 40 00") 33 34 SKIA_GVAR_SHARED_TUPLES = [ 35 {"wght": 1.0, "wdth": 0.0}, 36 {"wght": -1.0, "wdth": 0.0}, 37 {"wght": 0.0, "wdth": 1.0}, 38 {"wght": 0.0, "wdth": -1.0}, 39 {"wght": -1.0, "wdth": -1.0}, 40 {"wght": 1.0, "wdth": -1.0}, 41 {"wght": 1.0, "wdth": 1.0}, 42 {"wght": -1.0, "wdth": 1.0} 43 ] 44 45 46 # Tuple Variation Store of uppercase I in the Skia font, as printed in Apple's 47 # TrueType spec. The actual Skia font uses a different table for uppercase I 48 # than what is printed in Apple's spec, but we still want to make sure that 49 # we can parse the data as it appears in the specification. 50 # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html 51 SKIA_GVAR_I_DATA = deHexStr( 52 "00 08 00 24 00 33 20 00 00 15 20 01 00 1B 20 02 " 53 "00 24 20 03 00 15 20 04 00 26 20 07 00 0D 20 06 " 54 "00 1A 20 05 00 40 01 01 01 81 80 43 FF 7E FF 7E " 55 "FF 7E FF 7E 00 81 45 01 01 01 03 01 04 01 04 01 " 56 "04 01 02 80 40 00 82 81 81 04 3A 5A 3E 43 20 81 " 57 "04 0E 40 15 45 7C 83 00 0D 9E F3 F2 F0 F0 F0 F0 " 58 "F3 9E A0 A1 A1 A1 9F 80 00 91 81 91 00 0D 0A 0A " 59 "09 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0B 80 00 15 81 " 60 "81 00 C4 89 00 C4 83 00 0D 80 99 98 96 96 96 96 " 61 "99 80 82 83 83 83 81 80 40 FF 18 81 81 04 E6 F9 " 62 "10 21 02 81 04 E8 E5 EB 4D DA 83 00 0D CE D3 D4 " 63 "D3 D3 D3 D5 D2 CE CC CD CD CD CD 80 00 A1 81 91 " 64 "00 0D 07 03 04 02 02 02 03 03 07 07 08 08 08 07 " 65 "80 00 09 81 81 00 28 40 00 A4 02 24 24 66 81 04 " 66 "08 FA FA FA 28 83 00 82 02 FF FF FF 83 02 01 01 " 67 "01 84 91 00 80 06 07 08 08 08 08 0A 07 80 03 FE " 68 "FF FF FF 81 00 08 81 82 02 EE EE EE 8B 6D 00") 69 70 71 class TupleVariationTest(unittest.TestCase): 72 def test_equal(self): 73 var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) 74 var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) 75 self.assertEqual(var1, var2) 76 77 def test_equal_differentAxes(self): 78 var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) 79 var2 = TupleVariation({"wght":(0.7, 0.8, 0.9)}, [(0,0), (9,8), (7,6)]) 80 self.assertNotEqual(var1, var2) 81 82 def test_equal_differentCoordinates(self): 83 var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) 84 var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8)]) 85 self.assertNotEqual(var1, var2) 86 87 def test_hasImpact_someDeltasNotZero(self): 88 axes = {"wght":(0.0, 1.0, 1.0)} 89 var = TupleVariation(axes, [(0,0), (9,8), (7,6)]) 90 self.assertTrue(var.hasImpact()) 91 92 def test_hasImpact_allDeltasZero(self): 93 axes = {"wght":(0.0, 1.0, 1.0)} 94 var = TupleVariation(axes, [(0,0), (0,0), (0,0)]) 95 self.assertTrue(var.hasImpact()) 96 97 def test_hasImpact_allDeltasNone(self): 98 axes = {"wght":(0.0, 1.0, 1.0)} 99 var = TupleVariation(axes, [None, None, None]) 100 self.assertFalse(var.hasImpact()) 101 102 def test_toXML_badDeltaFormat(self): 103 writer = XMLWriter(BytesIO()) 104 g = TupleVariation(AXES, ["String"]) 105 with CapturingLogHandler(log, "ERROR") as captor: 106 g.toXML(writer, ["wdth"]) 107 self.assertIn("bad delta format", [r.msg for r in captor.records]) 108 self.assertEqual([ 109 '<tuple>', 110 '<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>', 111 '<!-- bad delta #0 -->', 112 '</tuple>', 113 ], TupleVariationTest.xml_lines(writer)) 114 115 def test_toXML_constants(self): 116 writer = XMLWriter(BytesIO()) 117 g = TupleVariation(AXES, [42, None, 23, 0, -17, None]) 118 g.toXML(writer, ["wdth", "wght", "opsz"]) 119 self.assertEqual([ 120 '<tuple>', 121 '<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>', 122 '<coord axis="wght" value="1.0"/>', 123 '<coord axis="opsz" value="-0.7"/>', 124 '<delta cvt="0" value="42"/>', 125 '<delta cvt="2" value="23"/>', 126 '<delta cvt="3" value="0"/>', 127 '<delta cvt="4" value="-17"/>', 128 '</tuple>' 129 ], TupleVariationTest.xml_lines(writer)) 130 131 def test_toXML_points(self): 132 writer = XMLWriter(BytesIO()) 133 g = TupleVariation(AXES, [(9,8), None, (7,6), (0,0), (-1,-2), None]) 134 g.toXML(writer, ["wdth", "wght", "opsz"]) 135 self.assertEqual([ 136 '<tuple>', 137 '<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>', 138 '<coord axis="wght" value="1.0"/>', 139 '<coord axis="opsz" value="-0.7"/>', 140 '<delta pt="0" x="9" y="8"/>', 141 '<delta pt="2" x="7" y="6"/>', 142 '<delta pt="3" x="0" y="0"/>', 143 '<delta pt="4" x="-1" y="-2"/>', 144 '</tuple>' 145 ], TupleVariationTest.xml_lines(writer)) 146 147 def test_toXML_allDeltasNone(self): 148 writer = XMLWriter(BytesIO()) 149 axes = {"wght":(0.0, 1.0, 1.0)} 150 g = TupleVariation(axes, [None] * 5) 151 g.toXML(writer, ["wght", "wdth"]) 152 self.assertEqual([ 153 '<tuple>', 154 '<coord axis="wght" value="1.0"/>', 155 '<!-- no deltas -->', 156 '</tuple>' 157 ], TupleVariationTest.xml_lines(writer)) 158 159 def test_fromXML_badDeltaFormat(self): 160 g = TupleVariation({}, []) 161 with CapturingLogHandler(log, "WARNING") as captor: 162 for name, attrs, content in parseXML('<delta a="1" b="2"/>'): 163 g.fromXML(name, attrs, content) 164 self.assertIn("bad delta format: a, b", 165 [r.msg for r in captor.records]) 166 167 def test_fromXML_constants(self): 168 g = TupleVariation({}, [None] * 4) 169 for name, attrs, content in parseXML( 170 '<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>' 171 '<coord axis="wght" value="1.0"/>' 172 '<coord axis="opsz" value="-0.7"/>' 173 '<delta cvt="1" value="42"/>' 174 '<delta cvt="2" value="-23"/>'): 175 g.fromXML(name, attrs, content) 176 self.assertEqual(AXES, g.axes) 177 self.assertEqual([None, 42, -23, None], g.coordinates) 178 179 def test_fromXML_points(self): 180 g = TupleVariation({}, [None] * 4) 181 for name, attrs, content in parseXML( 182 '<coord axis="wdth" min="0.3" value="0.4" max="0.5"/>' 183 '<coord axis="wght" value="1.0"/>' 184 '<coord axis="opsz" value="-0.7"/>' 185 '<delta pt="1" x="33" y="44"/>' 186 '<delta pt="2" x="-2" y="170"/>'): 187 g.fromXML(name, attrs, content) 188 self.assertEqual(AXES, g.axes) 189 self.assertEqual([None, (33, 44), (-2, 170), None], g.coordinates) 190 191 def test_compile_sharedPeaks_nonIntermediate_sharedPoints(self): 192 var = TupleVariation( 193 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 194 [(7,4), (8,5), (9,6)]) 195 axisTags = ["wght", "wdth"] 196 sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } 197 tup, deltas, _ = var.compile(axisTags, sharedPeakIndices, 198 sharedPoints={0,1,2}) 199 # len(deltas)=8; flags=None; tupleIndex=0x77 200 # embeddedPeaks=[]; intermediateCoord=[] 201 self.assertEqual("00 08 00 77", hexencode(tup)) 202 self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] 203 "02 04 05 06", # deltaY: [4, 5, 6] 204 hexencode(deltas)) 205 206 def test_compile_sharedPeaks_intermediate_sharedPoints(self): 207 var = TupleVariation( 208 {"wght": (0.3, 0.5, 0.7), "wdth": (0.1, 0.8, 0.9)}, 209 [(7,4), (8,5), (9,6)]) 210 axisTags = ["wght", "wdth"] 211 sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } 212 tup, deltas, _ = var.compile(axisTags, sharedPeakIndices, 213 sharedPoints={0,1,2}) 214 # len(deltas)=8; flags=INTERMEDIATE_REGION; tupleIndex=0x77 215 # embeddedPeak=[]; intermediateCoord=[(0.3, 0.1), (0.7, 0.9)] 216 self.assertEqual("00 08 40 77 13 33 06 66 2C CD 39 9A", hexencode(tup)) 217 self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] 218 "02 04 05 06", # deltaY: [4, 5, 6] 219 hexencode(deltas)) 220 221 def test_compile_sharedPeaks_nonIntermediate_privatePoints(self): 222 var = TupleVariation( 223 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 224 [(7,4), (8,5), (9,6)]) 225 axisTags = ["wght", "wdth"] 226 sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } 227 tup, deltas, _ = var.compile(axisTags, sharedPeakIndices, 228 sharedPoints=None) 229 # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77 230 # embeddedPeak=[]; intermediateCoord=[] 231 self.assertEqual("00 09 20 77", hexencode(tup)) 232 self.assertEqual("00 " # all points in glyph 233 "02 07 08 09 " # deltaX: [7, 8, 9] 234 "02 04 05 06", # deltaY: [4, 5, 6] 235 hexencode(deltas)) 236 237 def test_compile_sharedPeaks_intermediate_privatePoints(self): 238 var = TupleVariation( 239 {"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 1.0)}, 240 [(7,4), (8,5), (9,6)]) 241 axisTags = ["wght", "wdth"] 242 sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } 243 tuple, deltas, _ = var.compile(axisTags, 244 sharedPeakIndices, sharedPoints=None) 245 # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77 246 # embeddedPeak=[]; intermediateCoord=[(0.0, 0.0), (1.0, 1.0)] 247 self.assertEqual("00 09 60 77 00 00 00 00 40 00 40 00", 248 hexencode(tuple)) 249 self.assertEqual("00 " # all points in glyph 250 "02 07 08 09 " # deltaX: [7, 8, 9] 251 "02 04 05 06", # deltaY: [4, 5, 6] 252 hexencode(deltas)) 253 254 def test_compile_embeddedPeak_nonIntermediate_sharedPoints(self): 255 var = TupleVariation( 256 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 257 [(7,4), (8,5), (9,6)]) 258 tup, deltas, _ = var.compile(axisTags=["wght", "wdth"], 259 sharedCoordIndices={}, sharedPoints={0, 1, 2}) 260 # len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE 261 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] 262 self.assertEqual("00 08 80 00 20 00 33 33", hexencode(tup)) 263 self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] 264 "02 04 05 06", # deltaY: [4, 5, 6] 265 hexencode(deltas)) 266 267 def test_compile_embeddedPeak_nonIntermediate_sharedConstants(self): 268 var = TupleVariation( 269 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 270 [3, 1, 4]) 271 tup, deltas, _ = var.compile(axisTags=["wght", "wdth"], 272 sharedCoordIndices={}, sharedPoints={0, 1, 2}) 273 # len(deltas)=4; flags=EMBEDDED_PEAK_TUPLE 274 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] 275 self.assertEqual("00 04 80 00 20 00 33 33", hexencode(tup)) 276 self.assertEqual("02 03 01 04", # delta: [3, 1, 4] 277 hexencode(deltas)) 278 279 def test_compile_embeddedPeak_intermediate_sharedPoints(self): 280 var = TupleVariation( 281 {"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 0.8)}, 282 [(7,4), (8,5), (9,6)]) 283 tup, deltas, _ = var.compile(axisTags=["wght", "wdth"], 284 sharedCoordIndices={}, 285 sharedPoints={0, 1, 2}) 286 # len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE 287 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[(0.0, 0.0), (1.0, 0.8)] 288 self.assertEqual("00 08 C0 00 20 00 33 33 00 00 00 00 40 00 33 33", 289 hexencode(tup)) 290 self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] 291 "02 04 05 06", # deltaY: [4, 5, 6] 292 hexencode(deltas)) 293 294 def test_compile_embeddedPeak_nonIntermediate_privatePoints(self): 295 var = TupleVariation( 296 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 297 [(7,4), (8,5), (9,6)]) 298 tup, deltas, _ = var.compile( 299 axisTags=["wght", "wdth"], sharedCoordIndices={}, sharedPoints=None) 300 # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE 301 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] 302 self.assertEqual("00 09 A0 00 20 00 33 33", hexencode(tup)) 303 self.assertEqual("00 " # all points in glyph 304 "02 07 08 09 " # deltaX: [7, 8, 9] 305 "02 04 05 06", # deltaY: [4, 5, 6] 306 hexencode(deltas)) 307 308 def test_compile_embeddedPeak_nonIntermediate_privateConstants(self): 309 var = TupleVariation( 310 {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, 311 [7, 8, 9]) 312 tup, deltas, _ = var.compile( 313 axisTags=["wght", "wdth"], sharedCoordIndices={}, sharedPoints=None) 314 # len(deltas)=5; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE 315 # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] 316 self.assertEqual("00 05 A0 00 20 00 33 33", hexencode(tup)) 317 self.assertEqual("00 " # all points in glyph 318 "02 07 08 09", # delta: [7, 8, 9] 319 hexencode(deltas)) 320 321 def test_compile_embeddedPeak_intermediate_privatePoints(self): 322 var = TupleVariation( 323 {"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)}, 324 [(7,4), (8,5), (9,6)]) 325 tup, deltas, _ = var.compile( 326 axisTags = ["wght", "wdth"], 327 sharedCoordIndices={}, sharedPoints=None) 328 # len(deltas)=9; 329 # flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE 330 # embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)] 331 self.assertEqual("00 09 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A", 332 hexencode(tup)) 333 self.assertEqual("00 " # all points in glyph 334 "02 07 08 09 " # deltaX: [7, 8, 9] 335 "02 04 05 06", # deltaY: [4, 5, 6] 336 hexencode(deltas)) 337 338 def test_compile_embeddedPeak_intermediate_privateConstants(self): 339 var = TupleVariation( 340 {"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)}, 341 [7, 8, 9]) 342 tup, deltas, _ = var.compile( 343 axisTags = ["wght", "wdth"], 344 sharedCoordIndices={}, sharedPoints=None) 345 # len(deltas)=5; 346 # flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE 347 # embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)] 348 self.assertEqual("00 05 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A", 349 hexencode(tup)) 350 self.assertEqual("00 " # all points in glyph 351 "02 07 08 09", # delta: [7, 8, 9] 352 hexencode(deltas)) 353 354 def test_compileCoord(self): 355 var = TupleVariation({"wght": (-1.0, -1.0, -1.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4) 356 self.assertEqual("C0 00 20 00", hexencode(var.compileCoord(["wght", "wdth"]))) 357 self.assertEqual("20 00 C0 00", hexencode(var.compileCoord(["wdth", "wght"]))) 358 self.assertEqual("C0 00", hexencode(var.compileCoord(["wght"]))) 359 360 def test_compileIntermediateCoord(self): 361 var = TupleVariation({"wght": (-1.0, -1.0, 0.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4) 362 self.assertEqual("C0 00 19 9A 00 00 26 66", hexencode(var.compileIntermediateCoord(["wght", "wdth"]))) 363 self.assertEqual("19 9A C0 00 26 66 00 00", hexencode(var.compileIntermediateCoord(["wdth", "wght"]))) 364 self.assertEqual(None, var.compileIntermediateCoord(["wght"])) 365 self.assertEqual("19 9A 26 66", hexencode(var.compileIntermediateCoord(["wdth"]))) 366 367 def test_decompileCoord(self): 368 decompileCoord = TupleVariation.decompileCoord_ 369 data = deHexStr("DE AD C0 00 20 00 DE AD") 370 self.assertEqual(({"wght": -1.0, "wdth": 0.5}, 6), decompileCoord(["wght", "wdth"], data, 2)) 371 372 def test_decompileCoord_roundTrip(self): 373 # Make sure we are not affected by https://github.com/fonttools/fonttools/issues/286 374 data = deHexStr("7F B9 80 35") 375 values, _ = TupleVariation.decompileCoord_(["wght", "wdth"], data, 0) 376 axisValues = {axis:(val, val, val) for axis, val in values.items()} 377 var = TupleVariation(axisValues, [None] * 4) 378 self.assertEqual("7F B9 80 35", hexencode(var.compileCoord(["wght", "wdth"]))) 379 380 def test_compilePoints(self): 381 compilePoints = lambda p: TupleVariation.compilePoints(set(p), numPointsInGlyph=999) 382 self.assertEqual("00", hexencode(compilePoints(range(999)))) # all points in glyph 383 self.assertEqual("01 00 07", hexencode(compilePoints([7]))) 384 self.assertEqual("01 80 FF FF", hexencode(compilePoints([65535]))) 385 self.assertEqual("02 01 09 06", hexencode(compilePoints([9, 15]))) 386 self.assertEqual("06 05 07 01 F7 02 01 F2", hexencode(compilePoints([7, 8, 255, 257, 258, 500]))) 387 self.assertEqual("03 01 07 01 80 01 EC", hexencode(compilePoints([7, 8, 500]))) 388 self.assertEqual("04 01 07 01 81 BE E7 0C 0F", hexencode(compilePoints([7, 8, 0xBEEF, 0xCAFE]))) 389 self.maxDiff = None 390 self.assertEqual("81 2C" + # 300 points (0x12c) in total 391 " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] 392 " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] 393 " 2B" + (44 * " 01"), # third run, contains 44 points: [256 .. 299] 394 hexencode(compilePoints(range(300)))) 395 self.assertEqual("81 8F" + # 399 points (0x18f) in total 396 " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] 397 " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] 398 " 7F" + (128 * " 01") + # third run, contains 128 points: [256 .. 383] 399 " 0E" + (15 * " 01"), # fourth run, contains 15 points: [384 .. 398] 400 hexencode(compilePoints(range(399)))) 401 402 def test_decompilePoints(self): 403 numPointsInGlyph = 65536 404 allPoints = list(range(numPointsInGlyph)) 405 def decompilePoints(data, offset): 406 points, offset = TupleVariation.decompilePoints_(numPointsInGlyph, deHexStr(data), offset, "gvar") 407 # Conversion to list needed for Python 3. 408 return (list(points), offset) 409 # all points in glyph 410 self.assertEqual((allPoints, 1), decompilePoints("00", 0)) 411 # all points in glyph (in overly verbose encoding, not explicitly prohibited by spec) 412 self.assertEqual((allPoints, 2), decompilePoints("80 00", 0)) 413 # 2 points; first run: [9, 9+6] 414 self.assertEqual(([9, 15], 4), decompilePoints("02 01 09 06", 0)) 415 # 2 points; first run: [0xBEEF, 0xCAFE]. (0x0C0F = 0xCAFE - 0xBEEF) 416 self.assertEqual(([0xBEEF, 0xCAFE], 6), decompilePoints("02 81 BE EF 0C 0F", 0)) 417 # 1 point; first run: [7] 418 self.assertEqual(([7], 3), decompilePoints("01 00 07", 0)) 419 # 1 point; first run: [7] in overly verbose encoding 420 self.assertEqual(([7], 4), decompilePoints("01 80 00 07", 0)) 421 # 1 point; first run: [65535]; requires words to be treated as unsigned numbers 422 self.assertEqual(([65535], 4), decompilePoints("01 80 FF FF", 0)) 423 # 4 points; first run: [7, 8]; second run: [255, 257]. 257 is stored in delta-encoded bytes (0xFF + 2). 424 self.assertEqual(([7, 8, 263, 265], 7), decompilePoints("04 01 07 01 01 FF 02", 0)) 425 # combination of all encodings, preceded and followed by 4 bytes of unused data 426 data = "DE AD DE AD 04 01 07 01 81 BE E7 0C 0F DE AD DE AD" 427 self.assertEqual(([7, 8, 0xBEEF, 0xCAFE], 13), decompilePoints(data, 4)) 428 self.assertSetEqual(set(range(300)), set(decompilePoints( 429 "81 2C" + # 300 points (0x12c) in total 430 " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] 431 " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] 432 " AB" + (44 * " 00 01"), # third run, contains 44 points: [256 .. 299] 433 0)[0])) 434 self.assertSetEqual(set(range(399)), set(decompilePoints( 435 "81 8F" + # 399 points (0x18f) in total 436 " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] 437 " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] 438 " FF" + (128 * " 00 01") + # third run, contains 128 points: [256 .. 383] 439 " 8E" + (15 * " 00 01"), # fourth run, contains 15 points: [384 .. 398] 440 0)[0])) 441 442 def test_decompilePoints_shouldAcceptBadPointNumbers(self): 443 decompilePoints = TupleVariation.decompilePoints_ 444 # 2 points; first run: [3, 9]. 445 numPointsInGlyph = 8 446 with CapturingLogHandler(log, "WARNING") as captor: 447 decompilePoints(numPointsInGlyph, 448 deHexStr("02 01 03 06"), 0, "cvar") 449 self.assertIn("point 9 out of range in 'cvar' table", 450 [r.msg for r in captor.records]) 451 452 def test_decompilePoints_roundTrip(self): 453 numPointsInGlyph = 500 # greater than 255, so we also exercise code path for 16-bit encoding 454 compile = lambda points: TupleVariation.compilePoints(points, numPointsInGlyph) 455 decompile = lambda data: set(TupleVariation.decompilePoints_(numPointsInGlyph, data, 0, "gvar")[0]) 456 for i in range(50): 457 points = set(random.sample(range(numPointsInGlyph), 30)) 458 self.assertSetEqual(points, decompile(compile(points)), 459 "failed round-trip decompile/compilePoints; points=%s" % points) 460 allPoints = set(range(numPointsInGlyph)) 461 self.assertSetEqual(allPoints, decompile(compile(allPoints))) 462 463 def test_compileDeltas_points(self): 464 var = TupleVariation({}, [(0,0), (1, 0), (2, 0), None, (4, 0), (5, 0)]) 465 points = {1, 2, 3, 4} 466 # deltaX for points: [1, 2, 4]; deltaY for points: [0, 0, 0] 467 self.assertEqual("02 01 02 04 82", hexencode(var.compileDeltas(points))) 468 469 def test_compileDeltas_constants(self): 470 var = TupleVariation({}, [0, 1, 2, None, 4, 5]) 471 cvts = {1, 2, 3, 4} 472 # delta for cvts: [1, 2, 4] 473 self.assertEqual("02 01 02 04", hexencode(var.compileDeltas(cvts))) 474 475 def test_compileDeltaValues(self): 476 compileDeltaValues = lambda values: hexencode(TupleVariation.compileDeltaValues_(values)) 477 # zeroes 478 self.assertEqual("80", compileDeltaValues([0])) 479 self.assertEqual("BF", compileDeltaValues([0] * 64)) 480 self.assertEqual("BF 80", compileDeltaValues([0] * 65)) 481 self.assertEqual("BF A3", compileDeltaValues([0] * 100)) 482 self.assertEqual("BF BF BF BF", compileDeltaValues([0] * 256)) 483 # bytes 484 self.assertEqual("00 01", compileDeltaValues([1])) 485 self.assertEqual("06 01 02 03 7F 80 FF FE", compileDeltaValues([1, 2, 3, 127, -128, -1, -2])) 486 self.assertEqual("3F" + (64 * " 7F"), compileDeltaValues([127] * 64)) 487 self.assertEqual("3F" + (64 * " 7F") + " 00 7F", compileDeltaValues([127] * 65)) 488 # words 489 self.assertEqual("40 66 66", compileDeltaValues([0x6666])) 490 self.assertEqual("43 66 66 7F FF FF FF 80 00", compileDeltaValues([0x6666, 32767, -1, -32768])) 491 self.assertEqual("7F" + (64 * " 11 22"), compileDeltaValues([0x1122] * 64)) 492 self.assertEqual("7F" + (64 * " 11 22") + " 40 11 22", compileDeltaValues([0x1122] * 65)) 493 # bytes, zeroes, bytes: a single zero is more compact when encoded as part of the bytes run 494 self.assertEqual("04 7F 7F 00 7F 7F", compileDeltaValues([127, 127, 0, 127, 127])) 495 self.assertEqual("01 7F 7F 81 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 127, 127])) 496 self.assertEqual("01 7F 7F 82 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 127, 127])) 497 self.assertEqual("01 7F 7F 83 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 0, 127, 127])) 498 # bytes, zeroes 499 self.assertEqual("01 01 00", compileDeltaValues([1, 0])) 500 self.assertEqual("00 01 81", compileDeltaValues([1, 0, 0])) 501 # words, bytes, words: a single byte is more compact when encoded as part of the words run 502 self.assertEqual("42 66 66 00 02 77 77", compileDeltaValues([0x6666, 2, 0x7777])) 503 self.assertEqual("40 66 66 01 02 02 40 77 77", compileDeltaValues([0x6666, 2, 2, 0x7777])) 504 # words, zeroes, words 505 self.assertEqual("40 66 66 80 40 77 77", compileDeltaValues([0x6666, 0, 0x7777])) 506 self.assertEqual("40 66 66 81 40 77 77", compileDeltaValues([0x6666, 0, 0, 0x7777])) 507 self.assertEqual("40 66 66 82 40 77 77", compileDeltaValues([0x6666, 0, 0, 0, 0x7777])) 508 # words, zeroes, bytes 509 self.assertEqual("40 66 66 80 02 01 02 03", compileDeltaValues([0x6666, 0, 1, 2, 3])) 510 self.assertEqual("40 66 66 81 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 1, 2, 3])) 511 self.assertEqual("40 66 66 82 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 0, 1, 2, 3])) 512 # words, zeroes 513 self.assertEqual("40 66 66 80", compileDeltaValues([0x6666, 0])) 514 self.assertEqual("40 66 66 81", compileDeltaValues([0x6666, 0, 0])) 515 # bytes or words from floats 516 self.assertEqual("00 01", compileDeltaValues([1.1])) 517 self.assertEqual("00 02", compileDeltaValues([1.9])) 518 self.assertEqual("40 66 66", compileDeltaValues([0x6666 + 0.1])) 519 self.assertEqual("40 66 66", compileDeltaValues([0x6665 + 0.9])) 520 521 def test_decompileDeltas(self): 522 decompileDeltas = TupleVariation.decompileDeltas_ 523 # 83 = zero values (0x80), count = 4 (1 + 0x83 & 0x3F) 524 self.assertEqual(([0, 0, 0, 0], 1), decompileDeltas(4, deHexStr("83"), 0)) 525 # 41 01 02 FF FF = signed 16-bit values (0x40), count = 2 (1 + 0x41 & 0x3F) 526 self.assertEqual(([258, -1], 5), decompileDeltas(2, deHexStr("41 01 02 FF FF"), 0)) 527 # 01 81 07 = signed 8-bit values, count = 2 (1 + 0x01 & 0x3F) 528 self.assertEqual(([-127, 7], 3), decompileDeltas(2, deHexStr("01 81 07"), 0)) 529 # combination of all three encodings, preceded and followed by 4 bytes of unused data 530 data = deHexStr("DE AD BE EF 83 40 01 02 01 81 80 DE AD BE EF") 531 self.assertEqual(([0, 0, 0, 0, 258, -127, -128], 11), decompileDeltas(7, data, 4)) 532 533 def test_decompileDeltas_roundTrip(self): 534 numDeltas = 30 535 compile = TupleVariation.compileDeltaValues_ 536 decompile = lambda data: TupleVariation.decompileDeltas_(numDeltas, data, 0)[0] 537 for i in range(50): 538 deltas = random.sample(range(-128, 127), 10) 539 deltas.extend(random.sample(range(-32768, 32767), 10)) 540 deltas.extend([0] * 10) 541 random.shuffle(deltas) 542 self.assertListEqual(deltas, decompile(compile(deltas))) 543 544 def test_compileSharedTuples(self): 545 # Below, the peak coordinate {"wght": 1.0, "wdth": 0.7} appears 546 # three times; {"wght": 1.0, "wdth": 0.8} appears twice. 547 # Because the start and end of variation ranges is not encoded 548 # into the shared pool, they should get ignored. 549 deltas = [None] * 4 550 variations = [ 551 TupleVariation({ 552 "wght": (1.0, 1.0, 1.0), 553 "wdth": (0.5, 0.7, 1.0) 554 }, deltas), 555 TupleVariation({ 556 "wght": (1.0, 1.0, 1.0), 557 "wdth": (0.2, 0.7, 1.0) 558 }, deltas), 559 TupleVariation({ 560 "wght": (1.0, 1.0, 1.0), 561 "wdth": (0.2, 0.8, 1.0) 562 }, deltas), 563 TupleVariation({ 564 "wght": (1.0, 1.0, 1.0), 565 "wdth": (0.3, 0.7, 1.0) 566 }, deltas), 567 TupleVariation({ 568 "wght": (1.0, 1.0, 1.0), 569 "wdth": (0.3, 0.8, 1.0) 570 }, deltas), 571 TupleVariation({ 572 "wght": (1.0, 1.0, 1.0), 573 "wdth": (0.3, 0.9, 1.0) 574 }, deltas) 575 ] 576 result = compileSharedTuples(["wght", "wdth"], variations) 577 self.assertEqual([hexencode(c) for c in result], 578 ["40 00 2C CD", "40 00 33 33"]) 579 580 def test_decompileSharedTuples_Skia(self): 581 sharedTuples = decompileSharedTuples( 582 axisTags=["wght", "wdth"], sharedTupleCount=8, 583 data=SKIA_GVAR_SHARED_TUPLES_DATA, offset=0) 584 self.assertEqual(sharedTuples, SKIA_GVAR_SHARED_TUPLES) 585 586 def test_decompileSharedTuples_empty(self): 587 self.assertEqual(decompileSharedTuples(["wght"], 0, b"", 0), []) 588 589 def test_compileTupleVariationStore_allVariationsRedundant(self): 590 axes = {"wght": (0.3, 0.4, 0.5), "opsz": (0.7, 0.8, 0.9)} 591 variations = [ 592 TupleVariation(axes, [None] * 4), 593 TupleVariation(axes, [None] * 4), 594 TupleVariation(axes, [None] * 4) 595 ] 596 self.assertEqual( 597 compileTupleVariationStore(variations, pointCount=8, 598 axisTags=["wght", "opsz"], 599 sharedTupleIndices={}), 600 (0, b"", b"")) 601 602 def test_compileTupleVariationStore_noVariations(self): 603 self.assertEqual( 604 compileTupleVariationStore(variations=[], pointCount=8, 605 axisTags=["wght", "opsz"], 606 sharedTupleIndices={}), 607 (0, b"", b"")) 608 609 def test_compileTupleVariationStore_roundTrip_cvar(self): 610 deltas = [1, 2, 3, 4] 611 variations = [ 612 TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, 613 deltas), 614 TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, 615 deltas) 616 ] 617 tupleVariationCount, tuples, data = compileTupleVariationStore( 618 variations, pointCount=4, axisTags=["wght", "wdth"], 619 sharedTupleIndices={}) 620 self.assertEqual( 621 decompileTupleVariationStore("cvar", ["wght", "wdth"], 622 tupleVariationCount, pointCount=4, 623 sharedTuples={}, data=(tuples + data), 624 pos=0, dataPos=len(tuples)), 625 variations) 626 627 def test_compileTupleVariationStore_roundTrip_gvar(self): 628 deltas = [(1,1), (2,2), (3,3), (4,4)] 629 variations = [ 630 TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, 631 deltas), 632 TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, 633 deltas) 634 ] 635 tupleVariationCount, tuples, data = compileTupleVariationStore( 636 variations, pointCount=4, axisTags=["wght", "wdth"], 637 sharedTupleIndices={}) 638 self.assertEqual( 639 decompileTupleVariationStore("gvar", ["wght", "wdth"], 640 tupleVariationCount, pointCount=4, 641 sharedTuples={}, data=(tuples + data), 642 pos=0, dataPos=len(tuples)), 643 variations) 644 645 def test_decompileTupleVariationStore_Skia_I(self): 646 tvar = decompileTupleVariationStore( 647 tableTag="gvar", axisTags=["wght", "wdth"], 648 tupleVariationCount=8, pointCount=18, 649 sharedTuples=SKIA_GVAR_SHARED_TUPLES, 650 data=SKIA_GVAR_I_DATA, pos=4, dataPos=36) 651 self.assertEqual(len(tvar), 8) 652 self.assertEqual(tvar[0].axes, {"wght": (0.0, 1.0, 1.0)}) 653 self.assertEqual( 654 " ".join(["%d,%d" % c for c in tvar[0].coordinates]), 655 "257,0 -127,0 -128,58 -130,90 -130,62 -130,67 -130,32 -127,0 " 656 "257,0 259,14 260,64 260,21 260,69 258,124 0,0 130,0 0,0 0,0") 657 658 def test_decompileTupleVariationStore_empty(self): 659 self.assertEqual( 660 decompileTupleVariationStore(tableTag="gvar", axisTags=[], 661 tupleVariationCount=0, pointCount=5, 662 sharedTuples=[], 663 data=b"", pos=4, dataPos=4), 664 []) 665 666 def test_getTupleSize(self): 667 getTupleSize = TupleVariation.getTupleSize_ 668 numAxes = 3 669 self.assertEqual(4 + numAxes * 2, getTupleSize(0x8042, numAxes)) 670 self.assertEqual(4 + numAxes * 4, getTupleSize(0x4077, numAxes)) 671 self.assertEqual(4, getTupleSize(0x2077, numAxes)) 672 self.assertEqual(4, getTupleSize(11, numAxes)) 673 674 def test_inferRegion(self): 675 start, end = inferRegion_({"wght": -0.3, "wdth": 0.7}) 676 self.assertEqual(start, {"wght": -0.3, "wdth": 0.0}) 677 self.assertEqual(end, {"wght": 0.0, "wdth": 0.7}) 678 679 @staticmethod 680 def xml_lines(writer): 681 content = writer.file.getvalue().decode("utf-8") 682 return [line.strip() for line in content.splitlines()][1:] 683 684 685 if __name__ == "__main__": 686 import sys 687 sys.exit(unittest.main()) 688