Compare commits

...

4 Commits

Author SHA1 Message Date
Jake Archibald
ecd9e06665 1.4.0 2019-03-06 17:20:54 +00:00
Jake Archibald
9e5b66d5f4 Better resize methods (#498)
* 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
2019-03-06 17:20:25 +00:00
Surma
8c35c3cdaa Merge pull request #497 from GoogleChromeLabs/renovate/node-10.x
Update Node.js to v10.15.3
2019-03-05 23:26:23 +00:00
renovate[bot]
828a6240fe Update Node.js to v10.15.3 2019-03-05 20:51:40 +00:00
29 changed files with 438 additions and 63 deletions

2
.nvmrc
View File

@@ -1 +1 @@
10.15.2 10.15.3

5
codecs/resize/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
**/*.rs.bk
target
Cargo.lock
bin/
pkg/README.md

37
codecs/resize/Cargo.toml Normal file
View File

@@ -0,0 +1,37 @@
[package]
name = "resize"
version = "0.1.0"
authors = ["Surma <surma@surma.link>"]
[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

18
codecs/resize/Dockerfile Normal file
View File

@@ -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

22
codecs/resize/build.sh Executable file
View File

@@ -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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

4
codecs/resize/package-lock.json generated Normal file
View File

@@ -0,0 +1,4 @@
{
"name": "resize",
"lockfileVersion": 1
}

View File

@@ -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"
}
}

11
codecs/resize/pkg/resize.d.ts vendored Normal file
View File

@@ -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;

112
codecs/resize/pkg/resize.js Normal file
View File

@@ -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);
})();

6
codecs/resize/pkg/resize_bg.d.ts vendored Normal file
View File

@@ -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;

Binary file not shown.

52
codecs/resize/src/lib.rs Normal file
View File

@@ -0,0 +1,52 @@
extern crate cfg_if;
extern crate resize;
extern crate wasm_bindgen;
mod utils;
use cfg_if::cfg_if;
use resize::Pixel::RGBA;
use resize::Type;
use wasm_bindgen::prelude::*;
cfg_if! {
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
if #[cfg(feature = "wee_alloc")] {
extern crate wee_alloc;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
}
}
#[wasm_bindgen]
#[no_mangle]
pub fn resize(
input_image: Vec<u8>,
input_width: usize,
input_height: usize,
output_width: usize,
output_height: usize,
typ_idx: usize,
) -> Vec<u8> {
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::<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());
return output_image;
}

View File

@@ -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() {}
}
}

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "squoosh", "name": "squoosh",
"version": "1.3.4", "version": "1.4.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,12 +1,12 @@
{ {
"private": true, "private": true,
"name": "squoosh", "name": "squoosh",
"version": "1.3.4", "version": "1.4.0",
"license": "apache-2.0", "license": "apache-2.0",
"scripts": { "scripts": {
"start": "webpack-dev-server --host 0.0.0.0 --hot", "start": "webpack-dev-server --host 0.0.0.0 --hot",
"build": "webpack -p", "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}'", "lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'",
"sizereport": "node config/size-report.js" "sizereport": "node config/size-report.js"
}, },

View File

@@ -1,12 +1,12 @@
import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant'; import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant';
import wasmUrl from '../../../codecs/imagequant/imagequant.wasm'; import wasmUrl from '../../../codecs/imagequant/imagequant.wasm';
import { QuantizeOptions } from './processor-meta'; import { QuantizeOptions } from './processor-meta';
import { initWasmModule } from '../util'; import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<QuantizerModule>; let emscriptenModule: Promise<QuantizerModule>;
export async function process(data: ImageData, opts: QuantizeOptions): Promise<ImageData> { export async function process(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
if (!emscriptenModule) emscriptenModule = initWasmModule(imagequant, wasmUrl); if (!emscriptenModule) emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
const module = await emscriptenModule; const module = await emscriptenModule;

View File

@@ -1,12 +1,12 @@
import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc'; import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
import wasmUrl from '../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm'; import wasmUrl from '../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm';
import { EncodeOptions } from './encoder-meta'; import { EncodeOptions } from './encoder-meta';
import { initWasmModule } from '../util'; import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<MozJPEGModule>; let emscriptenModule: Promise<MozJPEGModule>;
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> { export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initWasmModule(mozjpeg_enc, wasmUrl); if (!emscriptenModule) emscriptenModule = initEmscriptenModule(mozjpeg_enc, wasmUrl);
const module = await emscriptenModule; const module = await emscriptenModule;
const resultView = module.encode(data.data, data.width, data.height, options); const resultView = module.encode(data.data, data.width, data.height, options);

View File

@@ -1,12 +1,12 @@
import optipng, { OptiPngModule } from '../../../codecs/optipng/optipng'; import optipng, { OptiPngModule } from '../../../codecs/optipng/optipng';
import wasmUrl from '../../../codecs/optipng/optipng.wasm'; import wasmUrl from '../../../codecs/optipng/optipng.wasm';
import { EncodeOptions } from './encoder-meta'; import { EncodeOptions } from './encoder-meta';
import { initWasmModule } from '../util'; import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<OptiPngModule>; let emscriptenModule: Promise<OptiPngModule>;
export async function compress(data: BufferSource, options: EncodeOptions): Promise<ArrayBuffer> { export async function compress(data: BufferSource, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initWasmModule(optipng, wasmUrl); if (!emscriptenModule) emscriptenModule = initEmscriptenModule(optipng, wasmUrl);
const module = await emscriptenModule; const module = await emscriptenModule;
const resultView = module.compress(data, options); const resultView = module.compress(data, options);

View File

@@ -28,6 +28,16 @@ async function rotate(
return rotate(data, opts); return rotate(data, opts);
} }
async function resize(
data: ImageData, opts: import('../resize/processor-meta').WorkerResizeOptions,
): Promise<ImageData> {
const { resize } = await import(
/* webpackChunkName: "process-resize" */
'../resize/processor');
return resize(data, opts);
}
async function optiPngEncode( async function optiPngEncode(
data: BufferSource, options: import('../optipng/encoder-meta').EncodeOptions, data: BufferSource, options: import('../optipng/encoder-meta').EncodeOptions,
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
@@ -53,7 +63,7 @@ async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
return decode(data); 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; export type ProcessorWorkerApi = typeof exports;
expose(exports, self); expose(exports, self);

View File

@@ -6,8 +6,8 @@ import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta';
import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta'; import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta';
import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta'; import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta';
import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta'; import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta';
import { BitmapResizeOptions, VectorResizeOptions } from './resize/processor-meta'; import { BrowserResizeOptions, VectorResizeOptions } from './resize/processor-meta';
import { resize, vectorResize } from './resize/processor'; import { browserResize, vectorResize } from './resize/processor-sync';
import * as browserBMP from './browser-bmp/encoder'; import * as browserBMP from './browser-bmp/encoder';
import * as browserPNG from './browser-png/encoder'; import * as browserPNG from './browser-png/encoder';
import * as browserJPEG from './browser-jpeg/encoder'; import * as browserJPEG from './browser-jpeg/encoder';
@@ -130,6 +130,13 @@ export default class Processor {
return this._workerApi!.rotate(data, opts); return this._workerApi!.rotate(data, opts);
} }
@Processor._processingJob({ needsWorker: true })
workerResize(
data: ImageData, opts: import('./resize/processor-meta').WorkerResizeOptions,
): Promise<ImageData> {
return this._workerApi!.resize(data, opts);
}
@Processor._processingJob({ needsWorker: true }) @Processor._processingJob({ needsWorker: true })
mozjpegEncode( mozjpegEncode(
data: ImageData, opts: MozJPEGEncoderOptions, data: ImageData, opts: MozJPEGEncoderOptions,
@@ -202,9 +209,9 @@ export default class Processor {
// Synchronous jobs // Synchronous jobs
resize(data: ImageData, opts: BitmapResizeOptions) { resize(data: ImageData, opts: BrowserResizeOptions) {
this.abortCurrent(); this.abortCurrent();
return resize(data, opts); return browserResize(data, opts);
} }
vectorResize(data: HTMLImageElement, opts: VectorResizeOptions) { vectorResize(data: HTMLImageElement, opts: VectorResizeOptions) {

View File

@@ -87,6 +87,10 @@ export default class ResizerOptions extends Component<Props, State> {
onChange={this.onChange} onChange={this.onChange}
> >
{isVector && <option value="vector">Vector</option>} {isVector && <option value="vector">Vector</option>}
<option value="lanczos3">Lanczos3</option>
<option value="mitchell">Mitchell</option>
<option value="catrom">Catmull-Rom</option>
<option value="triangle">Triangle</option>
<option value="browser-pixelated">Browser pixelated</option> <option value="browser-pixelated">Browser pixelated</option>
<option value="browser-low">Browser low quality</option> <option value="browser-low">Browser low quality</option>
<option value="browser-medium">Browser medium quality</option> <option value="browser-medium">Browser medium quality</option>

View File

@@ -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 { export interface ResizeOptions {
width: number; width: number;
height: number; height: number;
method: 'vector' | BitmapResizeMethods; method: 'vector' | BrowserResizeMethods | WorkerResizeMethods;
fitMethod: 'stretch' | 'contain'; fitMethod: 'stretch' | 'contain';
} }
export interface BitmapResizeOptions extends ResizeOptions { export interface BrowserResizeOptions extends ResizeOptions {
method: BitmapResizeMethods; method: BrowserResizeMethods;
}
export interface WorkerResizeOptions extends ResizeOptions {
method: WorkerResizeMethods;
} }
export interface VectorResizeOptions extends ResizeOptions { export interface VectorResizeOptions extends ResizeOptions {
@@ -21,6 +26,6 @@ export const defaultOptions: ResizeOptions = {
width: 1, width: 1,
height: 1, height: 1,
// This will be set to 'vector' if the input is SVG. // This will be set to 'vector' if the input is SVG.
method: 'browser-high', method: 'lanczos3',
fitMethod: 'stretch', fitMethod: 'stretch',
}; };

View File

@@ -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,
});
}

View File

@@ -1,49 +1,52 @@
import { nativeResize, NativeResizeMethod, drawableToImageData } from '../../lib/util'; import wasmUrl from '../../../codecs/resize/pkg/resize_bg.wasm';
import { BitmapResizeOptions, VectorResizeOptions } from './processor-meta'; import '../../../codecs/resize/pkg/resize';
import { WorkerResizeOptions } from './processor-meta';
import { getContainOffsets } from './util';
function getContainOffsets(sw: number, sh: number, dw: number, dh: number) { interface WasmBindgenExports {
const currentAspect = sw / sh; resize: typeof import('../../../codecs/resize/pkg/resize').resize;
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 };
} }
export function resize(data: ImageData, opts: BitmapResizeOptions): ImageData { type WasmBindgen = ((url: string) => Promise<void>) & WasmBindgenExports;
let sx = 0;
let sy = 0;
let sw = data.width;
let sh = data.height;
if (opts.fitMethod === 'contain') { declare var wasm_bindgen: WasmBindgen;
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
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( return new ImageData(
data, sx, sy, sw, sh, opts.width, opts.height, new Uint8ClampedArray(inputPixels.buffer.slice(0, sw * sh * 4)),
opts.method.slice('browser-'.length) as NativeResizeMethod, sw, sh,
); );
} }
export function vectorResize(data: HTMLImageElement, opts: VectorResizeOptions): ImageData { /** Resize methods by index */
let sx = 0; const resizeMethods: WorkerResizeOptions['method'][] = [
let sy = 0; 'triangle', 'catrom', 'mitchell', 'lanczos3',
let sw = data.width; ];
let sh = data.height;
export async function resize(data: ImageData, opts: WorkerResizeOptions): Promise<ImageData> {
let input = data;
if (opts.fitMethod === 'contain') { 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, { await ready;
sx, sy, sw, sh,
width: opts.width, height: opts.height, 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);
} }

14
src/codecs/resize/util.ts Normal file
View File

@@ -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 };
}

View File

@@ -2,7 +2,7 @@ type ModuleFactory<M extends EmscriptenWasm.Module> = (
opts: EmscriptenWasm.ModuleOpts, opts: EmscriptenWasm.ModuleOpts,
) => M; ) => M;
export function initWasmModule<T extends EmscriptenWasm.Module>( export function initEmscriptenModule<T extends EmscriptenWasm.Module>(
moduleFactory: ModuleFactory<T>, moduleFactory: ModuleFactory<T>,
wasmUrl: string, wasmUrl: string,
): Promise<T> { ): Promise<T> {

View File

@@ -1,11 +1,11 @@
import webp_dec, { WebPModule } from '../../../codecs/webp_dec/webp_dec'; import webp_dec, { WebPModule } from '../../../codecs/webp_dec/webp_dec';
import wasmUrl from '../../../codecs/webp_dec/webp_dec.wasm'; import wasmUrl from '../../../codecs/webp_dec/webp_dec.wasm';
import { initWasmModule } from '../util'; import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<WebPModule>; let emscriptenModule: Promise<WebPModule>;
export async function decode(data: ArrayBuffer): Promise<ImageData> { export async function decode(data: ArrayBuffer): Promise<ImageData> {
if (!emscriptenModule) emscriptenModule = initWasmModule(webp_dec, wasmUrl); if (!emscriptenModule) emscriptenModule = initEmscriptenModule(webp_dec, wasmUrl);
const module = await emscriptenModule; const module = await emscriptenModule;
const rawImage = module.decode(data); const rawImage = module.decode(data);

View File

@@ -1,12 +1,12 @@
import webp_enc, { WebPModule } from '../../../codecs/webp_enc/webp_enc'; import webp_enc, { WebPModule } from '../../../codecs/webp_enc/webp_enc';
import wasmUrl from '../../../codecs/webp_enc/webp_enc.wasm'; import wasmUrl from '../../../codecs/webp_enc/webp_enc.wasm';
import { EncodeOptions } from './encoder-meta'; import { EncodeOptions } from './encoder-meta';
import { initWasmModule } from '../util'; import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<WebPModule>; let emscriptenModule: Promise<WebPModule>;
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> { export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initWasmModule(webp_enc, wasmUrl); if (!emscriptenModule) emscriptenModule = initEmscriptenModule(webp_enc, wasmUrl);
const module = await emscriptenModule; const module = await emscriptenModule;
const resultView = module.encode(data.data, data.width, data.height, options); const resultView = module.encode(data.data, data.width, data.height, options);

View File

@@ -31,7 +31,11 @@ import {
import { decodeImage } from '../../codecs/decoders'; import { decodeImage } from '../../codecs/decoders';
import { cleanMerge, cleanSet } from '../../lib/clean-modify'; import { cleanMerge, cleanSet } from '../../lib/clean-modify';
import Processor from '../../codecs/processor'; 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 './custom-els/MultiPanel';
import Results from '../results'; import Results from '../results';
import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons'; import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons';
@@ -112,8 +116,10 @@ async function preprocessImage(
source.vectorImage, source.vectorImage,
preprocessData.resize as VectorResizeOptions, preprocessData.resize as VectorResizeOptions,
); );
} else if (preprocessData.resize.method.startsWith('browser-')) {
result = processor.resize(result, preprocessData.resize as BrowserResizeOptions);
} else { } else {
result = processor.resize(result, preprocessData.resize as BitmapResizeOptions); result = await processor.workerResize(result, preprocessData.resize as WorkerResizeOptions);
} }
} }
if (preprocessData.quantizer.enabled) { if (preprocessData.quantizer.enabled) {
@@ -441,7 +447,7 @@ export default class Compress extends Component<Props, State> {
newState = cleanMerge(newState, `sides.${i}.latestSettings.preprocessorState.resize`, { newState = cleanMerge(newState, `sides.${i}.latestSettings.preprocessorState.resize`, {
width: processed.width, width: processed.width,
height: processed.height, height: processed.height,
method: vectorImage ? 'vector' : 'browser-high', method: vectorImage ? 'vector' : 'lanczos3',
}); });
} }