feat: separate AGPL libraries and add dynamic WASM loading
- Add WASM settings page for configuring external AGPL modules - Implement dynamic loading for PyMuPDF, Ghostscript, and CoherentPDF - Add Cloudflare Worker proxy for serving WASM files with CORS - Update all affected tool pages to check WASM availability - Add showWasmRequiredDialog for missing module configuration Documentation: - Update README, licensing.html, and docs to clarify AGPL components are not bundled and must be configured separately - Add WASM-PROXY.md deployment guide with recommended source URLs - Rename "CPDF" to "CoherentPDF" for consistency
This commit is contained in:
89
src/js/utils/ghostscript-dynamic-loader.ts
Normal file
89
src/js/utils/ghostscript-dynamic-loader.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { WasmProvider } from './wasm-provider.js';
|
||||
|
||||
let cachedGS: any = null;
|
||||
let loadPromise: Promise<any> | null = null;
|
||||
|
||||
export interface GhostscriptInterface {
|
||||
convertToPDFA(pdfBuffer: ArrayBuffer, profile: string): Promise<ArrayBuffer>;
|
||||
fontToOutline(pdfBuffer: ArrayBuffer): Promise<ArrayBuffer>;
|
||||
}
|
||||
|
||||
export async function loadGhostscript(): Promise<GhostscriptInterface> {
|
||||
if (cachedGS) {
|
||||
return cachedGS;
|
||||
}
|
||||
|
||||
if (loadPromise) {
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
loadPromise = (async () => {
|
||||
const baseUrl = WasmProvider.getUrl('ghostscript');
|
||||
if (!baseUrl) {
|
||||
throw new Error(
|
||||
'Ghostscript is not configured. Please configure it in Advanced Settings.'
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
||||
|
||||
try {
|
||||
const wrapperUrl = `${normalizedUrl}gs.js`;
|
||||
|
||||
await loadScript(wrapperUrl);
|
||||
|
||||
const globalScope =
|
||||
typeof globalThis !== 'undefined' ? globalThis : window;
|
||||
|
||||
if (typeof (globalScope as any).loadGS === 'function') {
|
||||
cachedGS = await (globalScope as any).loadGS({
|
||||
baseUrl: normalizedUrl,
|
||||
});
|
||||
} else if (typeof (globalScope as any).GhostscriptWASM === 'function') {
|
||||
cachedGS = new (globalScope as any).GhostscriptWASM(normalizedUrl);
|
||||
await cachedGS.init?.();
|
||||
} else {
|
||||
throw new Error(
|
||||
'Ghostscript wrapper did not expose expected interface. Expected loadGS() or GhostscriptWASM class.'
|
||||
);
|
||||
}
|
||||
|
||||
return cachedGS;
|
||||
} catch (error: any) {
|
||||
loadPromise = null;
|
||||
throw new Error(
|
||||
`Failed to load Ghostscript from ${normalizedUrl}: ${error.message}`
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
function loadScript(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (document.querySelector(`script[src="${url}"]`)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.type = 'text/javascript';
|
||||
script.async = true;
|
||||
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
export function isGhostscriptAvailable(): boolean {
|
||||
return WasmProvider.isConfigured('ghostscript');
|
||||
}
|
||||
|
||||
export function clearGhostscriptCache(): void {
|
||||
cachedGS = null;
|
||||
loadPromise = null;
|
||||
}
|
||||
Reference in New Issue
Block a user