Source code for alhambra.drawing
from __future__ import annotations
import base64
import re
from typing import Any, Optional, Union
from typing_extensions import TypeAlias
import xml.etree.ElementTree as ET
import attrs
from attrs import Factory
from abc import ABC, abstractmethod
[docs]Number: TypeAlias = Union[int, float]
[docs]class DrawingElement(ABC):
@abstractmethod
[docs] def to_xml(self) -> ET.Element:
...
[docs]class XMLElement(DrawingElement, ET.Element):
[docs] def to_xml(self) -> ET.Element:
return self
@attrs.define()
[docs]class Group(DrawingElement):
[docs] elements: list[DrawingElement] = Factory(list)
[docs] id: Optional[str] = None
[docs] def to_xml(self) -> ET.Element:
e = ET.Element("g")
if self.id is not None:
e.attrib["id"] = self.id
for elem in self.elements:
e.append(elem.to_xml())
return e
[docs] def append(self, v: DrawingElement) -> None:
self.elements.append(v)
[docs]class Rectangle(DrawingElement):
[docs] def to_xml(self):
return self._el
def __init__(
self,
x: Number = 0,
y: Number = 0,
width: Number = 1,
height: Number = 1,
/,
**kwargs,
):
e = ET.Element("rect")
e.attrib["x"] = str(x)
e.attrib["y"] = str(y)
e.attrib["width"] = str(width)
e.attrib["height"] = str(height)
for k, v in kwargs.items():
e.attrib[k] = str(v)
self._el = e
[docs]class Text(DrawingElement):
[docs] def to_xml(self):
return self._el
def __init__(
self, text: str, size: Number = 10, x: Number = 0, y: Number = 0, /, **kwargs
):
e = ET.Element("text")
e.attrib["x"] = str(x)
e.attrib["y"] = str(y)
e.attrib["font-size"] = str(size)
e.text = text
for k, v in kwargs.items():
e.attrib[k.replace("_", "-")] = str(v)
self._el = e
[docs]class Use(DrawingElement):
[docs] def to_xml(self):
return self._el
def __init__(
self, id_or_link: str | Any, x: Number = 0, y: Number = 0, /, **kwargs
):
e = ET.Element("use")
e.attrib["x"] = str(x)
e.attrib["y"] = str(y)
if not isinstance(id_or_link, str):
assert hasattr(id_or_link, "id")
id = str(id_or_link.id)
else:
id = id_or_link
e.attrib["xlink:href"] = "#" + id
for k, v in kwargs.items():
e.attrib[k] = str(v)
self._el = e
@attrs.define()
[docs]class Drawing(DrawingElement):
[docs] defs: list[DrawingElement] = Factory(list)
[docs] elements: list[DrawingElement] = Factory(list)
[docs] viewBox: Optional[tuple[Number, Number, Number, Number]] = None
[docs] def to_xml(self) -> ET.Element:
e = ET.Element("svg")
e.attrib["xmlns"] = "http://www.w3.org/2000/svg"
e.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
e.attrib["width"] = str(self.width)
e.attrib["height"] = str(self.height)
if self.viewBox is not None:
e.attrib["viewBox"] = " ".join(str(i) for i in self.viewBox)
if self.defs:
s = ET.SubElement(e, "defs")
for se in self.defs:
s.append(se.to_xml())
for se in self.elements:
e.append(se.to_xml())
return e
[docs] def save_svg(self, filename: str):
e = self.to_xml()
d = ET.ElementTree(e)
d.write(filename)
[docs] saveSvg = save_svg # for backwards compatibility
[docs] def to_et(self) -> ET.ElementTree:
return ET.ElementTree(self.to_xml())
[docs] def to_string(self) -> str:
e = self.to_xml()
return ET.tostring(e, encoding="unicode", xml_declaration=True)
[docs] def to_bytes(self) -> bytes:
e = self.to_xml()
return ET.tostring(e, encoding="utf-8", xml_declaration=True)
[docs] def _repr_svg_(self):
return self.to_string()
# From https://github.com/cduck/drawSvg
[docs] def _repr_html_(self):
prefix = b"data:image/svg+xml;base64,"
data = base64.b64encode(self.to_bytes())
src = (prefix + data).decode(encoding="ascii")
return '<img src="{}">'.format(src)