"""This module maps instructions to SVG.
Use :func:`default_instructions_to_svg` to load the svg files provided by
this package.
"""
import os
import xmltodict
from knittingpattern.Loader import PathLoader
#: The string to replace with the pattern name in the SVG file.
REPLACE_IN_DEFAULT_SVG = "{instruction.type}"
[docs]class InstructionToSVG(object):
"""This class maps instructions to SVGs."""
@property
def _loader_class(self):
""":return: the loader to load svgs from different locations
:rtype: knittingpattern.Loader.PathLoader ."""
return PathLoader
[docs] def __init__(self):
"""create a InstructionToSVG object without arguments."""
self._instruction_type_to_file_content = {}
@property
def load(self):
""":return: a loader object that allows loading SVG files from
various sources such as files and folders.
:rtype: knittingpattern.Loader.PathLoader
Examples:
- ``instruction_to_svg.load.path(path)`` loads an SVG from a file named
path
- ``instruction_to_svg.load.folder(path)`` loads all SVG files for
instructions in the folder recursively.
If multiple files have the same name, the last occurrence is used.
"""
return self._loader_class(self._process_loaded_object)
def _process_loaded_object(self, path):
"""process the :paramref:`path`.
:param str path: the path to load an svg from
"""
file_name = os.path.basename(path)
name = os.path.splitext(file_name)[0]
with open(path) as file:
string = file.read()
self._instruction_type_to_file_content[name] = string
[docs] def instruction_to_svg_dict(self, instruction):
"""
:return: an xml-dictionary with the same content as
:meth:`instruction_to_svg`.
"""
instruction_type = instruction.type
if instruction_type in self._instruction_type_to_file_content:
svg = self._instruction_type_to_file_content[instruction_type]
return self._set_fills_in_color_layer(svg, instruction.hex_color)
return self.default_instruction_to_svg_dict(instruction)
[docs] def instruction_to_svg(self, instruction):
""":return: an SVG representing the instruction.
The SVG file is determined by the type attribute of the instruction.
An instruction of type ``"knit"`` is looked for in a file named
``"knit.svg"``.
Every element inside a group labeled ``"color"`` of mode ``"layer"``
that has a ``"fill"`` style gets this fill replaced by the color of
the instruction.
Example of a recangle that gets filled like the instruction:
.. code:: xml
<g inkscape:label="color" inkscape:groupmode="layer">
<rect style="fill:#ff0000;fill-opacity:1;fill-rule:nonzero"
id="rectangle1" width="10" height="10" x="0" y="0" />
</g>
If nothing was loaded to display this instruction, a default image is
be generated by :meth:`default_instruction_to_svg`.
"""
return xmltodict.unparse(self.instruction_to_svg_dict(instruction))
def _set_fills_in_color_layer(self, svg_string, color):
"""replaces fill colors in ``<g inkscape:label="color"
inkscape:groupmode="layer">`` with :paramref:`color`
:param color: a color fill the objects in the layer with
"""
structure = xmltodict.parse(svg_string)
if color is None:
return structure
layers = structure["svg"]["g"]
if not isinstance(layers, list):
layers = [layers]
for layer in layers:
if not isinstance(layer, dict):
continue
if layer.get("@inkscape:label") == "color" and \
layer.get("@inkscape:groupmode") == "layer":
for key, elements in layer.items():
if key.startswith("@") or key.startswith("#"):
continue
if not isinstance(elements, list):
elements = [elements]
for element in elements:
style = element.get("@style", None)
if style:
style = style.split(";")
processed_style = []
for style_element in style:
if style_element.startswith("fill:"):
style_element = "fill:" + color
processed_style.append(style_element)
style = ";".join(processed_style)
element["@style"] = style
return structure
[docs] def has_svg_for_instruction(self, instruction):
""":return: whether there is an image for the instruction
:rtype: bool
This can be used before :meth:`instruction_to_svg` as it determines
whether
- the default value is used (:obj:`False`)
- or there is a dedicated svg representation (:obj:`True`).
"""
instruction_type = instruction.type
return instruction_type in self._instruction_type_to_file_content
[docs] def default_instruction_to_svg(self, instruction):
"""As :meth:`instruction_to_svg` but it only takes the ``default.svg``
file into account.
In case no file is found for an instruction in
:meth:`instruction_to_svg`,
this method is used to determine the default svg for it.
The content is created by replacing the text ``{instruction.type}`` in
the whole svg file named ``default.svg``.
If no file ``default.svg`` was loaded, an empty string is returned.
"""
svg_dict = self.default_instruction_to_svg_dict(instruction)
return xmltodict.unparse(svg_dict)
[docs] def default_instruction_to_svg_dict(self, instruction):
"""Returns an xml-dictionary with the same content as
:meth:`default_instruction_to_svg`
If no file ``default.svg`` was loaded, an empty svg-dict is returned.
"""
instruction_type = instruction.type
default_type = "default"
rep_str = "{instruction.type}"
if default_type not in self._instruction_type_to_file_content:
return {"svg": ""}
default_svg = self._instruction_type_to_file_content[default_type]
default_svg = default_svg.replace(rep_str, instruction_type)
colored_svg = self._set_fills_in_color_layer(default_svg,
instruction.hex_color)
return colored_svg
#: The name of the folder containing the svg files for the default
#: instructions.
DEFAULT_SVG_FOLDER = "instruction-svgs"
[docs]def default_instructions_to_svg():
"""load the default set of svg files for instructions
:return: the default svg files for the instructions in this package
:rtype: knittingpattern.InstructionToSVG.InstructionToSVG
"""
instruction_to_svg = InstructionToSVG()
instruction_to_svg.load.relative_folder(__name__, DEFAULT_SVG_FOLDER)
return instruction_to_svg
__all__ = ["InstructionToSVG", "default_instructions_to_svg",
"DEFAULT_SVG_FOLDER"]