From 9e5b66d5f411dafd1f49c1b587f5e54fa9595c5d Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Wed, 6 Mar 2019 17:20:25 +0000 Subject: [PATCH] Better resize methods (#498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Port resize to wasm * Expose resize algorithms * Lanczos3 working! * lol copy paste * Adding support for other resizers * Don’t track generated README * Cache wasm instance --- codecs/resize/.gitignore | 5 ++ codecs/resize/Cargo.toml | 37 +++++++++ codecs/resize/Dockerfile | 18 +++++ codecs/resize/build.sh | 22 ++++++ codecs/resize/package-lock.json | 4 + codecs/resize/package.json | 7 ++ codecs/resize/pkg/resize.d.ts | 11 +++ codecs/resize/pkg/resize.js | 112 +++++++++++++++++++++++++++ codecs/resize/pkg/resize_bg.d.ts | 6 ++ codecs/resize/pkg/resize_bg.wasm | Bin 0 -> 16956 bytes codecs/resize/src/lib.rs | 52 +++++++++++++ codecs/resize/src/utils.rs | 17 ++++ package.json | 2 +- src/codecs/imagequant/processor.ts | 4 +- src/codecs/mozjpeg/encoder.ts | 4 +- src/codecs/optipng/encoder.ts | 4 +- src/codecs/processor-worker/index.ts | 12 ++- src/codecs/processor.ts | 15 +++- src/codecs/resize/options.tsx | 4 + src/codecs/resize/processor-meta.ts | 15 ++-- src/codecs/resize/processor-sync.ts | 35 +++++++++ src/codecs/resize/processor.ts | 73 ++++++++--------- src/codecs/resize/util.ts | 14 ++++ src/codecs/util.ts | 2 +- src/codecs/webp/decoder.ts | 4 +- src/codecs/webp/encoder.ts | 4 +- src/components/compress/index.tsx | 12 ++- 27 files changed, 435 insertions(+), 60 deletions(-) create mode 100644 codecs/resize/.gitignore create mode 100644 codecs/resize/Cargo.toml create mode 100644 codecs/resize/Dockerfile create mode 100755 codecs/resize/build.sh create mode 100644 codecs/resize/package-lock.json create mode 100644 codecs/resize/package.json create mode 100644 codecs/resize/pkg/resize.d.ts create mode 100644 codecs/resize/pkg/resize.js create mode 100644 codecs/resize/pkg/resize_bg.d.ts create mode 100644 codecs/resize/pkg/resize_bg.wasm create mode 100644 codecs/resize/src/lib.rs create mode 100644 codecs/resize/src/utils.rs create mode 100644 src/codecs/resize/processor-sync.ts create mode 100644 src/codecs/resize/util.ts diff --git a/codecs/resize/.gitignore b/codecs/resize/.gitignore new file mode 100644 index 00000000..53f30e50 --- /dev/null +++ b/codecs/resize/.gitignore @@ -0,0 +1,5 @@ +**/*.rs.bk +target +Cargo.lock +bin/ +pkg/README.md diff --git a/codecs/resize/Cargo.toml b/codecs/resize/Cargo.toml new file mode 100644 index 00000000..b1f14fe6 --- /dev/null +++ b/codecs/resize/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "resize" +version = "0.1.0" +authors = ["Surma "] + +[lib] +#crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib"] + +[features] +default = ["console_error_panic_hook", "wee_alloc"] + +[dependencies] +cfg-if = "0.1.2" +wasm-bindgen = "0.2.38" +resize = "0.3.0" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.1", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.2", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.2" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" +lto = true diff --git a/codecs/resize/Dockerfile b/codecs/resize/Dockerfile new file mode 100644 index 00000000..1142b581 --- /dev/null +++ b/codecs/resize/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu +RUN apt-get update && \ + apt-get install -qqy git build-essential cmake python2.7 +RUN git clone --recursive https://github.com/WebAssembly/wabt /usr/src/wabt +RUN mkdir -p /usr/src/wabt/build +WORKDIR /usr/src/wabt/build +RUN cmake .. -DCMAKE_INSTALL_PREFIX=/opt/wabt && \ + make && \ + make install + +FROM rust +RUN rustup install nightly && \ + rustup target add --toolchain nightly wasm32-unknown-unknown && \ + cargo install wasm-pack + +COPY --from=0 /opt/wabt /opt/wabt +ENV PATH="/opt/wabt/bin:${PATH}" +WORKDIR /src diff --git a/codecs/resize/build.sh b/codecs/resize/build.sh new file mode 100755 index 00000000..240b16ba --- /dev/null +++ b/codecs/resize/build.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +echo "=============================================" +echo "Compiling wasm" +echo "=============================================" +( + rustup run nightly \ + wasm-pack build --target no-modules + wasm-strip pkg/resize_bg.wasm +) +echo "=============================================" +echo "Compiling wasm done" +echo "=============================================" + +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" +echo "Did you update your docker image?" +echo "Run \`docker pull ubuntu\`" +echo "Run \`docker pull rust\`" +echo "Run \`docker build -t squoosh-resize .\`" +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" diff --git a/codecs/resize/package-lock.json b/codecs/resize/package-lock.json new file mode 100644 index 00000000..ca1a498d --- /dev/null +++ b/codecs/resize/package-lock.json @@ -0,0 +1,4 @@ +{ + "name": "resize", + "lockfileVersion": 1 +} diff --git a/codecs/resize/package.json b/codecs/resize/package.json new file mode 100644 index 00000000..439f4262 --- /dev/null +++ b/codecs/resize/package.json @@ -0,0 +1,7 @@ +{ + "name": "resize", + "scripts": { + "build:image": "docker build -t squoosh-resize .", + "build": "docker run --rm -v $(pwd):/src squoosh-resize ./build.sh" + } +} diff --git a/codecs/resize/pkg/resize.d.ts b/codecs/resize/pkg/resize.d.ts new file mode 100644 index 00000000..3f68ed7b --- /dev/null +++ b/codecs/resize/pkg/resize.d.ts @@ -0,0 +1,11 @@ +/* tslint:disable */ +/** +* @param {Uint8Array} arg0 +* @param {number} arg1 +* @param {number} arg2 +* @param {number} arg3 +* @param {number} arg4 +* @param {number} arg5 +* @returns {Uint8Array} +*/ +export function resize(arg0: Uint8Array, arg1: number, arg2: number, arg3: number, arg4: number, arg5: number): Uint8Array; diff --git a/codecs/resize/pkg/resize.js b/codecs/resize/pkg/resize.js new file mode 100644 index 00000000..ec76ff7d --- /dev/null +++ b/codecs/resize/pkg/resize.js @@ -0,0 +1,112 @@ +(function() { + var wasm; + const __exports = {}; + + + let cachegetUint8Memory = null; + function getUint8Memory() { + if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) { + cachegetUint8Memory = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory; + } + + let WASM_VECTOR_LEN = 0; + + function passArray8ToWasm(arg) { + const ptr = wasm.__wbindgen_malloc(arg.length * 1); + getUint8Memory().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; + } + + function getArrayU8FromWasm(ptr, len) { + return getUint8Memory().subarray(ptr / 1, ptr / 1 + len); + } + + let cachedGlobalArgumentPtr = null; + function globalArgumentPtr() { + if (cachedGlobalArgumentPtr === null) { + cachedGlobalArgumentPtr = wasm.__wbindgen_global_argument_ptr(); + } + return cachedGlobalArgumentPtr; + } + + let cachegetUint32Memory = null; + function getUint32Memory() { + if (cachegetUint32Memory === null || cachegetUint32Memory.buffer !== wasm.memory.buffer) { + cachegetUint32Memory = new Uint32Array(wasm.memory.buffer); + } + return cachegetUint32Memory; + } + /** + * @param {Uint8Array} arg0 + * @param {number} arg1 + * @param {number} arg2 + * @param {number} arg3 + * @param {number} arg4 + * @param {number} arg5 + * @returns {Uint8Array} + */ + __exports.resize = function(arg0, arg1, arg2, arg3, arg4, arg5) { + const ptr0 = passArray8ToWasm(arg0); + const len0 = WASM_VECTOR_LEN; + const retptr = globalArgumentPtr(); + wasm.resize(retptr, ptr0, len0, arg1, arg2, arg3, arg4, arg5); + const mem = getUint32Memory(); + const rustptr = mem[retptr / 4]; + const rustlen = mem[retptr / 4 + 1]; + + const realRet = getArrayU8FromWasm(rustptr, rustlen).slice(); + wasm.__wbindgen_free(rustptr, rustlen * 1); + return realRet; + + }; + + const heap = new Array(32); + + heap.fill(undefined); + + heap.push(undefined, null, true, false); + + let heap_next = heap.length; + + function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; + } + + __exports.__wbindgen_object_drop_ref = function(i) { dropObject(i); }; + + function init(path_or_module) { + let instantiation; + const imports = { './resize': __exports }; + if (path_or_module instanceof WebAssembly.Module) { + instantiation = WebAssembly.instantiate(path_or_module, imports) + .then(instance => { + return { instance, module: path_or_module } + }); + } else { + const data = fetch(path_or_module); + if (typeof WebAssembly.instantiateStreaming === 'function') { + instantiation = WebAssembly.instantiateStreaming(data, imports) + .catch(e => { + console.warn("`WebAssembly.instantiateStreaming` failed. Assuming this is because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + return data + .then(r => r.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, imports)); + }); + } else { + instantiation = data + .then(response => response.arrayBuffer()) + .then(buffer => WebAssembly.instantiate(buffer, imports)); + } + } + return instantiation.then(({instance}) => { + wasm = init.wasm = instance.exports; + + }); +}; +self.wasm_bindgen = Object.assign(init, __exports); +})(); diff --git a/codecs/resize/pkg/resize_bg.d.ts b/codecs/resize/pkg/resize_bg.d.ts new file mode 100644 index 00000000..58426e0e --- /dev/null +++ b/codecs/resize/pkg/resize_bg.d.ts @@ -0,0 +1,6 @@ +/* tslint:disable */ +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; diff --git a/codecs/resize/pkg/resize_bg.wasm b/codecs/resize/pkg/resize_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..f7d98afdbb271d3234cecc74ddd86b3abcd5b3a1 GIT binary patch literal 16956 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 qe%r|#p8pPnh4&@giyrUd5, + input_width: usize, + input_height: usize, + output_width: usize, + output_height: usize, + typ_idx: usize, +) -> Vec { + let typ = match typ_idx { + 0 => Type::Triangle, + 1 => Type::Catrom, + 2 => Type::Mitchell, + 3 => Type::Lanczos3, + _ => panic!("Nope"), + }; + let num_output_pixels = output_width * output_height; + let mut resizer = resize::new( + input_width, + input_height, + output_width, + output_height, + RGBA, + typ, + ); + 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()); + return output_image; +} diff --git a/codecs/resize/src/utils.rs b/codecs/resize/src/utils.rs new file mode 100644 index 00000000..2ffc954d --- /dev/null +++ b/codecs/resize/src/utils.rs @@ -0,0 +1,17 @@ +use cfg_if::cfg_if; + +cfg_if! { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + if #[cfg(feature = "console_error_panic_hook")] { + extern crate console_error_panic_hook; + pub use self::console_error_panic_hook::set_once as set_panic_hook; + } else { + #[inline] + pub fn set_panic_hook() {} + } +} diff --git a/package.json b/package.json index 535b09be..9498f94e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "webpack-dev-server --host 0.0.0.0 --hot", "build": "webpack -p", - "lint": "tslint -c tslint.json -p tsconfig.json -t verbose 'src/**/*.{ts,tsx,js,jsx}'", + "lint": "tslint -c tslint.json -p tsconfig.json -t verbose", "lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'", "sizereport": "node config/size-report.js" }, diff --git a/src/codecs/imagequant/processor.ts b/src/codecs/imagequant/processor.ts index c9362211..be6f4da2 100644 --- a/src/codecs/imagequant/processor.ts +++ b/src/codecs/imagequant/processor.ts @@ -1,12 +1,12 @@ import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant'; import wasmUrl from '../../../codecs/imagequant/imagequant.wasm'; import { QuantizeOptions } from './processor-meta'; -import { initWasmModule } from '../util'; +import { initEmscriptenModule } from '../util'; let emscriptenModule: Promise; export async function process(data: ImageData, opts: QuantizeOptions): Promise { - if (!emscriptenModule) emscriptenModule = initWasmModule(imagequant, wasmUrl); + if (!emscriptenModule) emscriptenModule = initEmscriptenModule(imagequant, wasmUrl); const module = await emscriptenModule; diff --git a/src/codecs/mozjpeg/encoder.ts b/src/codecs/mozjpeg/encoder.ts index 6514a169..b1a65173 100644 --- a/src/codecs/mozjpeg/encoder.ts +++ b/src/codecs/mozjpeg/encoder.ts @@ -1,12 +1,12 @@ import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc'; import wasmUrl from '../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm'; import { EncodeOptions } from './encoder-meta'; -import { initWasmModule } from '../util'; +import { initEmscriptenModule } from '../util'; let emscriptenModule: Promise; export async function encode(data: ImageData, options: EncodeOptions): Promise { - if (!emscriptenModule) emscriptenModule = initWasmModule(mozjpeg_enc, wasmUrl); + if (!emscriptenModule) emscriptenModule = initEmscriptenModule(mozjpeg_enc, wasmUrl); const module = await emscriptenModule; const resultView = module.encode(data.data, data.width, data.height, options); diff --git a/src/codecs/optipng/encoder.ts b/src/codecs/optipng/encoder.ts index 0e5b00a1..164c4063 100644 --- a/src/codecs/optipng/encoder.ts +++ b/src/codecs/optipng/encoder.ts @@ -1,12 +1,12 @@ import optipng, { OptiPngModule } from '../../../codecs/optipng/optipng'; import wasmUrl from '../../../codecs/optipng/optipng.wasm'; import { EncodeOptions } from './encoder-meta'; -import { initWasmModule } from '../util'; +import { initEmscriptenModule } from '../util'; let emscriptenModule: Promise; export async function compress(data: BufferSource, options: EncodeOptions): Promise { - if (!emscriptenModule) emscriptenModule = initWasmModule(optipng, wasmUrl); + if (!emscriptenModule) emscriptenModule = initEmscriptenModule(optipng, wasmUrl); const module = await emscriptenModule; const resultView = module.compress(data, options); diff --git a/src/codecs/processor-worker/index.ts b/src/codecs/processor-worker/index.ts index 1c7217a4..c4eac690 100644 --- a/src/codecs/processor-worker/index.ts +++ b/src/codecs/processor-worker/index.ts @@ -28,6 +28,16 @@ async function rotate( return rotate(data, opts); } +async function resize( + data: ImageData, opts: import('../resize/processor-meta').WorkerResizeOptions, +): Promise { + const { resize } = await import( + /* webpackChunkName: "process-resize" */ + '../resize/processor'); + + return resize(data, opts); +} + async function optiPngEncode( data: BufferSource, options: import('../optipng/encoder-meta').EncodeOptions, ): Promise { @@ -53,7 +63,7 @@ async function webpDecode(data: ArrayBuffer): Promise { return decode(data); } -const exports = { mozjpegEncode, quantize, rotate, optiPngEncode, webpEncode, webpDecode }; +const exports = { mozjpegEncode, quantize, rotate, resize, optiPngEncode, webpEncode, webpDecode }; export type ProcessorWorkerApi = typeof exports; expose(exports, self); diff --git a/src/codecs/processor.ts b/src/codecs/processor.ts index acaa69df..68063248 100644 --- a/src/codecs/processor.ts +++ b/src/codecs/processor.ts @@ -6,8 +6,8 @@ import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta'; import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta'; import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta'; import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta'; -import { BitmapResizeOptions, VectorResizeOptions } from './resize/processor-meta'; -import { resize, vectorResize } from './resize/processor'; +import { BrowserResizeOptions, VectorResizeOptions } from './resize/processor-meta'; +import { browserResize, vectorResize } from './resize/processor-sync'; import * as browserBMP from './browser-bmp/encoder'; import * as browserPNG from './browser-png/encoder'; import * as browserJPEG from './browser-jpeg/encoder'; @@ -130,6 +130,13 @@ export default class Processor { return this._workerApi!.rotate(data, opts); } + @Processor._processingJob({ needsWorker: true }) + workerResize( + data: ImageData, opts: import('./resize/processor-meta').WorkerResizeOptions, + ): Promise { + return this._workerApi!.resize(data, opts); + } + @Processor._processingJob({ needsWorker: true }) mozjpegEncode( data: ImageData, opts: MozJPEGEncoderOptions, @@ -202,9 +209,9 @@ export default class Processor { // Synchronous jobs - resize(data: ImageData, opts: BitmapResizeOptions) { + resize(data: ImageData, opts: BrowserResizeOptions) { this.abortCurrent(); - return resize(data, opts); + return browserResize(data, opts); } vectorResize(data: HTMLImageElement, opts: VectorResizeOptions) { diff --git a/src/codecs/resize/options.tsx b/src/codecs/resize/options.tsx index a6624434..cc9dc15a 100644 --- a/src/codecs/resize/options.tsx +++ b/src/codecs/resize/options.tsx @@ -87,6 +87,10 @@ export default class ResizerOptions extends Component { onChange={this.onChange} > {isVector && } + + + + diff --git a/src/codecs/resize/processor-meta.ts b/src/codecs/resize/processor-meta.ts index fad86f72..a08b8475 100644 --- a/src/codecs/resize/processor-meta.ts +++ b/src/codecs/resize/processor-meta.ts @@ -1,14 +1,19 @@ -type BitmapResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high'; +type BrowserResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high'; +type WorkerResizeMethods = 'point' | 'triangle' | 'catrom' | 'mitchell' | 'lanczos3'; export interface ResizeOptions { width: number; height: number; - method: 'vector' | BitmapResizeMethods; + method: 'vector' | BrowserResizeMethods | WorkerResizeMethods; fitMethod: 'stretch' | 'contain'; } -export interface BitmapResizeOptions extends ResizeOptions { - method: BitmapResizeMethods; +export interface BrowserResizeOptions extends ResizeOptions { + method: BrowserResizeMethods; +} + +export interface WorkerResizeOptions extends ResizeOptions { + method: WorkerResizeMethods; } export interface VectorResizeOptions extends ResizeOptions { @@ -21,6 +26,6 @@ export const defaultOptions: ResizeOptions = { width: 1, height: 1, // This will be set to 'vector' if the input is SVG. - method: 'browser-high', + method: 'lanczos3', fitMethod: 'stretch', }; diff --git a/src/codecs/resize/processor-sync.ts b/src/codecs/resize/processor-sync.ts new file mode 100644 index 00000000..f2192185 --- /dev/null +++ b/src/codecs/resize/processor-sync.ts @@ -0,0 +1,35 @@ +import { nativeResize, NativeResizeMethod, drawableToImageData } from '../../lib/util'; +import { BrowserResizeOptions, VectorResizeOptions } from './processor-meta'; +import { getContainOffsets } from './util'; + +export function browserResize(data: ImageData, opts: BrowserResizeOptions): ImageData { + let sx = 0; + let sy = 0; + let sw = data.width; + let sh = data.height; + + if (opts.fitMethod === 'contain') { + ({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height)); + } + + return nativeResize( + data, sx, sy, sw, sh, opts.width, opts.height, + opts.method.slice('browser-'.length) as NativeResizeMethod, + ); +} + +export function vectorResize(data: HTMLImageElement, opts: VectorResizeOptions): ImageData { + let sx = 0; + let sy = 0; + let sw = data.width; + let sh = data.height; + + if (opts.fitMethod === 'contain') { + ({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height)); + } + + return drawableToImageData(data, { + sx, sy, sw, sh, + width: opts.width, height: opts.height, + }); +} diff --git a/src/codecs/resize/processor.ts b/src/codecs/resize/processor.ts index 5db7aae8..ac2c0feb 100644 --- a/src/codecs/resize/processor.ts +++ b/src/codecs/resize/processor.ts @@ -1,49 +1,52 @@ -import { nativeResize, NativeResizeMethod, drawableToImageData } from '../../lib/util'; -import { BitmapResizeOptions, VectorResizeOptions } from './processor-meta'; +import wasmUrl from '../../../codecs/resize/pkg/resize_bg.wasm'; +import '../../../codecs/resize/pkg/resize'; +import { WorkerResizeOptions } from './processor-meta'; +import { getContainOffsets } from './util'; -function getContainOffsets(sw: number, sh: number, dw: number, dh: number) { - const currentAspect = sw / sh; - const endAspect = dw / dh; - - if (endAspect > currentAspect) { - const newSh = sw / endAspect; - const newSy = (sh - newSh) / 2; - return { sw, sh: newSh, sx: 0, sy: newSy }; - } - - const newSw = sh * endAspect; - const newSx = (sw - newSw) / 2; - return { sh, sw: newSw, sx: newSx, sy: 0 }; +interface WasmBindgenExports { + resize: typeof import('../../../codecs/resize/pkg/resize').resize; } -export function resize(data: ImageData, opts: BitmapResizeOptions): ImageData { - let sx = 0; - let sy = 0; - let sw = data.width; - let sh = data.height; +type WasmBindgen = ((url: string) => Promise) & WasmBindgenExports; - if (opts.fitMethod === 'contain') { - ({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height)); +declare var wasm_bindgen: WasmBindgen; + +const ready = wasm_bindgen(wasmUrl); + +function crop(data: ImageData, sx: number, sy: number, sw: number, sh: number): ImageData { + const inputPixels = new Uint32Array(data.data.buffer); + + // Copy within the same buffer for speed and memory efficiency. + for (let y = 0; y < sh; y += 1) { + const start = ((y + sy) * data.width) + sx; + inputPixels.copyWithin(y * sw, start, start + sw); } - return nativeResize( - data, sx, sy, sw, sh, opts.width, opts.height, - opts.method.slice('browser-'.length) as NativeResizeMethod, + return new ImageData( + new Uint8ClampedArray(inputPixels.buffer.slice(0, sw * sh * 4)), + sw, sh, ); } -export function vectorResize(data: HTMLImageElement, opts: VectorResizeOptions): ImageData { - let sx = 0; - let sy = 0; - let sw = data.width; - let sh = data.height; +/** Resize methods by index */ +const resizeMethods: WorkerResizeOptions['method'][] = [ + 'triangle', 'catrom', 'mitchell', 'lanczos3', +]; + +export async function resize(data: ImageData, opts: WorkerResizeOptions): Promise { + let input = data; if (opts.fitMethod === 'contain') { - ({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height)); + const { sx, sy, sw, sh } = getContainOffsets(data.width, data.height, opts.width, opts.height); + input = crop(input, Math.round(sx), Math.round(sy), Math.round(sw), Math.round(sh)); } - return drawableToImageData(data, { - sx, sy, sw, sh, - width: opts.width, height: opts.height, - }); + await ready; + + const result = wasm_bindgen.resize( + new Uint8Array(input.data.buffer), input.width, input.height, opts.width, opts.height, + resizeMethods.indexOf(opts.method), + ); + + return new ImageData(new Uint8ClampedArray(result.buffer), opts.width, opts.height); } diff --git a/src/codecs/resize/util.ts b/src/codecs/resize/util.ts new file mode 100644 index 00000000..4e67c47d --- /dev/null +++ b/src/codecs/resize/util.ts @@ -0,0 +1,14 @@ +export function getContainOffsets(sw: number, sh: number, dw: number, dh: number) { + const currentAspect = sw / sh; + const endAspect = dw / dh; + + if (endAspect > currentAspect) { + const newSh = sw / endAspect; + const newSy = (sh - newSh) / 2; + return { sw, sh: newSh, sx: 0, sy: newSy }; + } + + const newSw = sh * endAspect; + const newSx = (sw - newSw) / 2; + return { sh, sw: newSw, sx: newSx, sy: 0 }; +} diff --git a/src/codecs/util.ts b/src/codecs/util.ts index 7498e816..03f9a284 100644 --- a/src/codecs/util.ts +++ b/src/codecs/util.ts @@ -2,7 +2,7 @@ type ModuleFactory = ( opts: EmscriptenWasm.ModuleOpts, ) => M; -export function initWasmModule( +export function initEmscriptenModule( moduleFactory: ModuleFactory, wasmUrl: string, ): Promise { diff --git a/src/codecs/webp/decoder.ts b/src/codecs/webp/decoder.ts index 6fa6e9e9..6b25cbd2 100644 --- a/src/codecs/webp/decoder.ts +++ b/src/codecs/webp/decoder.ts @@ -1,11 +1,11 @@ import webp_dec, { WebPModule } from '../../../codecs/webp_dec/webp_dec'; import wasmUrl from '../../../codecs/webp_dec/webp_dec.wasm'; -import { initWasmModule } from '../util'; +import { initEmscriptenModule } from '../util'; let emscriptenModule: Promise; export async function decode(data: ArrayBuffer): Promise { - if (!emscriptenModule) emscriptenModule = initWasmModule(webp_dec, wasmUrl); + if (!emscriptenModule) emscriptenModule = initEmscriptenModule(webp_dec, wasmUrl); const module = await emscriptenModule; const rawImage = module.decode(data); diff --git a/src/codecs/webp/encoder.ts b/src/codecs/webp/encoder.ts index b308fab4..5fafb577 100644 --- a/src/codecs/webp/encoder.ts +++ b/src/codecs/webp/encoder.ts @@ -1,12 +1,12 @@ import webp_enc, { WebPModule } from '../../../codecs/webp_enc/webp_enc'; import wasmUrl from '../../../codecs/webp_enc/webp_enc.wasm'; import { EncodeOptions } from './encoder-meta'; -import { initWasmModule } from '../util'; +import { initEmscriptenModule } from '../util'; let emscriptenModule: Promise; export async function encode(data: ImageData, options: EncodeOptions): Promise { - if (!emscriptenModule) emscriptenModule = initWasmModule(webp_enc, wasmUrl); + if (!emscriptenModule) emscriptenModule = initEmscriptenModule(webp_enc, wasmUrl); const module = await emscriptenModule; const resultView = module.encode(data.data, data.width, data.height, options); diff --git a/src/components/compress/index.tsx b/src/components/compress/index.tsx index 2bb90f03..71713ea0 100644 --- a/src/components/compress/index.tsx +++ b/src/components/compress/index.tsx @@ -31,7 +31,11 @@ import { import { decodeImage } from '../../codecs/decoders'; import { cleanMerge, cleanSet } from '../../lib/clean-modify'; import Processor from '../../codecs/processor'; -import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta'; +import { + VectorResizeOptions, + BrowserResizeOptions, + WorkerResizeOptions, +} from '../../codecs/resize/processor-meta'; import './custom-els/MultiPanel'; import Results from '../results'; import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons'; @@ -112,8 +116,10 @@ async function preprocessImage( source.vectorImage, preprocessData.resize as VectorResizeOptions, ); + } else if (preprocessData.resize.method.startsWith('browser-')) { + result = processor.resize(result, preprocessData.resize as BrowserResizeOptions); } else { - result = processor.resize(result, preprocessData.resize as BitmapResizeOptions); + result = await processor.workerResize(result, preprocessData.resize as WorkerResizeOptions); } } if (preprocessData.quantizer.enabled) { @@ -441,7 +447,7 @@ export default class Compress extends Component { newState = cleanMerge(newState, `sides.${i}.latestSettings.preprocessorState.resize`, { width: processed.width, height: processed.height, - method: vectorImage ? 'vector' : 'browser-high', + method: vectorImage ? 'vector' : 'lanczos3', }); }