Add svg rotation transform support (#8170)

* Add svg rotation transform support

Fixes #8166 - ChatGPT wrote the matrix transformations. I merely fixed type and import errors and verified the code works.

* Adding some simple screenshot-based Svg tests

* Simplify rotated example svg

* Clean up unnecessary lines
This commit is contained in:
Danny Dawson 2023-03-20 12:49:25 -07:00 committed by GitHub
parent e37e3b4dc0
commit 7f23e32441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 126 additions and 3 deletions

23
examples/svg/rotated.svg Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" standalone="no"?><svg width="256" height="256"
viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M102,42 Q 165,22 59 48 T 197 164" stroke="green" stroke-width="5.0"
fill="none" stroke-opacity="0.375" transform="" />
<path d="M102,42 Q 165,22 59 48 T 197 164" stroke="green" stroke-width="5.0"
fill="none" stroke-opacity="0.375" transform=" rotate(120.0 128 128)" />
<path d="M102,42 Q 165,22 59 48 T 197 164" stroke="green" stroke-width="5.0"
fill="none" stroke-opacity="0.375" transform=" rotate(240.0 128 128)" />
<rect stroke="blue" stroke-width="3.75" x="24" y="98" width="59" height="132"
fill="none" stroke-opacity="0.75" transform="" />
<rect stroke="blue" stroke-width="3.75" x="24" y="98" width="59" height="132"
fill="none" stroke-opacity="0.75" transform=" rotate(-120.0 128 128)" />
<rect stroke="blue" stroke-width="3.75" x="24" y="98" width="59" height="132"
fill="none" stroke-opacity="0.75" transform=" rotate(-240.0 128 128)" />
<path d="M6,39 Q 230,177 256 149 T 37 164" stroke="red" stroke-width="1.75"
fill="none" stroke-opacity="0.625" transform="" />
<path d="M6,39 Q 230,177 256 149 T 37 164" stroke="red" stroke-width="1.75"
fill="none" stroke-opacity="0.625" transform=" rotate(120.0 128 128)" />
<path d="M6,39 Q 230,177 256 149 T 37 164" stroke="red" stroke-width="1.75"
fill="none" stroke-opacity="0.625" transform=" rotate(240.0 128 128)" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -24,6 +24,7 @@ __all__ = ("Svg", )
include "common.pxi"
import math
import re
cimport cython
from xml.etree.cElementTree import parse
@ -229,6 +230,21 @@ cdef class Matrix(object):
print("SVG: unknown how to parse: {!r}".format(value))
self.mat[0] = <float>float(a)
self.mat[3] = <float>float(b)
elif string.startswith('rotate('):
value = parse_list(string[7:-1])
angle = <float>float(value[0])
if len(value) == 3:
cx, cy = map(float, value[1:])
else:
cx = cy = 0
cos_a = math.cos(math.radians(angle))
sin_a = math.sin(math.radians(angle))
self.mat[0] = cos_a
self.mat[1] = sin_a
self.mat[2] = -sin_a
self.mat[3] = cos_a
self.mat[4] = -cx * cos_a + cy * sin_a + cx
self.mat[5] = -cx * sin_a - cy * cos_a + cy
elif string is not None:
i = 0
for f in string:
@ -386,7 +402,7 @@ cdef class Svg(RenderContext):
:param color the default color to use for Svg elements that specify "currentColor"
.. note:: if you want to use SVGs from string, you can parse the source yourself
using `from xml.etree.cElementTree import fromstring` and pass the result to
using `from xml.etree.cElementTree import fromstring` and pass the result to
Svg().set_tree(). This will trigger the rendering of the Svg - as an alternative
to assigning a filepath to Svg.source. This is also viable to trigger reloading.
@ -526,7 +542,7 @@ cdef class Svg(RenderContext):
def set_tree(self, tree):
'''
sets the tree used to render the Svg and triggers reloading.
:param xml.etree.cElementTree tree: the tree parsed from the SVG source
.. versionadded:: 2.0.0
@ -937,7 +953,7 @@ cdef class Svg(RenderContext):
x_ = cp * dx + sp * dy
y_ = -sp * dx + cp * dy
r2 = (((rx * ry)**2 - (rx * y_)**2 - (ry * x_)**2)/
((rx * y_)**2 + (ry * x_)**2))
((rx * y_)**2 + (ry * x_)**2))
if r2 < 0: r2 = 0
r = sqrt(r2)
if large_arc == sweep:

View File

@ -0,0 +1,84 @@
'''
Svg tests
==============
Testing Svg rendering.
'''
from kivy.tests.common import GraphicUnitTest
SIMPLE_SVG = """<?xml version="1.0" standalone="no"?>
<svg width="256" height="256" viewBox="0 0 256 256" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<rect stroke="blue" stroke-width="4" x="24" y="30" width="92" height="166"
fill="none" stroke-opacity="0.5" />
</svg>
"""
SCALE_SVG = """<?xml version="1.0" standalone="no"?>
<svg width="256" height="256" viewBox="0 0 256 256" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<rect stroke="red" stroke-width="4" x="24" y="30" width="10" height="10"
fill="none" stroke-opacity="0.5" transform="scale(2, 3)"/>
</svg>
"""
ROTATE_SVG = """<?xml version="1.0" standalone="no"?>
<svg width="256" height="256" viewBox="0 0 256 256" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<rect stroke="green" stroke-width="4" x="24" y="30" width="50" height="100"
stroke-opacity="0.75" transform="rotate(60 128 128)" />
</svg>
"""
class SvgTest(GraphicUnitTest):
def test_simple(self):
import xml.etree.ElementTree as ET
from kivy.uix.widget import Widget
from kivy.graphics.svg import Svg
# create a root widget
wid = Widget()
# put some graphics instruction on it
with wid.canvas:
svg = Svg()
svg.set_tree(ET.ElementTree(ET.fromstring(SIMPLE_SVG)))
# render, and capture it directly
self.render(wid)
def test_scale(self):
import xml.etree.ElementTree as ET
from kivy.uix.widget import Widget
from kivy.graphics.svg import Svg
# create a root widget
wid = Widget()
# put some graphics instruction on it
with wid.canvas:
svg = Svg()
svg.set_tree(ET.ElementTree(ET.fromstring(SCALE_SVG)))
# render, and capture it directly
self.render(wid)
def test_rotate(self):
import xml.etree.ElementTree as ET
from kivy.uix.widget import Widget
from kivy.graphics.svg import Svg
# create a root widget
wid = Widget()
# put some graphics instruction on it
with wid.canvas:
svg = Svg()
svg.set_tree(ET.ElementTree(ET.fromstring(ROTATE_SVG)))
# render, and capture it directly
self.render(wid)