mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-12 16:57:26 +00:00
Merge pull request #1110 from styfle/libsquoosh-ts-remaining
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import { cpus } from 'os';
|
|||||||
hardwareConcurrency: cpus().length,
|
hardwareConcurrency: cpus().length,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface DecodeModule extends EmscriptenWasm.Module {
|
||||||
|
decode: (data: Uint8Array) => ImageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>;
|
||||||
|
|
||||||
interface RotateModuleInstance {
|
interface RotateModuleInstance {
|
||||||
exports: {
|
exports: {
|
||||||
memory: WebAssembly.Memory;
|
memory: WebAssembly.Memory;
|
||||||
@@ -33,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'];
|
||||||
}
|
}
|
||||||
@@ -41,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';
|
||||||
@@ -62,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';
|
||||||
@@ -257,15 +268,20 @@ export const preprocessors = {
|
|||||||
numRotations: 0,
|
numRotations: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export const codecs = {
|
export const codecs = {
|
||||||
mozjpeg: {
|
mozjpeg: {
|
||||||
name: 'MozJPEG',
|
name: 'MozJPEG',
|
||||||
extension: 'jpg',
|
extension: 'jpg',
|
||||||
detectors: [/^\xFF\xD8\xFF/],
|
detectors: [/^\xFF\xD8\xFF/],
|
||||||
dec: () => instantiateEmscriptenWasm(mozDec, mozDecWasm),
|
dec: () =>
|
||||||
enc: () => instantiateEmscriptenWasm(mozEnc, mozEncWasm),
|
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm),
|
||||||
|
enc: () =>
|
||||||
|
instantiateEmscriptenWasm(
|
||||||
|
mozEnc as EmscriptenWasm.ModuleFactory<MozJPEGEncodeModule>,
|
||||||
|
mozEncWasm,
|
||||||
|
),
|
||||||
defaultEncoderOptions: {
|
defaultEncoderOptions: {
|
||||||
quality: 75,
|
quality: 75,
|
||||||
baseline: false,
|
baseline: false,
|
||||||
@@ -294,8 +310,13 @@ export const codecs = {
|
|||||||
name: 'WebP',
|
name: 'WebP',
|
||||||
extension: 'webp',
|
extension: 'webp',
|
||||||
detectors: [/^RIFF....WEBPVP8[LX ]/s],
|
detectors: [/^RIFF....WEBPVP8[LX ]/s],
|
||||||
dec: () => instantiateEmscriptenWasm(webpDec, webpDecWasm),
|
dec: () =>
|
||||||
enc: () => instantiateEmscriptenWasm(webpEnc, webpEncWasm),
|
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm),
|
||||||
|
enc: () =>
|
||||||
|
instantiateEmscriptenWasm(
|
||||||
|
webpEnc as EmscriptenWasm.ModuleFactory<WebPEncodeModule>,
|
||||||
|
webpEncWasm,
|
||||||
|
),
|
||||||
defaultEncoderOptions: {
|
defaultEncoderOptions: {
|
||||||
quality: 75,
|
quality: 75,
|
||||||
target_size: 0,
|
target_size: 0,
|
||||||
@@ -335,16 +356,20 @@ export const codecs = {
|
|||||||
name: 'AVIF',
|
name: 'AVIF',
|
||||||
extension: 'avif',
|
extension: 'avif',
|
||||||
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
|
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
|
||||||
dec: () => instantiateEmscriptenWasm(avifDec, avifDecWasm),
|
dec: () =>
|
||||||
|
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm),
|
||||||
enc: async () => {
|
enc: async () => {
|
||||||
if (await threads()) {
|
if (await threads()) {
|
||||||
return instantiateEmscriptenWasm(
|
return instantiateEmscriptenWasm(
|
||||||
avifEncMt,
|
avifEncMt as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
|
||||||
avifEncMtWasm,
|
avifEncMtWasm,
|
||||||
avifEncMtWorker,
|
avifEncMtWorker,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return instantiateEmscriptenWasm(avifEnc, avifEncWasm);
|
return instantiateEmscriptenWasm(
|
||||||
|
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
|
||||||
|
avifEncWasm,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
defaultEncoderOptions: {
|
defaultEncoderOptions: {
|
||||||
cqLevel: 33,
|
cqLevel: 33,
|
||||||
@@ -368,8 +393,13 @@ export const codecs = {
|
|||||||
name: 'JPEG-XL',
|
name: 'JPEG-XL',
|
||||||
extension: 'jxl',
|
extension: 'jxl',
|
||||||
detectors: [/^\xff\x0a/],
|
detectors: [/^\xff\x0a/],
|
||||||
dec: () => instantiateEmscriptenWasm(jxlDec, jxlDecWasm),
|
dec: () =>
|
||||||
enc: () => instantiateEmscriptenWasm(jxlEnc, jxlEncWasm),
|
instantiateEmscriptenWasm(jxlDec as DecodeModuleFactory, jxlDecWasm),
|
||||||
|
enc: () =>
|
||||||
|
instantiateEmscriptenWasm(
|
||||||
|
jxlEnc as EmscriptenWasm.ModuleFactory<JXLEncodeModule>,
|
||||||
|
jxlEncWasm,
|
||||||
|
),
|
||||||
defaultEncoderOptions: {
|
defaultEncoderOptions: {
|
||||||
speed: 4,
|
speed: 4,
|
||||||
quality: 75,
|
quality: 75,
|
||||||
@@ -389,8 +419,13 @@ export const codecs = {
|
|||||||
name: 'WebP2',
|
name: 'WebP2',
|
||||||
extension: 'wp2',
|
extension: 'wp2',
|
||||||
detectors: [/^\xF4\xFF\x6F/],
|
detectors: [/^\xF4\xFF\x6F/],
|
||||||
dec: () => instantiateEmscriptenWasm(wp2Dec, wp2DecWasm),
|
dec: () =>
|
||||||
enc: () => instantiateEmscriptenWasm(wp2Enc, wp2EncWasm),
|
instantiateEmscriptenWasm(wp2Dec as DecodeModuleFactory, wp2DecWasm),
|
||||||
|
enc: () =>
|
||||||
|
instantiateEmscriptenWasm(
|
||||||
|
wp2Enc as EmscriptenWasm.ModuleFactory<WP2EncodeModule>,
|
||||||
|
wp2EncWasm,
|
||||||
|
),
|
||||||
defaultEncoderOptions: {
|
defaultEncoderOptions: {
|
||||||
quality: 75,
|
quality: 75,
|
||||||
alpha_quality: 75,
|
alpha_quality: 75,
|
||||||
@@ -421,7 +456,7 @@ export const codecs = {
|
|||||||
await oxipngPromise;
|
await oxipngPromise;
|
||||||
return {
|
return {
|
||||||
encode: (
|
encode: (
|
||||||
buffer: Uint8Array,
|
buffer: Uint8ClampedArray | ArrayBuffer,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
opts: { level: number },
|
opts: { level: number },
|
||||||
@@ -444,4 +479,4 @@ export const codecs = {
|
|||||||
max: 1,
|
max: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} as const;
|
||||||
|
|||||||
@@ -5,10 +5,18 @@ import { promises as fsp } from 'fs';
|
|||||||
import { codecs as encoders, preprocessors } from './codecs.js';
|
import { codecs as encoders, preprocessors } from './codecs.js';
|
||||||
import WorkerPool from './worker_pool.js';
|
import WorkerPool from './worker_pool.js';
|
||||||
import { autoOptimize } from './auto-optimizer.js';
|
import { autoOptimize } from './auto-optimizer.js';
|
||||||
|
import type ImageData from './image_data';
|
||||||
|
|
||||||
export { ImagePool, encoders, preprocessors };
|
export { ImagePool, encoders, preprocessors };
|
||||||
|
type EncoderKey = keyof typeof encoders;
|
||||||
|
type PreprocessorKey = keyof typeof preprocessors;
|
||||||
|
type FileLike = Buffer | ArrayBuffer | string | ArrayBufferView;
|
||||||
|
|
||||||
async function decodeFile({ file }) {
|
async function decodeFile({
|
||||||
|
file,
|
||||||
|
}: {
|
||||||
|
file: FileLike;
|
||||||
|
}): Promise<{ bitmap: ImageData; size: number }> {
|
||||||
let buffer;
|
let buffer;
|
||||||
if (ArrayBuffer.isView(file)) {
|
if (ArrayBuffer.isView(file)) {
|
||||||
buffer = Buffer.from(file.buffer);
|
buffer = Buffer.from(file.buffer);
|
||||||
@@ -16,8 +24,9 @@ async function decodeFile({ file }) {
|
|||||||
} 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);
|
||||||
@@ -28,23 +37,33 @@ async function decodeFile({ file }) {
|
|||||||
const firstChunkString = Array.from(firstChunk)
|
const firstChunkString = Array.from(firstChunk)
|
||||||
.map((v) => String.fromCodePoint(v))
|
.map((v) => String.fromCodePoint(v))
|
||||||
.join('');
|
.join('');
|
||||||
const key = Object.entries(encoders).find(([name, { detectors }]) =>
|
const key = Object.entries(encoders).find(([_name, { detectors }]) =>
|
||||||
detectors.some((detector) => detector.exec(firstChunkString)),
|
detectors.some((detector) => detector.exec(firstChunkString)),
|
||||||
)?.[0];
|
)?.[0] as EncoderKey | undefined;
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw Error(`${file} has an unsupported format`);
|
throw Error(`${file} has an unsupported format`);
|
||||||
}
|
}
|
||||||
const rgba = (await encoders[key].dec()).decode(new Uint8Array(buffer));
|
const encoder = encoders[key];
|
||||||
|
const mod = await encoder.dec();
|
||||||
|
const rgba = mod.decode(new Uint8Array(buffer));
|
||||||
return {
|
return {
|
||||||
bitmap: rgba,
|
bitmap: rgba,
|
||||||
size: buffer.length,
|
size: buffer.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preprocessImage({ preprocessorName, options, image }) {
|
async function preprocessImage({
|
||||||
|
preprocessorName,
|
||||||
|
options,
|
||||||
|
image,
|
||||||
|
}: {
|
||||||
|
preprocessorName: PreprocessorKey;
|
||||||
|
options: any;
|
||||||
|
image: { bitmap: ImageData };
|
||||||
|
}) {
|
||||||
const preprocessor = await preprocessors[preprocessorName].instantiate();
|
const preprocessor = await preprocessors[preprocessorName].instantiate();
|
||||||
image.bitmap = await preprocessor(
|
image.bitmap = await preprocessor(
|
||||||
image.bitmap.data,
|
Uint8Array.from(image.bitmap.data),
|
||||||
image.bitmap.width,
|
image.bitmap.width,
|
||||||
image.bitmap.height,
|
image.bitmap.height,
|
||||||
options,
|
options,
|
||||||
@@ -58,26 +77,39 @@ async function encodeImage({
|
|||||||
encConfig,
|
encConfig,
|
||||||
optimizerButteraugliTarget,
|
optimizerButteraugliTarget,
|
||||||
maxOptimizerRounds,
|
maxOptimizerRounds,
|
||||||
|
}: {
|
||||||
|
bitmap: ImageData;
|
||||||
|
encName: EncoderKey;
|
||||||
|
encConfig: any;
|
||||||
|
optimizerButteraugliTarget: number;
|
||||||
|
maxOptimizerRounds: number;
|
||||||
}) {
|
}) {
|
||||||
let binary;
|
let binary: Uint8Array;
|
||||||
let optionsUsed = encConfig;
|
let optionsUsed = encConfig;
|
||||||
const encoder = await encoders[encName].enc();
|
const encoder = await encoders[encName].enc();
|
||||||
if (encConfig === 'auto') {
|
if (encConfig === 'auto') {
|
||||||
const optionToOptimize = encoders[encName].autoOptimize.option;
|
const optionToOptimize = encoders[encName].autoOptimize.option;
|
||||||
const decoder = await encoders[encName].dec();
|
const decoder = await encoders[encName].dec();
|
||||||
const encode = (bitmapIn, quality) =>
|
const encode = (bitmapIn: ImageData, quality: number) =>
|
||||||
encoder.encode(
|
encoder.encode(
|
||||||
bitmapIn.data,
|
bitmapIn.data,
|
||||||
bitmapIn.width,
|
bitmapIn.width,
|
||||||
bitmapIn.height,
|
bitmapIn.height,
|
||||||
Object.assign({}, encoders[encName].defaultEncoderOptions, {
|
Object.assign({}, encoders[encName].defaultEncoderOptions as any, {
|
||||||
[optionToOptimize]: quality,
|
[optionToOptimize]: quality,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const decode = (binary) => 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,
|
||||||
@@ -92,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,
|
||||||
@@ -107,10 +145,15 @@ async function encodeImage({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// both decoding and encoding go through the worker pool
|
type EncodeParams = { operation: 'encode' } & Parameters<typeof encodeImage>[0];
|
||||||
function handleJob(params) {
|
type DecodeParams = { operation: 'decode' } & Parameters<typeof decodeFile>[0];
|
||||||
const { operation } = params;
|
type PreprocessParams = { operation: 'preprocess' } & Parameters<
|
||||||
switch (operation) {
|
typeof preprocessImage
|
||||||
|
>[0];
|
||||||
|
type JobMessage = EncodeParams | DecodeParams | PreprocessParams;
|
||||||
|
|
||||||
|
function handleJob(params: JobMessage) {
|
||||||
|
switch (params.operation) {
|
||||||
case 'encode':
|
case 'encode':
|
||||||
return encodeImage(params);
|
return encodeImage(params);
|
||||||
case 'decode':
|
case 'decode':
|
||||||
@@ -118,7 +161,7 @@ function handleJob(params) {
|
|||||||
case 'preprocess':
|
case 'preprocess':
|
||||||
return preprocessImage(params);
|
return preprocessImage(params);
|
||||||
default:
|
default:
|
||||||
throw Error(`Invalid job "${operation}"`);
|
throw Error(`Invalid job "${(params as any).operation}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +169,12 @@ function handleJob(params) {
|
|||||||
* Represents an ingested image.
|
* Represents an ingested image.
|
||||||
*/
|
*/
|
||||||
class Image {
|
class Image {
|
||||||
constructor(workerPool, file) {
|
public file: FileLike;
|
||||||
|
public workerPool: WorkerPool<JobMessage, any>;
|
||||||
|
public decoded: Promise<{ bitmap: ImageData }>;
|
||||||
|
public encodedWith: { [key: string]: any };
|
||||||
|
|
||||||
|
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 });
|
||||||
@@ -143,14 +191,15 @@ class Image {
|
|||||||
if (!Object.keys(preprocessors).includes(name)) {
|
if (!Object.keys(preprocessors).includes(name)) {
|
||||||
throw Error(`Invalid preprocessor "${name}"`);
|
throw Error(`Invalid preprocessor "${name}"`);
|
||||||
}
|
}
|
||||||
|
const preprocessorName = name as PreprocessorKey;
|
||||||
const preprocessorOptions = Object.assign(
|
const preprocessorOptions = Object.assign(
|
||||||
{},
|
{},
|
||||||
preprocessors[name].defaultOptions,
|
preprocessors[preprocessorName].defaultOptions,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
this.decoded = this.workerPool.dispatchJob({
|
this.decoded = this.workerPool.dispatchJob({
|
||||||
operation: 'preprocess',
|
operation: 'preprocess',
|
||||||
preprocessorName: name,
|
preprocessorName,
|
||||||
image: await this.decoded,
|
image: await this.decoded,
|
||||||
options: preprocessorOptions,
|
options: preprocessorOptions,
|
||||||
});
|
});
|
||||||
@@ -161,14 +210,22 @@ class Image {
|
|||||||
/**
|
/**
|
||||||
* Define one or several encoders to use on the image.
|
* Define one or several encoders to use on the image.
|
||||||
* @param {object} encodeOptions - An object with encoders to use, and their settings.
|
* @param {object} encodeOptions - An object with encoders to use, and their settings.
|
||||||
* @returns {Promise<undefined>} - A promise that resolves when the image has been encoded with all the specified encoders.
|
* @returns {Promise<void>} - A promise that resolves when the image has been encoded with all the specified encoders.
|
||||||
*/
|
*/
|
||||||
async encode(encodeOptions = {}) {
|
async encode(
|
||||||
|
encodeOptions: {
|
||||||
|
optimizerButteraugliTarget?: number;
|
||||||
|
maxOptimizerRounds?: number;
|
||||||
|
} & {
|
||||||
|
[key in EncoderKey]?: any; // any is okay for now
|
||||||
|
} = {},
|
||||||
|
): Promise<void> {
|
||||||
const { bitmap } = await this.decoded;
|
const { bitmap } = await this.decoded;
|
||||||
for (const [encName, options] of Object.entries(encodeOptions)) {
|
for (const [name, options] of Object.entries(encodeOptions)) {
|
||||||
if (!Object.keys(encoders).includes(encName)) {
|
if (!Object.keys(encoders).includes(name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const encName = name as EncoderKey;
|
||||||
const encRef = encoders[encName];
|
const encRef = encoders[encName];
|
||||||
const encConfig =
|
const encConfig =
|
||||||
typeof options === 'string'
|
typeof options === 'string'
|
||||||
@@ -193,28 +250,30 @@ 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<JobMessage, any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new pool.
|
* Create a new pool.
|
||||||
* @param {number} [threads] - Number of concurrent image processes to run in the pool. Defaults to the number of CPU cores in the system.
|
* @param {number} [threads] - Number of concurrent image processes to run in the pool. Defaults to the number of CPU cores in the system.
|
||||||
*/
|
*/
|
||||||
constructor(threads) {
|
constructor(threads: number) {
|
||||||
this.workerPool = new WorkerPool(threads || cpus().length, __filename);
|
this.workerPool = new WorkerPool(threads || cpus().length, __filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ingest an image into the image pool.
|
* Ingest an image into the image pool.
|
||||||
* @param {string | Buffer | URL | object} image - The image or path to the image that should be ingested and decoded.
|
* @param {FileLike} image - The image or path to the image that should be ingested and decoded.
|
||||||
* @returns {Image} - A custom class reference to the decoded image.
|
* @returns {Image} - A custom class reference to the decoded image.
|
||||||
*/
|
*/
|
||||||
ingestImage(image) {
|
ingestImage(image: FileLike): Image {
|
||||||
return new Image(this.workerPool, image);
|
return new Image(this.workerPool, image);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the underlying image processing pipeline. The already processed images will still be there, but no new processing can start.
|
* Closes the underlying image processing pipeline. The already processed images will still be there, but no new processing can start.
|
||||||
* @returns {Promise<undefined>} - A promise that resolves when the underlying pipeline has closed.
|
* @returns {Promise<void>} - A promise that resolves when the underlying pipeline has closed.
|
||||||
*/
|
*/
|
||||||
async close() {
|
async close(): Promise<void> {
|
||||||
await this.workerPool.join();
|
await this.workerPool.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,26 +7,19 @@ function uuid() {
|
|||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function jobPromise(worker, msg) {
|
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> {
|
||||||
constructor(numWorkers, workerFile) {
|
public numWorkers: number;
|
||||||
|
public jobQueue: TransformStream<Job<I>, Job<I>>;
|
||||||
|
public workerQueue: TransformStream<Worker, Worker>;
|
||||||
|
public done: Promise<void>;
|
||||||
|
|
||||||
|
constructor(numWorkers: number, workerFile: string) {
|
||||||
this.numWorkers = numWorkers;
|
this.numWorkers = numWorkers;
|
||||||
this.jobQueue = new TransformStream();
|
this.jobQueue = new TransformStream();
|
||||||
this.workerQueue = new TransformStream();
|
this.workerQueue = new TransformStream();
|
||||||
@@ -48,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(() => {
|
||||||
@@ -66,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ export default class WorkerPool {
|
|||||||
await this.done;
|
await this.done;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchJob(msg) {
|
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 });
|
||||||
@@ -90,14 +92,32 @@ export default class WorkerPool {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static useThisThreadAsWorker(cb) {
|
private jobPromise(worker: Worker, msg: I) {
|
||||||
parentPort.on('message', async (data) => {
|
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) => {
|
||||||
const { msg, id } = data;
|
const { msg, id } = data;
|
||||||
try {
|
try {
|
||||||
const result = await cb(msg);
|
const result = await cb(msg);
|
||||||
parentPort.postMessage({ result, id });
|
parentPort!.postMessage({ result, id });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
parentPort.postMessage({ error: e.message, id });
|
parentPort!.postMessage({ error: e.message, id });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user