Conversation
|
lgtm Nit: The old code had match This could be merged as is and I can follow up with that fix. |
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>
`scale_coordinates` ended in `Some((x * sx, y * sy))` and could never return `None`, so the `None => return Ok(...)` arms in `apply_morphology` and `apply_displacement_map`, and the `?` in `resolve_std_dev`, were all unreachable. Return `(f32, f32)` directly and drop them; no behaviour is lost. Also documents what the function actually does. `Transform::get_scale` returns the lengths of the matrix rows, `sqrt(sx^2 + kx^2)` and `sqrt(ky^2 + sy^2)`, so rotation and skew still contribute their magnitude and only the direction is discarded. That is what makes it right for a radius and wrong for the offset vector that `transform_coordinates` now handles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3987b4d to
56fde4b
Compare
|
Thanks! Done — the
No behaviour was lost: While writing the doc comment for it I noticed the description I first reached for was wrong, so for the record: I also rebased onto
Separately, and out of scope here: 🤖 Generated with Claude Code |
|
Thank you Stefano |
|
lgtm @DJMcNab or @nicoburns do you have time to do a final review? |
|
Sorry for the earlier drive-by. Here's what I actually checked:
I think this one is ready. Fixes #949 |
DJMcNab
left a comment
There was a problem hiding this comment.
I don't have enough context to validate this, and the gut check with the snapshot tests against the browser isn't convincing.
Separately, We haven't got a defined policy on this written down, but I'm not going to merge any PR with Claude listed as a co-author.
| // The offset is a vector in user space, so it must be mapped through the | ||
| // full linear part of the transform (including rotation and skew), not just | ||
| // scaled by the transform's scale factors. |
There was a problem hiding this comment.
Why do you think that "not just scaled by the transform's scale factors." is a useful comment? I also see that you've repeated it...
If you're going to use Claude, at least read it's output and remove the garbage comments...
There was a problem hiding this comment.
This new screenshot still doesn't match my browser.
There was a problem hiding this comment.
This new screenshot seems to be a regression compared to Firefox/Chrome?
There was a problem hiding this comment.
This does look like an improvement.
|
@DJMcNab Sorry, I don't have time anymore to continue with the PR. You guys can take over the PR. |
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_coordinateshelper, 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
Note: I accidentally closed the original PR (#1063) by deleting my fork, which auto-closed it. This reopens the same change — the branch and commits are unchanged.