Stirling-PDF/frontend/src/services/pdfProcessingService.ts
Ludy 25bedf064f
Update PDF.js integration and dependencies (#4360)
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-09-24 13:07:27 +01:00

188 lines
5.3 KiB
TypeScript

import { ProcessedFile, ProcessingState, PDFPage } from '../types/processing';
import { ProcessingCache } from './processingCache';
import { pdfWorkerManager } from './pdfWorkerManager';
import { createQuickKey } from '../types/fileContext';
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);
// Check cache first
const cached = this.cache.get(fileKey);
if (cached) {
console.log('Cache hit for:', file.name);
return cached;
}
// Check if already processing
if (this.processing.has(fileKey)) {
console.log('Already processing:', file.name);
return null; // Will be available when processing completes
}
// 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,
startedAt: Date.now(),
strategy: 'immediate_full'
};
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);
// Update state to completed
state.status = 'completed';
state.progress = 100;
state.completedAt = Date.now();
this.notifyListeners();
// 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';
state.error = (error instanceof Error ? error.message : 'Unknown error') as any;
this.notifyListeners();
// Remove failed processing after delay
setTimeout(() => {
this.processing.delete(fileKey);
this.notifyListeners();
}, 5000);
}
}
private async processFileWithProgress(
file: File,
onProgress: (progress: number) => void
): Promise<ProcessedFile> {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfWorkerManager.createDocument(arrayBuffer);
const totalPages = pdf.numPages;
onProgress(10); // PDF loaded
const pages: PDFPage[] = [];
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;
const context = canvas.getContext('2d');
if (context) {
await page.render({ canvasContext: context, viewport, canvas }).promise;
const thumbnail = canvas.toDataURL();
pages.push({
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail,
rotation: 0,
selected: false
});
}
// Update progress
const progress = 10 + (i / totalPages) * 85; // 10-95%
onProgress(progress);
}
pdfWorkerManager.destroyDocument(pdf);
onProgress(100);
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
export const pdfProcessingService = PDFProcessingService.getInstance();