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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { const instance = new SmartPreview(); await instance.init(); return instance; } }