162 lines
6.0 KiB
TypeScript
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();
|