Source code for fibomat.default_backends.svg_backend

from typing import Optional, Dict
import io

import PIL.Image

import numpy as np

import svgwrite
import svgwrite.container
import svgwrite.shapes
import svgwrite.extensions

from fibomat.backend import BackendBase
from fibomat.site import Site
from fibomat.units import U_, Q_, scale_factor
from fibomat.utils import PathLike
from fibomat.pattern import Pattern
from fibomat.shapes import Rect, Polygon, Circle
from fibomat.linalg import DimBoundingBox, scale, translate, Vector


[docs]class SVGBackend(BackendBase):
[docs] def __init__(self, pixel_scale: Q_ = Q_('0.01 µm'), stroke_width: float = 0.1, description: Optional[str] = None): super().__init__() self._description = description self._pixel_scale = pixel_scale self._stroke_width = stroke_width # sites are mapped to svg groups self._layers = [] self._current_layer: Optional[Dict] = None self._current_layer_center: Optional[Vector] = None self._total_bounding_box = None
[docs] def process_site(self, new_site: Site) -> None: self._current_layer = {'id': new_site.description or None, 'elements': []} self._current_layer_center = new_site.center.vector_as(self._pixel_scale.u) self._layers.append(self._current_layer) if not self._total_bounding_box: self._total_bounding_box = new_site.fov_bounding_box else: self._total_bounding_box = self._total_bounding_box.extended(new_site.fov_bounding_box) super().process_site(new_site)
# def process_pattern(self, ptn: Pattern) -> None: # super().process_pattern(ptn) def _fill_or_stroke(self, svg_elem, raster_style): if raster_style.dimension == 2: svg_elem.fill('black') elif raster_style.dimension == 1: svg_elem.stroke('black', width=self._stroke_width) svg_elem['style'] = 'fill:none' else: raise RuntimeError('Only raster styles with dimension 1 or 2 are supported.') def _pixel_scale_factor(self, other_unit): return scale_factor(self._pixel_scale, other_unit) / self._pixel_scale.m def _scale_and_shift_shape(self, ptn: Pattern): fak = self._pixel_scale_factor(ptn.dim_shape.unit) return ptn.dim_shape.shape.transformed(scale(fak) | translate(self._current_layer_center))
[docs] def rect(self, ptn: Pattern[Rect]) -> None: scale = self._pixel_scale_factor(ptn.dim_shape.unit) scaled_rect: Rect = self._scale_and_shift_shape(ptn) svg_rect = svgwrite.shapes.Rect( insert=scaled_rect.center + (-scaled_rect.width/2, -scaled_rect.height/2), size=(scaled_rect.width, scaled_rect.height) ) svg_rect.rotate(np.rad2deg(scaled_rect.theta), center=scaled_rect.center) self._fill_or_stroke(svg_rect, ptn.raster_style) self._current_layer['elements'].append(svg_rect)
[docs] def polygon(self, ptn: Pattern[Polygon]) -> None: scaled_polygon: Polygon = self._scale_and_shift_shape(ptn) svg_poly = svgwrite.shapes.Polygon(scaled_polygon.points) self._fill_or_stroke(svg_poly, ptn.raster_style) self._current_layer['elements'].append(svg_poly)
[docs] def circle(self, ptn: Pattern[Circle]) -> None: scaled_circle: Circle = self._scale_and_shift_shape(ptn) svg_circle = svgwrite.shapes.Circle(center=scaled_circle.center, r=scaled_circle.r) self._fill_or_stroke(svg_circle, ptn.raster_style) self._current_layer['elements'].append(svg_circle)
def _to_svg(self) -> svgwrite.Drawing: if not self._total_bounding_box: raise RuntimeError('Site may not be empty.') total_width = self._total_bounding_box.width total_height = self._total_bounding_box.height total_width_scaled = self._pixel_scale_factor(total_width.u) * total_width.m total_height_scaled = self._pixel_scale_factor(total_height.u) * total_height.m center = self._total_bounding_box.center shift_x = self._pixel_scale_factor(center.x.u) * center.x.m shift_y = self._pixel_scale_factor(center.y.u) * center.y.m svg = svgwrite.Drawing(size=(total_width_scaled, total_height_scaled)) inkscape_svg = svgwrite.extensions.Inkscape(svg) for layer in self._layers: svg_layer = inkscape_svg.layer(label=layer['id'] or 'Layer') svg.add(svg_layer) svg_layer.translate(total_width_scaled/2 - shift_x, total_height_scaled/2 - shift_y) svg_layer.scale(sx=1, sy=-1) for elem in layer['elements']: svg_layer.add(elem) return svg
[docs] def save(self, filename: PathLike) -> None: self._to_svg().saveas(filename, pretty=True)
[docs] def to_pillow_image(self, background_color=(255, 255, 255, 255)): # https://github.com/manatools/dnfdragora/blob/acaa41e511c3ce026a9123fe494bd017cbfb99db/dnfdragora/updater.py try: import cairosvg except Exception as e: raise RuntimeError('Please install cairosvg manually') from e rendered_svg = PIL.Image.open(io.BytesIO(cairosvg.svg2png(bytestring=self._to_svg().tostring()))) image = PIL.Image.new('RGBA', size=rendered_svg.size, color=background_color) image.paste(rendered_svg, None, rendered_svg) image.convert('RGB') return image