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.