Update code for the review comments

* Make decode module return value `ImageData`
* Fix global definition of ImageData
* Use concrete Encoder types for encode functions
* Use ArrayBufferView in FileLike instead of using a similar type
* Throw error when the `encode` functions
return null
* Use generic types for WorkerPool
* Fix `encode` function typing
in `index.ts`
* Remove ts-ignore for web-streams-polyfill
and handle nulls for TransformStream
* Fix rollup entry point (now we need to have
`index.ts` instead of `index.js`)
This commit is contained in:
ergunsh
2021-08-06 16:05:14 +03:00
parent de4eb9c8f7
commit fafcf97f0c
4 changed files with 82 additions and 56 deletions

View File

@@ -10,7 +10,7 @@ import { builtinModules } from 'module';
/** @type {import('rollup').RollupOptions} */ /** @type {import('rollup').RollupOptions} */
export default { export default {
input: 'src/index.js', input: 'src/index.ts',
output: { output: {
dir: 'build', dir: 'build',
format: 'cjs', format: 'cjs',

View File

@@ -10,22 +10,11 @@ import { cpus } from 'os';
}; };
interface DecodeModule extends EmscriptenWasm.Module { interface DecodeModule extends EmscriptenWasm.Module {
decode: (data: Uint8Array) => any; decode: (data: Uint8Array) => ImageData;
}
interface EncodeModule extends EmscriptenWasm.Module {
encode: (
data: Uint8ClampedArray | ArrayBuffer,
width: number,
height: number,
opts: any,
) => Uint8Array;
} }
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>; type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>;
type EncodeModuleFactory = EmscriptenWasm.ModuleFactory<EncodeModule>;
interface RotateModuleInstance { interface RotateModuleInstance {
exports: { exports: {
memory: WebAssembly.Memory; memory: WebAssembly.Memory;
@@ -50,7 +39,7 @@ interface ResizeInstantiateOptions {
declare global { declare global {
// Needed for being able to use ImageData as type in codec types // 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` // Needed for being able to assign to `globalThis.ImageData`
var ImageData: ImageData['constructor']; var ImageData: ImageData['constructor'];
} }
@@ -58,18 +47,21 @@ declare global {
import type { QuantizerModule } from '../../codecs/imagequant/imagequant.js'; import type { QuantizerModule } from '../../codecs/imagequant/imagequant.js';
// MozJPEG // MozJPEG
import type { MozJPEGModule as MozJPEGEncodeModule } from '../../codecs/mozjpeg/enc/mozjpeg_enc';
import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js'; import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js';
import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm'; import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm';
import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js'; import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js';
import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm'; import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm';
// WebP // WebP
import type { WebPModule as WebPEncodeModule } from '../../codecs/webp/enc/webp_enc';
import webpEnc from '../../codecs/webp/enc/webp_node_enc.js'; import webpEnc from '../../codecs/webp/enc/webp_node_enc.js';
import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm'; import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm';
import webpDec from '../../codecs/webp/dec/webp_node_dec.js'; import webpDec from '../../codecs/webp/dec/webp_node_dec.js';
import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm'; import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm';
// AVIF // AVIF
import type { AVIFModule as AVIFEncodeModule } from '../../codecs/avif/enc/avif_enc';
import avifEnc from '../../codecs/avif/enc/avif_node_enc.js'; import avifEnc from '../../codecs/avif/enc/avif_node_enc.js';
import avifEncWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc.wasm'; import avifEncWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc.wasm';
import avifEncMt from '../../codecs/avif/enc/avif_node_enc_mt.js'; 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'; import avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm';
// JXL // JXL
import type { JXLModule as JXLEncodeModule } from '../../codecs/jxl/enc/jxl_enc';
import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js'; import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js';
import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm'; import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm';
import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js'; import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js';
import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm'; import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm';
// WP2 // WP2
import type { WP2Module as WP2EncodeModule } from '../../codecs/wp2/enc/wp2_enc';
import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js'; import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js';
import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm'; import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm';
import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js'; import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js';
@@ -284,7 +278,9 @@ export const codecs = {
dec: () => dec: () =>
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm), instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm),
enc: () => enc: () =>
instantiateEmscriptenWasm(mozEnc as EncodeModuleFactory, mozEncWasm), instantiateEmscriptenWasm(mozEnc, mozEncWasm) as Promise<
MozJPEGEncodeModule
>,
defaultEncoderOptions: { defaultEncoderOptions: {
quality: 75, quality: 75,
baseline: false, baseline: false,
@@ -316,7 +312,9 @@ export const codecs = {
dec: () => dec: () =>
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm), instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm),
enc: () => enc: () =>
instantiateEmscriptenWasm(webpEnc as EncodeModuleFactory, webpEncWasm), instantiateEmscriptenWasm(webpEnc, webpEncWasm) as Promise<
WebPEncodeModule
>,
defaultEncoderOptions: { defaultEncoderOptions: {
quality: 75, quality: 75,
target_size: 0, target_size: 0,
@@ -361,15 +359,14 @@ export const codecs = {
enc: async () => { enc: async () => {
if (await threads()) { if (await threads()) {
return instantiateEmscriptenWasm( return instantiateEmscriptenWasm(
avifEncMt as EncodeModuleFactory, avifEncMt,
avifEncMtWasm, avifEncMtWasm,
avifEncMtWorker, avifEncMtWorker,
); ) as Promise<AVIFEncodeModule>;
} }
return instantiateEmscriptenWasm( return instantiateEmscriptenWasm(avifEnc, avifEncWasm) as Promise<
avifEnc as EncodeModuleFactory, AVIFEncodeModule
avifEncWasm, >;
);
}, },
defaultEncoderOptions: { defaultEncoderOptions: {
cqLevel: 33, cqLevel: 33,
@@ -396,7 +393,7 @@ export const codecs = {
dec: () => dec: () =>
instantiateEmscriptenWasm(jxlDec as DecodeModuleFactory, jxlDecWasm), instantiateEmscriptenWasm(jxlDec as DecodeModuleFactory, jxlDecWasm),
enc: () => enc: () =>
instantiateEmscriptenWasm(jxlEnc as EncodeModuleFactory, jxlEncWasm), instantiateEmscriptenWasm(jxlEnc, jxlEncWasm) as Promise<JXLEncodeModule>,
defaultEncoderOptions: { defaultEncoderOptions: {
speed: 4, speed: 4,
quality: 75, quality: 75,
@@ -419,7 +416,7 @@ export const codecs = {
dec: () => dec: () =>
instantiateEmscriptenWasm(wp2Dec as DecodeModuleFactory, wp2DecWasm), instantiateEmscriptenWasm(wp2Dec as DecodeModuleFactory, wp2DecWasm),
enc: () => enc: () =>
instantiateEmscriptenWasm(wp2Enc as EncodeModuleFactory, wp2EncWasm), instantiateEmscriptenWasm(wp2Enc, wp2EncWasm) as Promise<WP2EncodeModule>,
defaultEncoderOptions: { defaultEncoderOptions: {
quality: 75, quality: 75,
alpha_quality: 75, alpha_quality: 75,

View File

@@ -10,7 +10,7 @@ import type ImageData from './image_data';
export { ImagePool, encoders, preprocessors }; export { ImagePool, encoders, preprocessors };
type EncoderKey = keyof typeof encoders; type EncoderKey = keyof typeof encoders;
type PreprocessorKey = keyof typeof preprocessors; type PreprocessorKey = keyof typeof preprocessors;
type FileLike = Buffer | ArrayBuffer | string | { buffer: Buffer }; type FileLike = Buffer | ArrayBuffer | string | ArrayBufferView;
async function decodeFile({ async function decodeFile({
file, file,
@@ -24,8 +24,9 @@ async function decodeFile({
} else if (file instanceof ArrayBuffer) { } else if (file instanceof ArrayBuffer) {
buffer = Buffer.from(file); buffer = Buffer.from(file);
file = 'Binary blob'; file = 'Binary blob';
} else if (file instanceof Buffer) { } else if ((file as unknown) instanceof Buffer) {
buffer = file; // TODO: Check why we need type assertions here.
buffer = (file as unknown) as Buffer;
file = 'Binary blob'; file = 'Binary blob';
} else if (typeof file === 'string') { } else if (typeof file === 'string') {
buffer = await fsp.readFile(file); buffer = await fsp.readFile(file);
@@ -99,9 +100,16 @@ async function encodeImage({
}), }),
); );
const decode = (binary: Uint8Array) => decoder.decode(binary); 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( const { binary: optimizedBinary, quality } = await autoOptimize(
bitmapIn, bitmapIn,
encode, nonNullEncode,
decode, decode,
{ {
min: encoders[encName].autoOptimize.min, min: encoders[encName].autoOptimize.min,
@@ -116,12 +124,18 @@ async function encodeImage({
[optionToOptimize]: Math.round(quality * 10000) / 10000, [optionToOptimize]: Math.round(quality * 10000) / 10000,
}; };
} else { } else {
binary = encoder.encode( const result = encoder.encode(
bitmapIn.data.buffer, bitmapIn.data.buffer,
bitmapIn.width, bitmapIn.width,
bitmapIn.height, bitmapIn.height,
encConfig, encConfig,
); );
if (!result) {
throw new Error('There was an error while encoding');
}
binary = result;
} }
return { return {
optionsUsed, optionsUsed,
@@ -155,11 +169,11 @@ function handleJob(params: JobMessage) {
*/ */
class Image { class Image {
public file: FileLike; public file: FileLike;
public workerPool: WorkerPool; public workerPool: WorkerPool<JobMessage, any>;
public decoded: Promise<{ bitmap: ImageData }>; public decoded: Promise<{ bitmap: ImageData }>;
public encodedWith: { [key: string]: any }; public encodedWith: { [key: string]: any };
constructor(workerPool: WorkerPool, file: FileLike) { constructor(workerPool: WorkerPool<JobMessage, any>, file: FileLike) {
this.file = file; this.file = file;
this.workerPool = workerPool; this.workerPool = workerPool;
this.decoded = workerPool.dispatchJob({ operation: 'decode', file }); this.decoded = workerPool.dispatchJob({ operation: 'decode', file });
@@ -201,6 +215,8 @@ class Image {
encodeOptions: { encodeOptions: {
optimizerButteraugliTarget?: number; optimizerButteraugliTarget?: number;
maxOptimizerRounds?: number; maxOptimizerRounds?: number;
} & {
[key in EncoderKey]?: any; // any is okay for now
} = {}, } = {},
): Promise<void> { ): Promise<void> {
const { bitmap } = await this.decoded; const { bitmap } = await this.decoded;
@@ -233,7 +249,7 @@ class Image {
* A pool where images can be ingested and squooshed. * A pool where images can be ingested and squooshed.
*/ */
class ImagePool { class ImagePool {
public workerPool: WorkerPool; public workerPool: WorkerPool<JobMessage, any>;
/** /**
* Create a new pool. * Create a new pool.

View File

@@ -1,7 +1,5 @@
import { Worker, parentPort } from 'worker_threads'; import { Worker, parentPort } from 'worker_threads';
// @ts-ignore
import { TransformStream } from 'web-streams-polyfill'; import { TransformStream } from 'web-streams-polyfill';
import type { JobMessage } from './index';
function uuid() { function uuid() {
return Array.from({ length: 16 }, () => return Array.from({ length: 16 }, () =>
@@ -9,28 +7,16 @@ function uuid() {
).join(''); ).join('');
} }
function jobPromise(worker: Worker, msg: JobMessage) { interface Job<I> {
return new Promise((resolve, reject) => { msg: I;
const id = uuid(); resolve: Function;
worker.postMessage({ msg, id }); reject: Function;
worker.on('message', function f({ error, result, id: rid }) {
if (rid !== id) {
return;
}
if (error) {
reject(error);
return;
}
worker.off('message', f);
resolve(result);
});
});
} }
export default class WorkerPool { export default class WorkerPool<I, O> {
public numWorkers: number; public numWorkers: number;
public jobQueue: TransformStream; public jobQueue: TransformStream<Job<I>, Job<I>>;
public workerQueue: TransformStream; public workerQueue: TransformStream<Worker, Worker>;
public done: Promise<void>; public done: Promise<void>;
constructor(numWorkers: number, workerFile: string) { constructor(numWorkers: number, workerFile: string) {
@@ -55,9 +41,14 @@ export default class WorkerPool {
await this._terminateAll(); await this._terminateAll();
return; return;
} }
if (!value) {
throw new Error('Reader did not return any value');
}
const { msg, resolve, reject } = value; const { msg, resolve, reject } = value;
const worker = await this._nextWorker(); const worker = await this._nextWorker();
jobPromise(worker, msg) this.jobPromise(worker, msg)
.then((result) => resolve(result)) .then((result) => resolve(result))
.catch((reason) => reject(reason)) .catch((reason) => reject(reason))
.finally(() => { .finally(() => {
@@ -73,6 +64,10 @@ export default class WorkerPool {
const reader = this.workerQueue.readable.getReader(); const reader = this.workerQueue.readable.getReader();
const { value } = await reader.read(); const { value } = await reader.read();
reader.releaseLock(); reader.releaseLock();
if (!value) {
throw new Error('No worker left');
}
return value; return value;
} }
@@ -89,7 +84,7 @@ export default class WorkerPool {
await this.done; await this.done;
} }
dispatchJob(msg: JobMessage): Promise<any> { dispatchJob(msg: I): Promise<O> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const writer = this.jobQueue.writable.getWriter(); const writer = this.jobQueue.writable.getWriter();
writer.write({ msg, resolve, reject }); 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<I, O>(cb: (msg: I) => O) {
parentPort!.on('message', async (data) => { parentPort!.on('message', async (data) => {
const { msg, id } = data; const { msg, id } = data;
try { try {