fabricjs / fabric.js

Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
http://fabricjs.com
Other
28.73k stars 3.49k forks source link

Incorrect shadow offset during export to SVG #7987

Open keba68 opened 2 years ago

keba68 commented 2 years ago

Version

5.1.0

Test Case

https://jsfiddle.net/58hje9ar/5

Information about environment

Browser, latest chrome

Steps To Reproduce

I have an issue with shadow offset when exporting fabric object / group to SVG.

In the first fiddle, https://jsfiddle.net/58hje9ar/5, I have created an rectangle and placed it in a group, then applying an orange shadow to the group. I have also applied 2x scale and 45 degree rotation. The exported SVG ( can be found in the browsers console ):

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1000" height="1000" viewBox="0 0 1000 1000" xml:space="preserve">
    <desc>Created with Fabric.js 5.1.0</desc>
    <defs>
</defs>
    <g transform="matrix(1.41 -1.41 0.71 0.71 398.64 152.98)">
        <filter id="SVGID_0" y="-118%" height="336%" x="-90%" width="280%">
            <feGaussianBlur in="SourceAlpha" stdDeviation="0.5"/>
            <feOffset dx="-70.71" dy="70.71" result="oBlur"/>
            <feFlood flood-color="rgb(255,165,0)" flood-opacity="1"/>
            <feComposite in2="oBlur" operator="in"/>
            <feMerge>
                <feMergeNode/>
                <feMergeNode in="SourceGraphic"/>
            </feMerge>
        </filter>
        <g style="filter: url(#SVGID_0);">
            <g transform="matrix(1 0 0 1 0 0)">
                <rect style="stroke: rgb(0,0,0); stroke-width: 3; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,0,0); fill-rule: nonzero; opacity: 1;" x="-50" y="-35" rx="0" ry="0" width="100" height="70"/>
            </g>
        </g>
    </g>
</svg>

fiddle1

The shadows transform in the svg is a bit off compared to what the canvas render. This issue becomes clear when both rotation and scaling is applied and the shadow offset is quite large.

In the second fiddle, https://jsfiddle.net/58hje9ar/7/, we have the same rectangle and shadow inserted into a group. This time the transformation is applied on the rectangle instead of the group. The exported SVG:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1000" height="1000" viewBox="0 0 1000 1000" xml:space="preserve">
    <desc>Created with Fabric.js 5.1.0</desc>
    <defs>
</defs>
    <g transform="matrix(1 0 0 1 398.64 198.64)">
        <filter id="SVGID_0" y="-71%" height="242%" x="-21%" width="142%">
            <feGaussianBlur in="SourceAlpha" stdDeviation="0.5"/>
            <feOffset dx="0" dy="100" result="oBlur"/>
            <feFlood flood-color="rgb(255,165,0)" flood-opacity="1"/>
            <feComposite in2="oBlur" operator="in"/>
            <feMerge>
                <feMergeNode/>
                <feMergeNode in="SourceGraphic"/>
            </feMerge>
        </filter>
        <g style="filter: url(#SVGID_0);">
            <g transform="matrix(1.41 -1.41 0.71 0.71 0 0)">
                <rect style="stroke: rgb(0,0,0); stroke-width: 3; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,0,0); fill-rule: nonzero; opacity: 1;" x="-50" y="-35" rx="0" ry="0" width="100" height="70"/>
            </g>
        </g>
    </g>
</svg>

fiddle2

The transformation of the shadow is now same in canvas and SVG.

This becomes an issue when loading SVG data into fabric using loadSVGFromString since we would like to group the incomming svg by using groupSVGElements. If the latter creates an Fabric.Group which we then apply a shadow and transformation, the exported SVG is not rendering shadow with the same transform as the canvas: https://jsfiddle.net/48rtpkhn/2/

The difference is more subtle but its clearly visible.

fiddle3

It seems to me that the transform of outer SVG group in the exported SVG ( containing the filter and the content ) is applied on the feOffset filter ( which is expected given the hierarchy ). Looking at the 'toSVG' function in shadow.class.js it seems that there is a compensation for the objects rotation, however not the scale:

offset = fabric.util.rotateVector( { x: this.offsetX, y: this.offsetY }, fabric.util.degreesToRadians(-object.angle))

I tried to override the function and change that to:

t = Fabric.util.invertTransform(object.calcTransformMatrix()); t[5] = 0; t[4] = 0; offset = Fabric.util.transformPoint({ x: this.offsetX, y: this.offsetY }, t),

The idea was to remove the objects entire transform from the offset before exporting to SVG except translation. Then feOffset would recieve the same transform from the parent group when rendering the SVG. This seems to be working for the first fiddle, https://jsfiddle.net/sr64avj2/4/ but not when loading content from SVG into a group.

I just started using Fabric so it's safe to say that my assumptions can be incorrect.

Expected Behavior

Shadow in exported SVG should align with shadow on canvas

Actual Behavior

Shadow in exported SVG is slightly off compared to canvas when applying transformations to a group with objects.

keba68 commented 2 years ago

I realized that removing the object transformation from shadow offsetX/offsetY seems to be a solution, if nonScaling is set to true. It would probably work with the noScaling flag set to false if we compensate for that transformation when exporting to SVG. This is not an issue for me since I prefer setting it to true.

Here is an updated fiddle implementing export of SVG Shadow that matches the canvas, regardless of transformation applied on the object.

http://jsfiddle.net/hprLmq8f/2 fiddle4

Since we are removing the entire objects transform, using the inverse matrix of 'object.calcTransformMatrix()', we get the side effect of also supporting group in group: http://jsfiddle.net/hprLmq8f/6/ fiddle5

This is not supported in the current version of fabric.Shadow.toSVG function: http://jsfiddle.net/hprLmq8f/7/ fiddle6

I hope this could help someone that has the issue of exported svg shadows not being consistent with canvas rendering.

keba68 commented 2 years ago

Also, we don't need to compensate for flipX/flipY when exporting shadow.

http://jsfiddle.net/4oeb37mn/3/

stevenhurth commented 7 months ago

This issue also applies to version 6.

When exporting an object with shadow to SVG, the shadow does not account for nonScaling being set to true.