Skip to content

Commit

Permalink
Implement alpha premultiplication (GoogleChromeLabs#507)
Browse files Browse the repository at this point in the history
* Implement alpha premultiplication

* Add benchmark to resize

* Only display "Premultiply alpha" if it's one of the rust resize types.

* Add comment about division by zero
  • Loading branch information
surma authored and jakearchibald committed Mar 8, 2019
1 parent d29cf2f commit 45221c0
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 35 deletions.
41 changes: 41 additions & 0 deletions codecs/resize/benchmark.js
Original file line number Diff line number Diff line change
@@ -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));
3 changes: 2 additions & 1 deletion codecs/resize/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion codecs/resize/pkg/resize.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 3 additions & 2 deletions codecs/resize/pkg/resize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
2 changes: 1 addition & 1 deletion codecs/resize/pkg/resize_bg.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Binary file modified codecs/resize/pkg/resize_bg.wasm
Binary file not shown.
29 changes: 28 additions & 1 deletion codecs/resize/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ cfg_if! {
#[wasm_bindgen]
#[no_mangle]
pub fn resize(
input_image: Vec<u8>,
mut input_image: Vec<u8>,
input_width: usize,
input_height: usize,
output_width: usize,
output_height: usize,
typ_idx: usize,
premultiply: bool,
) -> Vec<u8> {
let typ = match typ_idx {
0 => Type::Triangle,
Expand All @@ -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,
Expand All @@ -48,5 +61,19 @@ pub fn resize(
let mut output_image = Vec::<u8>::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;
}
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions src/codecs/resize/options.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,11 +19,13 @@ interface Props {

interface State {
maintainAspect: boolean;
premultiply: boolean;
}

export default class ResizerOptions extends Component<Props, State> {
state: State = {
maintainAspect: true,
premultiply: true,
};

form?: HTMLFormElement;
Expand All @@ -38,6 +42,7 @@ export default class ResizerOptions extends Component<Props, State> {
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'],
};
Expand Down Expand Up @@ -121,6 +126,19 @@ export default class ResizerOptions extends Component<Props, State> {
onInput={this.onHeightInput}
/>
</label>
<Expander>
{isWorkerOptions(options) ?
<label class={style.optionInputFirst}>
<Checkbox
name="premultiply"
checked={options.premultiply}
onChange={this.onChange}
/>
Premultiply alpha channel
</label>
: null
}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
name="maintainAspect"
Expand Down
25 changes: 19 additions & 6 deletions src/codecs/resize/processor-meta.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
type BrowserResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high';
type WorkerResizeMethods = 'point' | 'triangle' | 'catrom' | 'mitchell' | 'lanczos3';
type WorkerResizeMethods = 'triangle' | 'catrom' | 'mitchell' | 'lanczos3';
const workerResizeMethods: WorkerResizeMethods[] = ['triangle', 'catrom', 'mitchell', 'lanczos3'];

export interface ResizeOptions {
export type ResizeOptions = BrowserResizeOptions | WorkerResizeOptions | VectorResizeOptions;

export interface ResizeOptionsCommon {
width: number;
height: number;
method: 'vector' | BrowserResizeMethods | WorkerResizeMethods;
fitMethod: 'stretch' | 'contain';
}

export interface BrowserResizeOptions extends ResizeOptions {
export interface BrowserResizeOptions extends ResizeOptionsCommon {
method: BrowserResizeMethods;
}

export interface WorkerResizeOptions extends ResizeOptions {
export interface WorkerResizeOptions extends ResizeOptionsCommon {
method: WorkerResizeMethods;
premultiply: boolean;
}

export interface VectorResizeOptions extends ResizeOptions {
export interface VectorResizeOptions extends ResizeOptionsCommon {
method: 'vector';
}

/**
* Return whether a set of options are worker resize options.
*
* @param opts
*/
export function isWorkerOptions(opts: ResizeOptions): opts is WorkerResizeOptions {
return (workerResizeMethods as string[]).includes(opts.method);
}

export const defaultOptions: ResizeOptions = {
// Width and height will always default to the image size.
// This is set elsewhere.
Expand All @@ -28,4 +40,5 @@ export const defaultOptions: ResizeOptions = {
// This will be set to 'vector' if the input is SVG.
method: 'lanczos3',
fitMethod: 'stretch',
premultiply: true,
};
2 changes: 1 addition & 1 deletion src/codecs/resize/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function resize(data: ImageData, opts: WorkerResizeOptions): Promis

const result = wasm_bindgen.resize(
new Uint8Array(input.data.buffer), input.width, input.height, opts.width, opts.height,
resizeMethods.indexOf(opts.method),
resizeMethods.indexOf(opts.method), opts.premultiply,
);

return new ImageData(new Uint8ClampedArray(result.buffer), opts.width, opts.height);
Expand Down
24 changes: 7 additions & 17 deletions src/components/compress/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,13 @@ import * as browserTIFF from '../../codecs/browser-tiff/encoder-meta';
import * as browserJP2 from '../../codecs/browser-jp2/encoder-meta';
import * as browserBMP from '../../codecs/browser-bmp/encoder-meta';
import * as browserPDF from '../../codecs/browser-pdf/encoder-meta';
import {
EncoderState,
EncoderType,
EncoderOptions,
encoderMap,
} from '../../codecs/encoders';
import {
PreprocessorState,
defaultPreprocessorState,
} from '../../codecs/preprocessors';
import { EncoderState, EncoderType, EncoderOptions, encoderMap } from '../../codecs/encoders';
import { PreprocessorState, defaultPreprocessorState } from '../../codecs/preprocessors';
import { decodeImage } from '../../codecs/decoders';
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
import Processor from '../../codecs/processor';
import {
VectorResizeOptions,
BrowserResizeOptions,
WorkerResizeOptions,
BrowserResizeOptions, isWorkerOptions as isWorkerResizeOptions,
} from '../../codecs/resize/processor-meta';
import './custom-els/MultiPanel';
import Results from '../results';
Expand Down Expand Up @@ -114,12 +104,12 @@ async function preprocessImage(
if (preprocessData.resize.method === 'vector' && source.vectorImage) {
result = processor.vectorResize(
source.vectorImage,
preprocessData.resize as VectorResizeOptions,
preprocessData.resize,
);
} else if (preprocessData.resize.method.startsWith('browser-')) {
result = processor.resize(result, preprocessData.resize as BrowserResizeOptions);
} else if (isWorkerResizeOptions(preprocessData.resize)) {
result = await processor.workerResize(result, preprocessData.resize);
} else {
result = await processor.workerResize(result, preprocessData.resize as WorkerResizeOptions);
result = processor.resize(result, preprocessData.resize as BrowserResizeOptions);
}
}
if (preprocessData.quantizer.enabled) {
Expand Down

0 comments on commit 45221c0

Please sign in to comment.