mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-18 03:29:17 +00:00
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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
35
src/codecs/resize/processor-sync.ts
Normal file
35
src/codecs/resize/processor-sync.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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
14
src/codecs/resize/util.ts
Normal 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 };
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user