fossasia/knittingpattern

View on GitHub
knittingpattern/convert/SVGBuilder.py

Summary

Maintainability
A
2 hrs
Test Coverage
A
96%
"""build SVG files


"""
import xmltodict

#: an empty svg file as a basis
SVG_FILE = """
<svg
   xmlns:ns="http://PURL.org/dc/elements/1.1/"
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>knittingpattern</title>
    <defs></defs>
</svg>
"""


class SVGBuilder(object):
    """This class builds an SVG to a file.

    The class itself does not know what the objects look like.
    It offers a more convinient interface to build SVG files.
    """

    def __init__(self):
        """Initialize this object without arguments."""
        self._structure = xmltodict.parse(SVG_FILE)
        self._layer_id_to_layer = {}
        self._svg = self._structure["svg"]
        self._min_x = None
        self._min_y = None
        self._max_x = None
        self._max_y = None

    @property
    def bounding_box(self):
        """the bounding box of this SVG
        ``(min_x, min_y, max_x, max_y)``.

        .. code:: python

            svg_builder10x10.bounding_box = (0, 0, 10, 10)
            assert svg_builder10x10.bounding_box == (0, 0, 10, 10)

        ``viewBox``, ``width`` and ``height`` are computed from this.

        If the bounding box was never set, the result is a tuple of four
        :obj:`None`.
        """
        return (self._min_x, self._min_y, self._max_x, self._max_y)

    @bounding_box.setter
    def bounding_box(self, bbox):
        min_x, min_y, max_x, max_y = bbox
        self._min_x = min_x
        self._min_y = min_y
        self._max_x = max_x
        self._max_y = max_y
        self._svg["@height"] = str(max_y - min_y)
        self._svg["@width"] = str(max_x - min_x)
        self._svg["@viewBox"] = "{} {} {} {}".format(min_x, min_y,
                                                     max_x, max_y)

    def place(self, x, y, svg, layer_id):
        """Place the :paramref:`svg` content at ``(x, y)`` position
        in the SVG, in a layer with the id :paramref:`layer_id`.

        :param float x: the x position of the svg
        :param float y: the y position of the svg
        :param str svg: the SVG to place at ``(x, y)``
        :param str layer_id: the id of the layer that this
          :paramref:`svg` should be placed inside

        """
        content = xmltodict.parse(svg)
        self.place_svg_dict(x, y, content, layer_id)

    def place_svg_dict(self, x, y, svg_dict, layer_id, group=None):
        """Same as :meth:`place` but with a dictionary as :paramref:`svg_dict`.

        :param dict svg_dict: a dictionary returned by `xmltodict.parse()
          <https://github.com/martinblech/xmltodict>`__
        :param dict group: a dictionary of values to add to the group the
          :paramref:`svg_dict` will be added to or :obj:`None` if nothing
          should be added
        """
        if group is None:
            group = {}
        group_ = {
            "@transform": "translate({},{})".format(x, y),
            "g": list(svg_dict.values())
        }
        group_.update(group)
        layer = self._get_layer(layer_id)
        layer["g"].append(group_)

    def place_svg_use_coords(self, x, y, symbol_id, layer_id, group=None):
        """Similar to :meth:`place` but with an id as :paramref:`symbol_id`.

        :param str symbol_id: an id which identifies an svg object defined in
          the defs
        :param dict group: a dictionary of values to add to the group the
          use statement will be added to or :obj:`None` if nothing
          should be added
        """
        if group is None:
            group = {}
        use = {"@x": x, "@y": y, "@xlink:href": "#{}".format(symbol_id)}
        group_ = {"use": use}
        group_.update(group)
        layer = self._get_layer(layer_id)
        layer["g"].append(group_)

    def place_svg_use(self, symbol_id, layer_id, group=None):
        """Same as :meth:`place_svg_use_coords`.

        With implicit `x`  and `y` which are set to `0` in this method and then
        :meth:`place_svg_use_coords` is called.
        """
        self.place_svg_use_coords(0, 0, symbol_id, layer_id, group)

    def _get_layer(self, layer_id):
        """
        :return: the layer with the :paramref:`layer_id`. If the layer
          does not exist, it is created.
        :param str layer_id: the id of the layer
        """
        if layer_id not in self._layer_id_to_layer:
            self._svg.setdefault("g", [])
            layer = {
                "g": [],
                "@inkscape:label": layer_id,
                "@id": layer_id,
                "@inkscape:groupmode": "layer",
                "@class": "row"
            }
            self._layer_id_to_layer[layer_id] = layer
            self._svg["g"].append(layer)
        return self._layer_id_to_layer[layer_id]

    def insert_defs(self, defs):
        """Adds the defs to the SVG structure.

        :param defs: a list of SVG dictionaries, which contain the defs,
          which should be added to the SVG structure.
        """
        if self._svg["defs"] is None:
            self._svg["defs"] = {}
        for def_ in defs:
            for key, value in def_.items():
                if key.startswith("@"):
                    continue
                if key not in self._svg["defs"]:
                    self._svg["defs"][key] = []
                if not isinstance(value, list):
                    value = [value]
                self._svg["defs"][key].extend(value)

    def get_svg_dict(self):
        """Return the SVG structure generated."""
        return self._structure

    def write_to_file(self, file):
        """Writes the current SVG to the :paramref:`file`.

        :param file: a file-like object
        """
        xmltodict.unparse(self._structure, file, pretty=True)


__all__ = ["SVGBuilder", "SVG_FILE"]