Source code for cutcutcodec.core.io.read_svg

"""Decode the svg vectorial images based on `cairosvg` lib."""

import math
import pathlib
import typing
import xml
from fractions import Fraction

import cairosvg
import cv2
import numpy as np
import torch

from cutcutcodec.core.classes.colorspace import Colorspace
from cutcutcodec.core.classes.container import ContainerInput
from cutcutcodec.core.classes.frame_video import FrameVideo
from cutcutcodec.core.classes.stream import Stream
from cutcutcodec.core.classes.stream_video import StreamVideo
from cutcutcodec.core.exceptions import DecodeError, OutOfTimeRange


[docs] class ContainerInputSVG(ContainerInput): """Decode an svg image to a matricial image of any dimension. Attributes ---------- filename : pathlib.Path The path to the physical file that contains the svg data (readonly). Examples -------- >>> from cutcutcodec.core.io.read_svg import ContainerInputSVG >>> from cutcutcodec.utils import get_project_root >>> image = get_project_root() / "media" / "image" / "logo.svg" >>> (stream,) = ContainerInputSVG(image).out_streams >>> stream.snapshot(0, (9, 9))[..., 3] tensor([[0.0000, 0.0627, 0.5529, 0.8275, 0.9608, 0.8275, 0.5529, 0.0627, 0.0000], [0.0745, 0.8471, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.8471, 0.0745], [0.5686, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.5647], [0.8863, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.8824], [0.9765, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9765], [0.8863, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.8824], [0.5686, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.5647], [0.0745, 0.8471, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.8471, 0.0745], [0.0000, 0.0627, 0.5529, 0.8275, 0.9608, 0.8275, 0.5529, 0.0627, 0.0000]]) >>> """ def __init__(self, filename: pathlib.Path | str | bytes, *, unsafe=False): """Initialise and create the class. Parameters ---------- filename : pathlike Path to the file to be decoded. unsafe : bool Transmitted to ``cairosvg.svg2png``. Raises ------ cutcutcodec.core.exceptions.DecodeError If it fail to extract any multimedia stream from the provided file. """ filename = pathlib.Path(filename).expanduser().resolve() assert filename.is_file(), filename assert isinstance(unsafe, bool), unsafe.__class__.__name__ self._filename = filename self.unsafe = unsafe super().__init__([_StreamVideoSVG(self)]) def __enter__(self): """Make the object compatible with a context manager.""" return self def __exit__(self, *_): """Exit the context manager.""" def _getstate(self) -> dict: return { "filename": str(self.filename), "unsafe": self.unsafe, } def _setstate(self, in_streams: typing.Iterable[Stream], state: dict) -> None: keys = {"filename", "unsafe"} assert state.keys() == keys, set(state)-keys ContainerInputSVG.__init__(self, state["filename"], unsafe=state["unsafe"]) @property def filename(self) -> pathlib.Path: """Return the path to the physical file that contains the svg data.""" return self._filename
class _StreamVideoSVG(StreamVideo): """Read SVG as a video stream. Parameters ---------- height : int The preconised dimension i (vertical) of the picture in pxl (readonly). width : int The preconised dimension j (horizontal) of the picture in pxl (readonly). """ colorspace = Colorspace.from_default_target_rgb() def __init__(self, node: ContainerInputSVG): assert isinstance(node, ContainerInputSVG), node.__class__.__name__ super().__init__(node) with open(node.filename, "rb") as raw: self._bytestring = raw.read() try: pngdata = cairosvg.svg2png(self._bytestring, unsafe=self.node.unsafe) except xml.etree.ElementTree.ParseError as err: raise DecodeError(f"failed to read the svg file {node.filename} with cairosvg") from err img = torch.from_numpy(cv2.imdecode(np.frombuffer(pngdata, np.uint8), cv2.IMREAD_UNCHANGED)) self._height, self._width, _ = img.shape self._shape_and_img = ((self._height, self._width), img) def _get_img(self, shape: tuple[int, int]) -> torch.Tensor: """Cache the image.""" if self._shape_and_img[0] != shape: self._shape_and_img = ( shape, torch.from_numpy( cv2.imdecode( np.frombuffer( cairosvg.svg2png( self._bytestring, unsafe=self.node.unsafe, output_height=shape[0], output_width=shape[1], ), np.uint8, ), cv2.IMREAD_UNCHANGED, ), ).to(torch.float32) / 255.0, ) return self._shape_and_img[1] def _snapshot(self, timestamp: Fraction, mask: torch.Tensor) -> torch.Tensor: if timestamp < 0: raise OutOfTimeRange(f"there is no svg frame at timestamp {timestamp} (need >= 0)") return FrameVideo(timestamp, self._get_img(mask.shape).clone()) @property def beginning(self) -> Fraction: return Fraction(0) @property def duration(self) -> Fraction | float: return math.inf @property def height(self) -> int: """Return the preconised dimension i (vertical) of the picture in pxl.""" return self._height @property def width(self) -> int: """Return the preconised dimension j (horizontal) of the picture in pxl.""" return self._width