File indexing completed on 2025-04-27 04:01:22
0001 import unittest 0002 from xml.etree import ElementTree 0003 from .. import base 0004 from lottie import objects 0005 from lottie.parsers.svg import parse_svg_etree 0006 from lottie.nvector import NVector 0007 0008 0009 class PathTester(unittest.TestCase): 0010 def make_svg_paths(self, paths): 0011 svg = ElementTree.Element("svg") 0012 svg.attrib["viewBox"] = "0 0 512 512" 0013 for n, d in paths.items(): 0014 path = ElementTree.SubElement(svg, "path") 0015 path.attrib["id"] = n 0016 path.attrib["stroke"] = "red" 0017 path.attrib["fill"] = "none" 0018 path.attrib["d"] = d 0019 return ElementTree.ElementTree(svg) 0020 0021 def parse_svg_paths(self, paths): 0022 return parse_svg_etree(self.make_svg_paths(paths)) 0023 0024 def parse_svg_path(self, d): 0025 return self.parse_svg_paths({"path": d}) 0026 0027 def assert_path(self, path, vertices, in_tangents, out_tangents): 0028 anim = self.parse_svg_path(path) 0029 path = anim.find("path") 0030 self.assertIsInstance(path.shapes[0], objects.Path) 0031 self.assertIsInstance(path.shapes[1], objects.Stroke) 0032 bezier = path.shapes[0].shape.value 0033 self.assertListEqual(bezier.vertices, vertices) 0034 self.assertListEqual(bezier.in_tangents, in_tangents) 0035 self.assertListEqual(bezier.out_tangents, out_tangents) 0036 0037 def assert_list_almost_equal(self, a, b): 0038 self.assertEqual(len(a), len(b)) 0039 for aa, bb in zip(a, b): 0040 if isinstance(aa, list): 0041 self.assert_list_almost_equal(aa, bb) 0042 elif isinstance(aa, NVector): 0043 self.assert_list_almost_equal(aa.components, bb.components) 0044 else: 0045 self.assertAlmostEqual(aa, bb) 0046 0047 0048 class TestMove(PathTester): 0049 def test_abs(self): 0050 self.assert_path( 0051 "M 20,20 M 10,10 h 10", 0052 [NVector(10, 10), NVector(20, 10)], 0053 [NVector( 0, 0), NVector( 0, 0)], 0054 [NVector( 0, 0), NVector( 0, 0)], 0055 ) 0056 0057 def test_rel(self): 0058 self.assert_path( 0059 "M 20,20 m 10,10 h 10", 0060 [NVector(30, 30), NVector(40, 30)], 0061 [NVector( 0, 0), NVector( 0, 0)], 0062 [NVector( 0, 0), NVector( 0, 0)], 0063 ) 0064 0065 def test_breaks_abs(self): 0066 anim = self.parse_svg_path(""" 0067 M 10, 10 L 10, 20, 20, 20 0068 M 20, 10 L 10, 20 20, 20 0069 """) 0070 path = anim.find("path") 0071 bezier = path.shapes[0].shape.value 0072 self.assertListEqual(bezier.vertices, [ 0073 NVector(10, 10), NVector(10, 20), NVector(20, 20), 0074 ]) 0075 self.assertFalse(bezier.closed) 0076 0077 bezier = path.shapes[1].shape.value 0078 self.assertListEqual(bezier.vertices, [ 0079 NVector(20, 10), NVector(10, 20), NVector(20, 20), 0080 ]) 0081 self.assertFalse(bezier.closed) 0082 0083 def test_breaks_rel(self): 0084 anim = self.parse_svg_path(""" 0085 M 10, 10 L 10, 20, 20, 20 0086 m 0,-10 L 10, 20 20, 20 0087 """) 0088 path = anim.find("path") 0089 bezier = path.shapes[0].shape.value 0090 self.assertListEqual(bezier.vertices, [ 0091 NVector(10, 10), NVector(10, 20), NVector(20, 20), 0092 ]) 0093 self.assertFalse(bezier.closed) 0094 0095 bezier = path.shapes[1].shape.value 0096 self.assertListEqual(bezier.vertices, [ 0097 NVector(20, 10), NVector(10, 20), NVector(20, 20), 0098 ]) 0099 self.assertFalse(bezier.closed) 0100 0101 0102 class TestLineTo(PathTester): 0103 def test_line_abs(self): 0104 self.assert_path( 0105 "M 10,10 L 90,90 V 10 H 50", 0106 [NVector(10, 10), NVector(90, 90), NVector(90, 10), NVector(50, 10)], 0107 [NVector( 0, 0), NVector( 0, 0), NVector( 0, 0), NVector( 0, 0)], 0108 [NVector( 0, 0), NVector( 0, 0), NVector( 0, 0), NVector( 0, 0)], 0109 ) 0110 0111 def test_line_rel(self): 0112 self.assert_path( 0113 "M 10,10 l 80,80, v -80 h -40", 0114 [NVector(10, 10), NVector(90, 90), NVector(90, 10), NVector(50, 10)], 0115 [NVector( 0, 0), NVector( 0, 0), NVector( 0, 0), NVector( 0, 0)], 0116 [NVector( 0, 0), NVector( 0, 0), NVector( 0, 0), NVector( 0, 0)], 0117 ) 0118 0119 0120 class TestCubic(PathTester): 0121 def test_cubic_abs(self): 0122 self.assert_path( 0123 """ 0124 M 10,90 0125 C 30,90 25,10 50,10 0126 S 70,90 90,90 0127 """, 0128 [NVector(10, 90), NVector( 50, 10), NVector( 90, 90)], 0129 [NVector( 0, 0), NVector(-25, 0), NVector(-20, 0)], 0130 [NVector(20, 0), NVector( 25, 0), NVector( 0, 0)], 0131 ) 0132 0133 def test_cubic_rel(self): 0134 self.assert_path( 0135 """ 0136 M 10,90 0137 c 20,0 15,-80 40,-80 0138 s 20,80 40,80 0139 """, 0140 [NVector(10, 90), NVector( 50, 10), NVector( 90, 90)], 0141 [NVector( 0, 0), NVector(-25, 0), NVector(-20, 0)], 0142 [NVector(20, 0), NVector( 25, 0), NVector( 0, 0)], 0143 ) 0144 0145 0146 class TestQuadratic(PathTester): 0147 def test_q_abs(self): 0148 self.assert_path( 0149 """ 0150 M 10,50 0151 Q 25,25 40,50 0152 """, 0153 [NVector(10, 50), NVector( 40, 50)], 0154 [NVector( 0, 0), NVector(-15,-25)], 0155 [NVector( 0, 0), NVector( 0, 0)], 0156 ) 0157 0158 def test_q_rel(self): 0159 self.assert_path( 0160 """ 0161 M 10,50 0162 q 15,-25 30,0 0163 """, 0164 [NVector(10, 50), NVector( 40, 50)], 0165 [NVector( 0, 0), NVector(-15,-25)], 0166 [NVector( 0, 0), NVector( 0, 0)], 0167 ) 0168 0169 def test_t_abs(self): 0170 self.assert_path( 0171 """ 0172 M 10,50 0173 Q 25,25 40,50 0174 T 70,50 100,50 130,50 0175 """, 0176 [NVector(10, 50), NVector( 40, 50), NVector( 70, 50), NVector(100, 50), NVector(130, 50)], 0177 [NVector( 0, 0), NVector(-15,-25), NVector(-15, 25), NVector(-15,-25), NVector(-15, 25)], 0178 [NVector( 0, 0), NVector( 0, 0), NVector( 0, 0), NVector( 0, 0), NVector( 0, 0)], 0179 ) 0180 0181 def test_t_rel(self): 0182 self.assert_path( 0183 """ 0184 M 10,50 0185 q 15,-25 30,0 0186 t 30,0 30,0 30,0 0187 """, 0188 [NVector(10, 50), NVector( 40, 50), NVector( 70, 50), NVector(100, 50), NVector(130, 50)], 0189 [NVector( 0, 0), NVector(-15,-25), NVector(-15, 25), NVector(-15,-25), NVector(-15, 25)], 0190 [NVector( 0, 0), NVector( 0, 0), NVector( 0, 0), NVector( 0, 0), NVector( 0, 0)], 0191 ) 0192 0193 0194 class TestArc(PathTester): 0195 def test_large_nosweep(self): 0196 anim = self.parse_svg_path(""" 0197 M 6,10 0198 A 6 4 10 1 0 14,10 0199 """) 0200 path = anim.find("path") 0201 self.assertIsInstance(path.shapes[0], objects.Path) 0202 self.assertIsInstance(path.shapes[1], objects.Stroke) 0203 bezier = path.shapes[0].shape.value 0204 self.assert_list_almost_equal(bezier.vertices, [ 0205 NVector(6, 10), NVector(6.8626965, 15.758131), NVector(15.2322608, 15.9819042), NVector(14, 10), 0206 ]) 0207 self.assert_list_almost_equal(bezier.in_tangents, [ 0208 NVector(0, 0), NVector(-2.5323342, -1.6407878), NVector(-2.0590729, 1.5180295), NVector(2.5323342, 1.6407878), 0209 ]) 0210 self.assert_list_almost_equal(bezier.out_tangents, [ 0211 NVector(-2.0590729, 1.5180295), NVector(2.5323342, 1.6407878), NVector(2.0590729, -1.5180295), NVector(0, 0) 0212 ]) 0213 0214 def test_large_sweep(self): 0215 anim = self.parse_svg_path(""" 0216 M 6,10 0217 A 6 4 10 1 1 14,10 0218 """) 0219 path = anim.find("path") 0220 self.assertIsInstance(path.shapes[0], objects.Path) 0221 self.assertIsInstance(path.shapes[1], objects.Stroke) 0222 bezier = path.shapes[0].shape.value 0223 self.assert_list_almost_equal(bezier.vertices, [ 0224 NVector(6, 10), NVector(4.4903685, 4.24186895), NVector(12.7677392, 4.0180958), NVector(14, 10), 0225 ]) 0226 self.assert_list_almost_equal(bezier.in_tangents, [ 0227 NVector(0, 0), NVector(-1.8563359, 1.6407878), NVector(-2.6844953, -1.51802947), NVector(1.8563359, -1.6407878), 0228 ]) 0229 self.assert_list_almost_equal(bezier.out_tangents, [ 0230 NVector(-2.6844953, -1.5180295), NVector(1.8563359, -1.6407878), NVector(2.6844953, 1.5180295), NVector(0, 0) 0231 ]) 0232 0233 def test_small_nosweep(self): 0234 anim = self.parse_svg_path(""" 0235 M 6,10 0236 A 6 4 10 0 0 14,10 0237 """) 0238 path = anim.find("path") 0239 self.assertIsInstance(path.shapes[0], objects.Path) 0240 self.assertIsInstance(path.shapes[1], objects.Stroke) 0241 bezier = path.shapes[0].shape.value 0242 self.assert_list_almost_equal(bezier.vertices, [ 0243 NVector(6, 10), NVector(14, 10), 0244 ]) 0245 self.assert_list_almost_equal(bezier.in_tangents, [ 0246 NVector(0, 0), NVector(-1.9493793, 1.43715898), 0247 ]) 0248 self.assert_list_almost_equal(bezier.out_tangents, [ 0249 NVector(2.54148326, 1.43715898), NVector(0, 0) 0250 ]) 0251 0252 def test_small_sweep(self): 0253 anim = self.parse_svg_path(""" 0254 M 6,10 0255 A 6 4 10 0 1 14,10 0256 """) 0257 path = anim.find("path") 0258 self.assertIsInstance(path.shapes[0], objects.Path) 0259 self.assertIsInstance(path.shapes[1], objects.Stroke) 0260 bezier = path.shapes[0].shape.value 0261 self.assert_list_almost_equal(bezier.vertices, [ 0262 NVector(6, 10), NVector(14, 10), 0263 ]) 0264 self.assert_list_almost_equal(bezier.in_tangents, [ 0265 NVector(0, 0), NVector(-2.54148326, -1.43715898), 0266 ]) 0267 self.assert_list_almost_equal(bezier.out_tangents, [ 0268 NVector(1.9493793, -1.43715898), NVector(0, 0) 0269 ]) 0270 0271 0272 class TestClosePath(PathTester): 0273 def test_noclose(self): 0274 anim = self.parse_svg_path(""" 0275 M 10, 10 L 10, 20, 20, 20 0276 """) 0277 path = anim.find("path") 0278 bezier = path.shapes[0].shape.value 0279 self.assertListEqual(bezier.vertices, [ 0280 NVector(10, 10), NVector(10, 20), NVector(20, 20), 0281 ]) 0282 self.assertFalse(bezier.closed) 0283 0284 def test_single_upper(self): 0285 anim = self.parse_svg_path(""" 0286 M 10, 10 L 10, 20, 20, 20 Z 0287 """) 0288 path = anim.find("path") 0289 bezier = path.shapes[0].shape.value 0290 self.assertListEqual(bezier.vertices, [ 0291 NVector(10, 10), NVector(10, 20), NVector(20, 20), 0292 ]) 0293 self.assertTrue(bezier.closed) 0294 0295 def test_single_lower(self): 0296 anim = self.parse_svg_path(""" 0297 M 10, 10 L 10, 20, 20, 20 z 0298 """) 0299 path = anim.find("path") 0300 bezier = path.shapes[0].shape.value 0301 self.assertListEqual(bezier.vertices, [ 0302 NVector(10, 10), NVector(10, 20), NVector(20, 20), 0303 ]) 0304 self.assertTrue(bezier.closed) 0305 0306 def test_multi(self): 0307 anim = self.parse_svg_path(""" 0308 M 10, 10 L 10, 20, 20, 20 Z 0309 L 20, 10 20, 20 Z 0310 """) 0311 path = anim.find("path") 0312 bezier = path.shapes[0].shape.value 0313 self.assertListEqual(bezier.vertices, [ 0314 NVector(10, 10), NVector(10, 20), NVector(20, 20), 0315 ]) 0316 self.assertTrue(bezier.closed) 0317 0318 bezier = path.shapes[1].shape.value 0319 self.assertListEqual(bezier.vertices, [ 0320 NVector(10, 10), NVector(20, 10), NVector(20, 20), 0321 ]) 0322 self.assertTrue(bezier.closed) 0323 0324 0325 class TestIntegration(PathTester): 0326 def test_zero_values(self): 0327 self.assert_path( 0328 "M 10,10 L 90,90 0,0", 0329 [NVector(10, 10), NVector(90, 90), NVector(0, 0)], 0330 [NVector( 0, 0), NVector( 0, 0), NVector(0, 0)], 0331 [NVector( 0, 0), NVector( 0, 0), NVector(0, 0)], 0332 )