Skip to content

Fix feOffset/feDropShadow under rotated or skewed transforms#1063

Open
StefanoD wants to merge 1 commit into
linebender:mainfrom
StefanoD:fix-feoffset-rotated-transform
Open

Fix feOffset/feDropShadow under rotated or skewed transforms#1063
StefanoD wants to merge 1 commit into
linebender:mainfrom
StefanoD:fix-feoffset-rotated-transform

Conversation

@StefanoD

Copy link
Copy Markdown

feOffset's dx/dy were mapped through scale_coordinates, which only multiplies by the transform's scale factors and discards rotation and skew. As a result, an offset inside a rotated or skewed group was applied axis-aligned in device space instead of being rotated together with the filtered content, producing output inconsistent with browsers.

Unlike blur radii, an offset is a vector and can be represented exactly, so map dx/dy through the full linear part of the transform via a new transform_coordinates helper, used by both feOffset and feDropShadow.

Adds unit tests for the coordinate mapping and regenerates the feOffset/feMerge/feTile complex-transform references, which previously encoded the buggy behavior.

Regenerated references show a shift: in these tests the filter output is the offset result itself (e.g. feOffset's complex-transform has only an feOffset), so correcting the offset moves the whole shape. For feOffset's complex-transform (dx=20 dy=40, transform skewX(30) translate(-50), plus 1.5x viewport scale), skewX adds tan(30)*dy to the x offset: the device offset goes from ~(34.6, 60)px to ~(64.6, 60)px, i.e. ~30px further right, y unchanged. This matches the SVG spec and Chrome.

Known limitation (pre-existing, not introduced here): the filter region is still clipped using the axis-aligned bounding box of the (sheared or rotated) region rather than the exact transformed rectangle. With this fix the offset is correctly sheared further out, so it can now reach the default filter region boundary and get clipped there. The clip edge is therefore axis-aligned in device space instead of following the skew as in Chrome. Because the AABB is larger than the true region, resvg never clips more than a spec-perfect renderer would. Making the region clip exact would require reworking filter-region handling and is out of scope for this fix.

Fixes #949

Generated by Claude

feOffset's dx/dy were mapped through `scale_coordinates`, which only
multiplies by the transform's scale factors and discards rotation and
skew. As a result, an offset inside a rotated or skewed group was applied
axis-aligned in device space instead of being rotated together with the
filtered content, producing output inconsistent with browsers.

Unlike blur radii, an offset is a vector and can be represented exactly,
so map dx/dy through the full linear part of the transform via a new
`transform_coordinates` helper, used by both feOffset and feDropShadow.

Adds unit tests for the coordinate mapping and regenerates the
feOffset/feMerge/feTile complex-transform references, which previously
encoded the buggy behavior.

Regenerated references show a shift: in these tests the filter output is
the offset result itself (e.g. feOffset's complex-transform has only an
feOffset), so correcting the offset moves the whole shape. For feOffset's
complex-transform (dx=20 dy=40, transform skewX(30) translate(-50), plus
1.5x viewport scale), skewX adds tan(30)*dy to the x offset: the device
offset goes from ~(34.6, 60)px to ~(64.6, 60)px, i.e. ~30px further right,
y unchanged. This matches the SVG spec and Chrome.

Known limitation (pre-existing, not introduced here): the filter region
is still clipped using the axis-aligned bounding box of the (sheared or
rotated) region rather than the exact transformed rectangle. With this
fix the offset is correctly sheared further out, so it can now reach the
default filter region boundary and get clipped there. The clip edge is
therefore axis-aligned in device space instead of following the skew as
in Chrome. Because the AABB is larger than the true region, resvg never
clips more than a spec-perfect renderer would. Making the region clip
exact would require reworking filter-region handling and is out of scope
for this fix.

Fixes linebender#949

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

filter within <g transform="matrix(a b c d e f)"> cause inconsistent with chrome

1 participant