feat(initial): add comprehensive PDF to JPEG preview library with dual-environment support

- 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
This commit is contained in:
2025-08-03 21:44:01 +00:00
commit bc1c7edd35
23 changed files with 12822 additions and 0 deletions

37
ts_web/index.ts Normal file
View File

@@ -0,0 +1,37 @@
/**
* @push.rocks/smartpreview - Web implementation
*
* A library for generating efficient JPEG previews from PDFs
* in browser environments using PDF.js and Web Workers.
*/
// Main classes
export { SmartPreview } from './smartpreview.js';
export { WebPdfProcessor } from './pdfprocessor.js';
// Runtime exports (classes and functions)
export {
PreviewError
} from './interfaces.js';
// Type-only exports (interfaces and types)
export type {
IPreviewOptions,
IPreviewResult,
TWebInputType,
TSupportedInputFormat,
TSupportedOutputFormat,
IWebFormatProcessor,
IWebPdfProcessor,
TPreviewError,
IProgressCallback,
IWebPreviewOptions,
// Worker-related types
TWorkerMessageType,
IWorkerMessage,
IPdfProcessRequest,
IPdfProcessResponse
} from './interfaces.js';
// Default export for convenience
export { SmartPreview as default } from './smartpreview.js';

202
ts_web/interfaces.ts Normal file
View File

@@ -0,0 +1,202 @@
/**
* Configuration options for preview generation in browser environment
*/
export interface IPreviewOptions {
/**
* JPEG quality (1-100)
* @default 80
*/
quality?: number;
/**
* Maximum width in pixels
*/
width?: number;
/**
* Maximum height in pixels
*/
height?: number;
/**
* PDF page number to convert (1-based)
* @default 1
*/
page?: number;
/**
* Scale factor for rendering
* @default 1.0
*/
scale?: number;
}
/**
* Preview generation result for browser environment
*/
export interface IPreviewResult {
/**
* Generated JPEG image blob
*/
blob: Blob;
/**
* Image dimensions
*/
dimensions: {
width: number;
height: number;
};
/**
* File size in bytes
*/
size: number;
/**
* MIME type
*/
mimeType: 'image/jpeg';
/**
* Data URL for immediate use
*/
dataUrl: string;
}
/**
* Supported input types for browser environment
*/
export type TWebInputType = File | Blob | ArrayBuffer | Uint8Array | string;
/**
* Supported input formats (extensible)
*/
export type TSupportedInputFormat = 'pdf';
/**
* Supported output formats (extensible)
*/
export type TSupportedOutputFormat = 'jpeg';
/**
* Worker message types for communication
*/
export type TWorkerMessageType =
| 'INIT'
| 'PROCESS_PDF'
| 'PROCESS_COMPLETE'
| 'PROCESS_ERROR'
| 'WORKER_READY';
/**
* Worker message interface
*/
export interface IWorkerMessage {
type: TWorkerMessageType;
id: string;
data?: any;
error?: string;
}
/**
* PDF processing request for worker
*/
export interface IPdfProcessRequest {
pdfData: ArrayBuffer;
options: Required<IPreviewOptions>;
}
/**
* PDF processing response from worker
*/
export interface IPdfProcessResponse {
imageData: ArrayBuffer;
width: number;
height: number;
}
/**
* Base interface for format processors (extensible architecture)
*/
export interface IWebFormatProcessor {
/**
* Supported input format
*/
inputFormat: TSupportedInputFormat;
/**
* Supported output format
*/
outputFormat: TSupportedOutputFormat;
/**
* Process the input and generate preview
*/
processPreview(input: TWebInputType, options: IPreviewOptions): Promise<IPreviewResult>;
}
/**
* PDF-specific processor interface for web
*/
export interface IWebPdfProcessor extends IWebFormatProcessor {
inputFormat: 'pdf';
outputFormat: 'jpeg';
}
/**
* Error types for preview generation
*/
export type TPreviewError =
| 'INVALID_INPUT'
| 'UNSUPPORTED_FORMAT'
| 'PROCESSING_FAILED'
| 'INVALID_OPTIONS'
| 'PDF_CORRUPTED'
| 'PAGE_NOT_FOUND'
| 'WORKER_ERROR'
| 'WORKER_TIMEOUT';
/**
* Custom error class for preview operations in browser
*/
export class PreviewError extends Error {
public readonly errorType: TPreviewError;
public readonly originalError?: Error;
constructor(errorType: TPreviewError, message: string, originalError?: Error) {
super(message);
this.name = 'PreviewError';
this.errorType = errorType;
this.originalError = originalError;
}
}
/**
* Progress callback interface
*/
export interface IProgressCallback {
(progress: number, stage: string): void;
}
/**
* Advanced options for web processing
*/
export interface IWebPreviewOptions extends IPreviewOptions {
/**
* Progress callback function
*/
onProgress?: IProgressCallback;
/**
* Worker timeout in milliseconds
* @default 30000
*/
timeout?: number;
/**
* Whether to generate data URL
* @default true
*/
generateDataUrl?: boolean;
}

423
ts_web/pdfprocessor.ts Normal file
View File

@@ -0,0 +1,423 @@
import type {
IWebPdfProcessor,
IPreviewOptions,
IPreviewResult,
TWebInputType,
IWorkerMessage,
IPdfProcessRequest,
IPdfProcessResponse,
IWebPreviewOptions
} from './interfaces.js';
import { PreviewError } from './interfaces.js';
/**
* PDF processor implementation for browser using PDF.js worker
*/
export class WebPdfProcessor implements IWebPdfProcessor {
public readonly inputFormat = 'pdf' as const;
public readonly outputFormat = 'jpeg' as const;
private worker: Worker | null = null;
private isInitialized = false;
private pendingRequests = new Map<string, {
resolve: (result: IPdfProcessResponse) => void;
reject: (error: Error) => void;
timeout?: number;
}>();
private requestIdCounter = 0;
/**
* Initialize the PDF processor with worker
*/
public async init(): Promise<void> {
if (this.isInitialized) {
return;
}
try {
// Create worker from blob URL to avoid CORS issues
const workerBlob = await this.createWorkerBlob();
const workerUrl = URL.createObjectURL(workerBlob);
this.worker = new Worker(workerUrl);
this.setupWorkerEventHandlers();
// Wait for worker to be ready
await this.waitForWorkerReady();
this.isInitialized = true;
} catch (error) {
throw new PreviewError(
'PROCESSING_FAILED',
'Failed to initialize PDF processor',
error instanceof Error ? error : new Error(String(error))
);
}
}
/**
* Process PDF and generate JPEG preview
*/
public async processPreview(input: TWebInputType, options: IWebPreviewOptions = {}): Promise<IPreviewResult> {
if (!this.isInitialized || !this.worker) {
throw new PreviewError('PROCESSING_FAILED', 'PDF processor not initialized');
}
try {
// Convert input to ArrayBuffer
const arrayBuffer = await this.inputToArrayBuffer(input);
// Validate PDF
this.validatePdfBuffer(arrayBuffer);
// Set default options
const processOptions = {
quality: options.quality ?? 80,
width: options.width ?? 800,
height: options.height ?? 600,
page: options.page ?? 1,
scale: options.scale ?? 1.0,
};
// Validate options
this.validateOptions(processOptions);
// Process with worker
const response = await this.processWithWorker(arrayBuffer, processOptions, options.timeout);
// Create blob from response
const blob = new Blob([response.imageData], { type: 'image/jpeg' });
// Generate data URL if requested
let dataUrl = '';
if (options.generateDataUrl !== false) {
dataUrl = await this.blobToDataUrl(blob);
}
return {
blob,
dimensions: {
width: response.width,
height: response.height,
},
size: response.imageData.byteLength,
mimeType: 'image/jpeg',
dataUrl,
};
} catch (error) {
if (error instanceof PreviewError) {
throw error;
}
throw new PreviewError(
'PROCESSING_FAILED',
'Failed to process PDF',
error instanceof Error ? error : new Error(String(error))
);
}
}
/**
* Clean up resources
*/
public async cleanup(): Promise<void> {
if (this.worker) {
// Cancel pending requests
for (const [_id, request] of this.pendingRequests) {
request.reject(new Error('Worker cleanup'));
if (request.timeout) {
clearTimeout(request.timeout);
}
}
this.pendingRequests.clear();
// Terminate worker
this.worker.terminate();
this.worker = null;
}
this.isInitialized = false;
}
/**
* Convert various input types to ArrayBuffer
*/
private async inputToArrayBuffer(input: TWebInputType): Promise<ArrayBuffer> {
if (input instanceof ArrayBuffer) {
return input;
}
if (input instanceof Uint8Array) {
return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
}
if (input instanceof File || input instanceof Blob) {
return await input.arrayBuffer();
}
if (typeof input === 'string') {
// Assume it's a data URL or base64
if (input.startsWith('data:')) {
const base64 = input.split(',')[1];
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
throw new PreviewError('INVALID_INPUT', 'String input must be a data URL');
}
throw new PreviewError('INVALID_INPUT', 'Unsupported input type');
}
/**
* Validate PDF buffer
*/
private validatePdfBuffer(buffer: ArrayBuffer): void {
if (buffer.byteLength < 4) {
throw new PreviewError('INVALID_INPUT', 'Input is too small to be a valid PDF');
}
const header = new Uint8Array(buffer, 0, 4);
const pdfMagic = new Uint8Array([37, 80, 68, 70]); // %PDF
for (let i = 0; i < 4; i++) {
if (header[i] !== pdfMagic[i]) {
throw new PreviewError('INVALID_INPUT', 'Input is not a valid PDF file');
}
}
}
/**
* Validate processing options
*/
private validateOptions(options: Required<IPreviewOptions>): void {
if (options.quality < 1 || options.quality > 100) {
throw new PreviewError('INVALID_OPTIONS', 'Quality must be between 1 and 100');
}
if (options.width <= 0 || options.height <= 0) {
throw new PreviewError('INVALID_OPTIONS', 'Width and height must be positive numbers');
}
if (options.page < 1) {
throw new PreviewError('INVALID_OPTIONS', 'Page number must be 1 or greater');
}
if (options.scale <= 0) {
throw new PreviewError('INVALID_OPTIONS', 'Scale must be a positive number');
}
}
/**
* Process PDF with worker
*/
private async processWithWorker(
pdfData: ArrayBuffer,
options: Required<IPreviewOptions>,
timeout = 30000
): Promise<IPdfProcessResponse> {
return new Promise((resolve, reject) => {
const requestId = `req_${++this.requestIdCounter}`;
// Set up timeout
const timeoutHandle = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new PreviewError('WORKER_TIMEOUT', `Worker timeout after ${timeout}ms`));
}, timeout);
// Store request
this.pendingRequests.set(requestId, {
resolve,
reject,
timeout: timeoutHandle as any,
});
// Send request to worker
const request: IPdfProcessRequest = {
pdfData,
options,
};
this.worker!.postMessage({
type: 'PROCESS_PDF',
id: requestId,
data: request,
} as IWorkerMessage);
});
}
/**
* Create worker blob from source code
*/
private async createWorkerBlob(): Promise<Blob> {
// In a real implementation, you would bundle the worker code
// For now, we'll create a minimal worker that loads PDF.js from CDN
const workerCode = `
// Import PDF.js from CDN
importScripts('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js');
// Configure PDF.js
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
let isInitialized = false;
// Initialize
async function initialize() {
if (isInitialized) return;
isInitialized = true;
postMessage({ type: 'WORKER_READY', id: 'init' });
}
// Process PDF
async function processPdf(requestId, request) {
try {
const pdf = await pdfjsLib.getDocument({ data: request.pdfData }).promise;
const page = await pdf.getPage(request.options.page);
const viewport = page.getViewport({ scale: request.options.scale });
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
});
const canvas = new OffscreenCanvas(scaledViewport.width, scaledViewport.height);
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: scaledViewport }).promise;
const blob = await canvas.convertToBlob({
type: 'image/jpeg',
quality: request.options.quality / 100,
});
const arrayBuffer = await blob.arrayBuffer();
postMessage({
type: 'PROCESS_COMPLETE',
id: requestId,
data: {
imageData: arrayBuffer,
width: scaledViewport.width,
height: scaledViewport.height,
}
});
} catch (error) {
postMessage({
type: 'PROCESS_ERROR',
id: requestId,
error: error.message
});
}
}
// Message handler
self.addEventListener('message', async (event) => {
const { type, id, data } = event.data;
switch (type) {
case 'INIT':
await initialize();
break;
case 'PROCESS_PDF':
await processPdf(id, data);
break;
}
});
// Auto-initialize
initialize();
`;
return new Blob([workerCode], { type: 'application/javascript' });
}
/**
* Set up worker event handlers
*/
private setupWorkerEventHandlers(): void {
if (!this.worker) return;
this.worker.addEventListener('message', (event: MessageEvent<IWorkerMessage>) => {
const { type, id, data, error } = event.data;
const request = this.pendingRequests.get(id);
if (!request) return;
// Clear timeout
if (request.timeout) {
clearTimeout(request.timeout);
}
// Remove from pending
this.pendingRequests.delete(id);
switch (type) {
case 'PROCESS_COMPLETE':
request.resolve(data as IPdfProcessResponse);
break;
case 'PROCESS_ERROR':
request.reject(new PreviewError('WORKER_ERROR', error || 'Unknown worker error'));
break;
}
});
this.worker.addEventListener('error', (event) => {
// Handle worker errors
for (const [_id, request] of this.pendingRequests) {
request.reject(new PreviewError('WORKER_ERROR', `Worker error: ${event.message}`));
if (request.timeout) {
clearTimeout(request.timeout);
}
}
this.pendingRequests.clear();
});
}
/**
* Wait for worker to be ready
*/
private async waitForWorkerReady(timeout = 10000): Promise<void> {
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
reject(new PreviewError('WORKER_TIMEOUT', 'Worker initialization timeout'));
}, timeout);
const messageHandler = (event: MessageEvent<IWorkerMessage>) => {
if (event.data.type === 'WORKER_READY') {
clearTimeout(timeoutHandle);
this.worker!.removeEventListener('message', messageHandler);
resolve();
}
};
this.worker!.addEventListener('message', messageHandler);
this.worker!.postMessage({ type: 'INIT', id: 'init' });
});
}
/**
* Convert blob to data URL
*/
private async blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(new Error('Failed to read blob as data URL'));
reader.readAsDataURL(blob);
});
}
}

191
ts_web/pdfworker.ts Normal file
View File

@@ -0,0 +1,191 @@
/**
* 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();

302
ts_web/smartpreview.ts Normal file
View File

@@ -0,0 +1,302 @@
import type {
IPreviewOptions,
IPreviewResult,
TWebInputType,
TSupportedInputFormat,
IWebPreviewOptions
} from './interfaces.js';
import { PreviewError } from './interfaces.js';
import { WebPdfProcessor } from './pdfprocessor.js';
/**
* Main SmartPreview class for browser environment
* Provides unified API for generating previews from various document formats
*/
export class SmartPreview {
private pdfProcessor: WebPdfProcessor | null = null;
private isInitialized = false;
/**
* Create a new SmartPreview instance
*/
constructor() {
// Constructor kept minimal following async initialization pattern
}
/**
* Initialize the SmartPreview instance and all processors
*/
public async init(): Promise<void> {
if (this.isInitialized) {
return;
}
try {
// Initialize PDF processor
this.pdfProcessor = new WebPdfProcessor();
await this.pdfProcessor.init();
this.isInitialized = true;
} catch (error) {
await this.cleanup(); // Cleanup on initialization failure
throw new PreviewError(
'PROCESSING_FAILED',
'Failed to initialize SmartPreview',
error instanceof Error ? error : new Error(String(error))
);
}
}
/**
* Generate preview from input
* @param input - File, Blob, ArrayBuffer, Uint8Array, or data URL string
* @param options - Preview generation options
* @returns Promise resolving to preview result
*/
public async generatePreview(input: TWebInputType, options: IWebPreviewOptions = {}): Promise<IPreviewResult> {
if (!this.isInitialized) {
throw new PreviewError('PROCESSING_FAILED', 'SmartPreview not initialized. Call init() first.');
}
if (!input) {
throw new PreviewError('INVALID_INPUT', 'Input is required');
}
// Detect format and route to appropriate processor
const format = await this.detectFormat(input);
switch (format) {
case 'pdf':
if (!this.pdfProcessor) {
throw new PreviewError('PROCESSING_FAILED', 'PDF processor not available');
}
return await this.pdfProcessor.processPreview(input, options);
default:
throw new PreviewError('UNSUPPORTED_FORMAT', `Format '${format}' is not supported`);
}
}
/**
* Generate preview from File input (convenience method)
* @param file - File object from file input
* @param options - Preview generation options
* @returns Promise resolving to preview result
*/
public async generatePreviewFromFile(file: File, options: IWebPreviewOptions = {}): Promise<IPreviewResult> {
if (!(file instanceof File)) {
throw new PreviewError('INVALID_INPUT', 'Input must be a File object');
}
return await this.generatePreview(file, options);
}
/**
* Generate preview from URL (fetch and process)
* @param url - URL to fetch the document from
* @param options - Preview generation options
* @returns Promise resolving to preview result
*/
public async generatePreviewFromUrl(url: string, options: IWebPreviewOptions = {}): Promise<IPreviewResult> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const blob = await response.blob();
return await this.generatePreview(blob, options);
} catch (error) {
throw new PreviewError(
'PROCESSING_FAILED',
`Failed to fetch document from URL: ${url}`,
error instanceof Error ? error : new Error(String(error))
);
}
}
/**
* Create download link for preview
* @param input - Document input
* @param options - Preview generation options
* @param filename - Optional filename for download
* @returns Promise resolving to download URL and cleanup function
*/
public async createDownloadLink(
input: TWebInputType,
options: IWebPreviewOptions = {},
filename = 'preview.jpg'
): Promise<{ url: string; cleanup: () => void }> {
const result = await this.generatePreview(input, options);
const url = URL.createObjectURL(result.blob);
return {
url,
cleanup: () => URL.revokeObjectURL(url)
};
}
/**
* Trigger download of preview
* @param input - Document input
* @param options - Preview generation options
* @param filename - Filename for download
*/
public async downloadPreview(
input: TWebInputType,
options: IWebPreviewOptions = {},
filename = 'preview.jpg'
): Promise<void> {
const { url, cleanup } = await this.createDownloadLink(input, options, filename);
try {
// Create temporary download link
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} finally {
// Clean up after short delay to ensure download starts
setTimeout(cleanup, 1000);
}
}
/**
* Get supported input formats
*/
public getSupportedFormats(): TSupportedInputFormat[] {
return ['pdf'];
}
/**
* Check if a format is supported
*/
public isFormatSupported(format: string): format is TSupportedInputFormat {
return this.getSupportedFormats().includes(format as TSupportedInputFormat);
}
/**
* Check if File API is supported
*/
public static isFileApiSupported(): boolean {
return typeof File !== 'undefined' && typeof FileReader !== 'undefined';
}
/**
* Check if Web Workers are supported
*/
public static isWebWorkerSupported(): boolean {
return typeof Worker !== 'undefined';
}
/**
* Check if OffscreenCanvas is supported
*/
public static isOffscreenCanvasSupported(): boolean {
return typeof OffscreenCanvas !== 'undefined';
}
/**
* Get browser compatibility info
*/
public static getBrowserCompatibility(): {
fileApi: boolean;
webWorkers: boolean;
offscreenCanvas: boolean;
isSupported: boolean;
} {
const fileApi = this.isFileApiSupported();
const webWorkers = this.isWebWorkerSupported();
const offscreenCanvas = this.isOffscreenCanvasSupported();
return {
fileApi,
webWorkers,
offscreenCanvas,
isSupported: fileApi && webWorkers && offscreenCanvas
};
}
/**
* Clean up resources
*/
public async cleanup(): Promise<void> {
if (this.pdfProcessor) {
await this.pdfProcessor.cleanup();
this.pdfProcessor = null;
}
this.isInitialized = false;
}
/**
* Detect document format from input
* @private
*/
private async detectFormat(input: TWebInputType): Promise<TSupportedInputFormat> {
try {
let buffer: ArrayBuffer;
if (input instanceof ArrayBuffer) {
buffer = input;
} else if (input instanceof Uint8Array) {
buffer = input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
} else if (input instanceof File || input instanceof Blob) {
// Read first few bytes to detect format
const headerBlob = input.slice(0, 8);
buffer = await headerBlob.arrayBuffer();
} else if (typeof input === 'string') {
// Handle data URLs
if (input.startsWith('data:')) {
const base64 = input.split(',')[1];
const binaryString = atob(base64);
const bytes = new Uint8Array(Math.min(8, binaryString.length));
for (let i = 0; i < bytes.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
buffer = bytes.buffer;
} else {
throw new PreviewError('INVALID_INPUT', 'String input must be a data URL');
}
} else {
throw new PreviewError('INVALID_INPUT', 'Unsupported input type');
}
// Check format signatures
if (buffer.byteLength >= 4) {
const header = new Uint8Array(buffer, 0, 4);
// PDF signature: %PDF
if (header[0] === 37 && header[1] === 80 && header[2] === 68 && header[3] === 70) {
return 'pdf';
}
}
// Future format detection can be added here
// Example: JPEG (FF D8 FF), PNG (89 50 4E 47), etc.
throw new PreviewError('UNSUPPORTED_FORMAT', 'Unable to detect supported format from input');
} catch (error) {
if (error instanceof PreviewError) {
throw error;
}
throw new PreviewError(
'PROCESSING_FAILED',
'Error during format detection',
error instanceof Error ? error : new Error(String(error))
);
}
}
/**
* Static factory method for convenient instantiation
*/
public static async create(options: IWebPreviewOptions = {}): Promise<SmartPreview> {
const instance = new SmartPreview();
await instance.init();
return instance;
}
}