Skip to content

Commit 475a41c

Browse files
matmenclaude
andcommitted
faster SVG rendering
Layers remember the rectangle they painted. Compositing a layer (groups with opacity, clip paths, masks, nested viewports) and converting the canvas to RGBA only walk that rectangle instead of every pixel of the image: everything outside it is transparent and was skipped pixel by pixel before. The results are bit-identical: the 1822 SVGs of the resvg test suite checked against master at 300 and 1000 pixels wide. tests/svgs/features.svg at 1920 pixels wide renders 15-20% faster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
1 parent 6b80937 commit 475a41c

2 files changed

Lines changed: 42 additions & 20 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929
- GIF decoding is about 25% faster, with identical results: indices become colors through a lookup table, LZW
3030
strings are written without bounds checks unless they run past the image.
31+
- SVG rendering is about 15-20% faster, with identical results: groups with opacity, clip paths, masks and nested
32+
viewports are composited only where they painted.
3133

3234
### Deprecated
3335

‎src/render/svg.ts‎

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,17 +1010,30 @@ class Layer {
10101010
width: number;
10111011
height: number;
10121012
data: Float32Array;
1013+
// the rectangle painted so far (everything outside is transparent), so compositing and conversion skip the rest
1014+
x0 = Infinity;
1015+
y0 = Infinity;
1016+
x1 = -Infinity;
1017+
y1 = -Infinity;
10131018

10141019
constructor(width: number, height: number) {
10151020
this.width = width;
10161021
this.height = height;
10171022
this.data = new Float32Array(width * height * 4);
10181023
}
10191024

1025+
extend(x0: number, y0: number, x1: number, y1: number): void {
1026+
this.x0 = Math.min(this.x0, x0);
1027+
this.y0 = Math.min(this.y0, y0);
1028+
this.x1 = Math.max(this.x1, x1);
1029+
this.y1 = Math.max(this.y1, y1);
1030+
}
1031+
10201032
// composites a paint through a coverage mask (source over)
10211033
fill({ x0, y0, w, h, data }: Coverage, paint: Paint): void {
10221034
const d = this.data;
10231035
const solid = paint.solid;
1036+
this.extend(x0, y0, x0 + w, y0 + h);
10241037

10251038
for (let y = 0; y < h; y++) {
10261039
let i = ((y0 + y) * this.width + x0) * 4;
@@ -1064,34 +1077,41 @@ class Layer {
10641077
}
10651078

10661079
composite(layer: Layer, opacity: number, mask: Float32Array | null): void {
1080+
if (layer.x1 <= layer.x0) return;
1081+
this.extend(layer.x0, layer.y0, layer.x1, layer.y1);
1082+
10671083
const s = layer.data, d = this.data;
1068-
for (let p = 0, i = 0; p < this.width * this.height; p++, i += 4) {
1069-
if (s[i + 3] <= 0) continue;
1070-
const f = mask ? opacity * mask[p] : opacity;
1071-
if (f <= 0) continue;
1072-
1073-
const a = s[i + 3] * f;
1074-
const inv = 1 - a;
1075-
d[i] = s[i] * f + d[i] * inv;
1076-
d[i + 1] = s[i + 1] * f + d[i + 1] * inv;
1077-
d[i + 2] = s[i + 2] * f + d[i + 2] * inv;
1078-
d[i + 3] = a + d[i + 3] * inv;
1084+
for (let y = layer.y0; y < layer.y1; y++) {
1085+
for (let p = y * this.width + layer.x0, end = y * this.width + layer.x1, i = p * 4; p < end; p++, i += 4) {
1086+
if (s[i + 3] <= 0) continue;
1087+
const f = mask ? opacity * mask[p] : opacity;
1088+
if (f <= 0) continue;
1089+
1090+
const a = s[i + 3] * f;
1091+
const inv = 1 - a;
1092+
d[i] = s[i] * f + d[i] * inv;
1093+
d[i + 1] = s[i + 1] * f + d[i + 1] * inv;
1094+
d[i + 2] = s[i + 2] * f + d[i + 2] * inv;
1095+
d[i + 3] = a + d[i + 3] * inv;
1096+
}
10791097
}
10801098
}
10811099

10821100
toRGBA(): Uint8Array {
10831101
// clamped arrays round and clamp on assignment
10841102
const out = new Uint8ClampedArray(this.width * this.height * 4);
10851103
const d = this.data;
1086-
for (let i = 0; i < d.length; i += 4) {
1087-
const a = d[i + 3];
1088-
if (a <= 0) continue;
1089-
1090-
const f = a >= 1 ? 255 : 255 / a;
1091-
out[i] = d[i] * f;
1092-
out[i + 1] = d[i + 1] * f;
1093-
out[i + 2] = d[i + 2] * f;
1094-
out[i + 3] = a * 255;
1104+
for (let y = this.y0; y < this.y1; y++) {
1105+
for (let i = (y * this.width + this.x0) * 4, end = (y * this.width + this.x1) * 4; i < end; i += 4) {
1106+
const a = d[i + 3];
1107+
if (a <= 0) continue;
1108+
1109+
const f = a >= 1 ? 255 : 255 / a;
1110+
out[i] = d[i] * f;
1111+
out[i + 1] = d[i + 1] * f;
1112+
out[i + 2] = d[i + 2] * f;
1113+
out[i + 3] = a * 255;
1114+
}
10951115
}
10961116
return new Uint8Array(out.buffer);
10971117
}

0 commit comments

Comments
 (0)