1from fontTools.misc.fixedTools import otRound 2from fontTools.misc.testTools import getXML, parseXML 3from fontTools.pens.ttGlyphPen import TTGlyphPen 4from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen 5from fontTools.pens.pointPen import PointToSegmentPen 6from fontTools.ttLib import TTFont, newTable, TTLibError 7from fontTools.ttLib.tables._g_l_y_f import ( 8 Glyph, 9 GlyphCoordinates, 10 GlyphComponent, 11 ARGS_ARE_XY_VALUES, 12 SCALED_COMPONENT_OFFSET, 13 UNSCALED_COMPONENT_OFFSET, 14 WE_HAVE_A_SCALE, 15 WE_HAVE_A_TWO_BY_TWO, 16 WE_HAVE_AN_X_AND_Y_SCALE, 17) 18from fontTools.ttLib.tables import ttProgram 19import sys 20import array 21from io import StringIO 22import itertools 23import pytest 24import re 25import os 26import unittest 27 28 29class GlyphCoordinatesTest(object): 30 31 def test_translate(self): 32 g = GlyphCoordinates([(1,2)]) 33 g.translate((.5,0)) 34 assert g == GlyphCoordinates([(1.5,2.0)]) 35 36 def test_scale(self): 37 g = GlyphCoordinates([(1,2)]) 38 g.scale((.5,0)) 39 assert g == GlyphCoordinates([(0.5,0.0)]) 40 41 def test_transform(self): 42 g = GlyphCoordinates([(1,2)]) 43 g.transform(((.5,0),(.2,.5))) 44 assert g[0] == GlyphCoordinates([(0.9,1.0)])[0] 45 46 def test__eq__(self): 47 g = GlyphCoordinates([(1,2)]) 48 g2 = GlyphCoordinates([(1.0,2)]) 49 g3 = GlyphCoordinates([(1.5,2)]) 50 assert g == g2 51 assert not g == g3 52 assert not g2 == g3 53 assert not g == object() 54 55 def test__ne__(self): 56 g = GlyphCoordinates([(1,2)]) 57 g2 = GlyphCoordinates([(1.0,2)]) 58 g3 = GlyphCoordinates([(1.5,2)]) 59 assert not (g != g2) 60 assert g != g3 61 assert g2 != g3 62 assert g != object() 63 64 def test__pos__(self): 65 g = GlyphCoordinates([(1,2)]) 66 g2 = +g 67 assert g == g2 68 69 def test__neg__(self): 70 g = GlyphCoordinates([(1,2)]) 71 g2 = -g 72 assert g2 == GlyphCoordinates([(-1, -2)]) 73 74 @pytest.mark.skipif(sys.version_info[0] < 3, 75 reason="__round___ requires Python 3") 76 def test__round__(self): 77 g = GlyphCoordinates([(-1.5,2)]) 78 g2 = round(g) 79 assert g2 == GlyphCoordinates([(-1,2)]) 80 81 def test__add__(self): 82 g1 = GlyphCoordinates([(1,2)]) 83 g2 = GlyphCoordinates([(3,4)]) 84 g3 = GlyphCoordinates([(4,6)]) 85 assert g1 + g2 == g3 86 assert g1 + (1, 1) == GlyphCoordinates([(2,3)]) 87 with pytest.raises(TypeError) as excinfo: 88 assert g1 + object() 89 assert 'unsupported operand' in str(excinfo.value) 90 91 def test__sub__(self): 92 g1 = GlyphCoordinates([(1,2)]) 93 g2 = GlyphCoordinates([(3,4)]) 94 g3 = GlyphCoordinates([(-2,-2)]) 95 assert g1 - g2 == g3 96 assert g1 - (1, 1) == GlyphCoordinates([(0,1)]) 97 with pytest.raises(TypeError) as excinfo: 98 assert g1 - object() 99 assert 'unsupported operand' in str(excinfo.value) 100 101 def test__rsub__(self): 102 g = GlyphCoordinates([(1,2)]) 103 # other + (-self) 104 assert (1, 1) - g == GlyphCoordinates([(0,-1)]) 105 106 def test__mul__(self): 107 g = GlyphCoordinates([(1,2)]) 108 assert g * 3 == GlyphCoordinates([(3,6)]) 109 assert g * (3,2) == GlyphCoordinates([(3,4)]) 110 assert g * (1,1) == g 111 with pytest.raises(TypeError) as excinfo: 112 assert g * object() 113 assert 'unsupported operand' in str(excinfo.value) 114 115 def test__truediv__(self): 116 g = GlyphCoordinates([(1,2)]) 117 assert g / 2 == GlyphCoordinates([(.5,1)]) 118 assert g / (1, 2) == GlyphCoordinates([(1,1)]) 119 assert g / (1, 1) == g 120 with pytest.raises(TypeError) as excinfo: 121 assert g / object() 122 assert 'unsupported operand' in str(excinfo.value) 123 124 def test__iadd__(self): 125 g = GlyphCoordinates([(1,2)]) 126 g += (.5,0) 127 assert g == GlyphCoordinates([(1.5, 2.0)]) 128 g2 = GlyphCoordinates([(3,4)]) 129 g += g2 130 assert g == GlyphCoordinates([(4.5, 6.0)]) 131 132 def test__isub__(self): 133 g = GlyphCoordinates([(1,2)]) 134 g -= (.5, 0) 135 assert g == GlyphCoordinates([(0.5, 2.0)]) 136 g2 = GlyphCoordinates([(3,4)]) 137 g -= g2 138 assert g == GlyphCoordinates([(-2.5, -2.0)]) 139 140 def __test__imul__(self): 141 g = GlyphCoordinates([(1,2)]) 142 g *= (2,.5) 143 g *= 2 144 assert g == GlyphCoordinates([(4.0, 2.0)]) 145 g = GlyphCoordinates([(1,2)]) 146 g *= 2 147 assert g == GlyphCoordinates([(2, 4)]) 148 149 def test__itruediv__(self): 150 g = GlyphCoordinates([(1,3)]) 151 g /= (.5,1.5) 152 g /= 2 153 assert g == GlyphCoordinates([(1.0, 1.0)]) 154 155 def test__bool__(self): 156 g = GlyphCoordinates([]) 157 assert bool(g) == False 158 g = GlyphCoordinates([(0,0), (0.,0)]) 159 assert bool(g) == True 160 g = GlyphCoordinates([(0,0), (1,0)]) 161 assert bool(g) == True 162 g = GlyphCoordinates([(0,.5), (0,0)]) 163 assert bool(g) == True 164 165 def test_double_precision_float(self): 166 # https://github.com/fonttools/fonttools/issues/963 167 afloat = 242.50000000000003 168 g = GlyphCoordinates([(afloat, 0)]) 169 g.toInt() 170 # this would return 242 if the internal array.array typecode is 'f', 171 # since the Python float is truncated to a C float. 172 # when using typecode 'd' it should return the correct value 243 173 assert g[0][0] == otRound(afloat) 174 175 def test__checkFloat_overflow(self): 176 g = GlyphCoordinates([(1, 1)], typecode="h") 177 g.append((0x8000, 0)) 178 assert g.array.typecode == "d" 179 assert g.array == array.array("d", [1.0, 1.0, 32768.0, 0.0]) 180 181 182CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) 183DATA_DIR = os.path.join(CURR_DIR, 'data') 184 185GLYF_TTX = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.ttx") 186GLYF_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.glyf.bin") 187HEAD_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.head.bin") 188LOCA_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.loca.bin") 189MAXP_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.maxp.bin") 190 191 192def strip_ttLibVersion(string): 193 return re.sub(' ttLibVersion=".*"', '', string) 194 195 196class GlyfTableTest(unittest.TestCase): 197 198 def __init__(self, methodName): 199 unittest.TestCase.__init__(self, methodName) 200 # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, 201 # and fires deprecation warnings if a program uses the old name. 202 if not hasattr(self, "assertRaisesRegex"): 203 self.assertRaisesRegex = self.assertRaisesRegexp 204 205 @classmethod 206 def setUpClass(cls): 207 with open(GLYF_BIN, 'rb') as f: 208 cls.glyfData = f.read() 209 with open(HEAD_BIN, 'rb') as f: 210 cls.headData = f.read() 211 with open(LOCA_BIN, 'rb') as f: 212 cls.locaData = f.read() 213 with open(MAXP_BIN, 'rb') as f: 214 cls.maxpData = f.read() 215 with open(GLYF_TTX, 'r') as f: 216 cls.glyfXML = strip_ttLibVersion(f.read()).splitlines() 217 218 def test_toXML(self): 219 font = TTFont(sfntVersion="\x00\x01\x00\x00") 220 glyfTable = font['glyf'] = newTable('glyf') 221 font['head'] = newTable('head') 222 font['loca'] = newTable('loca') 223 font['maxp'] = newTable('maxp') 224 font['maxp'].decompile(self.maxpData, font) 225 font['head'].decompile(self.headData, font) 226 font['loca'].decompile(self.locaData, font) 227 glyfTable.decompile(self.glyfData, font) 228 out = StringIO() 229 font.saveXML(out) 230 glyfXML = strip_ttLibVersion(out.getvalue()).splitlines() 231 self.assertEqual(glyfXML, self.glyfXML) 232 233 def test_fromXML(self): 234 font = TTFont(sfntVersion="\x00\x01\x00\x00") 235 font.importXML(GLYF_TTX) 236 glyfTable = font['glyf'] 237 glyfData = glyfTable.compile(font) 238 self.assertEqual(glyfData, self.glyfData) 239 240 def test_recursiveComponent(self): 241 glyphSet = {} 242 pen_dummy = TTGlyphPen(glyphSet) 243 glyph_dummy = pen_dummy.glyph() 244 glyphSet["A"] = glyph_dummy 245 glyphSet["B"] = glyph_dummy 246 pen_A = TTGlyphPen(glyphSet) 247 pen_A.addComponent("B", (1, 0, 0, 1, 0, 0)) 248 pen_B = TTGlyphPen(glyphSet) 249 pen_B.addComponent("A", (1, 0, 0, 1, 0, 0)) 250 glyph_A = pen_A.glyph() 251 glyph_B = pen_B.glyph() 252 glyphSet["A"] = glyph_A 253 glyphSet["B"] = glyph_B 254 with self.assertRaisesRegex(TTLibError, "glyph '.' contains a recursive component reference"): 255 glyph_A.getCoordinates(glyphSet) 256 257 def test_trim_remove_hinting_composite_glyph(self): 258 glyphSet = {"dummy": TTGlyphPen(None).glyph()} 259 260 pen = TTGlyphPen(glyphSet) 261 pen.addComponent("dummy", (1, 0, 0, 1, 0, 0)) 262 composite = pen.glyph() 263 p = ttProgram.Program() 264 p.fromAssembly(['SVTCA[0]']) 265 composite.program = p 266 glyphSet["composite"] = composite 267 268 glyfTable = newTable("glyf") 269 glyfTable.glyphs = glyphSet 270 glyfTable.glyphOrder = sorted(glyphSet) 271 272 composite.compact(glyfTable) 273 274 self.assertTrue(hasattr(composite, "data")) 275 276 # remove hinting from the compacted composite glyph, without expanding it 277 composite.trim(remove_hinting=True) 278 279 # check that, after expanding the glyph, we have no instructions 280 composite.expand(glyfTable) 281 self.assertFalse(hasattr(composite, "program")) 282 283 # now remove hinting from expanded composite glyph 284 composite.program = p 285 composite.trim(remove_hinting=True) 286 287 # check we have no instructions 288 self.assertFalse(hasattr(composite, "program")) 289 290 composite.compact(glyfTable) 291 292 def test_bit6_draw_to_pen_issue1771(self): 293 # https://github.com/fonttools/fonttools/issues/1771 294 font = TTFont(sfntVersion="\x00\x01\x00\x00") 295 # glyph00003 contains a bit 6 flag on the first point, 296 # which triggered the issue 297 font.importXML(GLYF_TTX) 298 glyfTable = font['glyf'] 299 pen = RecordingPen() 300 glyfTable["glyph00003"].draw(pen, glyfTable=glyfTable) 301 expected = [('moveTo', ((501, 1430),)), 302 ('lineTo', ((683, 1430),)), 303 ('lineTo', ((1172, 0),)), 304 ('lineTo', ((983, 0),)), 305 ('lineTo', ((591, 1193),)), 306 ('lineTo', ((199, 0),)), 307 ('lineTo', ((12, 0),)), 308 ('closePath', ()), 309 ('moveTo', ((249, 514),)), 310 ('lineTo', ((935, 514),)), 311 ('lineTo', ((935, 352),)), 312 ('lineTo', ((249, 352),)), 313 ('closePath', ())] 314 self.assertEqual(pen.value, expected) 315 316 def test_bit6_draw_to_pointpen(self): 317 # https://github.com/fonttools/fonttools/issues/1771 318 font = TTFont(sfntVersion="\x00\x01\x00\x00") 319 # glyph00003 contains a bit 6 flag on the first point 320 # which triggered the issue 321 font.importXML(GLYF_TTX) 322 glyfTable = font['glyf'] 323 pen = RecordingPointPen() 324 glyfTable["glyph00003"].drawPoints(pen, glyfTable=glyfTable) 325 expected = [ 326 ('beginPath', (), {}), 327 ('addPoint', ((501, 1430), 'line', False, None), {}), 328 ('addPoint', ((683, 1430), 'line', False, None), {}), 329 ('addPoint', ((1172, 0), 'line', False, None), {}), 330 ('addPoint', ((983, 0), 'line', False, None), {}), 331 ] 332 self.assertEqual(pen.value[:len(expected)], expected) 333 334 def test_draw_vs_drawpoints(self): 335 font = TTFont(sfntVersion="\x00\x01\x00\x00") 336 font.importXML(GLYF_TTX) 337 glyfTable = font['glyf'] 338 pen1 = RecordingPen() 339 pen2 = RecordingPen() 340 glyfTable["glyph00003"].draw(pen1, glyfTable) 341 glyfTable["glyph00003"].drawPoints(PointToSegmentPen(pen2), glyfTable) 342 self.assertEqual(pen1.value, pen2.value) 343 344 def test_compile_empty_table(self): 345 font = TTFont(sfntVersion="\x00\x01\x00\x00") 346 font.importXML(GLYF_TTX) 347 glyfTable = font['glyf'] 348 # set all glyphs to zero contours 349 glyfTable.glyphs = {glyphName: Glyph() for glyphName in font.getGlyphOrder()} 350 glyfData = glyfTable.compile(font) 351 self.assertEqual(glyfData, b"\x00") 352 self.assertEqual(list(font["loca"]), [0] * (font["maxp"].numGlyphs+1)) 353 354 def test_decompile_empty_table(self): 355 font = TTFont() 356 glyphNames = [".notdef", "space"] 357 font.setGlyphOrder(glyphNames) 358 font["loca"] = newTable("loca") 359 font["loca"].locations = [0] * (len(glyphNames) + 1) 360 font["glyf"] = newTable("glyf") 361 font["glyf"].decompile(b"\x00", font) 362 self.assertEqual(len(font["glyf"]), 2) 363 self.assertEqual(font["glyf"][".notdef"].numberOfContours, 0) 364 self.assertEqual(font["glyf"]["space"].numberOfContours, 0) 365 366 367class GlyphTest: 368 369 def test_getCoordinates(self): 370 glyphSet = {} 371 pen = TTGlyphPen(glyphSet) 372 pen.moveTo((0, 0)) 373 pen.lineTo((100, 0)) 374 pen.lineTo((100, 100)) 375 pen.lineTo((0, 100)) 376 pen.closePath() 377 # simple contour glyph 378 glyphSet["a"] = a = pen.glyph() 379 380 assert a.getCoordinates(glyphSet) == ( 381 GlyphCoordinates([(0, 0), (100, 0), (100, 100), (0, 100)]), 382 [3], 383 array.array("B", [1, 1, 1, 1]), 384 ) 385 386 # composite glyph with only XY offset 387 pen = TTGlyphPen(glyphSet) 388 pen.addComponent("a", (1, 0, 0, 1, 10, 20)) 389 glyphSet["b"] = b = pen.glyph() 390 391 assert b.getCoordinates(glyphSet) == ( 392 GlyphCoordinates([(10, 20), (110, 20), (110, 120), (10, 120)]), 393 [3], 394 array.array("B", [1, 1, 1, 1]), 395 ) 396 397 # composite glyph with a scale (and referencing another composite glyph) 398 pen = TTGlyphPen(glyphSet) 399 pen.addComponent("b", (0.5, 0, 0, 0.5, 0, 0)) 400 glyphSet["c"] = c = pen.glyph() 401 402 assert c.getCoordinates(glyphSet) == ( 403 GlyphCoordinates([(5, 10), (55, 10), (55, 60), (5, 60)]), 404 [3], 405 array.array("B", [1, 1, 1, 1]), 406 ) 407 408 # composite glyph with unscaled offset (MS-style) 409 pen = TTGlyphPen(glyphSet) 410 pen.addComponent("a", (0.5, 0, 0, 0.5, 10, 20)) 411 glyphSet["d"] = d = pen.glyph() 412 d.components[0].flags |= UNSCALED_COMPONENT_OFFSET 413 414 assert d.getCoordinates(glyphSet) == ( 415 GlyphCoordinates([(10, 20), (60, 20), (60, 70), (10, 70)]), 416 [3], 417 array.array("B", [1, 1, 1, 1]), 418 ) 419 420 # composite glyph with a scaled offset (Apple-style) 421 pen = TTGlyphPen(glyphSet) 422 pen.addComponent("a", (0.5, 0, 0, 0.5, 10, 20)) 423 glyphSet["e"] = e = pen.glyph() 424 e.components[0].flags |= SCALED_COMPONENT_OFFSET 425 426 assert e.getCoordinates(glyphSet) == ( 427 GlyphCoordinates([(5, 10), (55, 10), (55, 60), (5, 60)]), 428 [3], 429 array.array("B", [1, 1, 1, 1]), 430 ) 431 432 # composite glyph where the 2nd and 3rd components use anchor points 433 pen = TTGlyphPen(glyphSet) 434 pen.addComponent("a", (1, 0, 0, 1, 0, 0)) 435 glyphSet["f"] = f = pen.glyph() 436 437 comp1 = GlyphComponent() 438 comp1.glyphName = "a" 439 # aling the new component's pt 0 to pt 2 of contour points added so far 440 comp1.firstPt = 2 441 comp1.secondPt = 0 442 comp1.flags = 0 443 f.components.append(comp1) 444 445 comp2 = GlyphComponent() 446 comp2.glyphName = "a" 447 # aling the new component's pt 0 to pt 6 of contour points added so far 448 comp2.firstPt = 6 449 comp2.secondPt = 0 450 comp2.transform = [[0.707107, 0.707107], [-0.707107, 0.707107]] # rotate 45 deg 451 comp2.flags = WE_HAVE_A_TWO_BY_TWO 452 f.components.append(comp2) 453 454 coords, end_pts, flags = f.getCoordinates(glyphSet) 455 assert end_pts == [3, 7, 11] 456 assert flags == array.array("B", [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) 457 assert list(sum(coords, ())) == pytest.approx( 458 [ 459 0, 0, 460 100, 0, 461 100, 100, 462 0, 100, 463 100, 100, 464 200, 100, 465 200, 200, 466 100, 200, 467 200, 200, 468 270.7107, 270.7107, 469 200.0, 341.4214, 470 129.2893, 270.7107, 471 ] 472 ) 473 474 def test_getCompositeMaxpValues(self): 475 # https://github.com/fonttools/fonttools/issues/2044 476 glyphSet = {} 477 pen = TTGlyphPen(glyphSet) # empty non-composite glyph 478 glyphSet["fraction"] = pen.glyph() 479 glyphSet["zero.numr"] = pen.glyph() 480 pen = TTGlyphPen(glyphSet) 481 pen.addComponent("zero.numr", (1, 0, 0, 1, 0, 0)) 482 glyphSet["zero.dnom"] = pen.glyph() 483 pen = TTGlyphPen(glyphSet) 484 pen.addComponent("zero.numr", (1, 0, 0, 1, 0, 0)) 485 pen.addComponent("fraction", (1, 0, 0, 1, 0, 0)) 486 pen.addComponent("zero.dnom", (1, 0, 0, 1, 0, 0)) 487 glyphSet["percent"] = pen.glyph() 488 pen = TTGlyphPen(glyphSet) 489 pen.addComponent("zero.numr", (1, 0, 0, 1, 0, 0)) 490 pen.addComponent("fraction", (1, 0, 0, 1, 0, 0)) 491 pen.addComponent("zero.dnom", (1, 0, 0, 1, 0, 0)) 492 pen.addComponent("zero.dnom", (1, 0, 0, 1, 0, 0)) 493 glyphSet["perthousand"] = pen.glyph() 494 assert glyphSet["zero.dnom"].getCompositeMaxpValues(glyphSet)[2] == 1 495 assert glyphSet["percent"].getCompositeMaxpValues(glyphSet)[2] == 2 496 assert glyphSet["perthousand"].getCompositeMaxpValues(glyphSet)[2] == 2 497 498 499class GlyphComponentTest: 500 501 def test_toXML_no_transform(self): 502 comp = GlyphComponent() 503 comp.glyphName = "a" 504 comp.flags = ARGS_ARE_XY_VALUES 505 comp.x, comp.y = 1, 2 506 507 assert getXML(comp.toXML) == [ 508 '<component glyphName="a" x="1" y="2" flags="0x2"/>' 509 ] 510 511 def test_toXML_transform_scale(self): 512 comp = GlyphComponent() 513 comp.glyphName = "a" 514 comp.flags = ARGS_ARE_XY_VALUES | WE_HAVE_A_SCALE 515 comp.x, comp.y = 1, 2 516 517 comp.transform = [[0.2999878, 0], [0, 0.2999878]] 518 assert getXML(comp.toXML) == [ 519 '<component glyphName="a" x="1" y="2" scale="0.3" flags="0xa"/>' 520 ] 521 522 def test_toXML_transform_xy_scale(self): 523 comp = GlyphComponent() 524 comp.glyphName = "a" 525 comp.flags = ARGS_ARE_XY_VALUES | WE_HAVE_AN_X_AND_Y_SCALE 526 comp.x, comp.y = 1, 2 527 528 comp.transform = [[0.5999756, 0], [0, 0.2999878]] 529 assert getXML(comp.toXML) == [ 530 '<component glyphName="a" x="1" y="2" scalex="0.6" ' 531 'scaley="0.3" flags="0x42"/>' 532 ] 533 534 def test_toXML_transform_2x2_scale(self): 535 comp = GlyphComponent() 536 comp.glyphName = "a" 537 comp.flags = ARGS_ARE_XY_VALUES | WE_HAVE_A_TWO_BY_TWO 538 comp.x, comp.y = 1, 2 539 540 comp.transform = [[0.5999756, -0.2000122], [0.2000122, 0.2999878]] 541 assert getXML(comp.toXML) == [ 542 '<component glyphName="a" x="1" y="2" scalex="0.6" scale01="-0.2" ' 543 'scale10="0.2" scaley="0.3" flags="0x82"/>' 544 ] 545 546 def test_fromXML_no_transform(self): 547 comp = GlyphComponent() 548 for name, attrs, content in parseXML( 549 ['<component glyphName="a" x="1" y="2" flags="0x2"/>'] 550 ): 551 comp.fromXML(name, attrs, content, ttFont=None) 552 553 assert comp.glyphName == "a" 554 assert comp.flags & ARGS_ARE_XY_VALUES != 0 555 assert (comp.x, comp.y) == (1, 2) 556 assert not hasattr(comp, "transform") 557 558 def test_fromXML_transform_scale(self): 559 comp = GlyphComponent() 560 for name, attrs, content in parseXML( 561 ['<component glyphName="a" x="1" y="2" scale="0.3" flags="0xa"/>'] 562 ): 563 comp.fromXML(name, attrs, content, ttFont=None) 564 565 assert comp.glyphName == "a" 566 assert comp.flags & ARGS_ARE_XY_VALUES != 0 567 assert comp.flags & WE_HAVE_A_SCALE != 0 568 assert (comp.x, comp.y) == (1, 2) 569 assert hasattr(comp, "transform") 570 for value, expected in zip( 571 itertools.chain(*comp.transform), [0.2999878, 0, 0, 0.2999878] 572 ): 573 assert value == pytest.approx(expected) 574 575 def test_fromXML_transform_xy_scale(self): 576 comp = GlyphComponent() 577 for name, attrs, content in parseXML( 578 [ 579 '<component glyphName="a" x="1" y="2" scalex="0.6" ' 580 'scaley="0.3" flags="0x42"/>' 581 ] 582 ): 583 comp.fromXML(name, attrs, content, ttFont=None) 584 585 assert comp.glyphName == "a" 586 assert comp.flags & ARGS_ARE_XY_VALUES != 0 587 assert comp.flags & WE_HAVE_AN_X_AND_Y_SCALE != 0 588 assert (comp.x, comp.y) == (1, 2) 589 assert hasattr(comp, "transform") 590 for value, expected in zip( 591 itertools.chain(*comp.transform), [0.5999756, 0, 0, 0.2999878] 592 ): 593 assert value == pytest.approx(expected) 594 595 def test_fromXML_transform_2x2_scale(self): 596 comp = GlyphComponent() 597 for name, attrs, content in parseXML( 598 [ 599 '<component glyphName="a" x="1" y="2" scalex="0.6" scale01="-0.2" ' 600 'scale10="0.2" scaley="0.3" flags="0x82"/>' 601 ] 602 ): 603 comp.fromXML(name, attrs, content, ttFont=None) 604 605 assert comp.glyphName == "a" 606 assert comp.flags & ARGS_ARE_XY_VALUES != 0 607 assert comp.flags & WE_HAVE_A_TWO_BY_TWO != 0 608 assert (comp.x, comp.y) == (1, 2) 609 assert hasattr(comp, "transform") 610 for value, expected in zip( 611 itertools.chain(*comp.transform), 612 [0.5999756, -0.2000122, 0.2000122, 0.2999878] 613 ): 614 assert value == pytest.approx(expected) 615 616 def test_toXML_reference_points(self): 617 comp = GlyphComponent() 618 comp.glyphName = "a" 619 comp.flags = 0 620 comp.firstPt = 1 621 comp.secondPt = 2 622 623 assert getXML(comp.toXML) == [ 624 '<component glyphName="a" firstPt="1" secondPt="2" flags="0x0"/>' 625 ] 626 627 def test_fromXML_reference_points(self): 628 comp = GlyphComponent() 629 for name, attrs, content in parseXML( 630 ['<component glyphName="a" firstPt="1" secondPt="2" flags="0x0"/>'] 631 ): 632 comp.fromXML(name, attrs, content, ttFont=None) 633 634 assert comp.glyphName == "a" 635 assert comp.flags == 0 636 assert (comp.firstPt, comp.secondPt) == (1, 2) 637 assert not hasattr(comp, "transform") 638 639 640if __name__ == "__main__": 641 import sys 642 sys.exit(unittest.main()) 643