Skip to content

Commit

Permalink
Add support for calculating basic bbox
Browse files Browse the repository at this point in the history
  • Loading branch information
reznakt committed Feb 7, 2025
1 parent b08abd7 commit 5f5067f
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ __pycache__
.hypothesis
.coverage
dist
*.png
90 changes: 90 additions & 0 deletions svglab/bbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import copy
import uuid
from collections.abc import Iterator

import PIL.Image
import pydantic
import typing_extensions
from typing_extensions import (
Protocol,
TypeAlias,
TypeVar,
runtime_checkable,
)

from svglab import utils
from svglab.attrparse import point
from svglab.elements import common


_TagT = TypeVar("_TagT", bound=common.Tag)
_SvgTag: TypeAlias = common.PairedTag


@runtime_checkable
class _SupportsRender(Protocol):
def render(self) -> PIL.Image.Image: ...


@pydantic.dataclasses.dataclass(frozen=True)
class BBox:
x_min: pydantic.NonNegativeInt
y_min: pydantic.NonNegativeInt
x_max: pydantic.NonNegativeInt
y_max: pydantic.NonNegativeInt

def as_tuple(self) -> tuple[int, int, int, int]:
return self.x_min, self.y_min, self.x_max, self.y_max

Check warning on line 37 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L37

Added line #L37 was not covered by tests

def as_rect(self) -> typing_extensions.Tuple[point.Point, point.Point]:
return point.Point(self.x_min, self.y_min), point.Point(

Check warning on line 40 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L40

Added line #L40 was not covered by tests
self.x_max, self.y_max
)

def __iter__(self) -> Iterator[int]:
return iter(self.as_tuple())

Check warning on line 45 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L45

Added line #L45 was not covered by tests


def _copy_tree(tag: _TagT) -> tuple[_TagT, _SvgTag]:
svg = utils.take_last(tag.parents)

Check warning on line 49 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L49

Added line #L49 was not covered by tests

# type(svg) -> tags.Svg
assert isinstance(svg, _SvgTag)

Check warning on line 52 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L52

Added line #L52 was not covered by tests

original_id = tag.id
tag.id = uuid.uuid4().hex

Check warning on line 55 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L54-L55

Added lines #L54 - L55 were not covered by tests

try:
svg = copy.deepcopy(svg)

Check warning on line 58 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L57-L58

Added lines #L57 - L58 were not covered by tests

candidates = svg.find_all(type(tag))

Check warning on line 60 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L60

Added line #L60 was not covered by tests

this = next(tag for tag in candidates if tag.id == tag.id)

Check warning on line 62 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L62

Added line #L62 was not covered by tests
finally:
tag.id = original_id

Check warning on line 64 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L64

Added line #L64 was not covered by tests

return this, svg

Check warning on line 66 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L66

Added line #L66 was not covered by tests


def _bbox_render(tag: common.Tag) -> PIL.Image.Image:
tag_copy, svg = _copy_tree(tag)

Check warning on line 70 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L70

Added line #L70 was not covered by tests

for t in svg.find_all():
t.visibility = "hidden"

Check warning on line 73 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L73

Added line #L73 was not covered by tests

tag_copy.visibility = "visible"

Check warning on line 75 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L75

Added line #L75 was not covered by tests

assert isinstance(svg, _SupportsRender)
return svg.render()

Check warning on line 78 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L77-L78

Added lines #L77 - L78 were not covered by tests


def bbox(tag: common.Tag) -> BBox | None:
img = _bbox_render(tag)
bbox = img.getbbox()

Check warning on line 83 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L82-L83

Added lines #L82 - L83 were not covered by tests

if bbox is None:
return None

Check warning on line 86 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L86

Added line #L86 was not covered by tests

x_min, y_min, x_max, y_max = bbox

Check warning on line 88 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L88

Added line #L88 was not covered by tests

return BBox(x_min, y_min, x_max, y_max)

Check warning on line 90 in svglab/bbox.py

View check run for this annotation

Codecov / codecov/patch

svglab/bbox.py#L90

Added line #L90 was not covered by tests
30 changes: 19 additions & 11 deletions svglab/elements/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)

from svglab import constants, errors, models, serialize, utils
from svglab.attrs import groups
from svglab.attrs import names as attr_names
from svglab.elements import names

Expand Down Expand Up @@ -94,6 +95,14 @@ def to_beautifulsoup_object(self) -> bs4.PageElement:
@abc.abstractmethod
def _eq(self, other: Self, /) -> bool: ...

@property
def parents(self) -> Generator[Element]:
curr = self.parent

Check warning on line 100 in svglab/elements/common.py

View check run for this annotation

Codecov / codecov/patch

svglab/elements/common.py#L100

Added line #L100 was not covered by tests

while curr is not None:
yield curr
curr = curr.parent

Check warning on line 104 in svglab/elements/common.py

View check run for this annotation

Codecov / codecov/patch

svglab/elements/common.py#L103-L104

Added lines #L103 - L104 were not covered by tests

@override
def __eq__(self, other: object) -> bool:
if not utils.basic_compare(other, self=self):
Expand All @@ -102,7 +111,9 @@ def __eq__(self, other: object) -> bool:
return self._eq(other)


class Tag(Element, metaclass=abc.ABCMeta):
class Tag(
Element, groups.Core, groups.Presentation, metaclass=abc.ABCMeta
):
"""A tag.
A tag is an element that has a name and a set of attributes.
Expand Down Expand Up @@ -306,14 +317,6 @@ def descendants(self) -> Generator[Element]:
if isinstance(child, PairedTag):
queue.extend(child.children)

@property
def parents(self) -> Generator[Element]:
curr = self.parent

while curr is not None:
yield curr
curr = curr.parent

@property
def next_siblings(self) -> Generator[Element]:
if self.parent is None or not isinstance(self.parent, PairedTag):
Expand Down Expand Up @@ -409,6 +412,9 @@ def to_beautifulsoup_object(self) -> bs4.Tag:

return tag

@overload
def find_all(self, /, *, recursive: bool = True) -> Generator[Tag]: ...

@overload
def find_all(
self, *tags: type[_T_tag], recursive: bool = True
Expand All @@ -426,6 +432,7 @@ def find_all(
Args:
tags: The tags to search for. Can be tag names or tag classes.
If no search criteria are provided, all tags are returned.
recursive: If `False`, only search the direct children of the tag,
otherwise search all descendants.
Expand All @@ -446,8 +453,9 @@ def find_all(
"""
for child in self.descendants if recursive else self.children:
if isinstance(child, Tag) and any(
_match_tag(child, search=tag) for tag in tags
if isinstance(child, Tag) and (
len(tags) == 0
or any(_match_tag(child, search=tag) for tag in tags)
):
yield child

Expand Down
13 changes: 12 additions & 1 deletion svglab/elements/traits.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
from __future__ import annotations

from svglab import bbox
from svglab.attrs import groups, regular
from svglab.elements import common


class Element(groups.Core, groups.Presentation, common.Tag):
# common attributes are defined directly on the Tag class
class Element(common.Tag):
pass


class _GraphicalOperations(Element):
def bbox(self) -> bbox.BBox | None:
return bbox.bbox(self)

Check warning on line 15 in svglab/elements/traits.py

View check run for this annotation

Codecov / codecov/patch

svglab/elements/traits.py#L15

Added line #L15 was not covered by tests


class GraphicsElement(
_GraphicalOperations,
groups.GraphicalEvents, # TODO: check if this is correct
Element,
):
Expand All @@ -28,6 +38,7 @@ class AnimationElement(


class ContainerElement(
_GraphicalOperations,
groups.GraphicalEvents, # TODO: check if this is correct
common.PairedTag,
):
Expand Down

0 comments on commit 5f5067f

Please sign in to comment.