Skip to content

Commit

Permalink
Add not fully tested basic SVG templater class
Browse files Browse the repository at this point in the history
  • Loading branch information
pablerass committed Dec 17, 2023
1 parent 635ae08 commit f66a8d9
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 1 deletion.
14 changes: 13 additions & 1 deletion cartuli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
from .card import Card, CardImage
from .deck import Deck
from .sheet import Sheet
from .output import sheet_output, sheet_pdf_output
from .filters import MultipleFilter, StraightenFilter, InpaintFilter, CropFilter
from .processing import inpaint, straighten, crop
from .templater import SVGTemplate
from .definition import Definition, DefinitionError


__version__ = "v0.1.0b2"
Expand All @@ -16,5 +21,12 @@
MINI_USA, MINI_CHIMERA, MINI_EURO, STANDARD_USA, CHIMERA, EURO, STANDARD, MAGNUM_COPPER,
MAGNUM_SPACE, SMALL_SQUARE, SQUARE, MAGNUM_SILVER, MAGNUM_GOLD, TAROT,
Coordinates, Point, Size, mm, cm, inch,
Card, CardImage, Sheet, Deck
Card, CardImage,
Deck,
Sheet,
sheet_output, sheet_pdf_output,
MultipleFilter, StraightenFilter, InpaintFilter, CropFilter,
inpaint, straighten, crop,
SVGTemplate,
Definition, DefinitionError
]
156 changes: 156 additions & 0 deletions cartuli/templater.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
from __future__ import annotations

import base64
import io

from cairosvg import svg2png
from copy import deepcopy
from lxml import etree
from pathlib import Path
from PIL import Image
from typing import Iterable


TemplateContent = str
ParameterKey = str
ParameterValue = Image.Image | str


def _image_to_uri(image: Image.Image | str | Path, encoding: str = 'UTF-8') -> str:
if isinstance(image, str):
image = Path(image)
if isinstance(image, Path):
image = Image.open(image)

image_buffer = io.BytesIO()
# TUNE: This should be done with JPEG but the tests are not consistent that way :/
image.save(image_buffer, format='PNG')

return f"data:image/png;base64,{base64.b64encode(image_buffer.getvalue()).decode(encoding)}"


def _set_template_text(text_element: etree._Element, value: str):
text_element.getchildren()[0].text = value


def _get_template_text(text_element: etree._Element) -> str:
return text_element.getchildren()[0].text


def _set_template_image(image_element: etree._Element, value: Image.Image, encoding: str = 'UTF-8'):
image_element.set('{http://www.w3.org/1999/xlink}href', _image_to_uri(value))


def _get_template_image(image_element: etree._Element, encoding: str = 'UTF-8') -> Image.Image:
# TODO: Add beter content management, there must be supported by pillow or other library
uri_image = image_element.get('{http://www.w3.org/1999/xlink}href')
base64_image = uri_image.split(',')[-1]
return Image.open(io.BytesIO(base64.b64decode(base64_image)))


class SVGTemplate:
def __init__(self, template: TemplateContent | etree._Element, parameters: Iterable[ParameterKey]):
if not parameters:
raise ValueError("A template withoyt parameters does not make any sense")

if isinstance(template, etree._Element):
self.__xml_tree = template
self.__encoding = self.__xml_tree.docinfo.encoding
else:
self.__xml_tree = etree.fromstring(bytes(template, 'UTF-8'))
self.__encoding = 'UTF-8'

for parameter in parameters:
# TUNE: svg contents are refered with {http://www.w3.org/2000/svg} in files created with my version of
# Inkscape. That part is being ignored to make this work in other conditions but probably should be
# properly managed. This assumtion is repeated along all class methods.
element = self.__xml_tree.find(f".//*[@id='{parameter}']")

if element is None:
raise ValueError(f"Parameter '{parameter}' not found in template")
if not (element.tag.endswith('image') or element.tag.endswith('text')):
raise ValueError(f"Parameter '{parameter}' element '{element.tag}' is unsupported")

self.__parameters = parameters

@property
def parameters(self) -> dict[ParameterKey, type]:
return self.__parameters.copy()

@classmethod
def from_file(cls, template_file: str | Path, parameters: Iterable[ParameterKey]) -> SVGTemplate:
if isinstance(template_file, str):
template_file = Path(template_file)

return cls(etree.parse(template_file), parameters)

def apply_parameters(self, parameters: dict[ParameterKey, ParameterValue]) -> TemplateContent:
# TUNE: Think if an error should be raised if not all parameters are specified
xml_tree = deepcopy(self.__xml_tree)

for parameter, value in parameters.items():
element = xml_tree.find(f".//*[@id='{parameter}']")

if element is None:
raise ValueError(f"Parameter '{parameter}' not found in template")
if element.tag.endswith('image'):
if isinstance(value, Image.Image):
_set_template_image(element, value, encoding=self.__encoding)
else:
raise ValueError((f"Parameter '{parameter}' value '{value}' is invalid "
f"for '{element.tag}'"))
if element.tag.endswith('text'):
if isinstance(value, str):
_set_template_text(element, value)
else:
raise ValueError((f"Parameter '{parameter}' value '{value}' is invalid "
f"for '{element.tag}'"))

return etree.tostring(xml_tree, pretty_print=True)

def create_image(self, parameters: dict[ParameterKey, ParameterValue]) -> Image.Image:
svg_content = self.apply_parameters(parameters)

image_data = svg2png(bytestring=svg_content)

return Image.open(io.BytesIO(image_data))

def get_values(self, content: TemplateContent | etree._Element,
parameters: tuple(ParameterKey) = None) -> dict[ParameterKey, ParameterValue]:
if parameters is None:
parameters = tuple(self.__parameters)

if isinstance(content, etree._Element):
content_tree = content
else:
content_tree = etree.fromstring(bytes(content))

values = {}
for parameter in parameters:
element = content_tree.find(f".//*[@id='{parameter}']")
if element is None:
raise ValueError(f"parameter '{parameter}' not found in content")
if element.tag.endswith('image'):
value = _get_template_image(element)
if element.tag.endswith('text'):
value = _get_template_text(element)

values |= {parameter: value}

return values

def apply_parameters_to_file(self, file: str | Path, parameters: dict[ParameterKey, ParameterValue]):
if isinstance(file, str):
file = Path(file)

file.write_text(self.apply_parameters(parameters))

def create_image_file(self, file: str | Path, parameters: dict[ParameterKey, ParameterValue]):
self.create_image(parameters).save(file)

def get_values_from_file(self, content_file: str | Path,
parameters: tuple(ParameterKey) = None) -> dict[ParameterKey, ParameterValue]:
if isinstance(content_file, str):
content_file = Path(content_file)

return self.get_values(content_file.read_text(), parameters)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ opencv-python==4.*
pillow==10.*
pyyaml==6.*
carpeta==0.1.0a2
cairosvg==2.*
lxml==4.*
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ def path(rel_path: Path | str):
return path


@pytest.fixture
def fixture_content() -> str:
def path(rel_path: Path | str):
return (TEST_FILES_PATH / rel_path).read_text()

return path


@pytest.fixture
def random_image() -> Image.Image:
def create_image(size: Size = None):
Expand Down
85 changes: 85 additions & 0 deletions tests/files/template.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/files/template_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions tests/test_templater.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pytest

from PIL import ImageChops

from cartuli.templater import SVGTemplate


def test_templater(fixture_content, random_image):
template_content = fixture_content("template.svg")

with pytest.raises(ValueError):
SVGTemplate(template_content, [])

with pytest.raises(ValueError):
SVGTemplate(template_content, ('name', 'image'))

template = SVGTemplate(template_content, ('image', 'text'))

parameters = {
'text': 'otro_texto',
'image': random_image(),
}
generated_content = template.apply_parameters(parameters)
content_values = template.get_values(generated_content)

assert parameters['text'] == content_values['text']

rgb_parameter_image = parameters['image'].convert('RGB')
rgb_content_image = content_values['image'].convert('RGB')
assert not ImageChops.difference(rgb_parameter_image, rgb_content_image).getbbox()

0 comments on commit f66a8d9

Please sign in to comment.