- 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
302 lines
8.7 KiB
TypeScript
302 lines
8.7 KiB
TypeScript
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;
|
|
}
|
|
} |