sharp icon indicating copy to clipboard operation
sharp copied to clipboard

Resizing SVG with drop-shadow filter and <= 0.31 scale factor removes path

Open jderusse opened this issue 9 months ago • 2 comments

Possible bug

Is this a possible bug in a feature of sharp, unrelated to installation?

  • [x] Running npm install sharp completes without error.
  • [x] Running node -e "require('sharp')" completes without error.

Are you using the latest version of sharp?

  • [x] I am using the latest version of sharp as reported by npm view sharp dist-tags.latest.

What is the output of running npx envinfo --binaries --system --npmPackages=sharp --npmGlobalPackages=sharp?

  System:
    OS: Linux 6.11 Alpine Linux
    CPU: (20) x64 12th Gen Intel(R) Core(TM) i9-12900HK
    Memory: 25.97 GB / 62.47 GB
    Container: Yes
    Shell: Unknown - /bin/sh
  Binaries:
    Node: 23.11.0 - /usr/local/bin/node
    npm: 10.9.2 - /usr/local/bin/npm
  npmPackages:
    sharp: ^0.34.1 => 0.34.1

Does this problem relate to file caching?

The default behaviour of libvips is to cache input files, which can lead to EBUSY or EPERM errors on Windows. Use sharp.cache(false) to switch this feature off.

  • [x] Adding sharp.cache(false) does not fix this problem.

Does this problem relate to images appearing to have been rotated by 90 degrees?

Images that contain EXIF Orientation metadata are not auto-oriented. By default, EXIF metadata is removed.

  • To auto-orient pixel values use the parameter-less rotate() operation.

  • To retain EXIF Orientation use keepExif().

  • [ ] Using rotate() or keepExif() does not fix this problem.

What are the steps to reproduce?

Given the following svg file

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 500" style="filter: drop-shadow(1px 2px 2px rgb(0 0 0 / 0.4));">
    <path d="M925,462c5,-22 4,-46 10,-67c2,-7 6,-14 7,-21c7,-37 -2,-81 0,-119c1,-28 7,-72 -7,-96" fill="none" stroke="#fd7e14" stroke-width="5"></path>
</svg>

When resizing the SVG image under a certain size, the output is empty, Resizing to 320x160 works

Image

but it fails for 310x155

Image

sharp.cache(false)

sharp('./input.svg')
    .resize(320, 160)
    .png()
    .toFile('./320x160.png');

sharp('./input.svg')
    .resize(310, 155)
    .png()
    .toFile('./310x155.png');

What is the expected behaviour?

When calling .resize(310, 155) the image generated should not be blank.

Side note: when the image does not have the filter drop-shadow(1px 2px 2px rgb(0 0 0 / 0.4), it does not have the same issue.

jderusse avatar May 15 '25 20:05 jderusse

Thanks for the clear report, I can reproduce locally. This appears to be related to newer versions of the underlying librsvg and/or cairo libraries so my best guess right now is that this might be a regression. I'll need to spend some time to narrow down where the problem was introduced.

lovell avatar May 19 '25 08:05 lovell

I cannot reproduce this using the latest librsvg on its own. The following zoom levels, which are equivalent to the example above, work as expected

$ rsvg-convert -v
rsvg-convert version 2.60.0
$ rsvg-convert --zoom 0.31 in.svg >out31.png
$ rsvg-convert --zoom 0.32 in.svg >out32.png
$ file *.png
out31.png: PNG image data, 310 x 155, 8-bit/color RGBA, non-interlaced
out32.png: PNG image data, 320 x 160, 8-bit/color RGBA, non-interlaced

I can reproduce using libvips at the command line (linked to the latest librsvg):

vips svgload in.svg out31.png --scale=0.31
vips svgload in.svg out32.png --scale=0.32

Scaling to 0.31 produces an empty image, whereas 0.32 works as expected.

Moving the style attribute to a child group element provides a possible workaround, as the following appears to render as expected:

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 500">
  <g style="filter: drop-shadow(1px 2px 2px rgb(0 0 0 / 0.4));">
    <path d="M925,462c5,-22 4,-46 10,-67c2,-7 6,-14 7,-21c7,-37 -2,-81 0,-119c1,-28 7,-72 -7,-96" fill="none" stroke="#fd7e14" stroke-width="5"></path>
  </g>
</svg>

My best guess would be that the values of the cairo viewport for the 0.31 scale factor have hit hit some kind of top-level rounding error/feature of cairo that is "filtering" the path away entirely.

rsvg-convert applies cairo scaling slightly differently to libvips, which would explain why the former still works, but libvips is probably using the "correct" method. 🤷

lovell avatar Jul 13 '25 15:07 lovell