diff --git a/codecs/wp2/Makefile b/codecs/wp2/Makefile index d6478b0e..39130e58 100644 --- a/codecs/wp2/Makefile +++ b/codecs/wp2/Makefile @@ -46,16 +46,11 @@ $(OUT_JS): %/libwebp2.a: %/Makefile $(MAKE) -C $(@D) -# Disable SIMD for non-SIMD builds. -$(CODEC_BASELINE_BUILD_DIR)/Makefile $(CODEC_MT_BUILD_DIR)/Makefile: CMAKE_FLAGS+=-DWP2_ENABLE_SIMD=0 - # Disable threads for the single-threaded build. $(CODEC_BASELINE_BUILD_DIR)/Makefile: CMAKE_FLAGS+=-DCMAKE_DISABLE_FIND_PACKAGE_Threads=1 -# The line below should be unnecessary in the future. -# See https://github.com/emscripten-core/emscripten/issues/12714 for -msimd128. -# See https://github.com/emscripten-core/emscripten/issues/12746 for -msse4.2. -$(CODEC_MT_SIMD_BUILD_DIR)/Makefile: CXXFLAGS+=-msimd128 -msse4.2 +# Enable SIMD on a SIMD build. +$(CODEC_MT_SIMD_BUILD_DIR)/Makefile: CMAKE_FLAGS+=-DWP2_ENABLE_SIMD=1 %/Makefile: $(CODEC_DIR)/CMakeLists.txt emcmake cmake \ diff --git a/src/codecs/processor.ts b/src/codecs/processor.ts new file mode 100644 index 00000000..397b23f7 --- /dev/null +++ b/src/codecs/processor.ts @@ -0,0 +1,253 @@ +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 WP2EncoderOptions } from './wp2/encoder-meta'; +import { EncodeOptions as AvifEncoderOptions } from './avif/encoder-meta'; +import { EncodeOptions as JXLEncoderOptions } from './jxl/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. + this._worker = new Worker( + './processor-worker', + { name: 'processor-worker', type: 'module' }, + ); + // 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 { + return this._workerApi!.quantize(data, opts); + } + + @Processor._processingJob({ needsWorker: true }) + rotate( + data: ImageData, opts: import('./rotate/processor-meta').RotateOptions, + ): Promise { + 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, + ): Promise { + return this._workerApi!.mozjpegEncode(data, opts); + } + + @Processor._processingJob({ needsWorker: true }) + async oxiPngEncode( + data: ImageData, opts: OxiPNGEncoderOptions, + ): Promise { + // 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 { + return this._workerApi!.webpEncode(data, opts); + } + + @Processor._processingJob({ needsWorker: true }) + wp2Encode(data: ImageData, opts: WP2EncoderOptions): Promise { + return this._workerApi!.wp2Encode(data, opts); + } + + @Processor._processingJob({ needsWorker: true }) + async webpDecode(blob: Blob): Promise { + const data = await blobToArrayBuffer(blob); + return this._workerApi!.webpDecode(data); + } + + @Processor._processingJob({ needsWorker: true }) + async wp2Decode(blob: Blob): Promise { + const data = await blobToArrayBuffer(blob); + return this._workerApi!.wp2Decode(data); + } + + @Processor._processingJob({ needsWorker: true }) + async avifDecode(blob: Blob): Promise { + const data = await blobToArrayBuffer(blob); + return this._workerApi!.avifDecode(data); + } + + @Processor._processingJob({ needsWorker: true }) + avifEncode(data: ImageData, opts: AvifEncoderOptions): Promise { + return this._workerApi!.avifEncode(data, opts); + } + + @Processor._processingJob({ needsWorker: true }) + jxlEncode(data: ImageData, opts: JXLEncoderOptions): Promise { + return this._workerApi!.jxlEncode(data, opts); + } + + @Processor._processingJob({ needsWorker: true }) + async jxlDecode(blob: Blob): Promise { + const data = await blobToArrayBuffer(blob); + return this._workerApi!.jxlDecode(data); + } + + // Not-worker jobs: + + @Processor._processingJob() + browserBmpEncode(data: ImageData): Promise { + return browserBMP.encode(data); + } + + @Processor._processingJob() + browserPngEncode(data: ImageData): Promise { + return browserPNG.encode(data); + } + + @Processor._processingJob() + browserJpegEncode(data: ImageData, opts: BrowserJPEGOptions): Promise { + return browserJPEG.encode(data, opts); + } + + @Processor._processingJob() + browserWebpEncode(data: ImageData, opts: BrowserWebpEncodeOptions): Promise { + return browserWebP.encode(data, opts); + } + + @Processor._processingJob() + browserGifEncode(data: ImageData): Promise { + return browserGIF.encode(data); + } + + @Processor._processingJob() + browserTiffEncode(data: ImageData): Promise { + return browserTIFF.encode(data); + } + + @Processor._processingJob() + browserJp2Encode(data: ImageData): Promise { + return browserJP2.encode(data); + } + + @Processor._processingJob() + browserPdfEncode(data: ImageData): Promise { + 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); + } +}