diff --git a/libsquoosh/rollup.config.js b/libsquoosh/rollup.config.js index 3ca3a7e2..8a941ea6 100644 --- a/libsquoosh/rollup.config.js +++ b/libsquoosh/rollup.config.js @@ -10,7 +10,7 @@ import { builtinModules } from 'module'; /** @type {import('rollup').RollupOptions} */ export default { - input: 'src/index.js', + input: 'src/index.ts', output: { dir: 'build', format: 'cjs', diff --git a/libsquoosh/src/codecs.ts b/libsquoosh/src/codecs.ts index 8b241ddf..edbf5694 100644 --- a/libsquoosh/src/codecs.ts +++ b/libsquoosh/src/codecs.ts @@ -10,22 +10,11 @@ import { cpus } from 'os'; }; interface DecodeModule extends EmscriptenWasm.Module { - decode: (data: Uint8Array) => any; -} - -interface EncodeModule extends EmscriptenWasm.Module { - encode: ( - data: Uint8ClampedArray | ArrayBuffer, - width: number, - height: number, - opts: any, - ) => Uint8Array; + decode: (data: Uint8Array) => ImageData; } type DecodeModuleFactory = EmscriptenWasm.ModuleFactory; -type EncodeModuleFactory = EmscriptenWasm.ModuleFactory; - interface RotateModuleInstance { exports: { memory: WebAssembly.Memory; @@ -50,7 +39,7 @@ interface ResizeInstantiateOptions { declare global { // Needed for being able to use ImageData as type in codec types - type ImageData = typeof import('./image_data.js'); + type ImageData = import('./image_data.js').default; // Needed for being able to assign to `globalThis.ImageData` var ImageData: ImageData['constructor']; } @@ -58,18 +47,21 @@ declare global { import type { QuantizerModule } from '../../codecs/imagequant/imagequant.js'; // MozJPEG +import type { MozJPEGModule as MozJPEGEncodeModule } from '../../codecs/mozjpeg/enc/mozjpeg_enc'; import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js'; import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm'; import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js'; import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm'; // WebP +import type { WebPModule as WebPEncodeModule } from '../../codecs/webp/enc/webp_enc'; import webpEnc from '../../codecs/webp/enc/webp_node_enc.js'; import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm'; import webpDec from '../../codecs/webp/dec/webp_node_dec.js'; import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm'; // AVIF +import type { AVIFModule as AVIFEncodeModule } from '../../codecs/avif/enc/avif_enc'; import avifEnc from '../../codecs/avif/enc/avif_node_enc.js'; import avifEncWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc.wasm'; import avifEncMt from '../../codecs/avif/enc/avif_node_enc_mt.js'; @@ -79,12 +71,14 @@ import avifDec from '../../codecs/avif/dec/avif_node_dec.js'; import avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm'; // JXL +import type { JXLModule as JXLEncodeModule } from '../../codecs/jxl/enc/jxl_enc'; import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js'; import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm'; import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js'; import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm'; // WP2 +import type { WP2Module as WP2EncodeModule } from '../../codecs/wp2/enc/wp2_enc'; import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js'; import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm'; import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js'; @@ -284,7 +278,9 @@ export const codecs = { dec: () => instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm), enc: () => - instantiateEmscriptenWasm(mozEnc as EncodeModuleFactory, mozEncWasm), + instantiateEmscriptenWasm(mozEnc, mozEncWasm) as Promise< + MozJPEGEncodeModule + >, defaultEncoderOptions: { quality: 75, baseline: false, @@ -316,7 +312,9 @@ export const codecs = { dec: () => instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm), enc: () => - instantiateEmscriptenWasm(webpEnc as EncodeModuleFactory, webpEncWasm), + instantiateEmscriptenWasm(webpEnc, webpEncWasm) as Promise< + WebPEncodeModule + >, defaultEncoderOptions: { quality: 75, target_size: 0, @@ -361,15 +359,14 @@ export const codecs = { enc: async () => { if (await threads()) { return instantiateEmscriptenWasm( - avifEncMt as EncodeModuleFactory, + avifEncMt, avifEncMtWasm, avifEncMtWorker, - ); + ) as Promise; } - return instantiateEmscriptenWasm( - avifEnc as EncodeModuleFactory, - avifEncWasm, - ); + return instantiateEmscriptenWasm(avifEnc, avifEncWasm) as Promise< + AVIFEncodeModule + >; }, defaultEncoderOptions: { cqLevel: 33, @@ -396,7 +393,7 @@ export const codecs = { dec: () => instantiateEmscriptenWasm(jxlDec as DecodeModuleFactory, jxlDecWasm), enc: () => - instantiateEmscriptenWasm(jxlEnc as EncodeModuleFactory, jxlEncWasm), + instantiateEmscriptenWasm(jxlEnc, jxlEncWasm) as Promise, defaultEncoderOptions: { speed: 4, quality: 75, @@ -419,7 +416,7 @@ export const codecs = { dec: () => instantiateEmscriptenWasm(wp2Dec as DecodeModuleFactory, wp2DecWasm), enc: () => - instantiateEmscriptenWasm(wp2Enc as EncodeModuleFactory, wp2EncWasm), + instantiateEmscriptenWasm(wp2Enc, wp2EncWasm) as Promise, defaultEncoderOptions: { quality: 75, alpha_quality: 75, diff --git a/libsquoosh/src/index.ts b/libsquoosh/src/index.ts index 6e726bfe..3d39d8ba 100644 --- a/libsquoosh/src/index.ts +++ b/libsquoosh/src/index.ts @@ -10,7 +10,7 @@ import type ImageData from './image_data'; export { ImagePool, encoders, preprocessors }; type EncoderKey = keyof typeof encoders; type PreprocessorKey = keyof typeof preprocessors; -type FileLike = Buffer | ArrayBuffer | string | { buffer: Buffer }; +type FileLike = Buffer | ArrayBuffer | string | ArrayBufferView; async function decodeFile({ file, @@ -24,8 +24,9 @@ async function decodeFile({ } else if (file instanceof ArrayBuffer) { buffer = Buffer.from(file); file = 'Binary blob'; - } else if (file instanceof Buffer) { - buffer = file; + } else if ((file as unknown) instanceof Buffer) { + // TODO: Check why we need type assertions here. + buffer = (file as unknown) as Buffer; file = 'Binary blob'; } else if (typeof file === 'string') { buffer = await fsp.readFile(file); @@ -99,9 +100,16 @@ async function encodeImage({ }), ); const decode = (binary: Uint8Array) => decoder.decode(binary); + const nonNullEncode = (bitmap: ImageData, quality: number): Uint8Array => { + const result = encode(bitmap, quality); + if (!result) { + throw new Error('There was an error while encoding'); + } + return result; + }; const { binary: optimizedBinary, quality } = await autoOptimize( bitmapIn, - encode, + nonNullEncode, decode, { min: encoders[encName].autoOptimize.min, @@ -116,12 +124,18 @@ async function encodeImage({ [optionToOptimize]: Math.round(quality * 10000) / 10000, }; } else { - binary = encoder.encode( + const result = encoder.encode( bitmapIn.data.buffer, bitmapIn.width, bitmapIn.height, encConfig, ); + + if (!result) { + throw new Error('There was an error while encoding'); + } + + binary = result; } return { optionsUsed, @@ -155,11 +169,11 @@ function handleJob(params: JobMessage) { */ class Image { public file: FileLike; - public workerPool: WorkerPool; + public workerPool: WorkerPool; public decoded: Promise<{ bitmap: ImageData }>; public encodedWith: { [key: string]: any }; - constructor(workerPool: WorkerPool, file: FileLike) { + constructor(workerPool: WorkerPool, file: FileLike) { this.file = file; this.workerPool = workerPool; this.decoded = workerPool.dispatchJob({ operation: 'decode', file }); @@ -201,6 +215,8 @@ class Image { encodeOptions: { optimizerButteraugliTarget?: number; maxOptimizerRounds?: number; + } & { + [key in EncoderKey]?: any; // any is okay for now } = {}, ): Promise { const { bitmap } = await this.decoded; @@ -233,7 +249,7 @@ class Image { * A pool where images can be ingested and squooshed. */ class ImagePool { - public workerPool: WorkerPool; + public workerPool: WorkerPool; /** * Create a new pool. diff --git a/libsquoosh/src/worker_pool.ts b/libsquoosh/src/worker_pool.ts index c0b41522..336d69fc 100644 --- a/libsquoosh/src/worker_pool.ts +++ b/libsquoosh/src/worker_pool.ts @@ -1,7 +1,5 @@ import { Worker, parentPort } from 'worker_threads'; -// @ts-ignore import { TransformStream } from 'web-streams-polyfill'; -import type { JobMessage } from './index'; function uuid() { return Array.from({ length: 16 }, () => @@ -9,28 +7,16 @@ function uuid() { ).join(''); } -function jobPromise(worker: Worker, msg: JobMessage) { - return new Promise((resolve, reject) => { - const id = uuid(); - worker.postMessage({ msg, id }); - worker.on('message', function f({ error, result, id: rid }) { - if (rid !== id) { - return; - } - if (error) { - reject(error); - return; - } - worker.off('message', f); - resolve(result); - }); - }); +interface Job { + msg: I; + resolve: Function; + reject: Function; } -export default class WorkerPool { +export default class WorkerPool { public numWorkers: number; - public jobQueue: TransformStream; - public workerQueue: TransformStream; + public jobQueue: TransformStream, Job>; + public workerQueue: TransformStream; public done: Promise; constructor(numWorkers: number, workerFile: string) { @@ -55,9 +41,14 @@ export default class WorkerPool { await this._terminateAll(); return; } + + if (!value) { + throw new Error('Reader did not return any value'); + } + const { msg, resolve, reject } = value; const worker = await this._nextWorker(); - jobPromise(worker, msg) + this.jobPromise(worker, msg) .then((result) => resolve(result)) .catch((reason) => reject(reason)) .finally(() => { @@ -73,6 +64,10 @@ export default class WorkerPool { const reader = this.workerQueue.readable.getReader(); const { value } = await reader.read(); reader.releaseLock(); + if (!value) { + throw new Error('No worker left'); + } + return value; } @@ -89,7 +84,7 @@ export default class WorkerPool { await this.done; } - dispatchJob(msg: JobMessage): Promise { + dispatchJob(msg: I): Promise { return new Promise((resolve, reject) => { const writer = this.jobQueue.writable.getWriter(); writer.write({ msg, resolve, reject }); @@ -97,7 +92,25 @@ export default class WorkerPool { }); } - static useThisThreadAsWorker(cb: (msg: JobMessage) => any) { + private jobPromise(worker: Worker, msg: I) { + return new Promise((resolve, reject) => { + const id = uuid(); + worker.postMessage({ msg, id }); + worker.on('message', function f({ error, result, id: rid }) { + if (rid !== id) { + return; + } + if (error) { + reject(error); + return; + } + worker.off('message', f); + resolve(result); + }); + }); + } + + static useThisThreadAsWorker(cb: (msg: I) => O) { parentPort!.on('message', async (data) => { const { msg, id } = data; try {