diff --git a/codecs/resize/benchmark.js b/codecs/resize/benchmark.js new file mode 100644 index 00000000..5053cbde --- /dev/null +++ b/codecs/resize/benchmark.js @@ -0,0 +1,41 @@ +// THIS IS NOT A NODE SCRIPT +// This is a d8 script. Please install jsvu[1] and install v8. +// Then run `npm run --silent benchmark`. +// [1]: https://github.com/GoogleChromeLabs/jsvu + +self = global = this; +load('./pkg/resize.js'); + +async function init() { + // Adjustable constants. + const inputDimensions = 2000; + const outputDimensions = 1500; + const algorithm = 3; // Lanczos + const iterations = new Array(100); + + // Constants. Don’t change. + const imageByteSize = inputDimensions * inputDimensions * 4; + const imageBuffer = new Uint8ClampedArray(imageByteSize); + + const module = await WebAssembly.compile(readbuffer("./pkg/resize_bg.wasm")); + await wasm_bindgen(module); + [false, true].forEach(premulti => { + print(`\npremultiplication: ${premulti}`); + print(`==============================`); + for (let i = 0; i < 100; i++) { + const start = Date.now(); + wasm_bindgen.resize(imageBuffer, inputDimensions, inputDimensions, outputDimensions, outputDimensions, algorithm, premulti); + iterations[i] = Date.now() - start; + } + const average = iterations.reduce((sum, c) => sum + c) / iterations.length; + const stddev = Math.sqrt( + iterations + .map(i => Math.pow(i - average, 2)) + .reduce((sum, c) => sum + c) / iterations.length + ); + print(`n = ${iterations.length}`); + print(`Average: ${average}`); + print(`StdDev: ${stddev}`); + }); +} +init().catch(e => console.error(e, e.stack)); diff --git a/codecs/resize/package.json b/codecs/resize/package.json index 439f4262..1786a997 100644 --- a/codecs/resize/package.json +++ b/codecs/resize/package.json @@ -2,6 +2,7 @@ "name": "resize", "scripts": { "build:image": "docker build -t squoosh-resize .", - "build": "docker run --rm -v $(pwd):/src squoosh-resize ./build.sh" + "build": "docker run --rm -v $(pwd):/src squoosh-resize ./build.sh", + "benchmark": "v8 --no-liftoff --no-wasm-tier-up ./benchmark.js" } } diff --git a/codecs/resize/pkg/resize.d.ts b/codecs/resize/pkg/resize.d.ts index 3f68ed7b..9e5b957e 100644 --- a/codecs/resize/pkg/resize.d.ts +++ b/codecs/resize/pkg/resize.d.ts @@ -6,6 +6,7 @@ * @param {number} arg3 * @param {number} arg4 * @param {number} arg5 +* @param {boolean} arg6 * @returns {Uint8Array} */ -export function resize(arg0: Uint8Array, arg1: number, arg2: number, arg3: number, arg4: number, arg5: number): Uint8Array; +export function resize(arg0: Uint8Array, arg1: number, arg2: number, arg3: number, arg4: number, arg5: number, arg6: boolean): Uint8Array; diff --git a/codecs/resize/pkg/resize.js b/codecs/resize/pkg/resize.js index ec76ff7d..9dffc88b 100644 --- a/codecs/resize/pkg/resize.js +++ b/codecs/resize/pkg/resize.js @@ -46,13 +46,14 @@ * @param {number} arg3 * @param {number} arg4 * @param {number} arg5 + * @param {boolean} arg6 * @returns {Uint8Array} */ - __exports.resize = function(arg0, arg1, arg2, arg3, arg4, arg5) { + __exports.resize = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { const ptr0 = passArray8ToWasm(arg0); const len0 = WASM_VECTOR_LEN; const retptr = globalArgumentPtr(); - wasm.resize(retptr, ptr0, len0, arg1, arg2, arg3, arg4, arg5); + wasm.resize(retptr, ptr0, len0, arg1, arg2, arg3, arg4, arg5, arg6); const mem = getUint32Memory(); const rustptr = mem[retptr / 4]; const rustlen = mem[retptr / 4 + 1]; diff --git a/codecs/resize/pkg/resize_bg.d.ts b/codecs/resize/pkg/resize_bg.d.ts index 58426e0e..70e7200b 100644 --- a/codecs/resize/pkg/resize_bg.d.ts +++ b/codecs/resize/pkg/resize_bg.d.ts @@ -3,4 +3,4 @@ export const memory: WebAssembly.Memory; export function __wbindgen_global_argument_ptr(): number; export function __wbindgen_malloc(a: number): number; export function __wbindgen_free(a: number, b: number): void; -export function resize(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number): void; +export function resize(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number): void; diff --git a/codecs/resize/pkg/resize_bg.wasm b/codecs/resize/pkg/resize_bg.wasm index f7d98afd..54a6472f 100644 Binary files a/codecs/resize/pkg/resize_bg.wasm and b/codecs/resize/pkg/resize_bg.wasm differ diff --git a/codecs/resize/src/lib.rs b/codecs/resize/src/lib.rs index bf04a497..d3229f8a 100644 --- a/codecs/resize/src/lib.rs +++ b/codecs/resize/src/lib.rs @@ -22,12 +22,13 @@ cfg_if! { #[wasm_bindgen] #[no_mangle] pub fn resize( - input_image: Vec, + mut input_image: Vec, input_width: usize, input_height: usize, output_width: usize, output_height: usize, typ_idx: usize, + premultiply: bool, ) -> Vec { let typ = match typ_idx { 0 => Type::Triangle, @@ -36,7 +37,19 @@ pub fn resize( 3 => Type::Lanczos3, _ => panic!("Nope"), }; + let num_input_pixels = input_width * input_height; let num_output_pixels = output_width * output_height; + + if premultiply { + for i in 0..num_input_pixels { + for j in 0..3 { + input_image[4 * i + j] = ((input_image[4 * i + j] as f32) + * (input_image[4 * i + 3] as f32) + / 255.0) as u8; + } + } + } + let mut resizer = resize::new( input_width, input_height, @@ -48,5 +61,19 @@ pub fn resize( let mut output_image = Vec::::with_capacity(num_output_pixels * 4); output_image.resize(num_output_pixels * 4, 0); resizer.resize(input_image.as_slice(), output_image.as_mut_slice()); + + if premultiply { + for i in 0..num_output_pixels { + for j in 0..3 { + // We don’t need to worry about division by zero, as division by zero + // is well-defined on floats to return `±Inf`. ±Inf is converted to 0 + // when casting to integers. + output_image[4 * i + j] = ((output_image[4 * i + j] as f32) * 255.0 + / (output_image[4 * i + 3] as f32)) + as u8; + } + } + } + return output_image; } diff --git a/package-lock.json b/package-lock.json index d7b30c75..4bc685fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15233,7 +15233,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, @@ -15245,7 +15245,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { @@ -15295,7 +15295,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, diff --git a/src/codecs/resize/options.tsx b/src/codecs/resize/options.tsx index cc9dc15a..3c6fc6e7 100644 --- a/src/codecs/resize/options.tsx +++ b/src/codecs/resize/options.tsx @@ -1,8 +1,10 @@ import { h, Component } from 'preact'; import linkState from 'linkstate'; import { bind, linkRef } from '../../lib/initial-util'; -import { inputFieldValueAsNumber, inputFieldValue, preventDefault } from '../../lib/util'; -import { ResizeOptions } from './processor-meta'; +import { + inputFieldValueAsNumber, inputFieldValue, preventDefault, inputFieldChecked, +} from '../../lib/util'; +import { ResizeOptions, isWorkerOptions } from './processor-meta'; import * as style from '../../components/Options/style.scss'; import Checkbox from '../../components/checkbox'; import Expander from '../../components/expander'; @@ -17,11 +19,13 @@ interface Props { interface State { maintainAspect: boolean; + premultiply: boolean; } export default class ResizerOptions extends Component { state: State = { maintainAspect: true, + premultiply: true, }; form?: HTMLFormElement; @@ -38,6 +42,7 @@ export default class ResizerOptions extends Component { width: inputFieldValueAsNumber(width), height: inputFieldValueAsNumber(height), method: form.resizeMethod.value, + premultiply: inputFieldChecked(form.premultiply, true), // Casting, as the formfield only returns the correct values. fitMethod: inputFieldValue(form.fitMethod, options.fitMethod) as ResizeOptions['fitMethod'], }; @@ -121,6 +126,19 @@ export default class ResizerOptions extends Component { onInput={this.onHeightInput} /> + + {isWorkerOptions(options) ? + + : null + } +