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

Background
A few resources discuss using variables inside SVG documents, including:
Variables in SVG: Is it possible?
SVG variable text
How do I define or reference a variable in SVG?
SVG: About using <defs> and <use> with variable text values
http://www.schepers.cc/w3c/svg/params/ref.html
https://www.w3.org/TR/2009/WD-SVGParamPrimer-20090430/#Introduction
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?

Another possible solution:
Set Object Properties in Inkscape
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>

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

One possible solution uses the following:
How can I parse a YAML file from a Linux shell script? (see the bash script)
rpl package (available on most distros for string replacement without regex)
bash
grep, sed, and awk
The following script:
Reads variable definitions in YAML format.
Loops over all files in the current directory.
Detects whether a file has any variables defined.
Substitutes values for all variable definitions.
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.

Related

How to output bash multiline EVAL statement to temp file [duplicate]

This question already has answers here:
How to substitute shell variables in complex text files
(12 answers)
Closed 3 years ago.
Trying to variable replace a templated yaml file.
I'm using eval to take the environment shell variables and replace whats in the file dynamically. I can't figure out how to take the output of this and save to a file.
I just want to take the evaluated output and save to a file.
eval "cat <<EOF
$(<${baseFileName})
EOF"
Exmaple test.yaml
---
value: ${PORT}
Bash environment variable:
PORT=8888
output temp.test.yaml
---
value: 8888
Right now the code will just print the evaluated text to the console.
I've tried.
eval "cat <<EOF
$(<${baseFileName})
EOF" > $newBaseFileName
but no joy. Didn't even create the file.
The reason I'm not using sed is because the file could have unlimited variable decelerations, and I want to replace any value matching a defined bash variable or environment variable. This is part of a template engine. For the life of me I can't remember how I did it before with pure bash.
It didn't work for me but what I did is this
renderTemplate() {
eval "cat <<EOF
$(<${1})
EOF"
}
baseFileName=$(basename $fileName)
templateOutput=`renderTemplate ${baseFileName}`
echo "${templateOutput}"
I'm using this as a temp file anyways so what I'll do is save to variable and then pump that variable in to the command to apply the template as a file. That way it's only ever stored in memory. This is a middleware cli to another cli to add variable replacement to in-memory web hosted files before applying them.
Thanks for your help.

remove an item from a list/variable - Bash

If I call a variable as a list of files:
files=$(ls *.txt)
I then want to remove the first item (file) in the list
Thanks
Clive
You should never parse the output of ls; see http://mywiki.wooledge.org/ParsingLs.
Fortunately, in your case you're not actually using any of ls's functionality; Bash already handles the *.txt part, so the ls is pretty redundant.
You can write this:
# Set files to an array of the files with names ending in '*.txt':
files=(*.txt)
# Set files to an array consisting of ${files[1]}, ${files[2]}, ...:
files=("${files[#]:1}")
(See the Bash Reference Manual, ยง 3.5.3 "Shell Parameter Expansion"; search for ${parameter:offset}.)

bulk conversion of textures, using DirectXTex Texconv, in the command line

Im able to use the following command - as shown in the documentation - to convert textures individually
texconv -pow2 -f BC1_UNORM cat.jpg
However I'd like to convert a whole folder full of textures. Following this advice I've tried using a wild card and the file directory:
texconv.exe -pow2 -f BC1_UNORM somepath\*.jpg
but the command prompt says
reading somepath\*.jpg "FAILED" <8007007b>
Given that I have never used texconv, this may not be the most efficient way of doing things, but it should work.
#!/bin/bash
# Set IFS to newline
SAVEIFS=$IFS
IFS='
'
for i in $(echo *.jpg); do
texconv -pow2 -f BC1_UNORM $i
done
# Restore IFS
IFS=$SAVEIFS
The SAVEIFS and IFS variables are related to bash's internal field separator.
In the code above, I'm replacing the default IFS to contain only \n(newline)
The loop goes through each file matching the pattern *.jpg and feeds that as input to texconv

Obtaining file names from directory in Bash

I am trying to create a zsh script to test my project. The teacher supplied us with some input files and expected output files. I need to diff the output files from myExecutable with the expected output files.
Question: Does $iF contain a string in the following code or some kind of bash reference to the file?
#!/bin/bash
inputFiles=~/project/tests/input/*
outputFiles=~/project/tests/output
for iF in $inputFiles
do
./myExecutable $iF > $outputFiles/$iF.out
done
Note:
Any tips in fulfilling my objectives would be nice. I am new to shell scripting and I am using the following websites to quickly write the script (since I have to focus on the project development and not wasting time on extra stuff):
Grammar for bash language
Begginer guide for bash
As your code is, $iF contains full path of file as a string.
N.B: Don't use for iF in $inputFiles
use for iF in ~/project/tests/input/* instead. Otherwise your code will fail if path contains spaces or newlines.
If you need to diff the files you can do another for loop on your output files. Grab just the file name with the basename command and then put that all together in a diff and output to a ".diff" file using the ">" operator to redirect standard out.
Then diff each one with the expected file, something like:
expectedOutput=~/<some path here>
diffFiles=~/<some path>
for oF in ~/project/tests/output/* ; do
file=`basename ${oF}`
diff $oF "${expectedOutput}/${file}" > "${diffFiles}/${file}.diff"
done

Automatizing 'simplify path' for a svg-file (using inkscape)

I would like to automatize the inkscape command "simplify path". Concretely, I would like a command line tool which takes a svg-file as input, applies "simplify path" to all paths in the figure and saves a new (smaller) svg-file. Is this possible using inkscape? Is there a free command line tool (I'm using linux) which does the job?
UPDATE:
Since the question/answer is quite old the inkscape command line changed.
inkscape file.svg --batch-process --actions='EditSelectAll;SelectionSimplify;FileSave;FileClose'
Also see comment of Oren Ben-Kiki or Pix answer.
ORIG:
Should be possible:
http://tavmjong.free.fr/INKSCAPE/MANUAL/html/CommandLine.html
shows how to call functions of inkscape (called "verbs") from the command line. To get a list of all verbs call inkscape --verb-list on commandline. What you are looking for is SelectionSimplify.
Therefore you have to write a small script that is filtering every id out of the svg and is calling inkscape with the ids. Something like this (optimize all pathes and quit from inkscape at the end)
inkscape filename.svg --verb=EditSelectAll --verb=SelectionSimplify --verb=FileSave --verb=FileClose --verb=FileQuit
Extending from Fabian's answer, to control the threshold of the simplification function, I found I needed to make a fake home directory with a minimal preferences file containing my desired threshold. Here is a simple script I just put together.
simplify.sh:
#!/bin/bash
FILENAME=$1
THRESHOLD=$2
FAKEHOME=$(mktemp -d)
mkdir -p $FAKEHOME/.config/inkscape
cat > $FAKEHOME/.config/inkscape/preferences.xml <<EOF
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<inkscape
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1">
<group
id="options">
<group
id="simplifythreshold"
value="${THRESHOLD}" />
</group>
</inkscape>
EOF
# for Inkscape < 1.0
#HOME=$FAKEHOME inkscape $FILENAME --verb=EditSelectAll --verb=SelectionSimplify --verb=FileSave --verb=FileClose
# for Inkscape > 1.0
HOME=$FAKEHOME inkscape --with-gui --batch-process $FILENAME --verb='EditSelectAll;SelectionSimplify;FileSave'
#rm -rf $FAKEHOME
Alternative to Inkscape
I've got much better results using SVGO (reduced a file from 2.7 MB to 350 KB).
You may use this online service for individual files: https://jakearchibald.github.io/svgomg/

Resources