Search code examples
linuxbashsvgyamlstring-interpolation

Replace variables in an SVG document (externally defined in YAML)


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?


Solution

  • 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.