Finally, I've found a satisfactory way of generating decent SVGs from ODG files. By ‘decent’, I mean that:
- They scale. The
width
andheight
attributes must be removed, or contain only percentages. Leaving these in with any other units (or no units) means you've got a VG, not an SVG!
- They are properly cropped. Export from ODG using unoconv places the graphic on a canvas as big as the page it happened to be created on.
- They have no background, unless that was explicitly defined in the source.
By ‘satisfactory’, I mean that you can automate the process in a headless environment.
Previously, I used unoconv to export to PDF, pdfcrop to crop to the area it actually uses, then pdf2svg to make an SVG from it, and finally xsltproc to strip its width, height and background. However, this stopped working for me recently, as the libraries that pdf2svg uses changed, so the background element could not so easily be found with xsltproc.
So here's the new way:
- Export with unoconv directly to SVG. It will still have the wrong viewbox, and an absolute size, but there will be no background, so no need to do anything special to remove it.
- Use xsltproc stripSize.xsl to remove the absolute size. This step also makes sure that the next step generates dimensions in the correct units for a viewbox.
- Use inkscape --query-all to get the desired viewbox. Only the first line of the output is required.
- Compute the new width and height as percentages. At least one will be 100%.
- Use xsltproc replaceViewBox.xsl to replace the current viewbox with the desired one, and add the width and height back in as percentages.
Here's a series of commands to try:
unoconv -f svg -o stage1.svg input.odg xsltproc --novalid stripSize.xsl stage1.svg > stage2.svg viewbox="$(inkscape -z --query-all stage2.svg | head -1 | \ awk -e 'BEGIN { FS="," } { print $2 " " $3 " " $4 " " $5 }')" vbp=($viewbox) if (( $( echo "${vbp[3]} > ${vbp[2]}" | bc -lq) )) ; then height="100%" width=$(echo "100 * ${vbp[2]} / ${vbp[3]}" | bc -lq) width="$(printf '%.4g%%\n' "$width")" else width="100%" height=$(echo "100 * ${vbp[3]} / ${vbp[2]}" | bc -lq) height="$(printf '%.4g%%\n' "$height")" fi xsltproc --novalid -param viewbox "'$viewbox'" \ -param width "'$width'" \ -param height "'$height'" \ replaceViewBox.xsl stage2.svg > output.svg
This use of inkscape does not start a GUI, so you can use it in headless environments. It would be nice if this could all fit into a single pipe, but I don't think there's a way, as stage2.svg is used twice. Is there a way to use process substitution to substitute the same file twice?
The two XSLT files are here:
<!-- stripSize.xsl --> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:svg="http://www.w3.org/2000/svg"> <xsl:output omit-xml-declaration="no"/> <xsl:template match="node()|@*"> <xsl:copy> <xsl:apply-templates select="node()|@*" /> </xsl:copy> </xsl:template> <xsl:template match="/svg:svg/@width" /> <xsl:template match="/svg:svg/@height" /> </xsl:stylesheet>
<!-- replaceViewBox.xsl --> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:svg="http://www.w3.org/2000/svg"> <xsl:output omit-xml-declaration="no"/> <xsl:template match="node()|@*"> <xsl:copy> <xsl:apply-templates select="node()|@*" /> </xsl:copy> </xsl:template> <xsl:template match="/svg:svg/@viewBox"> <xsl:attribute name="viewBox"> <xsl:value-of select="$viewbox" /> </xsl:attribute> <xsl:attribute name="width"> <xsl:value-of select="$width" /> </xsl:attribute> <xsl:attribute name="height"> <xsl:value-of select="$height" /> </xsl:attribute> </xsl:template> </xsl:stylesheet>
This feature has been added to Mokvino Web. However, conversion from PDF to SVG is now disabled to ensure that the new conversion path is chosen.
Update 2019-02-15: I added the width
and height
attributes back in, but only with computed percentages. This prevents some software balking at size-less SVGs, without turning scalability back off.