Convert SVG to PNG with CSS3 matrix3d ​​support

I need to convert SVG to PNG from a Python script. There are many tools for this that I tried (I'm on Ubuntu):

  • inkscape

    (Inkscape command line)
  • rsvg-convert

    of librsvg2-bin

  • convert

    (which is ImageMagick)

But none of them support CSS3 transform: matrix3d(...)

. The only software that supports this I've found so far is Firefox / Chrom [ium] / etc, but I don't seem to allow rendering the string to PNG.

Are there any special options that I could pass to one of the above options to get a render with full CSS3 support? Or is there another conversion option that I am not currently aware of?


Edit I tried more tools now, including:
  • wkhtmltoimage

    part wkhtmltopdf
  • nodeshot (with a lot of plumbing to serve the SVG to a local server and then upload the final image)

And probably, although I was not able to verify, because OS X only

  • webkit2png

    (thanks for the suggestion @Mark Setchell)

All of the above doesn't match my requirement because they are based on WebKit and WebKit just doesn't support matrix3d ​​in SVG , although it really works great for regular elements ...

+3


source to share


1 answer


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

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

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

Serve SVG / HTML on local server

First you will need to use SVG as an image in your HTML page. Inline SVG or straight SVG did not work for me. I recommend http.server.BaseHTTPRequestHandler

serving both HTML and simple SVG (which will be requested in the 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 am starting the server as Thread

with deamon=True

, so it will be closed after my script finishes.

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 BaseHTTPRequestHandler

(I assume there is or will be a way to directly access files from SlimerJS, because Firefox can do it with file://

, but I couldn't get it to work. Then this step would become obsolete.)



Edit it with SlimerJS

Now that the SVG is available to the browser, we can call SlimerJS. SlimerJS only accepts JavaScript files as input, so we'll better generate 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: Batching with Promises is much faster compared to running a separate SlimerJS for each SVG we want to render. I personally work with indexed SVGs, modifying as needed.

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 have our script ready, save it to 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 option run_headless

adds the XVFB command for headless transition .

You are done.

It was easy, quick and straightforward, wasn't it?

If you really can't follow the code snippets, have a look at the source code of the project I used for .

0


source







All Articles