Skip to content

Commit

Permalink
Add a transform() method to Path and segments
Browse files Browse the repository at this point in the history
  • Loading branch information
regebro committed May 1, 2023
1 parent 5548748 commit 8920910
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 0 deletions.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ where = src

[options.extras_require]
test =
svg.transform
pytest
pytest-cov
Pillow
Expand Down
58 changes: 58 additions & 0 deletions src/svg/path/path.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from math import sqrt, cos, sin, acos, degrees, radians, log, pi
from bisect import bisect
from abc import ABC, abstractmethod
from array import array
import math

try:
Expand All @@ -15,6 +16,11 @@
ERROR = 1e-12


def _xform(coord, matrix):
res = matrix @ array("f", [coord.real, coord.imag, 1])
return complex(res[0], res[1])


def _find_solutions_for_bezier(c2, c1, c0):
"""Find solutions of c2 * t^2 + c1 * t + c0 = 0 where t in [0, 1]"""
soln = []
Expand Down Expand Up @@ -152,6 +158,13 @@ def length(self, error=None, min_depth=None):
distance = self.end - self.start
return sqrt(distance.real**2 + distance.imag**2)

def transform(self, matrix):
return self.__class__(
_xform(self.start, matrix),
_xform(self.end, matrix),
relative=self.relative,
)


class Line(Linear):
def __init__(self, start, end, relative=False, vertical=False, horizontal=False):
Expand Down Expand Up @@ -200,6 +213,15 @@ def boundingbox(self):
y_max = max(self.start.imag, self.end.imag)
return [x_min, y_min, x_max, y_max]

def transform(self, matrix):
return self.__class__(
_xform(self.start, matrix),
_xform(self.end, matrix),
relative=self.relative,
vertical=self.vertical,
horizontal=self.horizontal,
)


class CubicBezier(NonLinear):
def __init__(self, start, control1, control2, end, relative=False, smooth=False):
Expand Down Expand Up @@ -323,6 +345,16 @@ def boundingbox(self):
y_min, y_max = min(y_coords), max(y_coords)
return [x_min, y_min, x_max, y_max]

def transform(self, matrix):
return self.__class__(
_xform(self.start, matrix),
_xform(self.control1, matrix),
_xform(self.control2, matrix),
_xform(self.end, matrix),
relative=self.relative,
smooth=self.smooth,
)


class QuadraticBezier(NonLinear):
def __init__(self, start, control, end, relative=False, smooth=False):
Expand Down Expand Up @@ -461,6 +493,15 @@ def boundingbox(self):
y_min, y_max = min(y_coords), max(y_coords)
return [x_min, y_min, x_max, y_max]

def transform(self, matrix):
return self.__class__(
_xform(self.start, matrix),
_xform(self.control, matrix),
_xform(self.end, matrix),
relative=self.relative,
smooth=self.smooth,
)


class Arc(NonLinear):
def __init__(self, start, radius, rotation, arc, sweep, end, relative=False):
Expand Down Expand Up @@ -697,6 +738,17 @@ def boundingbox(self):
y_min, y_max = min(y_coords), max(y_coords)
return [x_min, y_min, x_max, y_max]

def transform(self, matrix):
return self.__class__(
_xform(self.start, matrix),
self.radius,
self.rotation,
self.arc,
self.sweep,
_xform(self.end, matrix),
self.relative,
)


class Move:
"""Represents move commands. Does nothing, but is there to handle
Expand Down Expand Up @@ -747,6 +799,9 @@ def boundingbox(self):
y_max = max(self.start.imag, self.end.imag)
return [x_min, y_min, x_max, y_max]

def transform(self, matrix):
return self.__class__(_xform(self.start, matrix), self.relative)


class Close(Linear):
"""Represents the closepath command"""
Expand Down Expand Up @@ -898,3 +953,6 @@ def boundingbox(self):
x_min, x_max = min(x_coords), max(x_coords)
y_min, y_max = min(y_coords), max(y_coords)
return [x_min, y_min, x_max, y_max]

def transform(self, matrix):
return self.__class__(*[segment.transform(matrix) for segment in self])
16 changes: 16 additions & 0 deletions tests/test_transforms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import unittest

from svg.path.parser import parse_path
from svg.transform import make_matrix


class TransformTests(unittest.TestCase):
def test_svg_path_transform():
line = path.Line(0, 100 + 100j)
xline = line.transform(make_matrix(sx=0.1, sy=0.2))
assert xline == path.Line(0, 10 + 20j)

d = path.parser.parse_path("M 750,100 L 250,900 L 1250,900 z")
# Makes it 10% as big in x and 20% as big in y
td = d.transform(make_matrix(sx=0.1, sy=0.2))
assert td.d() == "M 75,20 L 25,180 L 125,180 z"

0 comments on commit 8920910

Please sign in to comment.