import encodeJpeg, { init as initJpeg } from "@jsquash/jpeg/encode.js"; import encodeWebp, { init as initWebp } from "@jsquash/webp/encode.js"; import { default as oxipng, init as initOxipng, } from "@jsquash/oxipng/optimise.js"; import oxipngMtWasmUrl from "@jsquash/oxipng/codec/pkg-parallel/squoosh_oxipng_bg.wasm?url"; import oxipngStWasmUrl from "@jsquash/oxipng/codec/pkg/squoosh_oxipng_bg.wasm?url"; import jpegWasmUrl from "@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm?url"; import webpSimdWasmUrl from "@jsquash/webp/codec/enc/webp_enc_simd.wasm?url"; import webpWasmUrl from "@jsquash/webp/codec/enc/webp_enc.wasm?url"; import { threads } from "wasm-feature-detect"; import { extensionOf, type OptimizeOptions, type OptimizeResult, type ProgressReporter, } from "./types"; /** * wasm-bindgen resolves its own wasm relative to the module, which bundlers * rewrite; hand it the asset URL that matches the threaded/single-threaded * build it is about to pick. This pipeline only ever runs inside the * optimization worker, so the threading branch mirrors jSquash's own check * (worker context + multiple cores + shared-memory support). */ let oxipngReady: Promise | null = null; async function ensureOxipng(): Promise { if (!oxipngReady) { const cores = globalThis.navigator?.hardwareConcurrency ?? 1; const useThreads = cores > 1 && (await threads()); oxipngReady = initOxipng({ module_or_path: useThreads ? oxipngMtWasmUrl : oxipngStWasmUrl, }); } await oxipngReady; } /** Copy into a fresh ArrayBuffer so shared wasm memory never reaches Blob. */ function toArrayBuffer(value: ArrayBuffer | Uint8Array): ArrayBuffer { const view = value instanceof Uint8Array ? value : new Uint8Array(value); const copy = new Uint8Array(view.byteLength); copy.set(view); return copy.buffer; } /** Decode with the browser, scale on a canvas, re-encode with jSquash codecs. */ export async function optimizeImage( blob: Blob, filename: string, options: OptimizeOptions, report: ProgressReporter, ): Promise { const extension = extensionOf(filename); report(0.05, "Decoding the image…"); const bitmap = await createImageBitmap(blob); const sourceWidth = bitmap.width; const sourceHeight = bitmap.height; let width = sourceWidth; let height = sourceHeight; if (options.maxDimension && Math.max(width, height) > options.maxDimension) { const factor = options.maxDimension / Math.max(width, height); width = Math.max(1, Math.round(width * factor)); height = Math.max(1, Math.round(height * factor)); } const resized = width !== sourceWidth || height !== sourceHeight; let imageData: ImageData | null = null; if (resized || extension !== "png") { const canvas = new OffscreenCanvas(width, height); const context = canvas.getContext("2d"); if (!context) { throw new Error("This browser does not support OffscreenCanvas."); } context.imageSmoothingEnabled = true; context.imageSmoothingQuality = "high"; context.drawImage(bitmap, 0, 0, width, height); imageData = context.getImageData(0, 0, width, height); } bitmap.close(); report(0.4, "Encoding…"); let buffer: ArrayBuffer; let type: string; if (extension === "png") { report(0.35, "Optimizing the PNG…"); await ensureOxipng(); buffer = imageData !== null ? toArrayBuffer(await oxipng(imageData, { level: options.pngLevel })) : toArrayBuffer( await oxipng(await blob.arrayBuffer(), { level: options.pngLevel, }), ); type = "image/png"; } else if (extension === "webp") { await initWebp({ locateFile: (path: string) => path.includes("simd") ? webpSimdWasmUrl : webpWasmUrl, }); buffer = await encodeWebp(imageData as ImageData, { quality: options.quality, }); type = "image/webp"; } else { await initJpeg({ locateFile: () => jpegWasmUrl }); buffer = await encodeJpeg(imageData as ImageData, { quality: options.quality, }); type = "image/jpeg"; } report(0.95, "Packaging…"); return { blob: new Blob([buffer], { type }), width, height, }; }