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         )