mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
Feature/v2/pageeditor improved (#4289)
# Description of Changes <!-- Please provide a summary of the changes, including: Rewrite of page editor to make it work properly. Added page breaks Added merged file support Added "insert file" support Slight Ux improvements Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
This commit is contained in:
176
frontend/src/services/documentManipulationService.ts
Normal file
176
frontend/src/services/documentManipulationService.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { PDFDocument, PDFPage } from '../types/pageEditor';
|
||||
|
||||
/**
|
||||
* Service for applying DOM changes to PDF document state
|
||||
* Reads current DOM state and updates the document accordingly
|
||||
*/
|
||||
export class DocumentManipulationService {
|
||||
/**
|
||||
* Apply all DOM changes (rotations, splits, reordering) to document state
|
||||
* Returns single document or multiple documents if splits are present
|
||||
*/
|
||||
applyDOMChangesToDocument(pdfDocument: PDFDocument, currentDisplayOrder?: PDFDocument, splitPositions?: Set<number>): PDFDocument | PDFDocument[] {
|
||||
console.log('DocumentManipulationService: Applying DOM changes to document');
|
||||
console.log('Original document page order:', pdfDocument.pages.map(p => p.pageNumber));
|
||||
console.log('Current display order:', currentDisplayOrder?.pages.map(p => p.pageNumber) || 'none provided');
|
||||
console.log('Split positions:', splitPositions ? Array.from(splitPositions).sort() : 'none');
|
||||
|
||||
// Use current display order (from React state) if provided, otherwise use original order
|
||||
const baseDocument = currentDisplayOrder || pdfDocument;
|
||||
console.log('Using page order:', baseDocument.pages.map(p => p.pageNumber));
|
||||
|
||||
// Apply DOM changes to each page (rotation only now, splits are position-based)
|
||||
let updatedPages = baseDocument.pages.map(page => this.applyPageChanges(page));
|
||||
|
||||
// Convert position-based splits to page-based splits for export
|
||||
if (splitPositions && splitPositions.size > 0) {
|
||||
updatedPages = updatedPages.map((page, index) => ({
|
||||
...page,
|
||||
splitAfter: splitPositions.has(index)
|
||||
}));
|
||||
}
|
||||
|
||||
// Create final document with reordered pages and applied changes
|
||||
const finalDocument = {
|
||||
...pdfDocument, // Use original document metadata but updated pages
|
||||
pages: updatedPages // Use reordered pages with applied changes
|
||||
};
|
||||
|
||||
// Check for splits and return multiple documents if needed
|
||||
if (splitPositions && splitPositions.size > 0) {
|
||||
return this.createSplitDocuments(finalDocument);
|
||||
}
|
||||
|
||||
return finalDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if document has split markers
|
||||
*/
|
||||
private hasSplitMarkers(document: PDFDocument): boolean {
|
||||
return document.pages.some(page => page.splitAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple documents from split markers
|
||||
*/
|
||||
private createSplitDocuments(document: PDFDocument): PDFDocument[] {
|
||||
const documents: PDFDocument[] = [];
|
||||
const splitPoints: number[] = [];
|
||||
|
||||
// Find split points
|
||||
document.pages.forEach((page, index) => {
|
||||
if (page.splitAfter) {
|
||||
console.log(`Found split marker at page ${page.pageNumber} (index ${index}), adding split point at ${index + 1}`);
|
||||
splitPoints.push(index + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Add end point if not already there
|
||||
if (splitPoints.length === 0 || splitPoints[splitPoints.length - 1] !== document.pages.length) {
|
||||
splitPoints.push(document.pages.length);
|
||||
}
|
||||
|
||||
console.log('Final split points:', splitPoints);
|
||||
console.log('Total pages to split:', document.pages.length);
|
||||
|
||||
let startIndex = 0;
|
||||
let partNumber = 1;
|
||||
|
||||
for (const endIndex of splitPoints) {
|
||||
const segmentPages = document.pages.slice(startIndex, endIndex);
|
||||
|
||||
console.log(`Creating split document ${partNumber}: pages ${startIndex}-${endIndex-1} (${segmentPages.length} pages)`);
|
||||
console.log(`Split document ${partNumber} page numbers:`, segmentPages.map(p => p.pageNumber));
|
||||
|
||||
if (segmentPages.length > 0) {
|
||||
documents.push({
|
||||
...document,
|
||||
id: `${document.id}_part_${partNumber}`,
|
||||
name: `${document.name.replace(/\.pdf$/i, '')}_part_${partNumber}.pdf`,
|
||||
pages: segmentPages,
|
||||
totalPages: segmentPages.length
|
||||
});
|
||||
partNumber++;
|
||||
}
|
||||
|
||||
startIndex = endIndex;
|
||||
}
|
||||
|
||||
console.log(`Created ${documents.length} split documents`);
|
||||
return documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply DOM changes for a single page
|
||||
*/
|
||||
private applyPageChanges(page: PDFPage): PDFPage {
|
||||
// Find the DOM element for this page
|
||||
const pageElement = document.querySelector(`[data-page-id="${page.id}"]`);
|
||||
if (!pageElement) {
|
||||
console.log(`Page ${page.pageNumber}: No DOM element found, keeping original state`);
|
||||
return page;
|
||||
}
|
||||
|
||||
const updatedPage = { ...page };
|
||||
|
||||
// Apply rotation changes from DOM
|
||||
updatedPage.rotation = this.getRotationFromDOM(pageElement, page);
|
||||
|
||||
|
||||
return updatedPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read rotation from DOM element
|
||||
*/
|
||||
private getRotationFromDOM(pageElement: Element, originalPage: PDFPage): number {
|
||||
const img = pageElement.querySelector('img');
|
||||
if (img && img.style.transform) {
|
||||
// Parse rotation from transform property (e.g., "rotate(90deg)" -> 90)
|
||||
const rotationMatch = img.style.transform.match(/rotate\((-?\d+)deg\)/);
|
||||
const domRotation = rotationMatch ? parseInt(rotationMatch[1]) : 0;
|
||||
|
||||
console.log(`Page ${originalPage.pageNumber}: DOM rotation = ${domRotation}°, original = ${originalPage.rotation}°`);
|
||||
return domRotation;
|
||||
}
|
||||
|
||||
console.log(`Page ${originalPage.pageNumber}: No DOM rotation found, keeping original = ${originalPage.rotation}°`);
|
||||
return originalPage.rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all DOM changes (useful for "discard changes" functionality)
|
||||
*/
|
||||
resetDOMToDocumentState(pdfDocument: PDFDocument): void {
|
||||
console.log('DocumentManipulationService: Resetting DOM to match document state');
|
||||
|
||||
pdfDocument.pages.forEach(page => {
|
||||
const pageElement = document.querySelector(`[data-page-id="${page.id}"]`);
|
||||
if (pageElement) {
|
||||
const img = pageElement.querySelector('img');
|
||||
if (img) {
|
||||
// Reset rotation to match document state
|
||||
img.style.transform = `rotate(${page.rotation}deg)`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if DOM state differs from document state
|
||||
*/
|
||||
hasUnsavedChanges(pdfDocument: PDFDocument): boolean {
|
||||
return pdfDocument.pages.some(page => {
|
||||
const pageElement = document.querySelector(`[data-page-id="${page.id}"]`);
|
||||
if (pageElement) {
|
||||
const domRotation = this.getRotationFromDOM(pageElement, page);
|
||||
return domRotation !== page.rotation;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const documentManipulationService = new DocumentManipulationService();
|
||||
@@ -4,20 +4,18 @@ import { PDFDocument, PDFPage } from '../types/pageEditor';
|
||||
export interface ExportOptions {
|
||||
selectedOnly?: boolean;
|
||||
filename?: string;
|
||||
splitDocuments?: boolean;
|
||||
appendSuffix?: boolean; // when false, do not append _edited/_selected
|
||||
}
|
||||
|
||||
export class PDFExportService {
|
||||
/**
|
||||
* Export PDF document with applied operations
|
||||
* Export PDF document with applied operations (single file source)
|
||||
*/
|
||||
async exportPDF(
|
||||
pdfDocument: PDFDocument,
|
||||
selectedPageIds: string[] = [],
|
||||
options: ExportOptions = {}
|
||||
): Promise<{ blob: Blob; filename: string } | { blobs: Blob[]; filenames: string[] }> {
|
||||
const { selectedOnly = false, filename, splitDocuments = false, appendSuffix = true } = options;
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const { selectedOnly = false, filename } = options;
|
||||
|
||||
try {
|
||||
// Determine which pages to export
|
||||
@@ -29,17 +27,13 @@ export class PDFExportService {
|
||||
throw new Error('No pages to export');
|
||||
}
|
||||
|
||||
// Load original PDF once
|
||||
// Load original PDF and create new document
|
||||
const originalPDFBytes = await pdfDocument.file.arrayBuffer();
|
||||
const sourceDoc = await PDFLibDocument.load(originalPDFBytes);
|
||||
const blob = await this.createSingleDocument(sourceDoc, pagesToExport);
|
||||
const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly, false);
|
||||
|
||||
if (splitDocuments) {
|
||||
return await this.createSplitDocuments(sourceDoc, pagesToExport, filename || pdfDocument.name);
|
||||
} else {
|
||||
const blob = await this.createSingleDocument(sourceDoc, pagesToExport);
|
||||
const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly, appendSuffix);
|
||||
return { blob, filename: exportFilename };
|
||||
}
|
||||
return { blob, filename: exportFilename };
|
||||
} catch (error) {
|
||||
console.error('PDF export error:', error);
|
||||
throw new Error(`Failed to export PDF: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
@@ -47,28 +41,85 @@ export class PDFExportService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single PDF document with all operations applied
|
||||
* Export PDF document with applied operations (multi-file source)
|
||||
*/
|
||||
private async createSingleDocument(
|
||||
sourceDoc: PDFLibDocument,
|
||||
async exportPDFMultiFile(
|
||||
pdfDocument: PDFDocument,
|
||||
sourceFiles: Map<string, File>,
|
||||
selectedPageIds: string[] = [],
|
||||
options: ExportOptions = {}
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const { selectedOnly = false, filename } = options;
|
||||
|
||||
try {
|
||||
// Determine which pages to export
|
||||
const pagesToExport = selectedOnly && selectedPageIds.length > 0
|
||||
? pdfDocument.pages.filter(page => selectedPageIds.includes(page.id))
|
||||
: pdfDocument.pages;
|
||||
|
||||
if (pagesToExport.length === 0) {
|
||||
throw new Error('No pages to export');
|
||||
}
|
||||
|
||||
const blob = await this.createMultiSourceDocument(sourceFiles, pagesToExport);
|
||||
const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly, false);
|
||||
|
||||
return { blob, filename: exportFilename };
|
||||
} catch (error) {
|
||||
console.error('Multi-file PDF export error:', error);
|
||||
throw new Error(`Failed to export PDF: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PDF document from multiple source files
|
||||
*/
|
||||
private async createMultiSourceDocument(
|
||||
sourceFiles: Map<string, File>,
|
||||
pages: PDFPage[]
|
||||
): Promise<Blob> {
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
// Load all source documents once and cache them
|
||||
const loadedDocs = new Map<string, PDFLibDocument>();
|
||||
|
||||
for (const [fileId, file] of sourceFiles) {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const doc = await PDFLibDocument.load(arrayBuffer);
|
||||
loadedDocs.set(fileId, doc);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load source file ${fileId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
// Get the original page from source document
|
||||
const sourcePageIndex = this.getOriginalSourceIndex(page);
|
||||
if (page.isBlankPage || page.originalPageNumber === -1) {
|
||||
// Create a blank page
|
||||
const blankPage = newDoc.addPage(PageSizes.A4);
|
||||
|
||||
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
|
||||
// Copy the page
|
||||
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
|
||||
|
||||
// Apply rotation
|
||||
// Apply rotation if needed
|
||||
if (page.rotation !== 0) {
|
||||
copiedPage.setRotation(degrees(page.rotation));
|
||||
blankPage.setRotation(degrees(page.rotation));
|
||||
}
|
||||
} else if (page.originalFileId && loadedDocs.has(page.originalFileId)) {
|
||||
// Get the correct source document for this page
|
||||
const sourceDoc = loadedDocs.get(page.originalFileId)!;
|
||||
const sourcePageIndex = page.originalPageNumber - 1;
|
||||
|
||||
newDoc.addPage(copiedPage);
|
||||
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
|
||||
// Copy the page from the correct source document
|
||||
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
|
||||
|
||||
// Apply rotation
|
||||
if (page.rotation !== 0) {
|
||||
copiedPage.setRotation(degrees(page.rotation));
|
||||
}
|
||||
|
||||
newDoc.addPage(copiedPage);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Cannot find source document for page ${page.pageNumber} (fileId: ${page.originalFileId})`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,103 +134,60 @@ export class PDFExportService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple PDF documents based on split markers
|
||||
* Create a single PDF document with all operations applied (single source)
|
||||
*/
|
||||
private async createSplitDocuments(
|
||||
private async createSingleDocument(
|
||||
sourceDoc: PDFLibDocument,
|
||||
pages: PDFPage[],
|
||||
baseFilename: string
|
||||
): Promise<{ blobs: Blob[]; filenames: string[] }> {
|
||||
const splitPoints: number[] = [];
|
||||
const blobs: Blob[] = [];
|
||||
const filenames: string[] = [];
|
||||
pages: PDFPage[]
|
||||
): Promise<Blob> {
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
// Find split points
|
||||
pages.forEach((page, index) => {
|
||||
if (page.splitBefore && index > 0) {
|
||||
splitPoints.push(index);
|
||||
}
|
||||
});
|
||||
for (const page of pages) {
|
||||
if (page.isBlankPage || page.originalPageNumber === -1) {
|
||||
// Create a blank page
|
||||
const blankPage = newDoc.addPage(PageSizes.A4);
|
||||
|
||||
// Add end point
|
||||
splitPoints.push(pages.length);
|
||||
|
||||
let startIndex = 0;
|
||||
let partNumber = 1;
|
||||
|
||||
for (const endIndex of splitPoints) {
|
||||
const segmentPages = pages.slice(startIndex, endIndex);
|
||||
|
||||
if (segmentPages.length > 0) {
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const page of segmentPages) {
|
||||
const sourcePageIndex = this.getOriginalSourceIndex(page);
|
||||
|
||||
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
|
||||
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
|
||||
|
||||
if (page.rotation !== 0) {
|
||||
copiedPage.setRotation(degrees(page.rotation));
|
||||
}
|
||||
|
||||
newDoc.addPage(copiedPage);
|
||||
}
|
||||
// Apply rotation if needed
|
||||
if (page.rotation !== 0) {
|
||||
blankPage.setRotation(degrees(page.rotation));
|
||||
}
|
||||
} else {
|
||||
// Get the original page from source document using originalPageNumber
|
||||
const sourcePageIndex = page.originalPageNumber - 1;
|
||||
|
||||
// Set metadata
|
||||
newDoc.setCreator('Stirling PDF');
|
||||
newDoc.setProducer('Stirling PDF');
|
||||
newDoc.setTitle(`${baseFilename} - Part ${partNumber}`);
|
||||
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
|
||||
// Copy the page
|
||||
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
|
||||
|
||||
const pdfBytes = await newDoc.save();
|
||||
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
const filename = this.generateSplitFilename(baseFilename, partNumber);
|
||||
// Apply rotation
|
||||
if (page.rotation !== 0) {
|
||||
copiedPage.setRotation(degrees(page.rotation));
|
||||
}
|
||||
|
||||
blobs.push(blob);
|
||||
filenames.push(filename);
|
||||
partNumber++;
|
||||
}
|
||||
|
||||
startIndex = endIndex;
|
||||
}
|
||||
|
||||
return { blobs, filenames };
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the original page index from a page's stable id.
|
||||
* Falls back to the current pageNumber if parsing fails.
|
||||
*/
|
||||
private getOriginalSourceIndex(page: PDFPage): number {
|
||||
const match = page.id.match(/-page-(\d+)$/);
|
||||
if (match) {
|
||||
const originalNumber = parseInt(match[1], 10);
|
||||
if (!Number.isNaN(originalNumber)) {
|
||||
return originalNumber - 1; // zero-based index for pdf-lib
|
||||
newDoc.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to the visible page number
|
||||
return Math.max(0, page.pageNumber - 1);
|
||||
|
||||
// Set metadata
|
||||
newDoc.setCreator('Stirling PDF');
|
||||
newDoc.setProducer('Stirling PDF');
|
||||
newDoc.setCreationDate(new Date());
|
||||
newDoc.setModificationDate(new Date());
|
||||
|
||||
const pdfBytes = await newDoc.save();
|
||||
return new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate appropriate filename for export
|
||||
*/
|
||||
private generateFilename(originalName: string, selectedOnly: boolean, appendSuffix: boolean): string {
|
||||
const baseName = originalName.replace(/\.pdf$/i, '');
|
||||
if (!appendSuffix) return `${baseName}.pdf`;
|
||||
const suffix = selectedOnly ? '_selected' : '_edited';
|
||||
return `${baseName}${suffix}.pdf`;
|
||||
return `${baseName}.pdf`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate filename for split documents
|
||||
*/
|
||||
private generateSplitFilename(baseName: string, partNumber: number): string {
|
||||
const cleanBaseName = baseName.replace(/\.pdf$/i, '');
|
||||
return `${cleanBaseName}_part_${partNumber}.pdf`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a single file
|
||||
@@ -203,7 +211,6 @@ export class PDFExportService {
|
||||
* Download multiple files as a ZIP
|
||||
*/
|
||||
async downloadAsZip(blobs: Blob[], filenames: string[], zipFilename: string): Promise<void> {
|
||||
// For now, download files wherindividually
|
||||
blobs.forEach((blob, index) => {
|
||||
setTimeout(() => {
|
||||
this.downloadFile(blob, filenames[index]);
|
||||
@@ -248,8 +255,8 @@ export class PDFExportService {
|
||||
? pdfDocument.pages.filter(page => selectedPageIds.includes(page.id))
|
||||
: pdfDocument.pages;
|
||||
|
||||
const splitCount = pagesToExport.reduce((count, page, index) => {
|
||||
return count + (page.splitBefore && index > 0 ? 1 : 0);
|
||||
const splitCount = pagesToExport.reduce((count, page) => {
|
||||
return count + (page.splitAfter ? 1 : 0);
|
||||
}, 1); // At least 1 document
|
||||
|
||||
// Rough size estimation (very approximate)
|
||||
|
||||
@@ -12,7 +12,7 @@ class PDFWorkerManager {
|
||||
private static instance: PDFWorkerManager;
|
||||
private activeDocuments = new Set<any>();
|
||||
private workerCount = 0;
|
||||
private maxWorkers = 3; // Limit concurrent workers
|
||||
private maxWorkers = 10; // Limit concurrent workers
|
||||
private isInitialized = false;
|
||||
|
||||
private constructor() {
|
||||
@@ -33,7 +33,6 @@ class PDFWorkerManager {
|
||||
if (!this.isInitialized) {
|
||||
GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||
this.isInitialized = true;
|
||||
console.log('🏭 PDF.js worker initialized');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +51,6 @@ class PDFWorkerManager {
|
||||
): Promise<any> {
|
||||
// Wait if we've hit the worker limit
|
||||
if (this.activeDocuments.size >= this.maxWorkers) {
|
||||
console.warn(`🏭 PDF Worker limit reached (${this.maxWorkers}), waiting for available worker...`);
|
||||
await this.waitForAvailableWorker();
|
||||
}
|
||||
|
||||
@@ -89,8 +87,6 @@ class PDFWorkerManager {
|
||||
this.activeDocuments.add(pdf);
|
||||
this.workerCount++;
|
||||
|
||||
console.log(`🏭 PDF document created (active: ${this.activeDocuments.size}/${this.maxWorkers})`);
|
||||
|
||||
return pdf;
|
||||
} catch (error) {
|
||||
// If document creation fails, make sure to clean up the loading task
|
||||
@@ -98,7 +94,6 @@ class PDFWorkerManager {
|
||||
try {
|
||||
loadingTask.destroy();
|
||||
} catch (destroyError) {
|
||||
console.warn('🏭 Error destroying failed loading task:', destroyError);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
@@ -114,10 +109,7 @@ class PDFWorkerManager {
|
||||
pdf.destroy();
|
||||
this.activeDocuments.delete(pdf);
|
||||
this.workerCount = Math.max(0, this.workerCount - 1);
|
||||
|
||||
console.log(`🏭 PDF document destroyed (active: ${this.activeDocuments.size}/${this.maxWorkers})`);
|
||||
} catch (error) {
|
||||
console.warn('🏭 Error destroying PDF document:', error);
|
||||
// Still remove from tracking even if destroy failed
|
||||
this.activeDocuments.delete(pdf);
|
||||
this.workerCount = Math.max(0, this.workerCount - 1);
|
||||
@@ -129,8 +121,6 @@ class PDFWorkerManager {
|
||||
* Destroy all active PDF documents
|
||||
*/
|
||||
destroyAllDocuments(): void {
|
||||
console.log(`🏭 Destroying all PDF documents (${this.activeDocuments.size} active)`);
|
||||
|
||||
const documentsToDestroy = Array.from(this.activeDocuments);
|
||||
documentsToDestroy.forEach(pdf => {
|
||||
this.destroyDocument(pdf);
|
||||
@@ -138,8 +128,6 @@ class PDFWorkerManager {
|
||||
|
||||
this.activeDocuments.clear();
|
||||
this.workerCount = 0;
|
||||
|
||||
console.log('🏭 All PDF documents destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,29 +161,23 @@ class PDFWorkerManager {
|
||||
* Force cleanup of all workers (emergency cleanup)
|
||||
*/
|
||||
emergencyCleanup(): void {
|
||||
console.warn('🏭 Emergency PDF worker cleanup initiated');
|
||||
|
||||
// Force destroy all documents
|
||||
this.activeDocuments.forEach(pdf => {
|
||||
try {
|
||||
pdf.destroy();
|
||||
} catch (error) {
|
||||
console.warn('🏭 Emergency cleanup - error destroying document:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.activeDocuments.clear();
|
||||
this.workerCount = 0;
|
||||
|
||||
console.warn('🏭 Emergency cleanup completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum concurrent workers
|
||||
*/
|
||||
setMaxWorkers(max: number): void {
|
||||
this.maxWorkers = Math.max(1, Math.min(max, 10)); // Between 1-10 workers
|
||||
console.log(`🏭 Max workers set to ${this.maxWorkers}`);
|
||||
this.maxWorkers = Math.max(1, Math.min(max, 15)); // Between 1-15 workers for multi-file support
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export class ThumbnailGenerationService {
|
||||
private pdfDocumentCache = new Map<string, CachedPDFDocument>();
|
||||
private maxPdfCacheSize = 10; // Keep up to 10 PDF documents cached
|
||||
|
||||
constructor(private maxWorkers: number = 3) {
|
||||
constructor(private maxWorkers: number = 10) {
|
||||
// PDF rendering requires DOM access, so we use optimized main thread processing
|
||||
}
|
||||
|
||||
@@ -207,6 +207,9 @@ export class ThumbnailGenerationService {
|
||||
|
||||
// Release reference to PDF document (don't destroy - keep in cache)
|
||||
this.releasePDFDocument(fileId);
|
||||
|
||||
this.cleanupCompletedDocument(fileId);
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
@@ -289,6 +292,18 @@ export class ThumbnailGenerationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up a PDF document from cache when thumbnail generation is complete
|
||||
* This frees up workers faster for better performance
|
||||
*/
|
||||
cleanupCompletedDocument(fileId: string): void {
|
||||
const cached = this.pdfDocumentCache.get(fileId);
|
||||
if (cached && cached.refCount <= 0) {
|
||||
pdfWorkerManager.destroyDocument(cached.pdf);
|
||||
this.pdfDocumentCache.delete(fileId);
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.clearCache();
|
||||
this.clearPDFCache();
|
||||
|
||||
Reference in New Issue
Block a user