- Add Node.js implementation using @push.rocks/smartpdf - Add browser implementation with PDF.js and Web Workers - Support configurable quality, dimensions, and page selection - Include comprehensive TypeScript definitions and error handling - Provide extensive test coverage for both environments - Add download functionality and browser compatibility checking
191 lines
4.7 KiB
TypeScript
191 lines
4.7 KiB
TypeScript
/**
|
|
* PDF.js worker for processing PDFs in the browser
|
|
* This file runs in a Web Worker context
|
|
*/
|
|
|
|
// Import types for worker context
|
|
import type {
|
|
IWorkerMessage,
|
|
IPdfProcessRequest,
|
|
IPdfProcessResponse,
|
|
TWorkerMessageType
|
|
} from './interfaces.js';
|
|
import { PreviewError } from './interfaces.js';
|
|
|
|
// PDF.js library (loaded from CDN or bundled)
|
|
declare const pdfjsLib: any;
|
|
|
|
/**
|
|
* Worker context interface
|
|
*/
|
|
declare const self: any;
|
|
|
|
/**
|
|
* PDF.js configuration
|
|
*/
|
|
const PDFJS_CDN_URL = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
|
|
const WORKER_CDN_URL = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
|
|
|
/**
|
|
* Worker state
|
|
*/
|
|
let isInitialized = false;
|
|
let pdfjsWorker: any = null;
|
|
|
|
/**
|
|
* Initialize PDF.js in worker context
|
|
*/
|
|
async function initializePdfJs(): Promise<void> {
|
|
if (isInitialized) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Load PDF.js library
|
|
self.importScripts(PDFJS_CDN_URL);
|
|
|
|
if (typeof pdfjsLib === 'undefined') {
|
|
throw new Error('Failed to load PDF.js library');
|
|
}
|
|
|
|
// Configure PDF.js worker
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_CDN_URL;
|
|
|
|
isInitialized = true;
|
|
postMessage({
|
|
type: 'WORKER_READY',
|
|
id: 'init',
|
|
data: { version: pdfjsLib.version }
|
|
} as IWorkerMessage);
|
|
} catch (error) {
|
|
postMessage({
|
|
type: 'PROCESS_ERROR',
|
|
id: 'init',
|
|
error: `Failed to initialize PDF.js: ${error instanceof Error ? error.message : String(error)}`
|
|
} as IWorkerMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process PDF and generate JPEG preview
|
|
*/
|
|
async function processPdf(requestId: string, request: IPdfProcessRequest): Promise<void> {
|
|
if (!isInitialized) {
|
|
postMessage({
|
|
type: 'PROCESS_ERROR',
|
|
id: requestId,
|
|
error: 'Worker not initialized'
|
|
} as IWorkerMessage);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Load PDF document
|
|
const loadingTask = pdfjsLib.getDocument({
|
|
data: request.pdfData,
|
|
cMapUrl: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/cmaps/',
|
|
cMapPacked: true,
|
|
});
|
|
|
|
const pdf = await loadingTask.promise;
|
|
|
|
// Validate page number
|
|
if (request.options.page > pdf.numPages || request.options.page < 1) {
|
|
throw new Error(`Page ${request.options.page} not found. Document has ${pdf.numPages} pages.`);
|
|
}
|
|
|
|
// Get the specified page
|
|
const page = await pdf.getPage(request.options.page);
|
|
|
|
// Calculate viewport
|
|
const viewport = page.getViewport({ scale: request.options.scale });
|
|
|
|
// Adjust viewport to fit within max dimensions
|
|
let { width, height } = viewport;
|
|
if (request.options.width && width > request.options.width) {
|
|
const scale = request.options.width / width;
|
|
width = request.options.width;
|
|
height = height * scale;
|
|
}
|
|
if (request.options.height && height > request.options.height) {
|
|
const scale = request.options.height / height;
|
|
height = request.options.height;
|
|
width = width * scale;
|
|
}
|
|
|
|
const scaledViewport = page.getViewport({
|
|
scale: Math.min(width / viewport.width, height / viewport.height) * request.options.scale
|
|
});
|
|
|
|
// Create canvas and render page
|
|
const canvas = new OffscreenCanvas(scaledViewport.width, scaledViewport.height);
|
|
const context = canvas.getContext('2d');
|
|
|
|
if (!context) {
|
|
throw new Error('Failed to get 2D rendering context');
|
|
}
|
|
|
|
const renderContext = {
|
|
canvasContext: context,
|
|
viewport: scaledViewport,
|
|
};
|
|
|
|
await page.render(renderContext).promise;
|
|
|
|
// Convert to JPEG
|
|
const blob = await canvas.convertToBlob({
|
|
type: 'image/jpeg',
|
|
quality: request.options.quality / 100,
|
|
});
|
|
|
|
// Convert blob to ArrayBuffer
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
|
|
// Send response
|
|
const response: IPdfProcessResponse = {
|
|
imageData: arrayBuffer,
|
|
width: scaledViewport.width,
|
|
height: scaledViewport.height,
|
|
};
|
|
|
|
postMessage({
|
|
type: 'PROCESS_COMPLETE',
|
|
id: requestId,
|
|
data: response
|
|
} as IWorkerMessage);
|
|
|
|
} catch (error) {
|
|
postMessage({
|
|
type: 'PROCESS_ERROR',
|
|
id: requestId,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
} as IWorkerMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle worker messages
|
|
*/
|
|
self.addEventListener('message', async (event: MessageEvent<IWorkerMessage>) => {
|
|
const { type, id, data } = event.data;
|
|
|
|
switch (type) {
|
|
case 'INIT':
|
|
await initializePdfJs();
|
|
break;
|
|
|
|
case 'PROCESS_PDF':
|
|
await processPdf(id, data as IPdfProcessRequest);
|
|
break;
|
|
|
|
default:
|
|
postMessage({
|
|
type: 'PROCESS_ERROR',
|
|
id: id || 'unknown',
|
|
error: `Unknown message type: ${type}`
|
|
} as IWorkerMessage);
|
|
}
|
|
});
|
|
|
|
// Auto-initialize when worker starts
|
|
initializePdfJs(); |