Resvg - SVG Rendering in Elixir with Rust

Greetings Elixir community!

Today, I’m thrilled to present you with resvg_nif, an open-source project that provides Elixir bindings for the Rust-based SVG rendering library resvg. After initially exploring resvg via a port, I decided to take a plunge into creating native bindings for it.

SVG to PNG conversion:

:ok = Resvg.svg_to_png("input.svg", "output.png")

Resvg also allows direct handling of SVG content as strings:

svg_string = """
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
  <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" />
</svg>
"""
:ok = Resvg.svg_string_to_png(svg_string, "output.png", resources_dir: "/tmp")

To see Resvg in action, you can try the Livebook !

This NIF originated from my endeavor to create a system for generating YouTube thumbnails. After experimenting with various approaches using ImageMagick and libvips, I discovered that utilizing SVGs for conversion yielded the best results, simplifying testing in the process. The impressive support for fonts and SVG images offered by Resvg is strikingly similar to the rendering you get in a browser, making adjustments and testing much easier.

One thumbnail generated from my svg template.
image

24 Likes

Something which is useful for complex drawings is to be able to determine the dimensions of text boxes. Resvg can be used to query the dimensions of SVG elements (in particular text elements). Maybe might be interesting to expose the query functionality? I won’t be usint that for my own projects as ExTypst fits my bill better, but it’s something to think about.

Cool library and great to see some bindings for the current go-to SVG renderer.

I’m curious about what issues you had with libvips (I maintain image which is ultimately built on libvips). I’ve not had any special issues with SVG rendering (although it currently binds with libsvg with plans to move to resvg).

Very cool! Are you planning to add Windows support at some point? Livebook crashes node after installing NIF:

I did not try --query-all before.

$ resvg drawing.svg --query-all
layer1,19.063,40.228,172.675,123.088
text236,19.063,40.228,172.675,20.681
rect398,41.69,93.372,144.021,69.943

It should be simple to expose in a function like the list_fonts/1
Not sure about the api to make and how to format the returned data.

Hello Kip,

Your work with image, and specifically the use of Operation.svgload_buffer(svg), was a significant source of inspiration while I was developing this library.

I encountered several issues with libvips / vix that led me to try alternative:

  1. Windows Support: One of my primary requirements was compatibility with Windows. Unfortunately, as of the time of development, vix did not support it.

  2. Font Loading with librsvg: I was unable to successfully get libvips to load my specific font.

  3. SVG local file resolution: Another challenge I faced pertained to the svg element <image> in librsvg when resolving local files.

I found resvg to be more compatible with my requirements. Resvg allowed you to specify the :resources_dir, facilitating the resolution of images from this path:

path = "/project/assets/"
svg_string = """
  <svg xmlns="http://www.w3.org/2000/svg" width="400" height="200"
    xmlns:xlink="http://www.w3.org/1999/xlink"
  >
    <image xlink:href="mdn-logo.png" width="200" height="200" />
    <image xlink:href="mdn-logo.png" x="200" width="200" height="200" />
  </svg>
"""
Resvg.svg_string_to_png_buffer(svg_string, resources_dir: path)

Moreover, I found that resvg provided results that closely matched those from the browser. This aspect was particularly important to me as I heavily rely on the browser to create the svg templates and making adjustments.

3 Likes

The :rustler_precompiled nif does not work on windows but the self compiled work. I’m new to rustler and friends. I will try to fix it asap.

2 Likes

@MRdotB, Thanks very much for the thoughtful response, really appreciate it. I agree with all three of your challenges with libsvg (under libvips) although I would like to try and debug a little further the font loading issues you had. Are you able to share your .ttf file for the font that wouldn’t load?

On the image loading part, typically Image would encourage composing images to achieve the result, but given your templating approach I can see why you want everything to be in an <svg> tag.

Also great to see more bindings for some good Rust libs too.

I have published resvg version 0.3.0

I added a query_all function which return the pos and size of nodes with id.

Resvg.query_all("rustacean.svg")
[
  %Resvg.Native.Node{
    id: "Layer-1",
    x: -63.99300003051758,
    y: 90.14399719238281,
    width: 1304.344970703125,
    height: 613.6170043945312
  }
]

The precompiled nif is now working on windows system.

4 Likes

Amazing thank you @MRdotB!
The Livebook example needed {:resvg, "~> 0.3"}, to succeed and it got the NIF like charm! Congrats, I’m imagining Windows is not a top priority for the majority of image processors, but you managed to figure it out! Thank you!
:slight_smile:

2 Likes

@MRdotB I wonder if it’s possible to return the PNG directly instead of having to save it into the file? My use case is generating those images “on the fly” and sending them as a response in Phoenix Framework.

Great library, thank you! The only issue I noticed is that the text wasn’t loading on Linux unless I specified the font-family, but that’s related to resvg itself and not the Elixir library.

If for some reason that’s not possible in revsg you can do it in image with:

"<svg> .....</svg>"
|> Image.from_svg!()
|> Image.write(conn)

Unfortunately the underlying vix isn’t supported on Windows so if that’s a requirement then this isn’t the solution for you. Also it uses librsvg as the svg renderer, not libresvg.

2 Likes

Is your input a svg file or a svg as a string ?
If it’s a svg as a string there is svg_string_to_png_buffer/2
I did not implement svg_to_png_buffer but it should be easy.

2 Likes

Unlike what I’ve written above, my data visualization library is currently using Resvg instead of Typst due to serious performance problems when I try to use Typst to render visualizations with > 1000 elements/datapoints.

Currently I’m generating SVG files, and I use Resvg (with the query_all function) to get text dimensions. And also to render SVGs into PNG.

However, one often wants to export SVG as PDF or at least to publish SVG files with embedded fonts so that rendering is reproducible.

I’ve thought of the following:

  1. Would you include functionality to render SVG into PDF using the svg2pdf crate? Or do you think it is out of scope for your library?

  2. Regarding the embedding of fonts… Does Resvg provide a way of querying the SVG file for the fonts that are actually used so that one knows which fonts to embed?

If this is considered out of scope for Resvg, would you like to add the relevant NIF functionality to my Quartz package? This later option would be quite convenient because I might want to add further rust libraries for more calculation-intensive tasks.

1 Like

:wave: Hello thanks for your last contribution,

  1. Resvg does not provide pdf export so this is out of scope.
  2. I did not found a way to access it out of the box in resvg but it’s in the data tree. So doing a filter by text node / uniq by font should work. This feature could be a useful addition to resvg_nif.

I don’t have much time to work on OSS this days I will maintain and review PR but that’s all.

Hey @MRdotB. I recently started playing with rustler and I wanted to check how did you annotate the functions with nif scheduler but It’s missing. Do you think it should include dirty_io when loading a file and dirty_cpu when processing it.

1 Like

In my current use case, the execution of NIFs for creating small images is instantaneous, which is ideal. However, this might pose a problem when rendering large and complex SVG which is more cpu intensive. Flagging them as DirtyCpu is probably what’s needed.
It’s worth noting that this is my first and only experience with Elixir NIFs, so I’m open to further insights :slight_smile:
doc

1 Like

For complex Quartz plots (i.e. > 20,000 data points) rendering to PNG is definitely not instantaneous, but that is admittedly an unusual case…

Would you mind if I copied the resvg_code into my package so that I could tweak it for my use case? I agree with you that Resvg shouldn’t try to be too generic. I promise I won’t bother you with questions regarding the modified code. I understand if you don’t want slightly modified versions of your code in other projects, though.

Sure it’s MIT license you can fork and do whatever :slight_smile: