Skip to content

Commit

Permalink
Add support for changing viewBox value
Browse files Browse the repository at this point in the history
  • Loading branch information
reznakt committed Feb 4, 2025
1 parent 3ed2898 commit be90045
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 14 deletions.
7 changes: 7 additions & 0 deletions svglab/attrparse/length.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ def serialize(self) -> str:
value = serialize.serialize(self.value)
return f"{value}{self.unit or ''}"

@classmethod
def zero(cls) -> Length:
return cls(0)

@override
def __add__(self, other: Length) -> Self:
other_value = other.to(self.unit).value
Expand All @@ -95,6 +99,9 @@ def __add__(self, other: Length) -> Self:
def __mul__(self, other: float) -> Self:
return type(self)(value=self.value * other, unit=self.unit)

def __bool__(self) -> bool:
return bool(self.value)

@override
def __float__(self) -> float:
return self.to(None).value
Expand Down
2 changes: 1 addition & 1 deletion svglab/attrparse/point.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def __eq__(self, other: object) -> bool:
)

def __bool__(self) -> bool:
return self == self.zero()
return self != self.zero()

def __complex__(self) -> complex:
return complex(self.x, self.y)
Expand Down
60 changes: 60 additions & 0 deletions svglab/elements/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing_extensions import final, overload

from svglab import protocols, serialize
from svglab.attrparse import point
from svglab.attrs import groups, regular
from svglab.elements import traits

Expand Down Expand Up @@ -106,3 +107,62 @@ def save(

if trailing_newline:
file.write("\n")

def set_viewbox(
self, viewbox: tuple[float, float, float, float]
) -> None:
"""Set a new value for the `viewBox` attribute.
This method sets a new value for the `viewBox` attribute and scales and
translates the SVG content so that the visual representation of the SVG
remains unchanged.
If the `viewBox` is not set, the method uses the `width` and `height`
attributes to calculate the initial viewBox. If the `width`
and `height` attributes are not set, the method raises an exception.
The new `viewBox` must have the same aspect ratio as the old `viewBox`.
If the aspect ratios differ, the method raises an exception.
Any attributes of type `Length` in the SVG must be convertible to
user units. If an attribute is not convertible, the method raises an
exception.
Args:
viewbox: A tuple of four numbers representing the new viewBox.
Raises:
ValueError: If `viewBox` is not set and `width` and `height` are not
set or if the aspect ratios of the old and new viewBox differ.
SvgUnitConversionError: If an attribute is not convertible to user
units.
"""
if self.viewBox is None:
if self.width is None or self.height is None:
raise ValueError(
"Either viewBox or width and height must be set"
)
old_viewbox = (0, 0, self.width, self.height)
else:
old_viewbox = self.viewBox

old_min_x = float(old_viewbox[0])
old_min_y = float(old_viewbox[1])
old_width = float(old_viewbox[2])
old_height = float(old_viewbox[3])

min_x, min_y, width, height = viewbox

translate = point.Point(min_x - old_min_x, min_y - old_min_y)

x_scale = width / old_width
y_scale = height / old_height

if x_scale != y_scale:
raise ValueError("Aspect ratios of old and new viewBox differ")

self.scale(x_scale)
self.translate(translate)

self.viewBox = (min_x, min_y, width, height)
40 changes: 27 additions & 13 deletions svglab/elements/transforms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing_extensions import TypeVar, cast

from svglab import utils
from svglab.attrparse import length, point, transform
from svglab.attrs import common, presentation, regular

Expand Down Expand Up @@ -93,6 +94,9 @@ def scale_distance_along_a_path_attrs(tag: object, by: float) -> None:


def scale(tag: object, by: float) -> None: # noqa: C901, PLR0912
if utils.is_close(by, 1):
return

if isinstance(tag, regular.Width):
tag.width = _scale_attr(tag.width, by)
if isinstance(tag, regular.Height):
Expand Down Expand Up @@ -121,6 +125,7 @@ def scale(tag: object, by: float) -> None: # noqa: C901, PLR0912
tag.points = list(transform.Scale(by) @ tag.points)
if isinstance(tag, regular.D) and tag.d is not None:
tag.d = transform.Scale(by) @ tag.d
# TODO: handle transform attribute

# these assignments have to be mutually exclusive, because the
# type checker doesn't know that x being a <number> implies that x is
Expand All @@ -147,9 +152,14 @@ def scale(tag: object, by: float) -> None: # noqa: C901, PLR0912
scale_distance_along_a_path_attrs(tag, by)


def translate(tag: object, by: point.Point) -> None: # noqa: C901
def translate(tag: object, by: point.Point) -> None: # noqa: C901, PLR0912
if not by:
return

x, y = by
zero = length.Length.zero()

# these attributes are mandatory for the respective elements
if isinstance(tag, regular.X1):
tag.x1 = _translate_attr(tag.x1, x)
if isinstance(tag, regular.Y1):
Expand All @@ -158,21 +168,25 @@ def translate(tag: object, by: point.Point) -> None: # noqa: C901
tag.x2 = _translate_attr(tag.x2, x)
if isinstance(tag, regular.Y2):
tag.y2 = _translate_attr(tag.y2, y)

# but these are not, so if they are not present, we initialize them to 0
if isinstance(tag, regular.Cx):
tag.cx = _translate_attr(tag.cx, x)
tag.cx = _translate_attr(tag.cx or zero, x)
if isinstance(tag, regular.Cy):
tag.cy = _translate_attr(tag.cy, y)
if isinstance(tag, regular.Points) and tag.points is not None:
tag.points = list(transform.Translate(x, y) @ tag.points)
if isinstance(tag, regular.D) and tag.d is not None:
tag.d += by
tag.cy = _translate_attr(tag.cy or zero, y)

if isinstance(tag, regular.XNumber): # noqa: SIM114
tag.x = _translate_attr(tag.x, x)
if isinstance(tag, regular.XNumber):
tag.x = _translate_attr(tag.x or 0, x)
elif isinstance(tag, regular.XCoordinate):
tag.x = _translate_attr(tag.x, x)
tag.x = _translate_attr(tag.x or zero, x)

if isinstance(tag, regular.YNumber): # noqa: SIM114
tag.y = _translate_attr(tag.y, y)
if isinstance(tag, regular.YNumber):
tag.y = _translate_attr(tag.y or 0, y)
elif isinstance(tag, regular.YCoordinate):
tag.y = _translate_attr(tag.y, y)
tag.y = _translate_attr(tag.y or zero, y)

if isinstance(tag, regular.Points) and tag.points is not None:
tag.points = list(transform.Translate(x, y) @ tag.points)
if isinstance(tag, regular.D) and tag.d is not None:
tag.d = tag.d + by
# TODO: handle transform attribute

0 comments on commit be90045

Please sign in to comment.