Skip to content

Instantly share code, notes, and snippets.

@sjlongland
Created July 18, 2024 07:48
Show Gist options
  • Save sjlongland/ead77ab3d05bc65013d9a3c5e5dd36ed to your computer and use it in GitHub Desktop.
Save sjlongland/ead77ab3d05bc65013d9a3c5e5dd36ed to your computer and use it in GitHub Desktop.
SVG Template engine in Python

Proof of concept SVG template engine

This is an attempt to do SSTV images using SVG-based templates. The idea is to be able to create "message images" that just contain arbitrary text, and "reply images" which are used to respond to a transmission.

Usage

Edit the template template.svg to taste, putting in your details for things like the call-sign.

Then, you can generate a reply image with something like the following:

python3 ./template.py \
        --set-value freq_khz "$( echo "12345.678 kHz" | jq -R )" \
        --set-value background "$( echo "/path/to/background/image.png" | jq -R )" \
        --set-value msgheading "$( echo "message title text" | jq -R )" \
        --set-value msgtext "$( echo "message body text" | jq -R )" \
        --set-value report "$( echo "signal report" | jq -R )" \
        --set-value callsign "$( echo "their call" | jq -R )" \
        --set-value name "$( echo "their name" | jq -R )" \
        --set-value replay "$( echo "/path/to/received/image.png" | jq -R )" \
        template.svg \
        output.svg
#!/usr/bin/env python3
from xml.etree import ElementTree
from collections import namedtuple
import argparse
import json
import os
import css_parser
import datetime
# Defaults to bake into the script
defaults = {
"opercall": "N0CALL",
"opername_first": "You",
"opergrid": "XY12ab",
"utcdt_iso": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M"),
}
SVGField = namedtuple("SVGField", ["element", "classes"])
ap = argparse.ArgumentParser()
ap.add_argument("--ignore-class", nargs=1, action='append', default=[],
help="Ignore classes with this name")
ap.add_argument("--set-default", nargs=2, action='append', default=[],
help="Set the default value for the named field")
ap.add_argument("--set-value", nargs=2, action='append', default=[],
help="Set the value for the named field")
ap.add_argument("--default", nargs=1,
help="Default value for unspecified template values")
ap.add_argument("template", help="Template SVG file")
ap.add_argument("output", help="Output SVG file")
ap.add_argument("inputs", nargs="*", help="Input JSON files")
args = ap.parse_args()
ignore_classes = set([x[0] for x in args.ignore_class])
defaults.update((k, json.loads(v)) for k, v in args.set_default)
values = defaults.copy()
for jsonfile in args.inputs:
print("Importing data from %r" % jsonfile)
with open(jsonfile, "r") as f:
values.update(json.loads(f.read()))
print("Setting values from command line")
values.update(dict((k, json.loads(v)) for k, v in args.set_value))
print("Template input:\n%s" % json.dumps(values, indent=4))
print("Importing template %r" % args.template)
svgdoc = ElementTree.parse(args.template)
svgroot = svgdoc.getroot()
if svgroot.tag == "svg":
namespaces = None
else:
assert svgroot.tag.startswith("{") and svgroot.tag.endswith(
"}svg"
), "Root element is not a SVG tag"
# Pick out the namespace URI
namespaces = dict(svg=svgroot.tag[1:-4])
# Figure out what the text and image tags will be called, they'll have
# the same namespace as the `<svg>` tag.
TEXT_TAG = "{%s}text" % namespaces["svg"]
IMAGE_TAG = "{%s}image" % namespaces["svg"]
# Pick up all the style tags to identify classes
template_fields = {}
for elem in svgdoc.iterfind(".//svg:style", namespaces=namespaces):
css = css_parser.parseString(elem.text)
for rule in css.cssRules:
if not rule.selectorText.startswith("."):
continue
fieldname = rule.selectorText[1:]
properties = {}
print("Parsing field %r" % fieldname)
for prop in rule.style.getProperties():
if not prop.name.startswith("-vk4msl-template-"):
continue
p = prop.name[17:]
print(" %r = %r" % (p, prop.value))
properties[p] = json.loads(prop.value)
if properties:
template_fields[rule.selectorText[1:]] = properties
print(template_fields)
# Figure out all classes defined and the elements using them
classnames = {}
for elem in svgdoc.iterfind(".//*[@class]", namespaces=namespaces):
elem_classes = set(
[cls for cls in elem.attrib.get("class", "").split(" ") if cls]
) - ignore_classes
if not elem_classes:
continue
field = SVGField(elem, elem_classes)
for cls in elem_classes:
classnames.setdefault(cls, []).append(field)
print("All field classes: %s" % ", ".join(sorted(classnames.keys())))
# These would be sourced from command line arguments or some file, the
# following shows how to replace the content of text fields.
for cls, fields in classnames.items():
try:
cls_props = template_fields[cls]
except KeyError:
continue
if "format" in cls_props:
value = cls_props["format"] % values
elif cls in values:
value = values[cls]
elif args.default is not None:
value = args.default
elif cls_props.get("required", False) is False:
value = ""
else:
raise KeyError("No value for fields of class %r" % cls)
value = str(value)
for field in fields:
if field.element.tag == TEXT_TAG:
for tspan in field.element.iterfind(
"./svg:tspan", namespaces=namespaces
):
tspan.text = value
elif field.element.tag == IMAGE_TAG:
# We set the href field. This must be a data URI or an absolute
# URI to a file.
if (not value.startswith("data:")) and (value.split(":", 1)[0] not
in ("http", "https",
"ftp", "file")):
# Assume it's the path to a file
# NB: this won't work with Windows file names. I have no
# solution to this.
value = "file://%s" % os.path.realpath(value)
# Iterate over the attributes and look for the href tag, which
# may be prefixed by a namespace. Make a note of the new value.
changes = {}
for attr in field.element.attrib.keys():
if attr == "href" or (
attr.startswith("{") and attr.endswith("}href")
):
changes[attr] = value
if changes:
# Apply the changes
field.element.attrib.update(changes)
print("Writing out %r" % args.output)
svgdoc.write(args.output)
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment