weisJ/jsvg

Filter bounds(?) cause graphics shift

stanio opened this issue · 13 comments

left_ptr.svg:
<svg xmlns="http://www.w3.org/2000/svg"
    width="64" height="64" viewBox="-2.5 -1.502 384 384">
<g filter="url(#drop-shadow)">
  <path id="align-anchor" d="m 51.5,28.498 h 26 l -26,36 z" fill="cyan" opacity=".9" display="none" />
  <path d="M 201.163,133.54 201.149,133.528 201.134,133.515 91.6855,36.4935
           C 86.5144,31.7659 81.4269,27.9549 76.5421,25.525
           C 71.7671,23.1497 66.0861,21.5569 60.4133,23.1213
           C 54.3118,24.8039 50.4875,29.4674 48.3639,34.759
           C 46.3122,39.8715 45.4999,46.2787 45.4999,53.5383
           L 45.4999,200.431
           V 200.493
           L 45.5008,200.555
           C 45.6218,208.862 50.4279,217.843 55.9963,223.894
           C 58.8934,227.043 62.5163,229.986 66.6704,231.742
           C 70.9172,233.537 76.217,234.254 81.4691,231.884
           C 85.7536,229.951 89.6754,226.055 92.8565,222.651
           C 94.6841,220.695 96.8336,218.252 99.0355,215.749
           C 100.71,213.847 102.414,211.91 104.03,210.126
           C 112.189,201.122 121.346,192.286 132.161,187.407
           C 143.013,182.511 155.809,181.375 167.963,181.146
           C 170.959,181.089 173.85,181.087 176.65,181.085
           H 176.663
           H 176.686
           C 179.447,181.083 182.164,181.081 184.662,181.019
           C 189.231,180.906 194.643,180.609 198.777,178.88
           C 208.711,174.723 210.972,163.838 210.753,156.445
           C 210.521,148.596 207.57,139.272 201.163,133.54
           Z"
      fill="#00FF00" stroke="#0000FF" stroke-width="12" />
</g>
  <defs>
    <filter id="drop-shadow" filterUnits="userSpaceOnUse">
      <feGaussianBlur in="SourceAlpha" stdDeviation="3" />
      <feOffset dx="12" dy="6" result="offsetblur" />
      <feFlood flood-color="black" flood-opacity="0.5" />
      <feComposite in2="offsetblur" operator="in" />
      <feMerge>
        <feMergeNode />
        <feMergeNode in="SourceGraphic" />
      </feMerge>
    </filter>
  </defs>
</svg>
FilterShiftTest.java:
import java.io.File;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Dimension2D;
import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;

import com.github.weisj.jsvg.SVGDocument;
import com.github.weisj.jsvg.parser.SVGLoader;

public class FilterShiftTest {

    public static void main(String[] args) throws Exception {
        String inputName = "left_ptr";
        SVGDocument svg = new SVGLoader()
                .load(new File(inputName + ".svg").toURI().toURL());
        Dimension2D size = svg.size();
        BufferedImage image = new BufferedImage((int) size.getWidth(),
                                                (int) size.getHeight(),
                                                BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                           RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                           RenderingHints.VALUE_STROKE_PURE);
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, image.getWidth(), image.getHeight());
        svg.render(null, g);
        g.dispose();
        ImageIO.write(image, "png", new File(inputName + ".png"));
        System.out.println("Done.");
    }

}
Actual Reference (no filter)
shifted-geometry pixel-aligned-outline

The source viewBox="-2.5 -1.502 384 384" origin (min-x/y) is adjusted so the left vertical outline of the shape gets aligned to the target pixel grid (width="64" height="64"). When the filter is applied this alignment is lost. I've noticed adjusting the upper left corner of the filter rendering area affects the result (still unexpected), f.e.:

    <filter id="drop-shadow" filterUnits="userSpaceOnUse" x="3" y="3">

Further observation, if I:

  • A) Remove the offset from the view-box:

    -    width="64" height="64" viewBox="-2.5 -1.502 384 384">
    +    width="64" height="64" viewBox="0 0 384 384">

    then

  • B) Apply the same offset to the top-left of the filter's area:

    -    <filter id="drop-shadow" filterUnits="userSpaceOnUse">
    +    <filter id="drop-shadow" filterUnits="userSpaceOnUse" x="-2.5" y="-1.502">

it appears to recreate the original intent:

A B
filter-A filter-B

Compare (B) with the reference (no filter) from the main description - the outlines match.

Could you elaborate what you mean by shifted? Both images look identical to me.

I'll try. Have a closer look at the left vertical outline of the shape:

zoom-in-side-by-side

You may open the two images from #62 (comment) in adjacent browser tabs, optionally zoom both in with the same factor, and then switch between the two tabs back and forth (Ctrl+PgUp/PgDn) to "animate" the shift. You may open the reference image (shadow filter removed) from the description next to these, also.

One may use an image comparison utility like WinMerge, also.

I see. This is really a matter of sub-pixel alignment. Any element with a filter is first rendered to an offscreen image which is the blitted to the destination at the location of the element. To have the same sub-pixel behaviour regarding to antialiasing etc when rendering to the offscreen image as to the original surface is very tricky - especially at low resolutions like 64x64.

The fact that the alignment went back to normal when moving the offset from the view box to the filter is more of a coincidence. First note that the resulting svg images are not equivalent. The x and y values on the filter do not move the rendered element. However on the view box it does move the element (as it changes the coordinate system). In your case the coordinate system of the svg has width/height 384 + 2.5/381 + 1.502 and afterwards removing the offset it becomes 384/384. This will move the element. But as you are rendering this on an image of size 64x64 the movement will be about 1/5 of a pixel which coincidentally resolves the misalignment in your case.

I will have a look whether sub-pixel alignment for filters can be improved, but there might not be much I can do at these low resolutions.

In your case the coordinate system of the svg has width/height 384 + 2.5/381 + 1.502 and afterwards removing the offset it becomes 384/384. This will move the element.

I don't get this one. The SVG viewBox width and height in all cases (no matter the min-x/min-y) is specified to be 384. Note, that the element "moves" (using the same viewBox) just when the filter is added – this I'm not expecting. I could generate reference results using the Chrome engine if you think it could be of help.

I will have a look whether sub-pixel alignment for filters can be improved, but there might not be much I can do at these low resolutions.

Thanks in advance. That's the point of the fractional offsets I'm doing – to align certain elements to the target size pixel grid and minimize sub-pixel anti-aliasing:

normal-preview outline-overlay visible-alignment-hints

I'm rendering the shapes at different target sizes, and I'm recalculating the necessary offsets for each one. I'm pretty satisfied with the results until adding the drop-shadow filter.


Just in case you're curious, I'm doing all this as part of stanio/Bibata_Cursor where the build implementation is found in a stanio-misc submodule. It is very experimental but I'm able to get useful results also.

  • B) Apply the same offset to the top-left of the filter's area:

    -    <filter id="drop-shadow" filterUnits="userSpaceOnUse">
    +    <filter id="drop-shadow" filterUnits="userSpaceOnUse" x="-2.5" y="-1.502">

I've pointed this out to hint it is the filter somehow messing up with the view-box, while it shouldn't. I may have confused you with this example, though. Please compare the two images from the main description – with and without the filter. The shape "moves" using the same viewBox, while it shouldn't.

My point is that the x and y values don't actually move the element the filter is applied to and I wanted to make sure you understand this.

In the meantime this will fix your issues

<svg xmlns="http://www.w3.org/2000/svg"
     width="64" height="64" viewBox="-2.5 -1.502 384 384">
    <use href="#cursor" filter="url(#drop-shadow)"/>
    <g id="cursor">
        <path id="align-anchor" d="m 51.5,28.498 h 26 l -26,36 z" fill="cyan" opacity=".9" display="none" />
        <path d="M 201.163,133.54 201.149,133.528 201.134,133.515 91.6855,36.4935
           C 86.5144,31.7659 81.4269,27.9549 76.5421,25.525
           C 71.7671,23.1497 66.0861,21.5569 60.4133,23.1213
           C 54.3118,24.8039 50.4875,29.4674 48.3639,34.759
           C 46.3122,39.8715 45.4999,46.2787 45.4999,53.5383
           L 45.4999,200.431
           V 200.493
           L 45.5008,200.555
           C 45.6218,208.862 50.4279,217.843 55.9963,223.894
           C 58.8934,227.043 62.5163,229.986 66.6704,231.742
           C 70.9172,233.537 76.217,234.254 81.4691,231.884
           C 85.7536,229.951 89.6754,226.055 92.8565,222.651
           C 94.6841,220.695 96.8336,218.252 99.0355,215.749
           C 100.71,213.847 102.414,211.91 104.03,210.126
           C 112.189,201.122 121.346,192.286 132.161,187.407
           C 143.013,182.511 155.809,181.375 167.963,181.146
           C 170.959,181.089 173.85,181.087 176.65,181.085
           H 176.663
           H 176.686
           C 179.447,181.083 182.164,181.081 184.662,181.019
           C 189.231,180.906 194.643,180.609 198.777,178.88
           C 208.711,174.723 210.972,163.838 210.753,156.445
           C 210.521,148.596 207.57,139.272 201.163,133.54
           Z"
              fill="#00FF00" stroke="#0000FF" stroke-width="12" />
    </g>
    <defs>
        <filter id="drop-shadow" filterUnits="userSpaceOnUse">
            <feGaussianBlur in="SourceAlpha" stdDeviation="3" />
            <feOffset dx="12" dy="6" result="offsetblur" />
            <feFlood flood-color="black" flood-opacity="0.5" />
            <feComposite in2="offsetblur" operator="in" />
        </filter>
    </defs>
</svg>

In the meantime this will fix your issues

Thank you very much for the provided workaround, and the JSVG library! 🙇‍♂️

The latest changes resolve the aliasing issues for this particular example. You can test out the changes with the current snapshot version. Please check whether there are any other files cause issues for you.

Tested with current 1.5.0-SNAPSHOT (1.5.0-20240226.002658) – works like a charm. It also appears to fix some sizing/offset issues I've noticed while employing the suggested workaround with v1.4.0. Compared to Chrome rendering, I could spot minimal differences only at the lowest resolutions I'm working with, but that could be caused by Chrome's engine deficiencies as well. Thank you, again!

Can you confirm that the latest snapshot didn't regress this (it shouldn't but just to make sure)

Looks great on my side.

Perfect