diff --git a/src/util/alpha.ts b/src/util/alpha.ts new file mode 100644 index 00000000..93e151d0 --- /dev/null +++ b/src/util/alpha.ts @@ -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; + } +} diff --git a/src/util/blur.ts b/src/util/blur.ts index 05094dad..03ba4d11 100644 --- a/src/util/blur.ts +++ b/src/util/blur.ts @@ -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; @@ -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); @@ -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; } diff --git a/src/util/resample.ts b/src/util/resample.ts index 8c85a9aa..ec2f32c0 100644 --- a/src/util/resample.ts +++ b/src/util/resample.ts @@ -3,6 +3,8 @@ // The arithmetic is integer (weights are fixed point numbers), and the vertical pass runs first: its weights are the // same for a whole row, so it's the cheaper pass to run on the larger image +import { littleEndian, isOpaque, premultiply8, unpremultiply8 } from './alpha.ts'; + export interface Filter { /** radius of the filter at scale 1 */ support: number; @@ -82,16 +84,14 @@ function coefficients(inSize: number, outSize: number, filter: Filter, bits: num /** most taps per pass of the fast resampling */ const FAST_TAPS = 4; -const littleEndian = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1; - /** * Resamples RGBA pixels with the given filter, in premultiplied alpha so transparent pixels don't bleed their color */ export function resample(src: Uint8ClampedArray, width: number, height: number, outWidth: number, outHeight: number, filter: Filter, out: Uint8ClampedArray, fast = false): void { // opaque pixels don't need premultiplying, and with non-negative weights two channels share a multiplication if (littleEndian && filter.support <= 1) { - if (!fast && resampleOpaque(src, width, height, outWidth, outHeight, filter, out)) return; - if (fast && (resampleOpaqueFast(src, width, height, outWidth, outHeight, filter, out) || resampleTransparentFast(src, width, height, outWidth, outHeight, filter, out))) return; + if (fast && resampleFast(src, width, height, outWidth, outHeight, filter, out)) return; + if (resampleOpaque(src, width, height, outWidth, outHeight, filter, out)) return; } resamplePremultiplied(src, width, height, outWidth, outHeight, filter, out); } @@ -242,76 +242,55 @@ function resampleOpaque(src: Uint8ClampedArray, width: number, height: number, o return true; } -// sums the words of `taps` pixels `step` apart with 8-bit weights, two channels per multiplication, and rounds them -// back to an opaque pixel: the weights sum to 256, so a lane holds at most 255 * 256 plus the rounding -function packed(pixels: Int32Array, s: number, step: number, weights: Int32Array, w: number, taps: number): number { - let rb = 0x800080, g = 128; +// sums the words of `taps` pixels `step` apart with 8-bit weights, red and blue, green and alpha as 16-bit lanes (the +// weights sum to 256, so a lane holds at most 255 * 256 plus the rounding), rounded back to a word +function packedSum(pixels: Int32Array, s: number, step: number, weights: Int32Array, w: number, taps: number): number { + let rb = 0x800080, ga = 0x800080; for (let k = 0; k < taps; k++, s += step) { const p = pixels[s], wk = weights[w + k]; rb += Math.imul(p & 0xff00ff, wk); - g += (p >> 8 & 255) * wk; + ga += Math.imul(p >>> 8 & 0xff00ff, wk); } - return (rb >>> 8 & 0xff00ff) | (g & 0xff00) | 0xff000000; + return (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); } -/** - * Like {@link resampleOpaque}, but with 8-bit weights in both passes and 8-bit pixels between them, so two channels - * share every multiplication of both passes. More values end up a step off the exact result - */ -function resampleOpaqueFast(src: Uint8ClampedArray, width: number, height: number, outWidth: number, outHeight: number, filter: Filter, out: Uint8ClampedArray): boolean { - // the rounding error of 8-bit weights adds up with the taps - const vertical = coefficients(height, outHeight, filter, 8, true), horizontal = coefficients(width, outWidth, filter, 8, true); - if (vertical.taps > FAST_TAPS || horizontal.taps > FAST_TAPS) return resampleOpaque(src, width, height, outWidth, outHeight, filter, out); - - const pixels = new Int32Array(src.buffer, src.byteOffset, width * height); +// the vertical pass of resampleFast: from `pixels` into `rows` (outHeight rows of the full width), returns the source +// pixels and-ed together (their alpha shows whether they're opaque) +function verticalFast(pixels: Int32Array, rows: Int32Array, width: number, outHeight: number, { start, weights, taps }: Coefficients): number { let all = -1; - for (let x = 0, last = (height - 1) * width; x < width; x++) all &= pixels[x] & pixels[last + x]; - if (all >>> 24 !== 255) return false; - - let rows = pixels; - if (outHeight === height) { - for (let i = 0; i < pixels.length; i++) all &= pixels[i]; - } else { - rows = new Int32Array(width * outHeight); - const { start, weights, taps } = vertical; - for (let y = 0, o = 0; y < outHeight; y++) { - const w = y * taps, r0 = start[y] * width; - if (taps === 4) { - const w0 = weights[w], w1 = weights[w + 1], w2 = weights[w + 2], w3 = weights[w + 3]; - const r1 = r0 + width, r2 = r1 + width, r3 = r2 + width; - for (let x = 0; x < width; x++, o++) { - const p0 = pixels[r0 + x], p1 = pixels[r1 + x], p2 = pixels[r2 + x], p3 = pixels[r3 + x]; - all &= p0 & p1 & p2 & p3; - const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + Math.imul(p2 & 0xff00ff, w2) + Math.imul(p3 & 0xff00ff, w3) + 0x800080; - const g = (p0 >> 8 & 255) * w0 + (p1 >> 8 & 255) * w1 + (p2 >> 8 & 255) * w2 + (p3 >> 8 & 255) * w3 + 128; - rows[o] = (rb >>> 8 & 0xff00ff) | (g & 0xff00) | 0xff000000; - } - } else if (taps === 2) { - const w0 = weights[w], w1 = weights[w + 1], r1 = r0 + width; - for (let x = 0; x < width; x++, o++) { - const p0 = pixels[r0 + x], p1 = pixels[r1 + x]; - all &= p0 & p1; - const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + 0x800080; - const g = (p0 >> 8 & 255) * w0 + (p1 >> 8 & 255) * w1 + 128; - rows[o] = (rb >>> 8 & 0xff00ff) | (g & 0xff00) | 0xff000000; - } - } else { - for (let x = 0; x < width; x++, o++) { - for (let k = 0, s = r0 + x; k < taps; k++, s += width) all &= pixels[s]; - rows[o] = packed(pixels, r0 + x, width, weights, w, taps); - } + for (let y = 0, o = 0; y < outHeight; y++) { + const w = y * taps, r0 = start[y] * width; + if (taps === 4) { + const w0 = weights[w], w1 = weights[w + 1], w2 = weights[w + 2], w3 = weights[w + 3]; + const r1 = r0 + width, r2 = r1 + width, r3 = r2 + width; + for (let x = 0; x < width; x++, o++) { + const p0 = pixels[r0 + x], p1 = pixels[r1 + x], p2 = pixels[r2 + x], p3 = pixels[r3 + x]; + all &= p0 & p1 & p2 & p3; + const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + Math.imul(p2 & 0xff00ff, w2) + Math.imul(p3 & 0xff00ff, w3) + 0x800080; + const ga = Math.imul(p0 >>> 8 & 0xff00ff, w0) + Math.imul(p1 >>> 8 & 0xff00ff, w1) + Math.imul(p2 >>> 8 & 0xff00ff, w2) + Math.imul(p3 >>> 8 & 0xff00ff, w3) + 0x800080; + rows[o] = (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); + } + } else if (taps === 2) { + const w0 = weights[w], w1 = weights[w + 1], r1 = r0 + width; + for (let x = 0; x < width; x++, o++) { + const p0 = pixels[r0 + x], p1 = pixels[r1 + x]; + all &= p0 & p1; + const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + 0x800080; + const ga = Math.imul(p0 >>> 8 & 0xff00ff, w0) + Math.imul(p1 >>> 8 & 0xff00ff, w1) + 0x800080; + rows[o] = (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); + } + } else { + for (let x = 0; x < width; x++, o++) { + for (let k = 0, s = r0 + x; k < taps; k++, s += width) all &= pixels[s]; + rows[o] = packedSum(pixels, r0 + x, width, weights, w, taps); } } } - if (all >>> 24 !== 255) return false; - - const result = new Int32Array(out.buffer, out.byteOffset, outWidth * outHeight); - if (outWidth === width) { - result.set(rows); - return true; - } + return all; +} - const { start, weights, taps } = horizontal; +// the horizontal pass of resampleFast: from `rows` (full width) into `result` +function horizontalFast(rows: Int32Array, result: Int32Array, width: number, outWidth: number, outHeight: number, { start, weights, taps }: Coefficients): void { for (let y = 0, o = 0; y < outHeight; y++) { const row = y * width; if (taps === 4) { @@ -320,135 +299,59 @@ function resampleOpaqueFast(src: Uint8ClampedArray, width: number, height: numbe const p0 = rows[s], p1 = rows[s + 1], p2 = rows[s + 2], p3 = rows[s + 3]; const w0 = weights[w], w1 = weights[w + 1], w2 = weights[w + 2], w3 = weights[w + 3]; const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + Math.imul(p2 & 0xff00ff, w2) + Math.imul(p3 & 0xff00ff, w3) + 0x800080; - const g = (p0 >> 8 & 255) * w0 + (p1 >> 8 & 255) * w1 + (p2 >> 8 & 255) * w2 + (p3 >> 8 & 255) * w3 + 128; - result[o] = (rb >>> 8 & 0xff00ff) | (g & 0xff00) | 0xff000000; + const ga = Math.imul(p0 >>> 8 & 0xff00ff, w0) + Math.imul(p1 >>> 8 & 0xff00ff, w1) + Math.imul(p2 >>> 8 & 0xff00ff, w2) + Math.imul(p3 >>> 8 & 0xff00ff, w3) + 0x800080; + result[o] = (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); } } else if (taps === 2) { for (let x = 0; x < outWidth; x++, o++) { const s = row + start[x], w = x * 2; const p0 = rows[s], p1 = rows[s + 1], w0 = weights[w], w1 = weights[w + 1]; const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + 0x800080; - const g = (p0 >> 8 & 255) * w0 + (p1 >> 8 & 255) * w1 + 128; - result[o] = (rb >>> 8 & 0xff00ff) | (g & 0xff00) | 0xff000000; + const ga = Math.imul(p0 >>> 8 & 0xff00ff, w0) + Math.imul(p1 >>> 8 & 0xff00ff, w1) + 0x800080; + result[o] = (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); } } else { - for (let x = 0; x < outWidth; x++, o++) result[o] = packed(rows, row + start[x], 1, weights, x * taps, taps); + for (let x = 0; x < outWidth; x++, o++) result[o] = packedSum(rows, row + start[x], 1, weights, x * taps, taps); } } - return true; -} - -// 255 / alpha with 16 fractional bits, for unpremultiplying -let reciprocals: Int32Array | undefined; - -// sums the rows `taps` words apart of packed premultiplied pixels with 8-bit weights: red and blue, green and alpha -// share a multiplication each -function packedTransparent(pixels: Int32Array, s: number, step: number, weights: Int32Array, w: number, taps: number): number { - let rb = 0x800080, ga = 0x800080; - for (let k = 0; k < taps; k++, s += step) { - const p = pixels[s], wk = weights[w + k]; - rb += Math.imul(p & 0xff00ff, wk); - ga += Math.imul(p >>> 8 & 0xff00ff, wk); - } - return (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); } /** - * Like {@link resampleOpaqueFast} for any pixels: they are premultiplied to 8 bits (as browsers blur and scale), both - * passes take two multiplications per pixel, and the result is unpremultiplied. Pixels that are almost transparent - * keep less of their color. Returns false (without writing anything) for too many taps + * Resamples with 8-bit weights in both passes and 8-bit pixels (as 32-bit words, little endian) between them, so two + * channels share every multiplication. Transparent pixels are premultiplied to 8 bits (as browsers do), which keeps + * less of the color of pixels that are almost transparent. Returns false (without writing anything) when a pass + * needs more than {@link FAST_TAPS} pixels: the rounding of the weights adds up */ -function resampleTransparentFast(src: Uint8ClampedArray, width: number, height: number, outWidth: number, outHeight: number, filter: Filter, out: Uint8ClampedArray): boolean { +function resampleFast(src: Uint8ClampedArray, width: number, height: number, outWidth: number, outHeight: number, filter: Filter, out: Uint8ClampedArray): boolean { const vertical = coefficients(height, outHeight, filter, 8, true), horizontal = coefficients(width, outWidth, filter, 8, true); if (vertical.taps > FAST_TAPS || horizontal.taps > FAST_TAPS) return false; - // premultiplied: channel * alpha / 255, rounded - const source = new Int32Array(src.buffer, src.byteOffset, width * height); - const pixels = new Int32Array(width * height); - for (let i = 0; i < pixels.length; i++) { - const p = source[i], a = p >>> 24; - if (a === 255) { pixels[i] = p; continue; } - let rb = Math.imul(p & 0xff00ff, a) + 0x800080; - rb = (rb + (rb >>> 8 & 0xff00ff)) >>> 8 & 0xff00ff; - let g = (p >> 8 & 255) * a + 128; - g = g + (g >> 8) & 0xff00; - pixels[i] = rb | g | a << 24; - } + // transparent images usually show it in the first or last row, otherwise the vertical pass finds out (and runs + // again on the premultiplied pixels) + let pixels = new Int32Array(src.buffer, src.byteOffset, width * height), all = -1; + for (let x = 0, last = (height - 1) * width; x < width; x++) all &= pixels[x] & pixels[last + x]; + let opaque = all >>> 24 === 255 && (outHeight !== height || isOpaque(src)); + const premultiplied = (): Int32Array => { + const bitmap = new Uint8ClampedArray(src.length); + premultiply8(src, bitmap); + return new Int32Array(bitmap.buffer); + }; + if (!opaque) pixels = premultiplied(); let rows = pixels; if (outHeight !== height) { rows = new Int32Array(width * outHeight); - const { start, weights, taps } = vertical; - for (let y = 0, o = 0; y < outHeight; y++) { - const w = y * taps, r0 = start[y] * width; - if (taps === 4) { - const w0 = weights[w], w1 = weights[w + 1], w2 = weights[w + 2], w3 = weights[w + 3]; - const r1 = r0 + width, r2 = r1 + width, r3 = r2 + width; - for (let x = 0; x < width; x++, o++) { - const p0 = pixels[r0 + x], p1 = pixels[r1 + x], p2 = pixels[r2 + x], p3 = pixels[r3 + x]; - const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + Math.imul(p2 & 0xff00ff, w2) + Math.imul(p3 & 0xff00ff, w3) + 0x800080; - const ga = Math.imul(p0 >>> 8 & 0xff00ff, w0) + Math.imul(p1 >>> 8 & 0xff00ff, w1) + Math.imul(p2 >>> 8 & 0xff00ff, w2) + Math.imul(p3 >>> 8 & 0xff00ff, w3) + 0x800080; - rows[o] = (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); - } - } else if (taps === 2) { - const w0 = weights[w], w1 = weights[w + 1], r1 = r0 + width; - for (let x = 0; x < width; x++, o++) { - const p0 = pixels[r0 + x], p1 = pixels[r1 + x]; - const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + 0x800080; - const ga = Math.imul(p0 >>> 8 & 0xff00ff, w0) + Math.imul(p1 >>> 8 & 0xff00ff, w1) + 0x800080; - rows[o] = (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); - } - } else { - for (let x = 0; x < width; x++, o++) rows[o] = packedTransparent(pixels, r0 + x, width, weights, w, taps); - } + if (verticalFast(pixels, rows, width, outHeight, vertical) >>> 24 !== 255 && opaque) { + opaque = false; + verticalFast(pixels = premultiplied(), rows, width, outHeight, vertical); } } const result = new Int32Array(out.buffer, out.byteOffset, outWidth * outHeight); - if (outWidth === width) { - result.set(rows); - } else { - const { start, weights, taps } = horizontal; - for (let y = 0, o = 0; y < outHeight; y++) { - const row = y * width; - if (taps === 4) { - for (let x = 0; x < outWidth; x++, o++) { - const s = row + start[x], w = x * 4; - const p0 = rows[s], p1 = rows[s + 1], p2 = rows[s + 2], p3 = rows[s + 3]; - const w0 = weights[w], w1 = weights[w + 1], w2 = weights[w + 2], w3 = weights[w + 3]; - const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + Math.imul(p2 & 0xff00ff, w2) + Math.imul(p3 & 0xff00ff, w3) + 0x800080; - const ga = Math.imul(p0 >>> 8 & 0xff00ff, w0) + Math.imul(p1 >>> 8 & 0xff00ff, w1) + Math.imul(p2 >>> 8 & 0xff00ff, w2) + Math.imul(p3 >>> 8 & 0xff00ff, w3) + 0x800080; - result[o] = (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); - } - } else if (taps === 2) { - for (let x = 0; x < outWidth; x++, o++) { - const s = row + start[x], w = x * 2; - const p0 = rows[s], p1 = rows[s + 1], w0 = weights[w], w1 = weights[w + 1]; - const rb = Math.imul(p0 & 0xff00ff, w0) + Math.imul(p1 & 0xff00ff, w1) + 0x800080; - const ga = Math.imul(p0 >>> 8 & 0xff00ff, w0) + Math.imul(p1 >>> 8 & 0xff00ff, w1) + 0x800080; - result[o] = (rb >>> 8 & 0xff00ff) | (ga & 0xff00ff00); - } - } else { - for (let x = 0; x < outWidth; x++, o++) result[o] = packedTransparent(rows, row + start[x], 1, weights, x * taps, taps); - } - } - } + if (outWidth === width) result.set(rows); + else horizontalFast(rows, result, width, outWidth, outHeight, horizontal); - // unpremultiplied: channel * 255 / alpha, clamped (the weighted sums can round a channel above its alpha) - if (!reciprocals) { - reciprocals = new Int32Array(256); - for (let a = 1; a < 256; a++) reciprocals[a] = Math.round(255 * 65536 / a); - } - for (let i = 0; i < result.length; i++) { - const p = result[i], a = p >>> 24; - if (a === 255) continue; - if (a === 0) { result[i] = 0; continue; } - const f = reciprocals[a]; - const r = Math.min(255, Math.imul(p & 255, f) + 32768 >>> 16); - const g = Math.min(255, Math.imul(p >> 8 & 255, f) + 32768 >>> 16); - const b = Math.min(255, Math.imul(p >> 16 & 255, f) + 32768 >>> 16); - result[i] = r | g << 8 | b << 16 | a << 24; - } + if (!opaque) unpremultiply8(out); return true; } diff --git a/tests/resize.js b/tests/resize.js index 9691942e..15f151be 100644 --- a/tests/resize.js +++ b/tests/resize.js @@ -19,8 +19,11 @@ const samePixels = require('./util/pixels'); const opaque = (await Image.decode(await fs.readFile('./bench/inputs/photo.jpg'))).crop(700, 300, 400, 300); const transparent = opaque.clone(); for (let i = 0; i < transparent.bitmap.length; i += 4) transparent.bitmap[i + 3] = (i >> 2) % transparent.width * 255 / (transparent.width - 1); + // only one translucent pixel in the middle, found by the vertical pass + const spot = opaque.clone(); + spot.bitmap[(spot.height >> 1) * spot.width * 4 + 403] = 128; const sizes = [[0.5, 0.5], [1 / 3, 0.4], [2, 1.5], [1, 0.7]]; - for (const source of [opaque, transparent]) + for (const source of [opaque, transparent, spot]) for (const mode of [Image.RESIZE_BILINEAR, Image.RESIZE_AREA, Image.RESIZE_BICUBIC]) for (const [sx, sy] of sizes) { const width = Math.round(source.width * sx), height = Math.round(source.height * sy); @@ -34,7 +37,7 @@ const samePixels = require('./util/pixels'); max = Math.max(max, source === opaque || alpha ? Math.abs(a[i] - b[i]) : Math.abs(a[i] * al - b[i] * bl) / 255); } if (max > limit || mode !== Image.RESIZE_BICUBIC && sx === 0.5 && !differ) { - console.error('fast resize', mode, sx, sy, source === opaque ? 'opaque' : 'transparent', max); + console.error('fast resize', mode, sx, sy, source === opaque ? 'opaque' : source === spot ? 'spot' : 'transparent', max); process.exit(1); } }