Typescriptify libsquoosh's codecs and emscripten-utils

This commit is contained in:
ergunsh
2021-06-04 18:54:44 +02:00
parent 08adfba8be
commit 1af5d1fa7b
5 changed files with 259 additions and 28 deletions

View File

@@ -1,5 +1,28 @@
import { promises as fsp } from 'fs'; import { promises as fsp } from 'fs';
import { instantiateEmscriptenWasm, pathify } from './emscripten-utils.js'; import { instantiateEmscriptenWasm, pathify } from './emscripten-utils';
interface RotateModuleInstance {
exports: {
memory: WebAssembly.Memory;
rotate(width: number, height: number, rotate: number): void;
};
}
interface ResizeWithAspectParams {
input_width: number;
input_height: number;
target_width: number;
target_height: number;
}
declare global {
// Needed for being able to use ImageData as type in codec types
type ImageData = typeof import('./image_data');
// Needed for being able to assign to `globalThis.ImageData`
var ImageData: ImageData['constructor'];
}
import type { QuantizerModule } from '../../codecs/imagequant/imagequant';
// MozJPEG // MozJPEG
import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js'; import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js';
@@ -51,16 +74,22 @@ const resizePromise = resize.default(fsp.readFile(pathify(resizeWasm)));
// rotate // rotate
import rotateWasm from 'asset-url:../../codecs/rotate/rotate.wasm'; import rotateWasm from 'asset-url:../../codecs/rotate/rotate.wasm';
// TODO(ergunsh): Type definitions of some modules do not exist
// Figure out creating type definitions for them and remove `allowJs` rule
// We shouldn't need to use Promise<QuantizerModule> below after getting type definitions for imageQuant
// ImageQuant // ImageQuant
import imageQuant from '../../codecs/imagequant/imagequant_node.js'; import imageQuant from '../../codecs/imagequant/imagequant_node.js';
import imageQuantWasm from 'asset-url:../../codecs/imagequant/imagequant_node.wasm'; import imageQuantWasm from 'asset-url:../../codecs/imagequant/imagequant_node.wasm';
const imageQuantPromise = instantiateEmscriptenWasm(imageQuant, imageQuantWasm); const imageQuantPromise: Promise<QuantizerModule> = instantiateEmscriptenWasm(
imageQuant,
imageQuantWasm,
);
// Our decoders currently rely on a `ImageData` global. // Our decoders currently rely on a `ImageData` global.
import ImageData from './image_data.js'; import ImageData from './image_data';
globalThis.ImageData = ImageData; globalThis.ImageData = ImageData;
function resizeNameToIndex(name) { function resizeNameToIndex(name: string) {
switch (name) { switch (name) {
case 'triangle': case 'triangle':
return 0; return 0;
@@ -80,26 +109,27 @@ function resizeWithAspect({
input_height, input_height,
target_width, target_width,
target_height, target_height,
}) { }: ResizeWithAspectParams): { width: number; height: number } {
if (!target_width && !target_height) { if (!target_width && !target_height) {
throw Error('Need to specify at least width or height when resizing'); throw Error('Need to specify at least width or height when resizing');
} }
if (target_width && target_height) { if (target_width && target_height) {
return { width: target_width, height: target_height }; return { width: target_width, height: target_height };
} }
if (!target_width) { if (!target_width) {
return { return {
width: Math.round((input_width / input_height) * target_height), width: Math.round((input_width / input_height) * target_height),
height: target_height, height: target_height,
}; };
} }
if (!target_height) {
return { return {
width: target_width, width: target_width,
height: Math.round((input_height / input_width) * target_width), height: Math.round((input_height / input_width) * target_width),
}; };
} }
}
export const preprocessors = { export const preprocessors = {
resize: { resize: {
@@ -108,10 +138,22 @@ export const preprocessors = {
instantiate: async () => { instantiate: async () => {
await resizePromise; await resizePromise;
return ( return (
buffer, buffer: Uint8Array,
input_width, input_width: number,
input_height, input_height: number,
{ width, height, method, premultiply, linearRGB }, {
width,
height,
method,
premultiply,
linearRGB,
}: {
width: number;
height: number;
method: string;
premultiply: boolean;
linearRGB: boolean;
},
) => { ) => {
({ width, height } = resizeWithAspect({ ({ width, height } = resizeWithAspect({
input_width, input_width,
@@ -148,7 +190,12 @@ export const preprocessors = {
description: 'Reduce the number of colors used (aka. paletting)', description: 'Reduce the number of colors used (aka. paletting)',
instantiate: async () => { instantiate: async () => {
const imageQuant = await imageQuantPromise; const imageQuant = await imageQuantPromise;
return (buffer, width, height, { numColors, dither }) => return (
buffer: Uint8Array,
width: number,
height: number,
{ numColors, dither }: { numColors: number; dither: number },
) =>
new ImageData( new ImageData(
imageQuant.quantize(buffer, width, height, numColors, dither), imageQuant.quantize(buffer, width, height, numColors, dither),
width, width,
@@ -164,13 +211,18 @@ export const preprocessors = {
name: 'Rotate', name: 'Rotate',
description: 'Rotate image', description: 'Rotate image',
instantiate: async () => { instantiate: async () => {
return async (buffer, width, height, { numRotations }) => { return async (
buffer: Uint8Array,
width: number,
height: number,
{ numRotations }: { numRotations: number },
) => {
const degrees = (numRotations * 90) % 360; const degrees = (numRotations * 90) % 360;
const sameDimensions = degrees == 0 || degrees == 180; const sameDimensions = degrees == 0 || degrees == 180;
const size = width * height * 4; const size = width * height * 4;
const { instance } = await WebAssembly.instantiate( const instance = (
await fsp.readFile(pathify(rotateWasm)), await WebAssembly.instantiate(await fsp.readFile(pathify(rotateWasm)))
); ).instance as RotateModuleInstance;
const { memory } = instance.exports; const { memory } = instance.exports;
const additionalPagesNeeded = Math.ceil( const additionalPagesNeeded = Math.ceil(
(size * 2 - memory.buffer.byteLength + 8) / (64 * 1024), (size * 2 - memory.buffer.byteLength + 8) / (64 * 1024),
@@ -346,13 +398,18 @@ export const codecs = {
await pngEncDecPromise; await pngEncDecPromise;
await oxipngPromise; await oxipngPromise;
return { return {
encode: (buffer, width, height, opts) => { encode: (
buffer: Uint8Array,
width: number,
height: number,
opts: { level: number },
) => {
const simplePng = pngEncDec.encode( const simplePng = pngEncDec.encode(
new Uint8Array(buffer), new Uint8Array(buffer),
width, width,
height, height,
); );
return oxipng.optimise(simplePng, opts.level); return oxipng.optimise(simplePng, opts.level, false);
}, },
}; };
}, },

View File

@@ -1,13 +1,16 @@
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
export function pathify(path) { export function pathify(path: string): string {
if (path.startsWith('file://')) { if (path.startsWith('file://')) {
path = fileURLToPath(path); path = fileURLToPath(path);
} }
return path; return path;
} }
export function instantiateEmscriptenWasm(factory, path) { export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
factory: EmscriptenWasm.ModuleFactory<T>,
path: string,
): Promise<T> {
return factory({ return factory({
locateFile() { locateFile() {
return pathify(path); return pathify(path);

View File

@@ -1,9 +1,13 @@
export default class ImageData { export default class ImageData {
readonly data: Uint8ClampedArray; readonly data: Uint8ClampedArray | Uint8Array;
readonly width: number; readonly width: number;
readonly height: number; readonly height: number;
constructor(data: Uint8ClampedArray, width: number, height: number) { constructor(
data: Uint8ClampedArray | Uint8Array,
width: number,
height: number,
) {
this.data = data; this.data = data;
this.width = width; this.width = width;
this.height = height; this.height = height;

166
libsquoosh/src/missing-types.d.ts vendored Normal file
View File

@@ -0,0 +1,166 @@
/// <reference path="../../missing-types.d.ts" />
declare module 'asset-url:*' {
const value: string;
export default value;
}
// Somehow TS picks up definitions from the module itself
// instead of using `asset-url:*`. It is probably related to
// specifity of the module declaration and these declarations below fix it
declare module 'asset-url:../../codecs/png/pkg/squoosh_png_bg.wasm' {
const value: string;
export default value;
}
declare module 'asset-url:../../codecs/oxipng/pkg/squoosh_oxipng_bg.wasm' {
const value: string;
export default value;
}
// These don't exist in NodeJS types so we're not able to use them but they are referenced in some emscripten and codec types
// Thus, we need to explicitly assign them to be `never`
// We're also not able to use the APIs that use these types
// So, if we want to use those APIs we need to supply its dependencies ourselves
// However, probably those APIs are more suited to be used in web (i.e. there can be other
// dependencies to web APIs that might not work in Node)
type RequestInfo = never;
type Response = never;
type WebGLRenderingContext = never;
type MessageEvent = never;
type BufferSource = ArrayBufferView | ArrayBuffer;
type URL = import('url').URL;
/**
* WebAssembly definitions are not available in `@types/node` yet,
* so these are copied from `lib.dom.d.ts`
*/
declare namespace WebAssembly {
interface CompileError {}
var CompileError: {
prototype: CompileError;
new (): CompileError;
};
interface Global {
value: any;
valueOf(): any;
}
var Global: {
prototype: Global;
new (descriptor: GlobalDescriptor, v?: any): Global;
};
interface Instance {
readonly exports: Exports;
}
var Instance: {
prototype: Instance;
new (module: Module, importObject?: Imports): Instance;
};
interface LinkError {}
var LinkError: {
prototype: LinkError;
new (): LinkError;
};
interface Memory {
readonly buffer: ArrayBuffer;
grow(delta: number): number;
}
var Memory: {
prototype: Memory;
new (descriptor: MemoryDescriptor): Memory;
};
interface Module {}
var Module: {
prototype: Module;
new (bytes: BufferSource): Module;
customSections(moduleObject: Module, sectionName: string): ArrayBuffer[];
exports(moduleObject: Module): ModuleExportDescriptor[];
imports(moduleObject: Module): ModuleImportDescriptor[];
};
interface RuntimeError {}
var RuntimeError: {
prototype: RuntimeError;
new (): RuntimeError;
};
interface Table {
readonly length: number;
get(index: number): Function | null;
grow(delta: number): number;
set(index: number, value: Function | null): void;
}
var Table: {
prototype: Table;
new (descriptor: TableDescriptor): Table;
};
interface GlobalDescriptor {
mutable?: boolean;
value: ValueType;
}
interface MemoryDescriptor {
initial: number;
maximum?: number;
}
interface ModuleExportDescriptor {
kind: ImportExportKind;
name: string;
}
interface ModuleImportDescriptor {
kind: ImportExportKind;
module: string;
name: string;
}
interface TableDescriptor {
element: TableKind;
initial: number;
maximum?: number;
}
interface WebAssemblyInstantiatedSource {
instance: Instance;
module: Module;
}
type ImportExportKind = 'function' | 'global' | 'memory' | 'table';
type TableKind = 'anyfunc';
type ValueType = 'f32' | 'f64' | 'i32' | 'i64';
type ExportValue = Function | Global | Memory | Table;
type Exports = Record<string, ExportValue>;
type ImportValue = ExportValue | number;
type ModuleImports = Record<string, ImportValue>;
type Imports = Record<string, ModuleImports>;
function compile(bytes: BufferSource): Promise<Module>;
// `compileStreaming` does not exist in NodeJS
// function compileStreaming(source: Response | Promise<Response>): Promise<Module>;
function instantiate(
bytes: BufferSource,
importObject?: Imports,
): Promise<WebAssemblyInstantiatedSource>;
function instantiate(
moduleObject: Module,
importObject?: Imports,
): Promise<Instance>;
// `instantiateStreaming` does not exist in NodeJS
// function instantiateStreaming(response: Response | PromiseLike<Response>, importObject?: Imports): Promise<WebAssemblyInstantiatedSource>;
function validate(bytes: BufferSource): boolean;
}

View File

@@ -2,7 +2,8 @@
"extends": "../generic-tsconfig.json", "extends": "../generic-tsconfig.json",
"compilerOptions": { "compilerOptions": {
"lib": ["esnext"], "lib": ["esnext"],
"types": ["node"] "types": ["node"],
"allowJs": true
}, },
"include": ["src/**/*"] "include": ["src/**/*", "../codecs/**/*"]
} }