Files
pdf/frontend/src/lib/wasmLoader.ts
T

162 lines
6.0 KiB
TypeScript

/**
* WASM C++ PDF Engine Loader
*
* Manages loading the compiled Emscripten WebAssembly modules.
* Implements fallback mock engines when WASM is not built yet (e.g. in G0a).
*/
export interface WasmEngineInstance {
version: string;
loadDocument: (buffer: ArrayBuffer) => number; // returns doc handle
renderPage: (handle: number, page: number, scale: number) => ImageData;
freeDocument: (handle: number) => void;
engineBuildInfo: () => string;
engineHasSkia: () => boolean;
getTextJson: (handle: number, page: number) => string;
}
class WasmLoader {
private isLoaded = false;
private instance: WasmEngineInstance | null = null;
async loadEngine(): Promise<WasmEngineInstance> {
if (this.isLoaded && this.instance) {
return this.instance;
}
try {
// Vite won't import /public files as modules, so fetch the module text and import it
// via a Blob URL (the browser loads it as an ES module; Vite never sees the import).
const resp = await fetch('/pdfengine.mjs');
if (!resp.ok) throw new Error(`pdfengine.mjs ${resp.status}`);
const blobUrl = URL.createObjectURL(new Blob([await resp.text()], { type: 'text/javascript' }));
let createModule: (opts: object) => Promise<any>;
try {
createModule = (await import(/* @vite-ignore */ blobUrl)).default;
} finally {
URL.revokeObjectURL(blobUrl);
}
const Module = await createModule({
locateFile: (path: string) => {
if (path.endsWith('.wasm')) {
return '/pdfengine.wasm';
}
return path;
}
});
console.log('[WASM Loader] Real C++ WebAssembly Engine loaded successfully.');
this.instance = {
version: '1.0.0-wasm-g0b-cpp',
loadDocument: (buffer: ArrayBuffer) => {
const size = buffer.byteLength;
const ptr = Module._malloc(size);
const heap = new Uint8Array(Module.HEAPU8.buffer, ptr, size);
heap.set(new Uint8Array(buffer));
const handle = Module.ccall('loadDocument', 'number', ['number', 'number'], [ptr, size]);
Module._free(ptr);
return handle;
},
renderPage: (handle: number, page: number, scale: number) => {
const width = Math.floor(800 * scale);
const height = Math.floor(1100 * scale);
const bufferSize = width * height * 4;
const ptr = Module._malloc(bufferSize);
const success = Module.ccall(
'renderPage',
'number',
['number', 'number', 'number', 'number', 'number', 'number'],
[handle, page, scale, ptr, width, height]
);
if (!success) {
Module._free(ptr);
throw new Error(`C++ renderPage failed for page ${page}`);
}
// Copy the pixel buffer so we can free the WASM heap allocation
const wasmPixels = new Uint8Array(Module.HEAPU8.buffer, ptr, bufferSize);
const clientPixels = new Uint8ClampedArray(wasmPixels);
Module._free(ptr);
return new ImageData(clientPixels, width, height);
},
freeDocument: (handle: number) => {
Module.ccall('freeDocument', 'void', ['number'], [handle]);
},
engineBuildInfo: () => {
return Module.ccall('engineBuildInfo', 'string', [], []);
},
engineHasSkia: () => {
return Module.ccall('engineHasSkia', 'number', [], []) !== 0;
},
getTextJson: (handle: number, page: number) => {
return Module.ccall('getPageTextJson', 'string', ['number', 'number'], [handle, page]);
}
};
this.isLoaded = true;
return this.instance;
} catch (err) {
console.warn('Real WASM loading failed, falling back to simulated JavaScript Canvas Mock rendering.', err);
this.isLoaded = true;
this.instance = this.createMockEngineInstance();
return this.instance;
}
}
private createMockEngineInstance(): WasmEngineInstance {
return {
version: '1.0.0-wasm-g0a-scaffold',
loadDocument: (buffer: ArrayBuffer) => {
console.log(`[WASM Engine] Loaded document of size ${buffer.byteLength} bytes.`);
return Math.floor(Math.random() * 1000) + 1; // handle
},
renderPage: (handle: number, page: number, scale: number) => {
console.log(`[WASM Engine] Render page ${page} with scale ${scale} for handle ${handle}.`);
// Return dummy white/gray pixels (ImageData) representing the mock render
const width = Math.floor(800 * scale);
const height = Math.floor(1100 * scale);
const buffer = new Uint8ClampedArray(width * height * 4);
// Fill image data with off-white and simulated drawing
for (let i = 0; i < buffer.length; i += 4) {
buffer[i] = 250; // R
buffer[i + 1] = 251; // G
buffer[i + 2] = 252; // B
buffer[i + 3] = 255; // A
}
return new ImageData(buffer, width, height);
},
freeDocument: (handle: number) => {
console.log(`[WASM Engine] Released document resources for handle ${handle}.`);
},
engineBuildInfo: () => {
return 'pdfengine 0.1.0 (pdfium=off) (skia=on)';
},
engineHasSkia: () => {
return true;
},
getTextJson: (handle: number, page: number) => {
void handle;
return JSON.stringify([
{ text: "P", x: 60.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
{ text: "a", x: 66.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
{ text: "g", x: 72.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
{ text: "e", x: 78.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
{ text: " ", x: 84.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
{ text: String(page + 1), x: 90.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 }
]);
}
};
}
}
export const wasmLoader = new WasmLoader();