Skip to content

Commit 3f020df

Browse files
committed
SVG: apply feDisplacementMap's scale once
1 parent fcebab7 commit 3f020df

3 files changed

Lines changed: 30 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
`Animation.decode()` (which reported every image as too large); fractional loop counts in `new Animation()`.
3838
- `Animation.decode()` reports invalid `threads` and `maxPixels` as such, not as damaged image data.
3939
- `Image.decode()` refuses GIFs with a hint at `Animation.decode()` instead of "Unsupported image type".
40+
- SVG `feDisplacementMap` applies its `scale` once, like the specification and browsers (it multiplied by it twice, like
41+
resvg, so at typical scales the whole input was displaced out of the filter region and disappeared), and reads the
42+
map's colors unpremultiplied.
4043

4144
## 3.13.2
4245

‎src/render/filter.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,14 +409,19 @@ function apply(kind: Kind, linear: boolean, ts: Matrix, region: Rect, subregion:
409409
case 'displacement': {
410410
const src = inColorSpace(input(kind.input), linear).pixmap, map = inColorSpace(input(kind.input2), linear).pixmap;
411411
const pixmap = create(region.width, region.height);
412-
// (like resvg: the scale counts twice, the map isn't unpremultiplied)
412+
// (the scale once, like the specification and browsers: resvg multiplies by it twice, moving everything out
413+
// of the region at the scales SVGs use; the map's colors unpremultiplied, as the specification says)
413414
const sx = kind.scale * scaleX, sy = kind.scale * scaleY, w = src.width, h = src.height;
414415
const round = (v: number): number => Math.sign(v) * Math.round(Math.abs(v));
416+
const channel = (m: number, c: number): number => {
417+
const a = map.data[m + 3];
418+
return c === 3 ? a / 255 : a ? map.data[m + c] / a : 0;
419+
};
415420
for (let y = 0; y < Math.min(h, map.height); y++)
416421
for (let x = 0; x < Math.min(w, map.width); x++) {
417422
const m = (y * map.width + x) * 4;
418-
const ox = round(x + (map.data[m + kind.xChannel] / 255 - 0.5) * sx * kind.scale);
419-
const oy = round(y + (map.data[m + kind.yChannel] / 255 - 0.5) * sy * kind.scale);
423+
const ox = round(x + (channel(m, kind.xChannel) - 0.5) * sx);
424+
const oy = round(y + (channel(m, kind.yChannel) - 0.5) * sy);
420425
if (ox < 0 || ox >= w || oy < 0 || oy >= h) continue;
421426
pixmap.data.set(src.data.subarray((oy * w + ox) * 4, (oy * w + ox) * 4 + 4), (y * pixmap.width + x) * 4);
422427
}

‎tests/svg-primitives.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,22 @@ const panic = message => {
77
(async () => {
88
await require('./util/resvg')(file => file.startsWith('filters/fe'), panic);
99
})();
10+
11+
// feDisplacementMap applies its scale once (resvg multiplies by it twice) and reads the map's colors unpremultiplied,
12+
// like browsers: a map of R = 1 (half transparent) moves every pixel's source 0.5 * scale to the right
13+
(async () => {
14+
const {Image} = require('..');
15+
const svg = opacity => `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="20">
16+
<filter id="f" x="0" y="0" width="100%" height="100%" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
17+
<feFlood flood-color="#ff8000" flood-opacity="${opacity}" result="map"/>
18+
<feDisplacementMap in="SourceGraphic" in2="map" scale="20" xChannelSelector="R" yChannelSelector="A"/>
19+
</filter>
20+
<rect x="40" y="0" width="20" height="20" fill="#000" filter="url(#f)"/></svg>`;
21+
for (const opacity of [1, 0.5]) {
22+
const image = await Image.renderSVG(svg(opacity));
23+
// y offset: (alpha - 0.5) * 20, 10 rows down at opacity 1, none at 0.5; x: the rect drawn 10 pixels left
24+
const row = opacity === 1 ? 5 : 10;
25+
const covered = [...Array(100).keys()].filter(x => image.getRGBAAt(x, row)[3] > 128);
26+
if (covered[0] !== 30 || covered.at(-1) !== 49) panic(`feDisplacementMap (map opacity ${opacity}): covers ${covered[0]}-${covered.at(-1)}, not 30-49`);
27+
}
28+
})();

0 commit comments

Comments
 (0)