4

First I note that there are many related questions, but after a day of trying pyvip and cairo and the rest none of them work for me, even after installing other software that they seem to depend on. The exception is svglib with reportlab, it comes close but doesn't quite get there! This is the best post I found and may help some.

I have all my source images in SVG files. Most app stores require you to provide a set of PNGs with specific sizes and qualities. So I need to take an SVG and produce a PNG with width w and height h and specific dpi. I want to do this programmatically in python.

I have written a function that almost works, but scaling and dpi interact with each other in weird ways. I use svglib to convert the SVG to a ReportLab drawing then use reportlab to manipulate the drawing. The install went smoothly on Windows unlike some of the other options.

pip install svglib
pip install reportlab

The code is as follows. I inspected the above libraries to get the arguments, but added stuff to get specific size.

from svglib.svglib import svg2rlg
from reportlab.graphics import renderPM

def svg_to_png(in_path,out_path,fmt="PNG",
    scale=None,size=None,scale_x=1,size_x=None,scale_y=1,size_y=None,
    dpi=72, bg=0xffffff):
    # Convert SVG to ReportLab drawing. 
    drawing = svg2rlg(in_path)
    # Work out scale factors
    # Scale over-rides scale_x|y, ditto size
    scale_x = scale if scale else scale_x
    scale_y = scale if scale else scale_y
    size_x = size if size else size_x
    size_y = size if size else size_y
    # Size over-rides scale
    scaling_x = size_x/drawing.width if size_x else scale_x
    scaling_y = size_y/drawing.height if size_y else scale_y
    # Scale the drawing
    drawing.width = drawing.minWidth() * scaling_x
    drawing.height = drawing.height * scaling_y
    drawing.scale(scaling_x, scaling_y)
    # Render ReportLab drawing as a PNG
    renderPM.drawToFile(drawing, out_path, fmt=fmt, dpi=dpi, bg=bg)

if __name__ == '__main__': 
    breakpoint()
    in_path = 'C:\\Users\\...\\A.svg'
    out_path = 'C:\\Users\\...\\A.png'
    svg_to_png(in_path,out_path,scale=2,dpi=72)

The function works for resizing so long as I leave the dpi alone. The dpi=72 seems to be a universal default in reportlab and the source of the issues. I hoped that increasing the dpi would impove the quality of the PNG (reduce pixilation), but all it seems to do is expand the canvas.

The input SVG is of course pixel perfect. Here are four PNGs from the above function.

Scale 1 dpi 72 (dim 115x124 size 8.13kb), conversion with all defaults.

enter image description here

Scale 1 dpi 144 (dim 230x249 size 9.06kb), canvas doubled, pic in bottom-left quadrant, ignore black line bug.

enter image description here

Scale 2 dpi 72 (dim 230x249 size 17.5kb), scaled properly but pixelated (look at the eye)

enter image description here

Scale 2 dpi 144 (dim 461x497 size 19.8kb), canvas quadrupled? picture doubled.

enter image description here

Can someone please advise how to use reportlab properly to resize the picture to given scale or size and within that fixed size, increase the quality via the dpi.

Mark Kortink
  • 1,770
  • 4
  • 21
  • 36

2 Answers2

6

Answering my own question, after lots of code inspections within svglib it appears it is impossible to do resize and increase dpi to predefined values. This is because svglib renders an SVG to a ReportLab drawing without any ability to tell it to render the SVG at the resolution required, which is a shame given the whole point of vector is arbitrary resolution. Once its in ReportLab you are stuck with the resolution of the drawing.

I switched to pyvips which makes use of libvips.

Its a bit fiddly to install, you can't just pip install pyvips. You have to download the libvips package and unzip it on disk somewhere you keep programs. You can then pip install pyvips in the usual way. In your python code you then add the libvips /bin to your path, assuming you don't want to do this permanently from the operating system.

The following function can then be used to convert an SVG to a PNG and set the resolution of the PNG and either scale it or set its horizontal width.

def svg_to_png(svg_path,png_path,dpi=72,scale=1,size=None):
    # Documentation
    # Ref: https://libvips.github.io/libvips/API/current/
    # Ref: https://libvips.github.io/pyvips/
    # Initialise
    debug=False
    import os
    os.environ['path'] += r';C:\programs\vips\vips-dev-8.11\bin'
    import pyvips
    # Get the image
    if size:
        image = pyvips.Image.new_from_file(svg_path,dpi=dpi,scale=1)
        if debug: print({field:image.get(field) for field in image.get_fields()})
        scale = size/image.get('width')
        image = image.resize(scale)
    else:
        image = pyvips.Image.new_from_file(svg_path,dpi=dpi,scale=scale)
    # Write the image
    if debug: print({field:image.get(field) for field in image.get_fields()})
    image.write_to_file(png_path)

The function works properly when scaling. When setting a fixed output size it works well but a little bit of fiddling with the input dpi may be required to get the exact right output dpi.

Mark Kortink
  • 1,770
  • 4
  • 21
  • 36
  • I can't thank you enough for answering your own question! This was just what I needed. – migueldva Feb 16 '22 at 00:38
  • You can use `thumbnail` to load an SVG to a size. This is better than using `resize` after loading, since it'll immediately render the SVG at the correct resolution rather than scaling the pixels. Try: `image = pyvips.Image.thumbnail("something.svg", 1000, height=10000000); image.write_to_file("something.png")` to render the SVG at 1000 pixels across. – jcupitt Oct 23 '22 at 10:25
0

Here's my solution which I also posted to a relevant Github Issue. This uses pymupdf to convert the intermediary PDF generated with svglib and reportlab to an SVG.

The advantage of this solution is that it doesn't need any fiddling with external dependencies like poppler, cairo or libvips, as pymupdf has prebuild wheels for Linux, Windows and MacOS.

Another advantage is the support for a transparent background.

import fitz
from svglib import svglib
from reportlab.graphics import renderPDF

# Convert svg to pdf in memory with svglib+reportlab
# directly rendering to png does not support transparency nor scaling
drawing = svglib.svg2rlg(path="input.svg")
pdf = renderPDF.drawToString(drawing)

# Open pdf with fitz (pyMuPdf) to convert to PNG
doc = fitz.Document(stream=pdf)
pix = doc.load_page(0).get_pixmap(alpha=True, dpi=300)
pix.save("output.png")

Cheers!

M4a1x
  • 114
  • 1
  • 8
  • I just checked your code and it seems it doesn't work, maybe the functions need to be called with different arguments? I used the following [file](https://pl.wikipedia.org/wiki/Scalable_Vector_Graphics#/media/Plik:SVG_logo.svg) for conversion. I messed with opacity of various SVG elements and this opacity seems to be ignored. – Roman Koziolek Jul 04 '23 at 15:43
  • I ran the code again and it seems to work fine on my end. What do you mean by "ignored"? Is it fully white or black? I noticed that when opening the file with chrome (which is my default for some reason) it ignores opacity, but it looks fine in e.g. GIMP – M4a1x Jul 18 '23 at 00:12
  • On a different note: I recently used [resvg](https://github.com/RazrFalcon/resvg) for that task, which unfortunately doesn' t have python bindings (yet?). But can be very easily called directly from python with a simple `subprocess.run()`. – M4a1x Jul 18 '23 at 00:18