From 1af5d1fa7bbb83013937ed96f630feb1d4b1fbe0 Mon Sep 17 00:00:00 2001 From: ergunsh Date: Fri, 4 Jun 2021 18:54:44 +0200 Subject: [PATCH] Typescriptify libsquoosh's codecs and emscripten-utils --- libsquoosh/src/{codecs.js => codecs.ts} | 101 ++++++++--- ...mscripten-utils.js => emscripten-utils.ts} | 7 +- libsquoosh/src/image_data.ts | 8 +- libsquoosh/src/missing-types.d.ts | 166 ++++++++++++++++++ libsquoosh/tsconfig.json | 5 +- 5 files changed, 259 insertions(+), 28 deletions(-) rename libsquoosh/src/{codecs.js => codecs.ts} (80%) rename libsquoosh/src/{emscripten-utils.js => emscripten-utils.ts} (51%) create mode 100644 libsquoosh/src/missing-types.d.ts diff --git a/libsquoosh/src/codecs.js b/libsquoosh/src/codecs.ts similarity index 80% rename from libsquoosh/src/codecs.js rename to libsquoosh/src/codecs.ts index 61b90d7e..a0b694be 100644 --- a/libsquoosh/src/codecs.js +++ b/libsquoosh/src/codecs.ts @@ -1,5 +1,28 @@ 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 import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js'; @@ -51,16 +74,22 @@ const resizePromise = resize.default(fsp.readFile(pathify(resizeWasm))); // rotate 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 below after getting type definitions for imageQuant // ImageQuant import imageQuant from '../../codecs/imagequant/imagequant_node.js'; import imageQuantWasm from 'asset-url:../../codecs/imagequant/imagequant_node.wasm'; -const imageQuantPromise = instantiateEmscriptenWasm(imageQuant, imageQuantWasm); +const imageQuantPromise: Promise = instantiateEmscriptenWasm( + imageQuant, + imageQuantWasm, +); // Our decoders currently rely on a `ImageData` global. -import ImageData from './image_data.js'; +import ImageData from './image_data'; globalThis.ImageData = ImageData; -function resizeNameToIndex(name) { +function resizeNameToIndex(name: string) { switch (name) { case 'triangle': return 0; @@ -80,25 +109,26 @@ function resizeWithAspect({ input_height, target_width, target_height, -}) { +}: ResizeWithAspectParams): { width: number; height: number } { if (!target_width && !target_height) { throw Error('Need to specify at least width or height when resizing'); } + if (target_width && target_height) { return { width: target_width, height: target_height }; } + if (!target_width) { return { width: Math.round((input_width / input_height) * target_height), height: target_height, }; } - if (!target_height) { - return { - width: target_width, - height: Math.round((input_height / input_width) * target_width), - }; - } + + return { + width: target_width, + height: Math.round((input_height / input_width) * target_width), + }; } export const preprocessors = { @@ -108,10 +138,22 @@ export const preprocessors = { instantiate: async () => { await resizePromise; return ( - buffer, - input_width, - input_height, - { width, height, method, premultiply, linearRGB }, + buffer: Uint8Array, + input_width: number, + input_height: number, + { + width, + height, + method, + premultiply, + linearRGB, + }: { + width: number; + height: number; + method: string; + premultiply: boolean; + linearRGB: boolean; + }, ) => { ({ width, height } = resizeWithAspect({ input_width, @@ -148,7 +190,12 @@ export const preprocessors = { description: 'Reduce the number of colors used (aka. paletting)', instantiate: async () => { 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( imageQuant.quantize(buffer, width, height, numColors, dither), width, @@ -164,13 +211,18 @@ export const preprocessors = { name: 'Rotate', description: 'Rotate image', 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 sameDimensions = degrees == 0 || degrees == 180; const size = width * height * 4; - const { instance } = await WebAssembly.instantiate( - await fsp.readFile(pathify(rotateWasm)), - ); + const instance = ( + await WebAssembly.instantiate(await fsp.readFile(pathify(rotateWasm))) + ).instance as RotateModuleInstance; const { memory } = instance.exports; const additionalPagesNeeded = Math.ceil( (size * 2 - memory.buffer.byteLength + 8) / (64 * 1024), @@ -346,13 +398,18 @@ export const codecs = { await pngEncDecPromise; await oxipngPromise; return { - encode: (buffer, width, height, opts) => { + encode: ( + buffer: Uint8Array, + width: number, + height: number, + opts: { level: number }, + ) => { const simplePng = pngEncDec.encode( new Uint8Array(buffer), width, height, ); - return oxipng.optimise(simplePng, opts.level); + return oxipng.optimise(simplePng, opts.level, false); }, }; }, diff --git a/libsquoosh/src/emscripten-utils.js b/libsquoosh/src/emscripten-utils.ts similarity index 51% rename from libsquoosh/src/emscripten-utils.js rename to libsquoosh/src/emscripten-utils.ts index d0f301b7..255aa4cd 100644 --- a/libsquoosh/src/emscripten-utils.js +++ b/libsquoosh/src/emscripten-utils.ts @@ -1,13 +1,16 @@ import { fileURLToPath } from 'url'; -export function pathify(path) { +export function pathify(path: string): string { if (path.startsWith('file://')) { path = fileURLToPath(path); } return path; } -export function instantiateEmscriptenWasm(factory, path) { +export function instantiateEmscriptenWasm( + factory: EmscriptenWasm.ModuleFactory, + path: string, +): Promise { return factory({ locateFile() { return pathify(path); diff --git a/libsquoosh/src/image_data.ts b/libsquoosh/src/image_data.ts index 9125199f..73a3a389 100644 --- a/libsquoosh/src/image_data.ts +++ b/libsquoosh/src/image_data.ts @@ -1,9 +1,13 @@ export default class ImageData { - readonly data: Uint8ClampedArray; + readonly data: Uint8ClampedArray | Uint8Array; readonly width: number; readonly height: number; - constructor(data: Uint8ClampedArray, width: number, height: number) { + constructor( + data: Uint8ClampedArray | Uint8Array, + width: number, + height: number, + ) { this.data = data; this.width = width; this.height = height; diff --git a/libsquoosh/src/missing-types.d.ts b/libsquoosh/src/missing-types.d.ts new file mode 100644 index 00000000..fcbdbd5d --- /dev/null +++ b/libsquoosh/src/missing-types.d.ts @@ -0,0 +1,166 @@ +/// + +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; + type ImportValue = ExportValue | number; + type ModuleImports = Record; + type Imports = Record; + function compile(bytes: BufferSource): Promise; + // `compileStreaming` does not exist in NodeJS + // function compileStreaming(source: Response | Promise): Promise; + function instantiate( + bytes: BufferSource, + importObject?: Imports, + ): Promise; + function instantiate( + moduleObject: Module, + importObject?: Imports, + ): Promise; + // `instantiateStreaming` does not exist in NodeJS + // function instantiateStreaming(response: Response | PromiseLike, importObject?: Imports): Promise; + function validate(bytes: BufferSource): boolean; +} diff --git a/libsquoosh/tsconfig.json b/libsquoosh/tsconfig.json index b99cde25..8250ad36 100644 --- a/libsquoosh/tsconfig.json +++ b/libsquoosh/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../generic-tsconfig.json", "compilerOptions": { "lib": ["esnext"], - "types": ["node"] + "types": ["node"], + "allowJs": true }, - "include": ["src/**/*"] + "include": ["src/**/*", "../codecs/**/*"] }