3

Background

A few resources discuss using variables inside SVG documents, including:

While CSS-, JavaScript-, and HTML-based solutions are great for the Web, there are other occasions where SVG is useful and it would be equally handy to have the ability to define external sources for variables.

Problem

SVG does not provide a mechanism to define reusable text that SVG-related software packages (such as Inkscape and rsvg-convert) can reuse. For example, the following would be superb:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg ...>
<input href="definitions.svg" />
...
<text ...>${variableName}</text>
</svg>

The image element can be overloaded to import an external file, but it is hackish and doesn't allow assigning text values to variable names for reuse.

Question

How would you read variable names and values from an external file on the server (e.g., a YAML file, but could be a database) and replace those variables in an SVG file prior to rendering?

dreftymac
  • 31,404
  • 26
  • 119
  • 182
Dave Jarvis
  • 30,436
  • 41
  • 178
  • 315
  • The proper XML solution is to use XSLT to perform the substitution. Creating an XSLT template from YAML sounds a bit daunting, but then at least you should have all weird corner cases covered. – tripleee Aug 31 '17 at 04:25
  • Translating XML to YAML using XSLT is [straightforward](http://yaml.org/xml/xml2yaml.xsl). Transcoding YAML to an XML dictionary via XSLT that can be applied to text nodes and attribute values would take a lot of effort. Even [expanding](https://github.com/FasterXML/jackson) the [tool chain](https://www.npmjs.com/package/data-convert) to convert from YAML to XML entails an extra data conversion step that's avoided using simple string substitution. If there's a solution to read YAML into XSLT that wouldn't take hours to implement, do tell. – Dave Jarvis Aug 31 '17 at 07:44

3 Answers3

3

Another possible solution:

Set Object Properties in Inkscape

enter image description here

save it, we will have something like

...
<text
   xml:space="preserve"
   style="font-style:normal;font-weight:normal;font-size:60px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;image-rendering:auto"
   x="262.91638"
   y="86.339157"
   id="mytest"
   sodipodi:linespacing="125%"
   inkscape:label="#myvar"><desc
     id="desc4150">The test object to replace with a var</desc><title
     id="title4148">myobj</title><tspan
     sodipodi:role="line"
     id="tspan4804"
     x="262.91638"
     y="86.339157"
     style="fill:#ffffff">sample</tspan></text>
...

then create the yaml file with the key value pairs

myvar: hello world

and parse the SVG and replace the values

#! /usr/bin/env python

import sys
from xml.dom import minidom
import yaml

yvars = yaml.load(file('drawing.yaml', 'r'))
xmldoc = minidom.parse('drawing.svg')
for s in xmldoc.getElementsByTagName('text'):
    for c in s.getElementsByTagName('tspan'):
        c.firstChild.replaceWholeText(yvars[s.attributes['inkscape:label'].value[1:]])
print xmldoc.toxml()

and the values will be replaced

<text id="mytest" inkscape:label="#myvar" sodipodi:linespacing="125%" style="font-style:normal;font-weight:normal;font-size:60px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;image-rendering:auto" x="262.91638" xml:space="preserve" y="86.339157"><desc id="desc4150">The test object to replace with a var</desc><title id="title4148">myobj</title>
    <tspan id="tspan4804" sodipodi:role="line" style="fill:#ffffff" x="262.91638" y="86.339157">hello world</tspan></text>
Diego Torres Milano
  • 65,697
  • 9
  • 111
  • 134
  • Thanks for this! Would this work for all types of text nodes (assuming other types exist)? The question, as posed, is about changing text, but the topic is on variables, which implies the ability to replace variables in other parts of the SVG file (e.g., colours, sizes, matrix transformation arguments, Bezier points, and such). If the text is not inside a `tspan`, for example, the Python code would have to be modified. It'd be nice to have a general purpose solution. – Dave Jarvis Aug 31 '17 at 07:49
  • 1
    This is just an example if you need to change other tags or attributes extend the script. – Diego Torres Milano Aug 31 '17 at 14:14
3

One approach I have seen is using Jinja template to customize a Postscript file before converting it to PDF.

You can use the same method.

Put your SVG text file as Jinja template, and your variable in YAML.

Use Python to load the Jinja template, then applying the variable found in the YAML file

dreftymac
  • 31,404
  • 26
  • 119
  • 182
Sharuzzaman Ahmat Raslan
  • 1,557
  • 2
  • 22
  • 34
  • +1 for this solution if you want the SVG files to be maintainable and preserve the clarity of the SVG syntax. – dreftymac Nov 30 '17 at 16:12
2

One possible solution uses the following:

The following script:

  1. Reads variable definitions in YAML format.
  2. Loops over all files in the current directory.
  3. Detects whether a file has any variables defined.
  4. Substitutes values for all variable definitions.
  5. Runs Inkscape to convert the SVG file to a PDF.

There are a number of improvements that can be made, but for anyone looking to perform basic variable substitution within SVG documents using YAML with minimal dependencies, this ought to be a good start.

No sanitation is performed, so ensure inputs are clean prior to running this script.

#!/bin/bash

COMMAND="inkscape -z"

DEFINITIONS=../variables.yaml

# Parses YAML files.
#
# Courtesy of https://stackoverflow.com/a/21189044/59087
function parse_yaml {
   local prefix=$2
   local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
   sed -ne "s|^\($s\):|\1|" \
        -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
        -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p"  $1 |
   awk -F$fs '{
      indent = length($1)/2;
      vname[indent] = $2;
      for (i in vname) {if (i > indent) {delete vname[i]}}
      if (length($3) > 0) {
         vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
         printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3);
      }
   }'
}

# Load variable definitions into this environment.
eval $(parse_yaml $DEFINITIONS )

for i in *.svg; do
  INPUT=$i
  OUTPUT=$i

  # Replace strings in the file with values from the variable definitions.
  REPLACE_INPUT=tmp-$INPUT

  echo "Converting $INPUT..."

  # Subsitute if there's at least one match.
  if grep -q -o -m 1 -h  \${.*} $INPUT; then
    cp $INPUT $REPLACE_INPUT

    # Loop over all the definitions in the file.
    for svgVar in $(grep -oh \${.*} $INPUT); do
      # Strip off ${} to get the variable name and then the value.
      varName=${svgVar:2:-1}
      varValue=${!varName}

      # Substitute the variable name for its value.
      rpl -fi "$svgVar" "$varValue" $REPLACE_INPUT > /dev/null 2>&1
    done

    INPUT=$REPLACE_INPUT
  fi

  $COMMAND $INPUT -A m_k_i_v_$OUTPUT.pdf
  rm -f $REPLACE_INPUT
done

By performing a general search and replace on the SVG document, no maintenance is required on the script. Additionally, the variables can be defined anywhere in the file, not only within text blocks.

Dave Jarvis
  • 30,436
  • 41
  • 178
  • 315