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/README.md b/libsquoosh/README.md index 6b9c0d2b..9692196d 100644 --- a/libsquoosh/README.md +++ b/libsquoosh/README.md @@ -39,9 +39,9 @@ The returned `image` object is a representation of the original image, that you When an image has been ingested, you can start preprocessing it and encoding it to other formats. This example will resize the image and then encode it to a `.jpg` and `.jxl` image: ```js -await image.decoded; //Wait until the image is decoded before running preprocessors +await image.decoded; //Wait until the image is decoded before running preprocessors. -const preprocessOptions: { +const preprocessOptions = { resize: { enabled: true, width: 100, @@ -50,7 +50,7 @@ const preprocessOptions: { } await image.preprocess(preprocessOptions); -const encodeOptions: { +const encodeOptions = { mozjpeg: {}, //an empty object means 'use default settings' jxl: { quality: 90, 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 1f7cd760..6fdb5e82 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/**/*"] } diff --git a/src/client/lazy-app/Compress/Output/custom-els/TwoUp/index.ts b/src/client/lazy-app/Compress/Output/custom-els/TwoUp/index.ts index 6b5b5715..24041912 100644 --- a/src/client/lazy-app/Compress/Output/custom-els/TwoUp/index.ts +++ b/src/client/lazy-app/Compress/Output/custom-els/TwoUp/index.ts @@ -34,6 +34,8 @@ export default class TwoUp extends HTMLElement { */ private _everConnected = false; + private _resizeObserver?: ResizeObserver; + constructor() { super(); this._handle.className = styles.twoUpHandle; @@ -45,13 +47,6 @@ export default class TwoUp extends HTMLElement { childList: true, }); - // Watch for element size changes. - if ('ResizeObserver' in window) { - new ResizeObserver(() => this._resetPosition()).observe(this); - } else { - window.addEventListener('resize', () => this._resetPosition()); - } - // Watch for pointers on the handle. const pointerTracker: PointerTracker = new PointerTracker(this._handle, { start: (_, event) => { @@ -68,8 +63,6 @@ export default class TwoUp extends HTMLElement { ); }, }); - - window.addEventListener('keydown', (event) => this._onKeyDown(event)); } connectedCallback() { @@ -84,12 +77,23 @@ export default class TwoUp extends HTMLElement { } `}`; + // Watch for element size changes. + this._resizeObserver = new ResizeObserver(() => this._resetPosition()); + this._resizeObserver.observe(this); + + window.addEventListener('keydown', this._onKeyDown); + if (!this._everConnected) { this._resetPosition(); this._everConnected = true; } } + disconnectedCallback() { + window.removeEventListener('keydown', this._onKeyDown); + if (this._resizeObserver) this._resizeObserver.disconnect(); + } + attributeChangedCallback(name: string) { if (name === orientationAttr) { this._resetPosition(); @@ -97,7 +101,7 @@ export default class TwoUp extends HTMLElement { } // KeyDown event handler - private _onKeyDown(event: KeyboardEvent) { + private _onKeyDown = (event: KeyboardEvent) => { const target = event.target; if (target instanceof HTMLElement && target.closest('input')) return; @@ -122,7 +126,7 @@ export default class TwoUp extends HTMLElement { this._relativePosition = this._position / bounds[dimensionAxis]; this._setPosition(); } - } + }; private _resetPosition() { // Set the initial position of the handle. diff --git a/src/client/lazy-app/Compress/index.tsx b/src/client/lazy-app/Compress/index.tsx index 4793a5ee..a7d7464a 100644 --- a/src/client/lazy-app/Compress/index.tsx +++ b/src/client/lazy-app/Compress/index.tsx @@ -377,6 +377,7 @@ export default class Compress extends Component { componentWillUnmount(): void { updateDocumentTitle({ loading: false }); + this.widthQuery.removeListener(this.onMobileWidthChange); this.mainAbortController.abort(); for (const controller of this.sideAbortControllers) { controller.abort();