Sharp: SVG to PNG conversion produces pixelated output with height and width values

Created on 19 Oct 2018  路  14Comments  路  Source: lovell/sharp

When resizing svg files and converting them to png, the output is really pixelated if

  • the svg file has a height and a width specified in pixels and
  • the size it should be resized to is larger than these values.

I would expect svgs to be resized properly, no matter if a height and a width are specified.

Sample project

https://github.com/nikwen/sharp-demo/tree/svg-resize-height-width (make sure you are on the svg-resize-height-width branch)

Sample image conversion

Source (32x32px): https://github.com/nikwen/sharp-demo/blob/svg-resize-height-width/image.svg
Result (512x512px): https://github.com/nikwen/sharp-demo/blob/svg-resize-height-width/out.png

question

Most helpful comment

Hello, did you see the density property to set the DPI at which to render vector images?

- sharp('./image.svg')
+ sharp('./image.svg', { density: 300 })

All 14 comments

Hello, did you see the density property to set the DPI at which to render vector images?

- sharp('./image.svg')
+ sharp('./image.svg', { density: 300 })

Thank you, Lovell! Setting a density makes the output file look a lot better. For the demo, however, 300 is by far not enough and I need to increase it to 800 or something in that range to make it somewhat sharp. I attached an image of how it looks like with a density of 300.

So to make sure that my app works fine with all SVGs, I need to set a high enough density. Therefore, I need to set it to the max value: 2400.

Please have a look at the sample project again, where I set things to max density. I am still able to find a sample image that produces a pixelated result.

Now, when I remove the width and height properties of the SVG, I get a perfectly fine result. With them, stuff is pixelated.

In this case, one can argue that setting such properties (especially to such low values) is bad practice and I'd agree. However, I just recently received an SVG with size 16x16px from a designer who used Adobe Illustrator to export it and I believe the library should do its best to render the SVG at the greates possible (but still reasonable) quality. A simple solution would be to ignore height and width pixel values and only use them to calculate the image's aspect ratio.

That should produce good results for every image.

If you do not want to implement that or know corner cases where this would break stuff, feel free to close this issue. I just stumbled across it and thought others might run into the same trap as me.

Thank you for all your time!


Image at a density of 300:

Sample image

Glad to hear it worked. As you probably guessed density: 300 was an example value rather than specific to this image.

For all manner of reasons I'd prefer that sharp passes image data unmodified to the underlying renderer, in this case librsvg.

Thank you for your reply!

For all manner of reasons I'd prefer that |sharp| passes image data
unmodified to the underlying renderer, in this case |librsvg|.

This is something I can totally understand. ;)

I bumped into this same issue using https://github.com/itgalaxy/favicons but increasing the density value as described in https://github.com/lovell/sharp/issues/1421#issuecomment-431415218 also caused a pixelated result after processing sharp's resize result. Could be also something in that library that caused the result but I did not dig deeper because of a side effect which caused many tests to break in that library. This was due to some errors raised from high density values, e.g. scaling up a 64x64 SVG to 2208x2208, (72*2208/64=2484).

As a workaround I ended up increasing the SVG's width and height attributes before passing it to sharp. That seemed to work properly and I opened a PR for favicons: https://github.com/itgalaxy/favicons/pull/265

I started to think whether this could be something that could be directly fixed to sharp as this same issue seems to arise from time to time (rel. https://github.com/lovell/sharp/issues/729).

As per my previous comment (https://github.com/lovell/sharp/issues/1421#issuecomment-493707752), I tested the same scaling outside of favicons, with sharp only and using the density option seems to work correctly in this particular case. It is therefore something in the underlying code that is causing the blurred result, not sharp.

Would be still great in case this was "hidden" from sharp users and SVGs would be automatically scaled up to the required size.

How about automatically calculating the density value in case:
a) An SVG file is provided as the input
b) No density option is provided to the sharp constructor
c) The target (resized) image size is larger than the input image size

Would this be something to be considered?

Actually, seems like those invalid density value errors were actually generated by sharp:
https://github.com/lovell/sharp/blob/05d76eeadfe54713606a615185b2da047923406b/lib/input.js#L46

E.g. when scaling 64x64 to 2208x2208, the density value would be 72*2208/64 = 2484 = not in range.

I tried librsvg directly with the librsvg2-bin package and it scales the lower scale SVG image correctly to a higher resolution with the following command:
rsvg-convert -w 2208 testicon.svg -o testicon_2208.png

The generated image is sharp and clear, no pixelation.

Possibly this could be something related to how sharp passes the source image to libvips or alternatively how libvips passes the source image to librsvg...?

Happy to accept a PR to increase the density range above 2400 as libvips/librsvg accepts a value in the 0.001 to 100000 range for this.

As for automagic density setting, it might be possible for sharp to attempt to do this, but that could start to get rather complex as output image dimensions are calculated after the image is rendered.

A separate module that calculates the density an SVG should be rendered at for given output dimensions would also be useful here, if such a thing exists.

Thanks for the answer @lovell!

Sure thing, I also read a bit of the sharp source code and noticed the problem with the added complexity during the image loading. It would need to be somehow preloaded (or loaded twice) in order to determine the source image's dimensions, which would also have a performance impact. I guess it's an understandable tradeoff towards performance with the current implementation.

Currently I want to investigate what is different between the librsvg image conversion utility and sharp+libvips image loading, as librsvg converts correctly. It would be obviously the best solution to use librsvg's own image size detection in case it is already built in there (may be also that it's only a feature in the rsvg-convert tool).

I'm no C/C++ expert, so it'll take some time for me to do the investigation. I try to progress this as I have some spare time and collect more information. I would hope this could be solved in sharp with very minor changes.

This is some super rough code that seems to work ok:

  let instance = sharp(params.buffer)

  const metadata = await instance.metadata()

  if (metadata.format === 'svg' && width && metadata.width && width > metadata.width) {
    let density = (72 * width) / metadata.width
    instance = sharp(buffer, { density: density > 2400 ? 2400 : density})
  }

  if (metadata.format === 'svg' && height && metadata.height && height > metadata.height) {
    let density = (72 * height) / metadata.height
    instance = sharp(buffer, { density: density > 2400 ? 2400 : density})
  }

  return instance.resize(width, height)

Wow. Can't believe I should care about all those things just to convert an image.

@mynameisrufus where did you get this formula? (72 * width) / metadata.width this doesn't seems to provide the best quality

The libvips thumbnail operator has this logic built in, fwiw, I don't know if that could be worth copying.

If you do:

$ vips thumbnail plane.svg plane.png 512
$ vipsheader plane.png
plane.png: 512x395 uchar, 4 bands, srgb, pngload

It renders:

plane

(your web browser will probably be upsizing slightly -- click on the image to see native pixels)

That's opening once to get the dimensions, then calculating a scale and opening a second time at the correct DPI to fit the target.

Was this page helpful?
0 / 5 - 0 ratings