Skip to content

Commit 3b72822

Browse files
matmenclaude
andcommitted
one fast resampling path, shared 8-bit alpha helpers
- resampleOpaqueFast and resampleTransparentFast become one resampleFast (red+blue and green+alpha lanes work for opaque pixels too), split into a vertical and a horizontal pass. Transparency in the middle of an image is found by the vertical pass, which then reruns premultiplied. - src/util/alpha.ts: isOpaque, premultiply8 and unpremultiply8 (lookup table, rounding like the clamped arrays), used by resample and blur; one littleEndian flag. - Blur results are unchanged (corpus). Fast resizing of transparent images differs by 1 only where unpremultiplying hits exactly .5 (0.003-0.007% of the values). Doubling a photo with alpha: 117 -> 103 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
1 parent 87af62c commit 3b72822

4 files changed

Lines changed: 130 additions & 198 deletions

File tree

‎src/util/alpha.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// 8-bit premultiplied alpha on the pixels as 32-bit words (little endian: alpha is the top byte), for the fast paths
2+
3+
export const littleEndian = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
4+
5+
const words = (bitmap: Uint8ClampedArray): Int32Array => new Int32Array(bitmap.buffer, bitmap.byteOffset, bitmap.length >> 2);
6+
7+
/** Whether every pixel of the RGBA bitmap is opaque */
8+
export function isOpaque(bitmap: Uint8ClampedArray): boolean {
9+
const pixels = words(bitmap);
10+
let all = -1;
11+
for (let i = 0; i < pixels.length; i++) all &= pixels[i];
12+
return all >>> 24 === 255;
13+
}
14+
15+
/** Premultiplies RGBA into `out` (which may be `bitmap`): channel * alpha / 255, rounded */
16+
export function premultiply8(bitmap: Uint8ClampedArray, out: Uint8ClampedArray): void {
17+
const src = words(bitmap), dst = words(out);
18+
for (let i = 0; i < src.length; i++) {
19+
const p = src[i], a = p >>> 24;
20+
if (a === 255) {
21+
dst[i] = p;
22+
continue;
23+
}
24+
// red and blue as 16-bit lanes: x * a + 128, plus its high byte, shifted by 8
25+
let rb = Math.imul(p & 0xff00ff, a) + 0x800080;
26+
rb = (rb + (rb >>> 8 & 0xff00ff)) >>> 8 & 0xff00ff;
27+
const g = (p >> 8 & 255) * a + 128;
28+
dst[i] = rb | (g + (g >> 8) & 0xff00) | a << 24;
29+
}
30+
}
31+
32+
// channel * 255 / alpha at alpha << 8 | channel, rounded (half to even) and clamped like the clamped arrays
33+
let unpremultiplied: Uint8ClampedArray | undefined;
34+
35+
/** Unpremultiplies RGBA in place */
36+
export function unpremultiply8(bitmap: Uint8ClampedArray): void {
37+
if (!unpremultiplied) {
38+
unpremultiplied = new Uint8ClampedArray(65536);
39+
for (let i = 256; i < 65536; i++) unpremultiplied[i] = (i & 255) * (255 / (i >> 8));
40+
}
41+
const table = unpremultiplied, pixels = words(bitmap);
42+
for (let i = 0; i < pixels.length; i++) {
43+
const p = pixels[i], a = p >>> 24;
44+
if (a === 255) continue;
45+
const o = a << 8;
46+
pixels[i] = table[o | p & 255] | table[o | p >> 8 & 255] << 8 | table[o | p >> 16 & 255] << 16 | a << 24;
47+
}
48+
}

‎src/util/blur.ts‎

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// gaussian blur approximated by three box blurs (the box sizes match the gaussian's variance), each pass is a
22
// running sum, so the cost doesn't depend on the radius. Pixels outside the image repeat the edge pixels
33

4+
import { littleEndian, isOpaque, premultiply8, unpremultiply8 } from './alpha.ts';
5+
46
// widths of the three boxes for a gaussian with the given standard deviation
57
function boxSizes(sigma: number): number[] {
68
const n = 3;
@@ -203,26 +205,15 @@ function boxColumnsWords(src: Int32Array, dst: Int32Array, width: number, height
203205
* channels per sum, rounded to 8 bits after each box (within a step of blurring floats, several times faster).
204206
* Transparent images are premultiplied to 8 bits first: that rounds the color of pixels that are almost
205207
* transparent, which only shows unpremultiplied (composited, they stay within half a step). Returns false (and
206-
* does nothing) for radii the box approximation doesn't cover (below 2) or that don't fit the lanes
208+
* does nothing) on big endian machines and for radii the box approximation doesn't cover (below 2) or that don't
209+
* fit the lanes
207210
*/
208211
export function boxBlurRGBA(bitmap: Uint8ClampedArray, width: number, height: number, sigma: number): boolean {
209212
const sizes = boxSizes(sigma);
210-
if (!(sigma >= 2) || Math.max(...sizes) > 257) return false;
213+
if (!littleEndian || !(sigma >= 2) || Math.max(...sizes) > 257) return false;
211214

212-
let opaque = true;
213-
for (let i = 3; i < bitmap.length; i += 4) if (bitmap[i] !== 255) {
214-
opaque = false;
215-
break;
216-
}
217-
if (!opaque) {
218-
for (let i = 0; i < bitmap.length; i += 4) {
219-
const a = bitmap[i + 3];
220-
// x * a / 255, rounded
221-
bitmap[i] = (bitmap[i] * a + 128) * 257 >> 16;
222-
bitmap[i + 1] = (bitmap[i + 1] * a + 128) * 257 >> 16;
223-
bitmap[i + 2] = (bitmap[i + 2] * a + 128) * 257 >> 16;
224-
}
225-
}
215+
const opaque = isOpaque(bitmap);
216+
if (!opaque) premultiply8(bitmap, bitmap);
226217

227218
const pixels = new Int32Array(bitmap.buffer, bitmap.byteOffset, width * height);
228219
const tmp = new Int32Array(pixels.length), rbs = new Int32Array(width), gas = new Int32Array(width);
@@ -234,20 +225,7 @@ export function boxBlurRGBA(bitmap: Uint8ClampedArray, width: number, height: nu
234225
boxColumnsWords(tmp, pixels, width, height, radius, mult, rbs, gas);
235226
}
236227

237-
if (!opaque) {
238-
for (let i = 0; i < bitmap.length; i += 4) {
239-
const a = bitmap[i + 3];
240-
if (a === 0) {
241-
bitmap[i] = bitmap[i + 1] = bitmap[i + 2] = 0;
242-
continue;
243-
}
244-
// the clamped bitmap rounds and clamps
245-
const f = 255 / a;
246-
bitmap[i] *= f;
247-
bitmap[i + 1] *= f;
248-
bitmap[i + 2] *= f;
249-
}
250-
}
228+
if (!opaque) unpremultiply8(bitmap);
251229
return true;
252230
}
253231

0 commit comments

Comments
 (0)