Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/util/alpha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 8-bit premultiplied alpha on the pixels as 32-bit words (little endian: alpha is the top byte), for the fast paths

export const littleEndian = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;

const words = (bitmap: Uint8ClampedArray): Int32Array => new Int32Array(bitmap.buffer, bitmap.byteOffset, bitmap.length >> 2);

/** Whether every pixel of the RGBA bitmap is opaque */
export function isOpaque(bitmap: Uint8ClampedArray): boolean {
const pixels = words(bitmap);
let all = -1;
for (let i = 0; i < pixels.length; i++) all &= pixels[i];
return all >>> 24 === 255;
}

/** Premultiplies RGBA into `out` (which may be `bitmap`): channel * alpha / 255, rounded */
export function premultiply8(bitmap: Uint8ClampedArray, out: Uint8ClampedArray): void {
const src = words(bitmap), dst = words(out);
for (let i = 0; i < src.length; i++) {
const p = src[i], a = p >>> 24;
if (a === 255) {
dst[i] = p;
continue;
}
// red and blue as 16-bit lanes: x * a + 128, plus its high byte, shifted by 8
let rb = Math.imul(p & 0xff00ff, a) + 0x800080;
rb = (rb + (rb >>> 8 & 0xff00ff)) >>> 8 & 0xff00ff;
const g = (p >> 8 & 255) * a + 128;
dst[i] = rb | (g + (g >> 8) & 0xff00) | a << 24;
}
}

// channel * 255 / alpha at alpha << 8 | channel, rounded (half to even) and clamped like the clamped arrays
let unpremultiplied: Uint8ClampedArray | undefined;

/** Unpremultiplies RGBA in place */
export function unpremultiply8(bitmap: Uint8ClampedArray): void {
if (!unpremultiplied) {
unpremultiplied = new Uint8ClampedArray(65536);
for (let i = 256; i < 65536; i++) unpremultiplied[i] = (i & 255) * (255 / (i >> 8));
}
const table = unpremultiplied, pixels = words(bitmap);
for (let i = 0; i < pixels.length; i++) {
const p = pixels[i], a = p >>> 24;
if (a === 255) continue;
const o = a << 8;
pixels[i] = table[o | p & 255] | table[o | p >> 8 & 255] << 8 | table[o | p >> 16 & 255] << 16 | a << 24;
}
}
38 changes: 8 additions & 30 deletions src/util/blur.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// gaussian blur approximated by three box blurs (the box sizes match the gaussian's variance), each pass is a
// running sum, so the cost doesn't depend on the radius. Pixels outside the image repeat the edge pixels

import { littleEndian, isOpaque, premultiply8, unpremultiply8 } from './alpha.ts';

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

let opaque = true;
for (let i = 3; i < bitmap.length; i += 4) if (bitmap[i] !== 255) {
opaque = false;
break;
}
if (!opaque) {
for (let i = 0; i < bitmap.length; i += 4) {
const a = bitmap[i + 3];
// x * a / 255, rounded
bitmap[i] = (bitmap[i] * a + 128) * 257 >> 16;
bitmap[i + 1] = (bitmap[i + 1] * a + 128) * 257 >> 16;
bitmap[i + 2] = (bitmap[i + 2] * a + 128) * 257 >> 16;
}
}
const opaque = isOpaque(bitmap);
if (!opaque) premultiply8(bitmap, bitmap);

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

if (!opaque) {
for (let i = 0; i < bitmap.length; i += 4) {
const a = bitmap[i + 3];
if (a === 0) {
bitmap[i] = bitmap[i + 1] = bitmap[i + 2] = 0;
continue;
}
// the clamped bitmap rounds and clamps
const f = 255 / a;
bitmap[i] *= f;
bitmap[i + 1] *= f;
bitmap[i + 2] *= f;
}
}
if (!opaque) unpremultiply8(bitmap);
return true;
}

Expand Down
Loading
Loading