diff --git a/codecs/resize/.gitignore b/codecs/resize/.gitignore index 53f30e50..45db9b3b 100644 --- a/codecs/resize/.gitignore +++ b/codecs/resize/.gitignore @@ -3,3 +3,4 @@ target Cargo.lock bin/ pkg/README.md +lut.inc diff --git a/codecs/resize/Cargo.toml b/codecs/resize/Cargo.toml index b1f14fe6..cd47881c 100644 --- a/codecs/resize/Cargo.toml +++ b/codecs/resize/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "resize" +name = "squooshresize" version = "0.1.0" authors = ["Surma "] diff --git a/codecs/resize/benchmark.js b/codecs/resize/benchmark.js index 5053cbde..4182f611 100644 --- a/codecs/resize/benchmark.js +++ b/codecs/resize/benchmark.js @@ -4,7 +4,7 @@ // [1]: https://github.com/GoogleChromeLabs/jsvu self = global = this; -load('./pkg/resize.js'); +load("./pkg/resize.js"); async function init() { // Adjustable constants. @@ -19,23 +19,35 @@ async function init() { 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; + [[false, false], [true, false], [false, true], [true, true]].forEach( + opts => { + print(`\npremultiplication: ${opts[0]}`); + print(`color space conversion: ${opts[1]}`); + print(`==============================`); + for (let i = 0; i < 100; i++) { + const start = Date.now(); + wasm_bindgen.resize( + imageBuffer, + inputDimensions, + inputDimensions, + outputDimensions, + outputDimensions, + algorithm, + ...opts + ); + 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}`); } - 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/build.rs b/codecs/resize/build.rs new file mode 100644 index 00000000..8feb48a8 --- /dev/null +++ b/codecs/resize/build.rs @@ -0,0 +1,23 @@ +include!("./src/srgb.rs"); + +use std::io::Write; + +fn main() -> std::io::Result<()> { + let mut srgb_to_linear_lut = String::from("static SRGB_TO_LINEAR_LUT: [f32; 256] = ["); + let mut linear_to_srgb_lut = String::from("static LINEAR_TO_SRGB_LUT: [f32; 256] = ["); + for i in 0..256 { + srgb_to_linear_lut.push_str(&format!("{0:.7}", srgb_to_linear((i as f32) / 255.0))); + srgb_to_linear_lut.push_str(","); + linear_to_srgb_lut.push_str(&format!("{0:.7}", linear_to_srgb((i as f32) / 255.0))); + linear_to_srgb_lut.push_str(","); + } + srgb_to_linear_lut.pop().unwrap(); + linear_to_srgb_lut.pop().unwrap(); + srgb_to_linear_lut.push_str("];"); + linear_to_srgb_lut.push_str("];"); + + let mut file = std::fs::File::create("src/lut.inc")?; + file.write_all(srgb_to_linear_lut.as_bytes())?; + file.write_all(linear_to_srgb_lut.as_bytes())?; + Ok(()) +} diff --git a/codecs/resize/pkg/resize.d.ts b/codecs/resize/pkg/resize.d.ts index 9e5b957e..b0cc0a7b 100644 --- a/codecs/resize/pkg/resize.d.ts +++ b/codecs/resize/pkg/resize.d.ts @@ -7,6 +7,7 @@ * @param {number} arg4 * @param {number} arg5 * @param {boolean} arg6 +* @param {boolean} arg7 * @returns {Uint8Array} */ -export function resize(arg0: Uint8Array, arg1: number, arg2: number, arg3: number, arg4: number, arg5: number, arg6: boolean): Uint8Array; +export function resize(arg0: Uint8Array, arg1: number, arg2: number, arg3: number, arg4: number, arg5: number, arg6: boolean, arg7: boolean): Uint8Array; diff --git a/codecs/resize/pkg/resize.js b/codecs/resize/pkg/resize.js index 9dffc88b..656cee33 100644 --- a/codecs/resize/pkg/resize.js +++ b/codecs/resize/pkg/resize.js @@ -47,13 +47,14 @@ * @param {number} arg4 * @param {number} arg5 * @param {boolean} arg6 + * @param {boolean} arg7 * @returns {Uint8Array} */ - __exports.resize = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + __exports.resize = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { const ptr0 = passArray8ToWasm(arg0); const len0 = WASM_VECTOR_LEN; const retptr = globalArgumentPtr(); - wasm.resize(retptr, ptr0, len0, arg1, arg2, arg3, arg4, arg5, arg6); + wasm.resize(retptr, ptr0, len0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); 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 70e7200b..d8049a99 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, i: number): void; +export function resize(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number): void; diff --git a/codecs/resize/pkg/resize_bg.wasm b/codecs/resize/pkg/resize_bg.wasm index 54a6472f..dadb009e 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 d3229f8a..581de2cb 100644 --- a/codecs/resize/src/lib.rs +++ b/codecs/resize/src/lib.rs @@ -9,6 +9,9 @@ use resize::Pixel::RGBA; use resize::Type; use wasm_bindgen::prelude::*; +mod srgb; +use srgb::Clamp; + cfg_if! { // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. @@ -19,6 +22,39 @@ cfg_if! { } } +include!("./lut.inc"); + +// If `with_space_conversion` is true, this function returns 2 functions that +// convert from sRGB to linear RGB and vice versa. If `with_space_conversion` is +// false, the 2 functions returned do nothing. +fn converter_funcs(with_space_conversion: bool) -> ((fn(u8) -> f32), (fn(f32) -> u8)) { + if with_space_conversion { + ( + |v| SRGB_TO_LINEAR_LUT[v as usize] * 255.0, + |v| (LINEAR_TO_SRGB_LUT[v as usize] * 255.0) as u8, + ) + } else { + (|v| v as f32, |v| v as u8) + } +} + +// If `with_alpha_premultiplication` is true, this function returns a function +// that premultiply the alpha channel with the given channel value and another +// function that reverses that process. If `with_alpha_premultiplication` is +// false, the functions just return the channel value. +fn alpha_multiplier_funcs( + with_alpha_premultiplication: bool, +) -> ((fn(f32, u8) -> u8), (fn(u8, u8) -> f32)) { + if with_alpha_premultiplication { + ( + |v, a| (v * (a as f32) / 255.0) as u8, + |v, a| (v as f32) * 255.0 / (a as f32).clamp(0.0, 255.0), + ) + } else { + (|v, _a| v as u8, |v, _a| v as f32) + } +} + #[wasm_bindgen] #[no_mangle] pub fn resize( @@ -29,6 +65,7 @@ pub fn resize( output_height: usize, typ_idx: usize, premultiply: bool, + color_space_conversion: bool, ) -> Vec { let typ = match typ_idx { 0 => Type::Triangle, @@ -40,12 +77,16 @@ pub fn resize( let num_input_pixels = input_width * input_height; let num_output_pixels = output_width * output_height; - if premultiply { + let (to_linear, to_color_space) = converter_funcs(color_space_conversion); + let (premultiplier, demultiplier) = alpha_multiplier_funcs(premultiply); + + // If both options are false, there is no preprocessing on the pixel valus + // and we can skip the loop. + if premultiply || color_space_conversion { 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; + input_image[4 * i + j] = + premultiplier(to_linear(input_image[4 * i + j]), input_image[4 * i + 3]); } } } @@ -62,15 +103,16 @@ pub fn resize( output_image.resize(num_output_pixels * 4, 0); resizer.resize(input_image.as_slice(), output_image.as_mut_slice()); - if premultiply { + if premultiply || color_space_conversion { 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 + // 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; + output_image[4 * i + j] = to_color_space(demultiplier( + output_image[4 * i + j], + output_image[4 * i + 3], + )); } } } diff --git a/codecs/resize/src/srgb.rs b/codecs/resize/src/srgb.rs new file mode 100644 index 00000000..5781c5fd --- /dev/null +++ b/codecs/resize/src/srgb.rs @@ -0,0 +1,29 @@ +pub trait Clamp: std::cmp::PartialOrd + Sized { + fn clamp(self, min: Self, max: Self) -> Self { + if self.lt(&min) { + min + } else if self.gt(&max) { + max + } else { + self + } + } +} + +impl Clamp for f32 {} + +pub fn srgb_to_linear(v: f32) -> f32 { + if v < 0.04045 { + v / 12.92 + } else { + ((v + 0.055) / 1.055).powf(2.4).clamp(0.0, 1.0) + } +} + +pub fn linear_to_srgb(v: f32) -> f32 { + if v < 0.0031308 { + v * 12.92 + } else { + (1.055 * v.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0) + } +} diff --git a/src/codecs/resize/options.tsx b/src/codecs/resize/options.tsx index 3c6fc6e7..1b856d75 100644 --- a/src/codecs/resize/options.tsx +++ b/src/codecs/resize/options.tsx @@ -19,13 +19,11 @@ interface Props { interface State { maintainAspect: boolean; - premultiply: boolean; } export default class ResizerOptions extends Component { state: State = { maintainAspect: true, - premultiply: true, }; form?: HTMLFormElement; @@ -43,6 +41,7 @@ export default class ResizerOptions extends Component { height: inputFieldValueAsNumber(height), method: form.resizeMethod.value, premultiply: inputFieldChecked(form.premultiply, true), + linearRGB: inputFieldChecked(form.linearRGB, true), // Casting, as the formfield only returns the correct values. fitMethod: inputFieldValue(form.fitMethod, options.fitMethod) as ResizeOptions['fitMethod'], }; @@ -95,7 +94,7 @@ export default class ResizerOptions extends Component { - + @@ -138,6 +137,17 @@ export default class ResizerOptions extends Component { : null } + {isWorkerOptions(options) ? + + : null + }