3

I need to convert SVGs to PNGs from a Python script. There are plenty of tools for this, the ones I tried (I'm on Ubuntu):

  • inkscape (Inkscape command line)
  • rsvg-convert from librsvg2-bin
  • convert (which is ImageMagick)

But none of these support the CSS3 transform: matrix3d(...). The only software that supports this I have found so far is Firefox/Chrom[ium]/etc, but I they don't seem to allow command line rendering to PNG.

Are there any special options I could pass to one of the options above to get rendering with full CSS3 support? Or is there yet another converting option I am currently unaware of?


Edit I have tried more tools now, including:
  • wkhtmltoimage part of wkhtmltopdf
  • nodeshot (with a lot of plumbing to serve the SVG on a local server and then download the final image)

And probably, although I couldn't test because it's OS X only

  • webkit2png (thanks for the suggestion, @Mark Setchell)

All of the above do not meet my requirement, because they are WebKit based and WebKit just doesn't support matrix3d in SVG although it does apply it perfectly fine to regular elements...

Community
  • 1
  • 1
neo post modern
  • 2,262
  • 18
  • 30
  • Just an idea, hence a comment rather than an answer, but you may find that if you put the file up under a local web server, you can render it correctly with `webkit2png`. Untested! – Mark Setchell Jul 05 '15 at 08:43
  • Great suggestion! Unfortunately it's [OS X only](https://github.com/paulhammond/webkit2png/issues/55). But it made me think of WebKit based approaches, so I ran into [nodeshot](https://github.com/FWeinb/nodeshot) - which is currently [broke](https://github.com/FWeinb/nodeshot/issues/7). I'll post an update here if/when it gets fixed. – neo post modern Jul 06 '15 at 11:09
  • `webkit2png` is available on OS X - and easily installed using `homebrew`... like this `brew install webkit2png` – Mark Setchell Jul 06 '15 at 11:15
  • No, as pointed out in the question I am _not_ on OS X. My newest guess is [wkhtmltopdf](http://wkhtmltopdf.org/index.html) which, despite the name, exports to PNG with `wkhtmltoimage`. The results are as good as opening the SVG in Chromium directly, but still wrong. – neo post modern Jul 06 '15 at 11:44

1 Answers1

0

Hooray, I found a solution - but it has a huge overhead and requires tons of things:

The basic idea is to serve your SVG as a website and then render it with SlimerJS, a scriptable version of Firefox. Unlike the other approaches mentioned, this uses Gecko to render the SVG and as mentioned above, Gecko (unlike WebKit) renders CSS3 3D rotations in SVG correctly.

You probably want to use xvfb too, so you don't have to see the SlimerJS window while rendering (it doesn't support headless by itself yet).

Serve SVG/HTML on a local server

First you need to serve your SVG as an image in an HTML page. Inline SVG or the straightup SVG did not work for me. I recommend http.server.BaseHTTPRequestHandler, serving both the HTML and the plain SVG (which will be asked for in a second request).

html = """
<!DOCTYPE HTML>
<html>
    <head>
        <style>
            body {
                margin: 0;
            }
        </style>
    </head>
    <body>
        <img src="http://localhost:8000/svg/%s" />
    </body>
</html>
""" % svg_name

margin: 0; removes the default space around any website.

I start the server as a Thread with deamon=True so it will be shut down once my script completed.

class SvgServer:
    def __init__(self):
        self.server = http.server.HTTPServer(('', PORT), SvgRequestHandler)
        self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True).start()

SvgRequestHandler should be your instance of the BaseHTTPRequestHandler

(I assume there is - or will be - a way to directly access files from SlimerJS because Firefox can do this with file:// but I couldn't get it to work. Then this step would become obsolete.)

Render it with SlimerJS

Now that the SVG is accessible for a browser we can call SlimerJS. SlimerJS only accepts JavaScript files as input, so we better generate some JavaScript:

slimer_commands = """
var webpage = require('webpage').create();
webpage
  .open('%s')
  .then(function () {
    webpage.viewportSize = { width: 1920, height: 1080 };
    webpage.render('%s', { onlyViewport: true });
    slimer.exit()
  });
""" % (url_for_html_embedding_svg, output_file_name)

Bonus: Batch processing with Promises, this is much faster compared to launching an individual SlimerJS for each SVG we want to render. I personally work with indexed SVGs, change as you need.

slimer_command_head = "const { defer } = require('sdk/core/promise');" \
                      "var webpage = require('webpage').create();" \
                      "webpage.viewportSize = { width: 1920, height: 1080 };" \
                      "var deferred = defer();" \
                      "deferred.resolve();" \
                      "deferred.promise.then(function () {"

commands = [slimer_command_head]

for frame_index in range(frame_count):
    command = "return webpage.open('%s'); }).then(function () { webpage.render('%s', { onlyViewport: true });" % (
        'http://localhost:8000/html/%d' % frame_index,
        FileManagement.png_file_path_for_frame(frame_index)
    )

    commands.append(command)

commands.append("slimer.exit(); });")

slimer_commands = ''.join(commands)

Now that we've got our script ready, save it to a tempfile and execute it:

with tempfile.NamedTemporaryFile(suffix='.js') as slimer_file:
    slimer_file.write(bytes(slimer_commands, 'UTF-8'))
    slimer_file.flush()

    command = [
        SLIMER_EXECUTABLE,
        os.path.abspath(slimer_file.name)
    ]

    if run_headless:
        command.insert(0, 'xvfb-run')

    os.system(' '.join(command))

The run_headless option prepends the XVFB command to go headless.

You're done.

That was easy, fast and straight forward, wasn't it?

If you couldn't really follow the code snippets, check out the source code of the project I used it for.

Community
  • 1
  • 1
neo post modern
  • 2,262
  • 18
  • 30
  • And please, please, please be aware of the security implications of this -- if your SVGs come from untrusted sources, this might be really dangerous to do. – Marcus Müller Aug 05 '15 at 17:44
  • Could you outline the specific harm you see there? But as the example shows, all SVG comes from localhost. – neo post modern Aug 05 '15 at 17:58
  • The point is that e.g. Javascript served from localhost might have higher privileges than javascript served from elsewhere; also, across multiple simultaneous SVG-servings from the same domain (in your case, localhost), less strict cross-scripting attack mitigation rules apply. It's a bit of a theoretical risk, but if someone was to embed stuff in your SVG, make sure that stuff can't do harmful things, like reading other SVGs from your machine and rendering them into the same PNG! – Marcus Müller Aug 05 '15 at 18:02