Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/wasm-webp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
},
"scripts": {
"lint": "eslint .",
"test": "vitest",
"build": "tshy",
"dev": "tshy --watch",
"clean": "rm -rf node_modules .tshy .tshy-build dist .turbo"
Expand Down
45 changes: 45 additions & 0 deletions plugins/wasm-webp/src/index.node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, test } from "vitest";
import { readFile } from "node:fs/promises";

import webp from "./index.js";

const format = webp();

describe("WASM WebP (Node)", () => {
test("loads WebP fixtures in Node", async () => {
const image = await format.decode(
await readFile(new URL("./images/test.webp", import.meta.url))
);

expect(image.width).toBeGreaterThan(0);
expect(image.height).toBeGreaterThan(0);
expect(image.data.length).toBe(image.width * image.height * 4);
expect(Buffer.from(image.data).some((value) => value !== 0)).toBe(true);
});

test("exports WebP buffers in Node", async () => {
const bitmap = {
width: 3,
height: 3,
data: Buffer.from([
255, 0, 0, 255,
255, 0, 128, 255,
255, 0, 255, 255,
255, 0, 128, 255,
255, 0, 255, 255,
128, 0, 255, 255,
255, 0, 255, 255,
128, 0, 255, 255,
0, 0, 255, 255,
]),
};

const buffer = await format.encode(bitmap);
expect(buffer.toString("ascii", 0, 4)).toBe("RIFF");
expect(buffer.toString("ascii", 8, 12)).toBe("WEBP");

const roundTrip = await format.decode(buffer);
expect(roundTrip.width).toBe(3);
expect(roundTrip.height).toBe(3);
});
});
95 changes: 92 additions & 3 deletions plugins/wasm-webp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,96 @@ const WebpOptionsSchema = z.object({

type WebpOptions = z.infer<typeof WebpOptionsSchema>;

export default function png() {
const isNodeRuntime =
typeof process === "object" &&
typeof process.versions === "object" &&
typeof process.versions.node === "string";

const nodeWasmModuleCache = new Map<string, Promise<WebAssembly.Module>>();
let decoderInitPromise: Promise<unknown> | undefined;
let encoderInitPromise: Promise<unknown> | undefined;

async function supportsSimd() {
return WebAssembly.validate(
new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0,
10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11,
])
);
}

function resetPromiseOnError<T>(
promise: Promise<T>,
reset: () => void
): Promise<T> {
return promise.catch((error) => {
reset();
throw error;
});
}

async function loadNodeWasmModule(specifier: string): Promise<WebAssembly.Module> {
const cachedModule = nodeWasmModuleCache.get(specifier);
if (cachedModule) {
return cachedModule;
}

const modulePromise = (async () => {
try {
const { readFile } = await import("node:fs/promises");
const { default: Module } = await import("node:module");

const require = Module.createRequire(import.meta.url);
const wasmPath = require.resolve(specifier);
return WebAssembly.compile(await readFile(wasmPath));
} catch (error) {
nodeWasmModuleCache.delete(specifier);
throw error;
}
})();

nodeWasmModuleCache.set(specifier, modulePromise);
return modulePromise;
}

async function ensureDecoderInitialized() {
if (!decoderInitPromise) {
decoderInitPromise = resetPromiseOnError(
isNodeRuntime
? initDecoder(
await loadNodeWasmModule("@jsquash/webp/codec/dec/webp_dec.wasm")
)
: initDecoder(),
() => {
decoderInitPromise = undefined;
}
);
}

return decoderInitPromise;
}

async function ensureEncoderInitialized() {
if (!encoderInitPromise) {
const encoderWasmSpecifier =
isNodeRuntime && (await supportsSimd())
? "@jsquash/webp/codec/enc/webp_enc_simd.wasm"
: "@jsquash/webp/codec/enc/webp_enc.wasm";

encoderInitPromise = resetPromiseOnError(
isNodeRuntime
? initEncoder(await loadNodeWasmModule(encoderWasmSpecifier))
: initEncoder(),
() => {
encoderInitPromise = undefined;
}
);
}

return encoderInitPromise;
}

export default function webp() {
return {
mime: "image/webp",
hasAlpha: true,
Expand Down Expand Up @@ -135,7 +224,7 @@ export default function png() {
targetSize,
colorSpace = "srgb",
} = WebpOptionsSchema.parse(options);
await initEncoder();
await ensureEncoderInitialized();
const arrayBuffer = await encode(
{
...bitmap,
Expand Down Expand Up @@ -175,7 +264,7 @@ export default function png() {
return Buffer.from(arrayBuffer);
},
decode: async (data) => {
await initDecoder();
await ensureDecoderInitialized();
const result = await decode(new Uint8Array(data).buffer);

return {
Expand Down