Files
smartpreview/ts_web/smartpreview.ts
Juergen Kunz bc1c7edd35 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
2025-08-03 21:44:01 +00:00

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;
}
}