Skip to main content

Tools to help make SVG graphics with Python.

Project description

SVG helpers

Tools to help make SVG graphics with python.

Install

pip install svg-helpers

Getting started

The goal is to be as close as is practical to writing SVG directly in python.

Start with make_svg then add elements with add_element. All elements have the add_element method so they can be nested.

import svg_helpers

width = 150
height = width * 2 / 3
r = height * 3 / 10

svg = svg_helpers.make_svg(width=width, height=height)
group = svg.add_element("g")
group.add_element(
    "rect", width=width, height=height, fill="white", stroke="#eee"
)
group.add_element("circle", cx=width / 2, cy=height / 2, r=r, fill="#bc002d")
svg.save("japan.svg")

Japan flag

Though with something as simple as this, consider just using f-string formatting. It can sometimes be difficult to get the quotes right, though.

print(f"""
<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}">
  <g>
    <rect width="{width}" height="{height}" fill="white" stroke="#eee" />
    <circle cx="{width/2}" cy="{height/2}" r="{r}" fill="#bc002d" />
  </g>
</svg>
""")

Names always match the SVG names. For example, svg rect elements have an x and y property, so use x and y to make a rect. Underscores in attribute names become dashes (stroke_width=1stroke-width="1"), and a trailing underscore is stripped so you can escape Python keywords (class_="hi"class="hi", from_="0"from="0").

For attribute names that aren't valid Python identifiers at all (e.g. xlink:href, inkscape:groupmode), pass them via ** unpack or as an explicit attrib= dict. Both forms produce identical output:

svg.add_element("use", **{"xlink:href": "#circle"})
svg.add_element("use", attrib={"xlink:href": "#circle"})

Adding raw markup

Adding elements from strings can be helpful for text, especially when it has <tspan> elements in it:

from svg_helpers import make_svg

width = 500
height = 100
noun = "banana"

svg = make_svg(width=width, height=height)
svg.add_from_string("""
<style>
  .small {
    font: italic 24px serif;
  }
  .small > tspan {
    font: bold 20px sans-serif;
    fill: red;
  }
</style>
""")
svg.add_element(
    "rect", width=width, height=height, fill="white", stroke="#eee"
)
svg.add_from_string(
    f'<text x="{width / 2}" y="{height / 2}" text-anchor="middle" class="small">'
    f"You are <tspan>not</tspan> a {noun}!"
    "</text>"
)
svg.save("banana.svg")

Banana text example

Shapes from shapely

If you have shapely installed, you can pass any geometry directly to add_shape and it'll be drawn as a <path> (or a group of paths for compound shapes):

import math
import random

import shapely

from svg_helpers import make_svg

random.seed(0)

n = 7
r = 50
r2 = math.pi * r / n
svg = make_svg(width=200, height=200, viewBox="-100 -100 200 200")
circles = [shapely.Point(0, 0).buffer(r)]
for i in range(n):
    angle = 2 * math.pi * (i / n)
    x = r * math.cos(angle)
    y = r * math.sin(angle)
    circles.append(shapely.Point(x, y).buffer(r2 * (0.5 + random.random())))

union = shapely.unary_union(circles)
shape = union.exterior.buffer(10)
svg.add_shape(shape, fill_opacity=0.5, stroke="black", precision=1)
svg.save("cloud.svg")

Cloud from a union of shapely circles

For shapes with many points, pass precision=N to round coordinates and strip trailing zeros:

svg.add_shape(big_polygon, precision=2, fill="red")

Multi-line text

add_text is a convenience for laying out multi-line text using <tspan> children, with vertical_align of "top", "middle", or "bottom":

from svg_helpers import make_svg

svg = make_svg(width=300, height=200)
svg.add_element("rect", width=300, height=200, fill="white", stroke="#eee")
svg.add_text(
    "first line\nsecond line\nthird line",
    x=150,
    y=100,
    vertical_align="middle",
    text_anchor="middle",
    font_family="sans-serif",
    font_size=20,
)
svg.save("text.svg")

Multi-line text example

Animation

SVG supports animation via nested <animate>, <animateMotion>, and <animateTransform> elements. The library doesn't know about any of these — add_element accepts any tag, so it just works:

import svg_helpers

svg = svg_helpers.make_svg(width=500, height=350, viewBox="0 0 500 350")

motion_path_d = (
    "M202.4,58.3c-13.8,0.1-33.3,0.4-44.8,9.2"
    "c-14,10.7-26.2,29.2-31.9,45.6c-7.8,22.2-13.5,48-3.5,70.2"
    "c12.8,28.2,47.1,43.6,68.8,63.6c19.6,18.1,43.4,26.1,69.5,29.4"
    "c21.7,2.7,43.6,3.3,65.4,4.7c19.4,1.3,33.9-7.7,51.2-15.3"
    "c24.4-10.7,38.2-44,40.9-68.9c1.8-16.7,3.4-34.9-10.3-46.5"
    "c-9.5-8-22.6-8.1-33.2-14.1c-13.7-7.7-27.4-17.2-39.7-26.8"
    "c-5.4-4.2-10.4-8.8-15.8-12.9c-4.5-3.5-8.1-8.3-13.2-11"
    "c-6.2-3.3-14.3-5.4-20.9-8.2c-5-2.1-9.5-5.2-14.3-7.6"
    "c-6.5-3.3-12.1-7.4-19.3-8.9c-6-1.2-12.4-1.3-18.6-1.5"
    "C222.5,59,212.5,57.8,202.4,58.3"
)
svg.add_element(
    "path",
    id="motionPath",
    fill="none",
    stroke="black",
    stroke_miterlimit=10,
    d=motion_path_d,
)
circle = svg.add_element("circle", r=10, cx=0, cy=0, fill="tomato")
animate = circle.add_element(
    "animateMotion",
    dur="5s",
    fill="freeze",
    repeatCount="indefinite",
)
animate.add_element("mpath", href="#motionPath")

svg.save("animated.svg")

Circle animated along a curved path

Putting it together

A capstone example, showing how weird SVG can get without the library getting in the way. This one combines <defs>, a <linearGradient>, a multi-primitive <filter> (drop shadow), an exemplar <g> reused via <use>, and an <animateTransform> — about a dozen SVG features the library has no special knowledge of, all working transparently.

import svg_helpers

width = 600
height = 60
n_shapes = 10

svg = svg_helpers.make_svg(width=width, height=height)
defs = svg.add_element("defs")

# Orange to pink gradient
grad = defs.add_element(
    "linearGradient", id="grad", x1="0%", y1="0%", x2="100%", y2="0%"
)
grad.add_element("stop", offset="0%", stop_color="#ff8a3d")
grad.add_element("stop", offset="100%", stop_color="#ff3d96")

# Drop shadow
shadow = defs.add_element(
    "filter", id="shadow", x="-50%", y="-50%", width="200%", height="200%"
)
shadow.add_element(
    "feGaussianBlur", in_="SourceAlpha", stdDeviation=2, result="blur"
)
shadow.add_element("feOffset", in_="blur", dx=2, dy=2, result="offset")
merge = shadow.add_element("feMerge")
merge.add_element("feMergeNode", in_="offset")
merge.add_element("feMergeNode", in_="SourceGraphic")

# Reusable element
exemplar = defs.add_element("g", id="exemplar")
exemplar.add_element(
    "rect",
    x=-12,
    y=-12,
    width=24,
    height=24,
    fill="url(#grad)",
    filter="url(#shadow)",
)
exemplar.add_element(
    "animateTransform",
    attributeName="transform",
    type="rotate",
    from_="0",
    to="360",
    dur="6s",
    repeatCount="indefinite",
)

svg.add_element("rect", fill="#82c8e5", width="600", height=60)

# Use "use" to tile exemplar
for i in range(n_shapes):
    svg.add_element(
        "use",
        href="#exemplar",
        x=30 + i * (width - 60) / (n_shapes - 1),
        y=height / 2,
    )

svg.save("banner.svg")

Banner with gradient, drop-shadow, and use'd rotating squares

More examples

More examples are available in examples/.

Alternatives

This library is really just a few functions that wrap the standard library xml.etree. It doesn't check that what you produce is actually valid SVG.

If you want something more fully-featured:

Goals

  • Friendly syntax that's as close as is practical to writing SVG directly.
  • No dependencies. Can import just about anywhere, and other projects can import it without importing fifteen billion other packages.

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

svg_helpers-0.5.0.tar.gz (87.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

svg_helpers-0.5.0-py3-none-any.whl (10.2 kB view details)

Uploaded Python 3

File details

Details for the file svg_helpers-0.5.0.tar.gz.

File metadata

  • Download URL: svg_helpers-0.5.0.tar.gz
  • Upload date:
  • Size: 87.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for svg_helpers-0.5.0.tar.gz
Algorithm Hash digest
SHA256 e8360139f645114933b9a4bf44bb3d962dcc0fc76c01b2bddae6747295bb33bd
MD5 5bac6d6b4200c2504f832536e7905219
BLAKE2b-256 757dc184081747778fc0b12266693f1fea648acd875ce41ca7ccdf0d60ef3c74

See more details on using hashes here.

File details

Details for the file svg_helpers-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: svg_helpers-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 10.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for svg_helpers-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ec9cacaee34118af7fc83976f00a563e784b09a191a199bf2a57b430b540d0e7
MD5 0c141a1e8066d360ac628c19d996c47f
BLAKE2b-256 5d9fe348868f4053c4a97788e2cf895c8ce9d5b5467c84316d5a660544e41d8c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page