2025-07-16 17:53:50 +01:00
|
|
|
import { ProcessedFile, ProcessingState, PDFPage } from '../types/processing';
|
|
|
|
import { ProcessingCache } from './processingCache';
|
2025-08-21 17:30:26 +01:00
|
|
|
import { pdfWorkerManager } from './pdfWorkerManager';
|
2025-07-16 17:53:50 +01:00
|
|
|
|
|
|
|
export class PDFProcessingService {
|
|
|
|
private static instance: PDFProcessingService;
|
|
|
|
private cache = new ProcessingCache();
|
|
|
|
private processing = new Map<string, ProcessingState>();
|
|
|
|
private processingListeners = new Set<(states: Map<string, ProcessingState>) => void>();
|
|
|
|
|
|
|
|
private constructor() {}
|
|
|
|
|
|
|
|
static getInstance(): PDFProcessingService {
|
|
|
|
if (!PDFProcessingService.instance) {
|
|
|
|
PDFProcessingService.instance = new PDFProcessingService();
|
|
|
|
}
|
|
|
|
return PDFProcessingService.instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getProcessedFile(file: File): Promise<ProcessedFile | null> {
|
|
|
|
const fileKey = this.generateFileKey(file);
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
// Check cache first
|
|
|
|
const cached = this.cache.get(fileKey);
|
|
|
|
if (cached) {
|
|
|
|
console.log('Cache hit for:', file.name);
|
|
|
|
return cached;
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
// Check if already processing
|
|
|
|
if (this.processing.has(fileKey)) {
|
|
|
|
console.log('Already processing:', file.name);
|
|
|
|
return null; // Will be available when processing completes
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
// Start processing
|
|
|
|
this.startProcessing(file, fileKey);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async startProcessing(file: File, fileKey: string): Promise<void> {
|
|
|
|
// Set initial state
|
|
|
|
const state: ProcessingState = {
|
|
|
|
fileKey,
|
|
|
|
fileName: file.name,
|
|
|
|
status: 'processing',
|
|
|
|
progress: 0,
|
2025-08-11 09:16:16 +01:00
|
|
|
startedAt: Date.now(),
|
|
|
|
strategy: 'immediate_full'
|
2025-07-16 17:53:50 +01:00
|
|
|
};
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
this.processing.set(fileKey, state);
|
|
|
|
this.notifyListeners();
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Process the file with progress updates
|
|
|
|
const processedFile = await this.processFileWithProgress(file, (progress) => {
|
|
|
|
state.progress = progress;
|
|
|
|
this.notifyListeners();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Cache the result
|
|
|
|
this.cache.set(fileKey, processedFile);
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
// Update state to completed
|
|
|
|
state.status = 'completed';
|
|
|
|
state.progress = 100;
|
|
|
|
state.completedAt = Date.now();
|
|
|
|
this.notifyListeners();
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
// Remove from processing map after brief delay
|
|
|
|
setTimeout(() => {
|
|
|
|
this.processing.delete(fileKey);
|
|
|
|
this.notifyListeners();
|
|
|
|
}, 2000);
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Processing failed for', file.name, ':', error);
|
|
|
|
state.status = 'error';
|
2025-08-11 09:16:16 +01:00
|
|
|
state.error = (error instanceof Error ? error.message : 'Unknown error') as any;
|
2025-07-16 17:53:50 +01:00
|
|
|
this.notifyListeners();
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
// Remove failed processing after delay
|
|
|
|
setTimeout(() => {
|
|
|
|
this.processing.delete(fileKey);
|
|
|
|
this.notifyListeners();
|
|
|
|
}, 5000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async processFileWithProgress(
|
2025-08-11 09:16:16 +01:00
|
|
|
file: File,
|
2025-07-16 17:53:50 +01:00
|
|
|
onProgress: (progress: number) => void
|
|
|
|
): Promise<ProcessedFile> {
|
|
|
|
const arrayBuffer = await file.arrayBuffer();
|
2025-08-21 17:30:26 +01:00
|
|
|
const pdf = await pdfWorkerManager.createDocument(arrayBuffer);
|
2025-07-16 17:53:50 +01:00
|
|
|
const totalPages = pdf.numPages;
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
onProgress(10); // PDF loaded
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
const pages: PDFPage[] = [];
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
|
|
const page = await pdf.getPage(i);
|
|
|
|
const viewport = page.getViewport({ scale: 0.5 });
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
canvas.width = viewport.width;
|
|
|
|
canvas.height = viewport.height;
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
if (context) {
|
|
|
|
await page.render({ canvasContext: context, viewport }).promise;
|
|
|
|
const thumbnail = canvas.toDataURL();
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
pages.push({
|
|
|
|
id: `${file.name}-page-${i}`,
|
|
|
|
pageNumber: i,
|
|
|
|
thumbnail,
|
|
|
|
rotation: 0,
|
|
|
|
selected: false
|
|
|
|
});
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
// Update progress
|
|
|
|
const progress = 10 + (i / totalPages) * 85; // 10-95%
|
|
|
|
onProgress(progress);
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-08-21 17:30:26 +01:00
|
|
|
pdfWorkerManager.destroyDocument(pdf);
|
2025-07-16 17:53:50 +01:00
|
|
|
onProgress(100);
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-07-16 17:53:50 +01:00
|
|
|
return {
|
|
|
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
pages,
|
|
|
|
totalPages,
|
|
|
|
metadata: {
|
|
|
|
title: file.name,
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
modifiedAt: new Date().toISOString()
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// State subscription for components
|
|
|
|
onProcessingChange(callback: (states: Map<string, ProcessingState>) => void): () => void {
|
|
|
|
this.processingListeners.add(callback);
|
|
|
|
return () => this.processingListeners.delete(callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
getProcessingStates(): Map<string, ProcessingState> {
|
|
|
|
return new Map(this.processing);
|
|
|
|
}
|
|
|
|
|
|
|
|
private notifyListeners(): void {
|
|
|
|
this.processingListeners.forEach(callback => callback(this.processing));
|
|
|
|
}
|
|
|
|
|
|
|
|
generateFileKey(file: File): string {
|
|
|
|
return `${file.name}-${file.size}-${file.lastModified}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cleanup method for activeFiles changes
|
|
|
|
cleanup(removedFiles: File[]): void {
|
|
|
|
removedFiles.forEach(file => {
|
|
|
|
const key = this.generateFileKey(file);
|
|
|
|
this.cache.delete(key);
|
|
|
|
this.processing.delete(key);
|
|
|
|
});
|
|
|
|
this.notifyListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get cache stats (for debugging)
|
|
|
|
getCacheStats() {
|
|
|
|
return this.cache.getStats();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear all cache and processing
|
|
|
|
clearAll(): void {
|
|
|
|
this.cache.clear();
|
|
|
|
this.processing.clear();
|
|
|
|
this.notifyListeners();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Export singleton instance
|
2025-08-11 09:16:16 +01:00
|
|
|
export const pdfProcessingService = PDFProcessingService.getInstance();
|