mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-07 16:41:00 +01:00
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:
+123
-96
@@ -1,132 +1,159 @@
|
||||
import { getLibreOfficeConverter } from './libreoffice-loader.js';
|
||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
||||
import loadGsWASM from '@bentopdf/gs-wasm';
|
||||
import { setCachedGsModule } from './ghostscript-loader.js';
|
||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||
|
||||
export enum PreloadStatus {
|
||||
IDLE = 'idle',
|
||||
LOADING = 'loading',
|
||||
READY = 'ready',
|
||||
ERROR = 'error'
|
||||
IDLE = 'idle',
|
||||
LOADING = 'loading',
|
||||
READY = 'ready',
|
||||
ERROR = 'error',
|
||||
UNAVAILABLE = 'unavailable',
|
||||
}
|
||||
|
||||
interface PreloadState {
|
||||
libreoffice: PreloadStatus;
|
||||
pymupdf: PreloadStatus;
|
||||
ghostscript: PreloadStatus;
|
||||
libreoffice: PreloadStatus;
|
||||
pymupdf: PreloadStatus;
|
||||
ghostscript: PreloadStatus;
|
||||
}
|
||||
|
||||
const preloadState: PreloadState = {
|
||||
libreoffice: PreloadStatus.IDLE,
|
||||
pymupdf: PreloadStatus.IDLE,
|
||||
ghostscript: PreloadStatus.IDLE
|
||||
libreoffice: PreloadStatus.IDLE,
|
||||
pymupdf: PreloadStatus.IDLE,
|
||||
ghostscript: PreloadStatus.IDLE,
|
||||
};
|
||||
|
||||
let pymupdfInstance: PyMuPDF | null = null;
|
||||
|
||||
export function getPreloadStatus(): Readonly<PreloadState> {
|
||||
return { ...preloadState };
|
||||
}
|
||||
|
||||
export function getPymupdfInstance(): PyMuPDF | null {
|
||||
return pymupdfInstance;
|
||||
}
|
||||
|
||||
async function preloadLibreOffice(): Promise<void> {
|
||||
if (preloadState.libreoffice !== PreloadStatus.IDLE) return;
|
||||
|
||||
preloadState.libreoffice = PreloadStatus.LOADING;
|
||||
console.log('[Preloader] Starting LibreOffice WASM preload...');
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
await converter.initialize();
|
||||
preloadState.libreoffice = PreloadStatus.READY;
|
||||
console.log('[Preloader] LibreOffice WASM ready');
|
||||
} catch (e) {
|
||||
preloadState.libreoffice = PreloadStatus.ERROR;
|
||||
console.warn('[Preloader] LibreOffice preload failed:', e);
|
||||
}
|
||||
return { ...preloadState };
|
||||
}
|
||||
|
||||
async function preloadPyMuPDF(): Promise<void> {
|
||||
if (preloadState.pymupdf !== PreloadStatus.IDLE) return;
|
||||
if (preloadState.pymupdf !== PreloadStatus.IDLE) return;
|
||||
|
||||
preloadState.pymupdf = PreloadStatus.LOADING;
|
||||
console.log('[Preloader] Starting PyMuPDF preload...');
|
||||
if (!isWasmAvailable('pymupdf')) {
|
||||
preloadState.pymupdf = PreloadStatus.UNAVAILABLE;
|
||||
console.log('[Preloader] PyMuPDF not configured, skipping preload');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pymupdfBaseUrl = getWasmBaseUrl('pymupdf');
|
||||
pymupdfInstance = new PyMuPDF(pymupdfBaseUrl);
|
||||
await pymupdfInstance.load();
|
||||
preloadState.pymupdf = PreloadStatus.READY;
|
||||
console.log('[Preloader] PyMuPDF ready');
|
||||
} catch (e) {
|
||||
preloadState.pymupdf = PreloadStatus.ERROR;
|
||||
console.warn('[Preloader] PyMuPDF preload failed:', e);
|
||||
}
|
||||
preloadState.pymupdf = PreloadStatus.LOADING;
|
||||
console.log('[Preloader] Starting PyMuPDF preload...');
|
||||
|
||||
try {
|
||||
const pymupdfBaseUrl = getWasmBaseUrl('pymupdf')!;
|
||||
const gsBaseUrl = getWasmBaseUrl('ghostscript');
|
||||
const normalizedUrl = pymupdfBaseUrl.endsWith('/')
|
||||
? pymupdfBaseUrl
|
||||
: `${pymupdfBaseUrl}/`;
|
||||
|
||||
const wrapperUrl = `${normalizedUrl}dist/index.js`;
|
||||
const module = await import(/* @vite-ignore */ wrapperUrl);
|
||||
|
||||
const pymupdfInstance = new module.PyMuPDF({
|
||||
assetPath: `${normalizedUrl}assets/`,
|
||||
ghostscriptUrl: gsBaseUrl || '',
|
||||
});
|
||||
await pymupdfInstance.load();
|
||||
preloadState.pymupdf = PreloadStatus.READY;
|
||||
console.log('[Preloader] PyMuPDF ready');
|
||||
} catch (e) {
|
||||
preloadState.pymupdf = PreloadStatus.ERROR;
|
||||
console.warn('[Preloader] PyMuPDF preload failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function preloadGhostscript(): Promise<void> {
|
||||
if (preloadState.ghostscript !== PreloadStatus.IDLE) return;
|
||||
if (preloadState.ghostscript !== PreloadStatus.IDLE) return;
|
||||
|
||||
preloadState.ghostscript = PreloadStatus.LOADING;
|
||||
console.log('[Preloader] Starting Ghostscript WASM preload...');
|
||||
if (!isWasmAvailable('ghostscript')) {
|
||||
preloadState.ghostscript = PreloadStatus.UNAVAILABLE;
|
||||
console.log('[Preloader] Ghostscript not configured, skipping preload');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const gsBaseUrl = getWasmBaseUrl('ghostscript');
|
||||
const gsModule = await loadGsWASM({
|
||||
locateFile: (path: string) => {
|
||||
if (path.endsWith('.wasm')) {
|
||||
return gsBaseUrl + 'gs.wasm';
|
||||
}
|
||||
return path;
|
||||
},
|
||||
print: () => { },
|
||||
printErr: () => { },
|
||||
});
|
||||
setCachedGsModule(gsModule as any);
|
||||
preloadState.ghostscript = PreloadStatus.READY;
|
||||
console.log('[Preloader] Ghostscript WASM ready');
|
||||
} catch (e) {
|
||||
preloadState.ghostscript = PreloadStatus.ERROR;
|
||||
console.warn('[Preloader] Ghostscript preload failed:', e);
|
||||
preloadState.ghostscript = PreloadStatus.LOADING;
|
||||
console.log('[Preloader] Starting Ghostscript WASM preload...');
|
||||
|
||||
try {
|
||||
const gsBaseUrl = getWasmBaseUrl('ghostscript')!;
|
||||
|
||||
let packageBaseUrl = gsBaseUrl;
|
||||
if (packageBaseUrl.endsWith('/assets/')) {
|
||||
packageBaseUrl = packageBaseUrl.slice(0, -8);
|
||||
} else if (packageBaseUrl.endsWith('/assets')) {
|
||||
packageBaseUrl = packageBaseUrl.slice(0, -7);
|
||||
}
|
||||
const normalizedUrl = packageBaseUrl.endsWith('/')
|
||||
? packageBaseUrl
|
||||
: `${packageBaseUrl}/`;
|
||||
|
||||
const libUrl = `${normalizedUrl}dist/index.js`;
|
||||
const module = await import(/* @vite-ignore */ libUrl);
|
||||
const loadGsWASM = module.loadGhostscriptWASM || module.default;
|
||||
const { setCachedGsModule } = await import('./ghostscript-loader.js');
|
||||
|
||||
const gsModule = await loadGsWASM({
|
||||
baseUrl: `${normalizedUrl}assets/`,
|
||||
locateFile: (path: string) => {
|
||||
if (path.endsWith('.wasm')) {
|
||||
return `${normalizedUrl}assets/gs.wasm`;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
print: () => {},
|
||||
printErr: () => {},
|
||||
});
|
||||
setCachedGsModule(gsModule as any);
|
||||
preloadState.ghostscript = PreloadStatus.READY;
|
||||
console.log('[Preloader] Ghostscript WASM ready');
|
||||
} catch (e) {
|
||||
preloadState.ghostscript = PreloadStatus.ERROR;
|
||||
console.warn('[Preloader] Ghostscript preload failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleIdleTask(task: () => Promise<void>): void {
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => task(), { timeout: 5000 });
|
||||
} else {
|
||||
setTimeout(() => task(), 1000);
|
||||
}
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => task(), { timeout: 5000 });
|
||||
} else {
|
||||
setTimeout(() => task(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
export function startBackgroundPreload(): void {
|
||||
console.log('[Preloader] Scheduling background WASM preloads...');
|
||||
console.log('[Preloader] Scheduling background WASM preloads...');
|
||||
|
||||
const libreOfficePages = [
|
||||
'word-to-pdf', 'excel-to-pdf', 'ppt-to-pdf', 'powerpoint-to-pdf',
|
||||
'docx-to-pdf', 'xlsx-to-pdf', 'pptx-to-pdf', 'csv-to-pdf',
|
||||
'rtf-to-pdf', 'odt-to-pdf', 'ods-to-pdf', 'odp-to-pdf'
|
||||
];
|
||||
const libreOfficePages = [
|
||||
'word-to-pdf',
|
||||
'excel-to-pdf',
|
||||
'ppt-to-pdf',
|
||||
'powerpoint-to-pdf',
|
||||
'docx-to-pdf',
|
||||
'xlsx-to-pdf',
|
||||
'pptx-to-pdf',
|
||||
'csv-to-pdf',
|
||||
'rtf-to-pdf',
|
||||
'odt-to-pdf',
|
||||
'ods-to-pdf',
|
||||
'odp-to-pdf',
|
||||
];
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
const isLibreOfficePage = libreOfficePages.some(page => currentPath.includes(page));
|
||||
const currentPath = window.location.pathname;
|
||||
const isLibreOfficePage = libreOfficePages.some((page) =>
|
||||
currentPath.includes(page)
|
||||
);
|
||||
|
||||
if (isLibreOfficePage) {
|
||||
console.log('[Preloader] Skipping preloads on LibreOffice page to save memory');
|
||||
return;
|
||||
}
|
||||
if (isLibreOfficePage) {
|
||||
console.log(
|
||||
'[Preloader] Skipping preloads on LibreOffice page to save memory'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleIdleTask(async () => {
|
||||
console.log('[Preloader] Starting sequential WASM preloads...');
|
||||
scheduleIdleTask(async () => {
|
||||
console.log('[Preloader] Starting sequential WASM preloads...');
|
||||
|
||||
await preloadPyMuPDF();
|
||||
await preloadGhostscript();
|
||||
await preloadPyMuPDF();
|
||||
await preloadGhostscript();
|
||||
|
||||
console.log('[Preloader] Sequential preloads complete (LibreOffice skipped - loaded on demand)');
|
||||
});
|
||||
console.log('[Preloader] Sequential preloads complete');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user