
This makes building simpler and allows us to potentially use multithreading version in the future. For now points to a custom fork of OxiPNG that enables WebAssembly support, as PR is still pending review.
220 lines
7.2 KiB
TypeScript
220 lines
7.2 KiB
TypeScript
import { proxy } from 'comlink';
|
|
import { QuantizeOptions } from './imagequant/processor-meta';
|
|
import { canvasEncode, blobToArrayBuffer } from '../lib/util';
|
|
import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta';
|
|
import { EncodeOptions as OxiPNGEncoderOptions } from './oxipng/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 { 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';
|
|
import * as browserWebP from './browser-webp/encoder';
|
|
import * as browserGIF from './browser-gif/encoder';
|
|
import * as browserTIFF from './browser-tiff/encoder';
|
|
import * as browserJP2 from './browser-jp2/encoder';
|
|
import * as browserPDF from './browser-pdf/encoder';
|
|
import { bind } from '../lib/initial-util';
|
|
|
|
type ProcessorWorkerApi = import('./processor-worker').ProcessorWorkerApi;
|
|
|
|
/** How long the worker should be idle before terminating. */
|
|
const workerTimeout = 10000;
|
|
|
|
interface ProcessingJobOptions {
|
|
needsWorker?: boolean;
|
|
}
|
|
|
|
export default class Processor {
|
|
/** Worker instance associated with this processor. */
|
|
private _worker?: Worker;
|
|
/** Comlinked worker API. */
|
|
private _workerApi?: ProcessorWorkerApi;
|
|
/** Rejector for a pending promise. */
|
|
private _abortRejector?: (err: Error) => void;
|
|
/** Is work currently happening? */
|
|
private _busy = false;
|
|
/** Incementing ID so we can tell if a job has been superseded. */
|
|
private _latestJobId: number = 0;
|
|
/** setTimeout ID for killing the worker when idle. */
|
|
private _workerTimeoutId: number = 0;
|
|
|
|
/**
|
|
* Decorator that manages the (re)starting of the worker and aborting existing jobs. Not all
|
|
* processing jobs require a worker (e.g. the main thread canvas encodes), use the needsWorker
|
|
* option to control this.
|
|
*/
|
|
private static _processingJob(options: ProcessingJobOptions = {}) {
|
|
const { needsWorker = false } = options;
|
|
|
|
return (target: Processor, propertyKey: string, descriptor: PropertyDescriptor): void => {
|
|
const processingFunc = descriptor.value;
|
|
|
|
descriptor.value = async function (this: Processor, ...args: any[]) {
|
|
this._latestJobId += 1;
|
|
const jobId = this._latestJobId;
|
|
this.abortCurrent();
|
|
|
|
if (needsWorker) self.clearTimeout(this._workerTimeoutId);
|
|
|
|
if (!this._worker && needsWorker) {
|
|
// worker-loader does magic here.
|
|
// @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the
|
|
// definition can't be overwritten.
|
|
this._worker = new Worker(
|
|
'./processor-worker',
|
|
{ name: 'processor-worker', type: 'module' },
|
|
) as Worker;
|
|
// Need to do some TypeScript trickery to make the type match.
|
|
this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi;
|
|
}
|
|
|
|
this._busy = true;
|
|
|
|
const returnVal = Promise.race([
|
|
processingFunc.call(this, ...args),
|
|
new Promise((_, reject) => { this._abortRejector = reject; }),
|
|
]);
|
|
|
|
// Wait for the operation to settle.
|
|
await returnVal.catch(() => {});
|
|
|
|
// If no other jobs are happening, cleanup.
|
|
if (jobId === this._latestJobId) this._jobCleanup();
|
|
|
|
return returnVal;
|
|
};
|
|
};
|
|
}
|
|
|
|
private _jobCleanup(): void {
|
|
this._busy = false;
|
|
|
|
if (!this._worker) return;
|
|
|
|
// If the worker is unused for 10 seconds, remove it to save memory.
|
|
this._workerTimeoutId = self.setTimeout(this.terminateWorker, workerTimeout);
|
|
}
|
|
|
|
/** Abort the current job, if any */
|
|
abortCurrent() {
|
|
if (!this._busy) return;
|
|
if (!this._abortRejector) throw Error("There must be a rejector if it's busy");
|
|
this._abortRejector(new DOMException('Aborted', 'AbortError'));
|
|
this._abortRejector = undefined;
|
|
this._busy = false;
|
|
this.terminateWorker();
|
|
}
|
|
|
|
@bind
|
|
terminateWorker() {
|
|
if (!this._worker) return;
|
|
this._worker.terminate();
|
|
this._worker = undefined;
|
|
}
|
|
|
|
// Off main thread jobs:
|
|
@Processor._processingJob({ needsWorker: true })
|
|
imageQuant(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
|
|
return this._workerApi!.quantize(data, opts);
|
|
}
|
|
|
|
@Processor._processingJob({ needsWorker: true })
|
|
rotate(
|
|
data: ImageData, opts: import('./rotate/processor-meta').RotateOptions,
|
|
): Promise<ImageData> {
|
|
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,
|
|
): Promise<ArrayBuffer> {
|
|
return this._workerApi!.mozjpegEncode(data, opts);
|
|
}
|
|
|
|
@Processor._processingJob({ needsWorker: true })
|
|
async oxiPngEncode(
|
|
data: ImageData, opts: OxiPNGEncoderOptions,
|
|
): Promise<ArrayBuffer> {
|
|
// OxiPNG expects PNG input.
|
|
const pngBlob = await canvasEncode(data, 'image/png');
|
|
const pngBuffer = await blobToArrayBuffer(pngBlob);
|
|
return this._workerApi!.oxiPngEncode(pngBuffer, opts);
|
|
}
|
|
|
|
@Processor._processingJob({ needsWorker: true })
|
|
webpEncode(data: ImageData, opts: WebPEncoderOptions): Promise<ArrayBuffer> {
|
|
return this._workerApi!.webpEncode(data, opts);
|
|
}
|
|
|
|
@Processor._processingJob({ needsWorker: true })
|
|
async webpDecode(blob: Blob): Promise<ImageData> {
|
|
const data = await blobToArrayBuffer(blob);
|
|
return this._workerApi!.webpDecode(data);
|
|
}
|
|
|
|
// Not-worker jobs:
|
|
|
|
@Processor._processingJob()
|
|
browserBmpEncode(data: ImageData): Promise<Blob> {
|
|
return browserBMP.encode(data);
|
|
}
|
|
|
|
@Processor._processingJob()
|
|
browserPngEncode(data: ImageData): Promise<Blob> {
|
|
return browserPNG.encode(data);
|
|
}
|
|
|
|
@Processor._processingJob()
|
|
browserJpegEncode(data: ImageData, opts: BrowserJPEGOptions): Promise<Blob> {
|
|
return browserJPEG.encode(data, opts);
|
|
}
|
|
|
|
@Processor._processingJob()
|
|
browserWebpEncode(data: ImageData, opts: BrowserWebpEncodeOptions): Promise<Blob> {
|
|
return browserWebP.encode(data, opts);
|
|
}
|
|
|
|
@Processor._processingJob()
|
|
browserGifEncode(data: ImageData): Promise<Blob> {
|
|
return browserGIF.encode(data);
|
|
}
|
|
|
|
@Processor._processingJob()
|
|
browserTiffEncode(data: ImageData): Promise<Blob> {
|
|
return browserTIFF.encode(data);
|
|
}
|
|
|
|
@Processor._processingJob()
|
|
browserJp2Encode(data: ImageData): Promise<Blob> {
|
|
return browserJP2.encode(data);
|
|
}
|
|
|
|
@Processor._processingJob()
|
|
browserPdfEncode(data: ImageData): Promise<Blob> {
|
|
return browserPDF.encode(data);
|
|
}
|
|
|
|
// Synchronous jobs
|
|
|
|
resize(data: ImageData, opts: BrowserResizeOptions) {
|
|
this.abortCurrent();
|
|
return browserResize(data, opts);
|
|
}
|
|
|
|
vectorResize(data: HTMLImageElement, opts: VectorResizeOptions) {
|
|
this.abortCurrent();
|
|
return vectorResize(data, opts);
|
|
}
|
|
}
|