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
This commit is contained in:
Jake Archibald
2019-03-06 17:20:25 +00:00
committed by GitHub
parent 8c35c3cdaa
commit 9e5b66d5f4
27 changed files with 435 additions and 60 deletions

View File

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

View File

@@ -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<MozJPEGModule>;
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 resultView = module.encode(data.data, data.width, data.height, options);

View File

@@ -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<OptiPngModule>;
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 resultView = module.compress(data, options);

View File

@@ -28,6 +28,16 @@ async function rotate(
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(
data: BufferSource, options: import('../optipng/encoder-meta').EncodeOptions,
): Promise<ArrayBuffer> {
@@ -53,7 +63,7 @@ async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
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);

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 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<ImageData> {
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) {

View File

@@ -87,6 +87,10 @@ export default class ResizerOptions extends Component<Props, State> {
onChange={this.onChange}
>
{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-low">Browser low 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 {
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',
};

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 { 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<void>) & 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<ImageData> {
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);
}

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,
) => M;
export function initWasmModule<T extends EmscriptenWasm.Module>(
export function initEmscriptenModule<T extends EmscriptenWasm.Module>(
moduleFactory: ModuleFactory<T>,
wasmUrl: string,
): Promise<T> {

View File

@@ -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<WebPModule>;
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 rawImage = module.decode(data);

View File

@@ -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<WebPModule>;
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 resultView = module.encode(data.data, data.width, data.height, options);