Skip to content

geomotif.io.svg

Write a design as SVG, in pure standard library.

SVG is the format that goes everywhere from here: a browser, Illustrator or Inkscape, a laser cutter, a pen plotter's toolchain, and the gallery in these docs. Nothing is imported that is not already installed.

Two things about the output are worth knowing up front.

Y points down. SVG's origin is the top-left corner and y grows downward, which is the opposite of the convention every motif here is written in. So flip_y defaults to true and a design comes out the way you drew it. Turn it off only if you are feeding the result to something that shares SVG's axes already.

The coordinates are transformed, not the canvas. The design is fitted into the canvas before anything is written, rather than being scaled by a viewBox. That way stroke_width means the same thing whatever the design measured -- one unit of the file you are looking at -- and rounding coordinates to precision actually shrinks the file rather than throwing away detail that a later scale would have magnified.

A design carrying styles (:mod:geomotif.core.style) writes its layers as the labeled groups Inkscape and vpype read, and its colors as attributes on the individual elements. A design carrying none writes exactly the file it always did.

Functions:

Name Description
to_svg

Render a design as an SVG document.

save_svg

Write a design to an SVG file and return the path written.

to_svg

to_svg(design: Design, *, width: float | None = None, height: float | None = None, padding: float = 8.0, stroke: str = '#0b0b0b', stroke_width: float = 1.0, fill: str = 'none', background: str | None = None, dot_radius: float | None = None, flip_y: bool = True, precision: int = 3, group_by_path: bool = True, title: str | None = None, units: str = '') -> str

Render a design as an SVG document.

Parameters:

Name Type Description Default
design Design

What to draw. Its strokes become <path> elements and its loose points become <circle> elements.

required
width float

Canvas size in user units. Give both to fit the design into exactly that rectangle; give one and the other follows from the design's own proportions; give neither and the design keeps its own measurements, with padding added around it.

None
height float

Canvas size in user units. Give both to fit the design into exactly that rectangle; give one and the other follows from the design's own proportions; give neither and the design keeps its own measurements, with padding added around it.

None
padding float

Margin reserved on all four sides.

8.0
stroke (str, float, str)

Applied to the group holding the strokes. fill="none" is the default because most of this catalog is line work; name a color to fill the closed paths instead.

'#0b0b0b'
stroke_width (str, float, str)

Applied to the group holding the strokes. fill="none" is the default because most of this catalog is line work; name a color to fill the closed paths instead.

'#0b0b0b'
fill (str, float, str)

Applied to the group holding the strokes. fill="none" is the default because most of this catalog is line work; name a color to fill the closed paths instead.

'#0b0b0b'
background str

Draw a filled rectangle behind everything. Omitted by default, which leaves the canvas transparent.

None
dot_radius float

Radius for the loose points. Defaults to stroke_width, so dots read about as heavy as lines; pass 0 to leave them out entirely.

None
flip_y bool

Mirror vertically, so a design drawn y-up appears the right way up in SVG's y-down space. On by default.

True
precision int

Decimal places for coordinates. Trailing zeros are dropped, so a whole number costs one character rather than five.

3
group_by_path bool

Give every stroke its own <path> element, so an editor treats them as separate objects. Turn it off to merge them into one element with several subpaths, which is smaller but arrives as a single shape.

True
title str

The document's <title>. Defaults to the motif recorded in the design's metadata, which is what makes a gallery file self-labelling.

None
units str

A physical unit for the document's width and height, from :data:UNITS -- "mm", "in", "pt" and the rest. The viewBox stays in plain numbers, so one user unit becomes one of these and the drawing has a real size on paper. Empty by default, which leaves the size in user units and is what anything on a screen wants. See :mod:geomotif.io.plotter.

''

Returns:

Type Description
str

A complete SVG document, ending in a newline.

Raises:

Type Description
ValueError

If the design has no points, padding leaves no room inside the canvas asked for, or units is not one of :data:UNITS.

Source code in src/geomotif/io/svg.py
def to_svg(
    design: Design,
    *,
    width: float | None = None,
    height: float | None = None,
    padding: float = 8.0,
    stroke: str = "#0b0b0b",
    stroke_width: float = 1.0,
    fill: str = "none",
    background: str | None = None,
    dot_radius: float | None = None,
    flip_y: bool = True,
    precision: int = 3,
    group_by_path: bool = True,
    title: str | None = None,
    units: str = "",
) -> str:
    """Render a design as an SVG document.

    Parameters
    ----------
    design : Design
        What to draw. Its strokes become ``<path>`` elements and its loose
        points become ``<circle>`` elements.
    width, height : float, optional
        Canvas size in user units. Give both to fit the design into exactly
        that rectangle; give one and the other follows from the design's own
        proportions; give neither and the design keeps its own measurements,
        with ``padding`` added around it.
    padding : float, optional
        Margin reserved on all four sides.
    stroke, stroke_width, fill : str, float, str, optional
        Applied to the group holding the strokes. ``fill="none"`` is the
        default because most of this catalog is line work; name a color to
        fill the closed paths instead.
    background : str, optional
        Draw a filled rectangle behind everything. Omitted by default, which
        leaves the canvas transparent.
    dot_radius : float, optional
        Radius for the loose points. Defaults to ``stroke_width``, so dots
        read about as heavy as lines; pass ``0`` to leave them out entirely.
    flip_y : bool, optional
        Mirror vertically, so a design drawn y-up appears the right way up in
        SVG's y-down space. On by default.
    precision : int, optional
        Decimal places for coordinates. Trailing zeros are dropped, so a whole
        number costs one character rather than five.
    group_by_path : bool, optional
        Give every stroke its own ``<path>`` element, so an editor treats them
        as separate objects. Turn it off to merge them into one element with
        several subpaths, which is smaller but arrives as a single shape.
    title : str, optional
        The document's ``<title>``. Defaults to the motif recorded in the
        design's metadata, which is what makes a gallery file self-labelling.
    units : str, optional
        A physical unit for the document's ``width`` and ``height``, from
        :data:`UNITS` -- ``"mm"``, ``"in"``, ``"pt"`` and the rest. The
        ``viewBox`` stays in plain numbers, so one user unit becomes one of
        these and the drawing has a real size on paper. Empty by default,
        which leaves the size in user units and is what anything on a screen
        wants. See :mod:`geomotif.io.plotter`.

    Returns
    -------
    str
        A complete SVG document, ending in a newline.

    Raises
    ------
    ValueError
        If the design has no points, ``padding`` leaves no room inside the
        canvas asked for, or ``units`` is not one of :data:`UNITS`.
    """
    if not len(design):
        raise ValueError("cannot write an empty design to SVG: there is nothing to draw")
    if padding < 0:
        raise ValueError(f"padding must be >= 0, got {padding}")
    if precision < 0:
        raise ValueError(f"precision must be >= 0, got {precision}")
    if units not in UNITS:
        raise ValueError(f"units must be one of {UNITS}, got {units!r}")

    canvas_w, canvas_h = _canvas(design, width, height, padding)
    placed = design.fit(canvas_w, canvas_h, padding=padding, flip_y=flip_y)
    radius = stroke_width if dot_radius is None else dot_radius
    layers = by_layer(placed) if layer_names(placed) else {None: placed}

    def num(value: float) -> str:
        return _num(value, precision)

    namespaces = f'xmlns="{SVG_NS}"'
    if any(name is not None for name in layers):
        namespaces += f' xmlns:inkscape="{INKSCAPE_NS}"'
    lines = [
        '<?xml version="1.0" encoding="UTF-8"?>',
        f'<svg {namespaces} width="{num(canvas_w)}{units}" '
        f'height="{num(canvas_h)}{units}" viewBox="0 0 {num(canvas_w)} {num(canvas_h)}">',
    ]

    label = title if title is not None else str(design.meta.get(NAME_KEY, "") or "")
    if label:
        lines.append(f"  <title>{escape(label)}</title>")
    if background is not None:
        lines.append(
            f'  <rect width="{num(canvas_w)}" height="{num(canvas_h)}" '
            f"fill={quoteattr(background)}/>"
        )

    defaults = _Ink(stroke=stroke, stroke_width=stroke_width, fill=fill, radius=radius)
    for name, part in layers.items():
        body = _elements(
            part,
            defaults,
            indent=1 if name is None else 2,
            precision=precision,
            group_by_path=group_by_path,
        )
        if name is None:
            lines.extend(body)
            continue
        # Inkscape's own attributes, which is also what vpype reads a layer
        # from. Anything else opens the file as a plain group and loses only
        # the name.
        lines.append(
            f'  <g inkscape:groupmode="layer" inkscape:label={quoteattr(name)} '
            f"id={quoteattr(name)}>"
        )
        lines.extend(body)
        lines.append("  </g>")

    lines.append("</svg>")
    return "\n".join(lines) + "\n"

save_svg

save_svg(design: Design, path: str | PathLike[str], **kwargs: Any) -> Path

Write a design to an SVG file and return the path written.

Keyword arguments are passed straight through to :func:to_svg.

Source code in src/geomotif/io/svg.py
def save_svg(design: Design, path: str | PathLike[str], **kwargs: Any) -> pathlib.Path:
    """Write a design to an SVG file and return the path written.

    Keyword arguments are passed straight through to :func:`to_svg`.
    """
    target = pathlib.Path(path)
    target.write_text(to_svg(design, **kwargs))
    return target