diff --git a/codecs/resize/pkg/squoosh_resize.d.ts b/codecs/resize/pkg/squoosh_resize.d.ts index d0f4ca97..56114c7f 100644 --- a/codecs/resize/pkg/squoosh_resize.d.ts +++ b/codecs/resize/pkg/squoosh_resize.d.ts @@ -9,9 +9,9 @@ * @param {number} typ_idx * @param {boolean} premultiply * @param {boolean} color_space_conversion -* @returns {Uint8Array} +* @returns {Uint8ClampedArray} */ -export function resize(input_image: Uint8Array, input_width: number, input_height: number, output_width: number, output_height: number, typ_idx: number, premultiply: boolean, color_space_conversion: boolean): Uint8Array; +export function resize(input_image: Uint8Array, input_width: number, input_height: number, output_width: number, output_height: number, typ_idx: number, premultiply: boolean, color_space_conversion: boolean): Uint8ClampedArray; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; diff --git a/codecs/resize/pkg/squoosh_resize.js b/codecs/resize/pkg/squoosh_resize.js index ec3bfdeb..2e6a0dd4 100644 --- a/codecs/resize/pkg/squoosh_resize.js +++ b/codecs/resize/pkg/squoosh_resize.js @@ -26,8 +26,16 @@ function getInt32Memory0() { return cachegetInt32Memory0; } -function getArrayU8FromWasm0(ptr, len) { - return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +let cachegetUint8ClampedMemory0 = null; +function getUint8ClampedMemory0() { + if (cachegetUint8ClampedMemory0 === null || cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer) { + cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer); + } + return cachegetUint8ClampedMemory0; +} + +function getClampedArrayU8FromWasm0(ptr, len) { + return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len); } /** * @param {Uint8Array} input_image @@ -38,7 +46,7 @@ function getArrayU8FromWasm0(ptr, len) { * @param {number} typ_idx * @param {boolean} premultiply * @param {boolean} color_space_conversion -* @returns {Uint8Array} +* @returns {Uint8ClampedArray} */ export function resize(input_image, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion) { try { @@ -48,7 +56,7 @@ export function resize(input_image, input_width, input_height, output_width, out wasm.resize(retptr, ptr0, len0, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; - var v1 = getArrayU8FromWasm0(r0, r1).slice(); + var v1 = getClampedArrayU8FromWasm0(r0, r1).slice(); wasm.__wbindgen_free(r0, r1 * 1); return v1; } finally { diff --git a/codecs/resize/pkg/squoosh_resize_bg.wasm b/codecs/resize/pkg/squoosh_resize_bg.wasm index f9ceede2..b910c97b 100644 Binary files a/codecs/resize/pkg/squoosh_resize_bg.wasm and b/codecs/resize/pkg/squoosh_resize_bg.wasm differ diff --git a/codecs/resize/pkg/squoosh_resize_bg.wasm.d.ts b/codecs/resize/pkg/squoosh_resize_bg.wasm.d.ts new file mode 100644 index 00000000..05cc6841 --- /dev/null +++ b/codecs/resize/pkg/squoosh_resize_bg.wasm.d.ts @@ -0,0 +1,7 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export function resize(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number): void; +export function __wbindgen_add_to_stack_pointer(a: number): number; +export function __wbindgen_malloc(a: number): number; +export function __wbindgen_free(a: number, b: number): void; diff --git a/codecs/resize/src/lib.rs b/codecs/resize/src/lib.rs index fce921a4..6e550b10 100644 --- a/codecs/resize/src/lib.rs +++ b/codecs/resize/src/lib.rs @@ -8,6 +8,7 @@ use cfg_if::cfg_if; use resize::Pixel; use resize::Type; use wasm_bindgen::prelude::*; +use wasm_bindgen::Clamped; mod srgb; use srgb::{linear_to_srgb, Clamp}; @@ -66,7 +67,7 @@ pub fn resize( typ_idx: usize, premultiply: bool, color_space_conversion: bool, -) -> Vec { +) -> Clamped> { let typ = match typ_idx { 0 => Type::Triangle, 1 => Type::Catrom, @@ -91,7 +92,7 @@ pub fn resize( typ, ); resizer.resize(input_image.as_slice(), output_image.as_mut_slice()); - return output_image; + return Clamped(output_image); } // Otherwise, we convert to f32 images to keep the @@ -138,5 +139,5 @@ pub fn resize( .clamp(0.0, 255.0) as u8; } - return output_image; + return Clamped(output_image); } diff --git a/libsquoosh/src/WebAssembly.d.ts b/libsquoosh/src/WebAssembly.d.ts new file mode 100644 index 00000000..efa094bc --- /dev/null +++ b/libsquoosh/src/WebAssembly.d.ts @@ -0,0 +1,132 @@ +/** + * 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/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..e9619fbe 100644 --- a/libsquoosh/src/codecs.js +++ b/libsquoosh/src/codecs.ts @@ -1,6 +1,37 @@ import { promises as fsp } from 'fs'; import { instantiateEmscriptenWasm, pathify } from './emscripten-utils.js'; +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; +} + +interface ResizeInstantiateOptions { + width: number; + height: number; + method: string; + premultiply: boolean; + linearRGB: boolean; +} + +declare global { + // Needed for being able to use ImageData as type in codec types + type ImageData = typeof import('./image_data.js'); + // Needed for being able to assign to `globalThis.ImageData` + var ImageData: ImageData['constructor']; +} + +import type { QuantizerModule } from '../../codecs/imagequant/imagequant.js'; + // MozJPEG import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js'; import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm'; @@ -51,16 +82,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'; globalThis.ImageData = ImageData; -function resizeNameToIndex(name) { +function resizeNameToIndex(name: string) { switch (name) { case 'triangle': return 0; @@ -80,25 +117,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 +146,16 @@ 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, + }: ResizeInstantiateOptions, ) => { ({ width, height } = resizeWithAspect({ input_width, @@ -148,7 +192,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 +213,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 +400,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/missing-types.d.ts b/libsquoosh/src/missing-types.d.ts new file mode 100644 index 00000000..4319a188 --- /dev/null +++ b/libsquoosh/src/missing-types.d.ts @@ -0,0 +1,38 @@ +/// + +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; +} + +declare module 'asset-url:../../codecs/resize/pkg/squoosh_resize_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; 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/**/*"] }