Fully working with performant thumbnail generation

This commit is contained in:
Reece Browne 2025-08-24 11:24:51 +01:00
parent e1c30edddb
commit 62f92da0fe
2 changed files with 69 additions and 120 deletions

View File

@ -16,8 +16,7 @@ import { enhancedPDFProcessingService } from "../../services/enhancedPDFProcessi
import { fileProcessingService } from "../../services/fileProcessingService"; import { fileProcessingService } from "../../services/fileProcessingService";
import { pdfProcessingService } from "../../services/pdfProcessingService"; import { pdfProcessingService } from "../../services/pdfProcessingService";
import { pdfWorkerManager } from "../../services/pdfWorkerManager"; import { pdfWorkerManager } from "../../services/pdfWorkerManager";
import { useThumbnailGeneration } from "../../hooks/useThumbnailGeneration"; // Thumbnail generation is now handled by individual PageThumbnail components
import { calculateScaleFromFileSize } from "../../utils/thumbnailUtils";
import { fileStorage } from "../../services/fileStorage"; import { fileStorage } from "../../services/fileStorage";
import { indexedDBManager, DATABASE_CONFIGS } from "../../services/indexedDBManager"; import { indexedDBManager, DATABASE_CONFIGS } from "../../services/indexedDBManager";
import './PageEditor.module.css'; import './PageEditor.module.css';
@ -47,9 +46,12 @@ class RotatePageCommand extends DOMCommand {
if (pageElement) { if (pageElement) {
const img = pageElement.querySelector('img'); const img = pageElement.querySelector('img');
if (img) { if (img) {
const currentRotation = parseInt(img.style.rotate?.replace(/[^\d-]/g, '') || '0'); // Extract current rotation from transform property to match the animated CSS
const currentTransform = img.style.transform || '';
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
const newRotation = currentRotation + this.degrees; const newRotation = currentRotation + this.degrees;
img.style.rotate = `${newRotation}deg`; img.style.transform = `rotate(${newRotation}deg)`;
} }
} }
} }
@ -60,9 +62,12 @@ class RotatePageCommand extends DOMCommand {
if (pageElement) { if (pageElement) {
const img = pageElement.querySelector('img'); const img = pageElement.querySelector('img');
if (img) { if (img) {
const currentRotation = parseInt(img.style.rotate?.replace(/[^\d-]/g, '') || '0'); // Extract current rotation from transform property
const currentTransform = img.style.transform || '';
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
const previousRotation = currentRotation - this.degrees; const previousRotation = currentRotation - this.degrees;
img.style.rotate = `${previousRotation}deg`; img.style.transform = `rotate(${previousRotation}deg)`;
} }
} }
} }
@ -297,14 +302,18 @@ class BulkRotateCommand extends DOMCommand {
if (img) { if (img) {
// Store original rotation for undo (only on first execution) // Store original rotation for undo (only on first execution)
if (!this.originalRotations.has(pageId)) { if (!this.originalRotations.has(pageId)) {
const currentRotation = parseInt(img.style.rotate?.replace(/[^\d-]/g, '') || '0'); const currentTransform = img.style.transform || '';
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
this.originalRotations.set(pageId, currentRotation); this.originalRotations.set(pageId, currentRotation);
} }
// Apply rotation // Apply rotation using transform to trigger CSS animation
const currentRotation = parseInt(img.style.rotate?.replace(/[^\d-]/g, '') || '0'); const currentTransform = img.style.transform || '';
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
const newRotation = currentRotation + this.degrees; const newRotation = currentRotation + this.degrees;
img.style.rotate = `${newRotation}deg`; img.style.transform = `rotate(${newRotation}deg)`;
} }
} }
}); });
@ -316,7 +325,7 @@ class BulkRotateCommand extends DOMCommand {
if (pageElement) { if (pageElement) {
const img = pageElement.querySelector('img'); const img = pageElement.querySelector('img');
if (img && this.originalRotations.has(pageId)) { if (img && this.originalRotations.has(pageId)) {
img.style.rotate = `${this.originalRotations.get(pageId)}deg`; img.style.transform = `rotate(${this.originalRotations.get(pageId)}deg)`;
} }
} }
}); });
@ -476,50 +485,7 @@ const PageEditor = ({
// DOM-first undo manager (replaces the old React state undo system) // DOM-first undo manager (replaces the old React state undo system)
const undoManagerRef = useRef(new UndoManager()); const undoManagerRef = useRef(new UndoManager());
// Thumbnail generation (opt-in for visual tools) - MUST be before mergedPdfDocument // Thumbnail generation is now handled on-demand by individual PageThumbnail components using modern services
const {
generateThumbnails,
addThumbnailToCache,
getThumbnailFromCache,
stopGeneration,
destroyThumbnails
} = useThumbnailGeneration();
// Helper function to generate thumbnails in batches
const generateThumbnailBatch = useCallback(async (file: File, fileId: string, pageNumbers: number[]) => {
console.log(`📸 PageEditor: Starting thumbnail batch for ${file.name}, pages: [${pageNumbers.join(', ')}]`);
try {
// Load PDF array buffer for Web Workers
const arrayBuffer = await file.arrayBuffer();
// Calculate quality scale based on file size
const scale = calculateScaleFromFileSize(selectors.getFileRecord(fileId)?.size || 0);
// Start parallel thumbnail generation
const results = await generateThumbnails(
fileId,
arrayBuffer,
pageNumbers,
{
scale,
parallelBatches: Math.min(4, pageNumbers.length),
}
);
// Cache all generated thumbnails
results.forEach(({ pageNumber, thumbnail }) => {
if (thumbnail) {
const pageId = `${fileId}-${pageNumber}`;
addThumbnailToCache(pageId, thumbnail);
}
});
console.log(`📸 PageEditor: Thumbnail batch completed for ${file.name}. Generated ${results.length} thumbnails`);
} catch (error) {
console.error(`PageEditor: Thumbnail generation failed for ${file.name}:`, error);
}
}, [generateThumbnails, addThumbnailToCache, selectors]);
// Get primary file record outside useMemo to track processedFile changes // Get primary file record outside useMemo to track processedFile changes
@ -617,52 +583,13 @@ const PageEditor = ({
return mergedDoc; return mergedDoc;
}, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]); }, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
// Generate missing thumbnails for all loaded files // Large document detection for smart loading
const generateMissingThumbnails = useCallback(async () => { const isVeryLargeDocument = useMemo(() => {
if (!mergedPdfDocument || activeFileIds.length === 0) { return mergedPdfDocument ? mergedPdfDocument.totalPages > 2000 : false;
return; }, [mergedPdfDocument?.totalPages]);
}
console.log(`📸 PageEditor: Generating thumbnails for ${activeFileIds.length} files with ${mergedPdfDocument.totalPages} total pages`); // Thumbnails are now generated on-demand by PageThumbnail components
// No bulk generation needed - modern thumbnail service handles this efficiently
// Process files sequentially to avoid PDF document contention
for (const fileId of activeFileIds) {
const file = selectors.getFile(fileId);
const fileRecord = selectors.getFileRecord(fileId);
if (!file || !fileRecord?.processedFile) continue;
const fileTotalPages = fileRecord.processedFile.totalPages;
if (!fileTotalPages) continue;
// Find missing thumbnails for this file
const pageNumbersToGenerate: number[] = [];
for (let pageNum = 1; pageNum <= fileTotalPages; pageNum++) {
const pageId = `${fileId}-${pageNum}`;
if (!getThumbnailFromCache(pageId)) {
pageNumbersToGenerate.push(pageNum);
}
}
if (pageNumbersToGenerate.length > 0) {
console.log(`📸 PageEditor: Generating thumbnails for ${fileRecord.name}: pages [${pageNumbersToGenerate.join(', ')}]`);
await generateThumbnailBatch(file, fileId, pageNumbersToGenerate);
}
// Small delay between files to ensure proper sequential processing
if (activeFileIds.length > 1) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
}, [mergedPdfDocument, activeFileIds, selectors, getThumbnailFromCache, generateThumbnailBatch]);
// Generate missing thumbnails when document is ready
useEffect(() => {
if (mergedPdfDocument && mergedPdfDocument.totalPages > 0) {
console.log(`📸 PageEditor: Document ready with ${mergedPdfDocument.totalPages} pages, checking for missing thumbnails`);
generateMissingThumbnails();
}
}, [mergedPdfDocument, generateMissingThumbnails]);
// Selection and UI state management // Selection and UI state management
const [selectionMode, setSelectionMode] = useState(false); const [selectionMode, setSelectionMode] = useState(false);

View File

@ -72,7 +72,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
const [isMouseDown, setIsMouseDown] = useState(false); const [isMouseDown, setIsMouseDown] = useState(false);
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null); const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
const dragElementRef = useRef<HTMLDivElement>(null); const dragElementRef = useRef<HTMLDivElement>(null);
const { getThumbnailFromCache } = useThumbnailGeneration(); const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
// Update thumbnail URL when page prop changes // Update thumbnail URL when page prop changes
useEffect(() => { useEffect(() => {
@ -81,22 +81,43 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
} }
}, [page.thumbnail, page.id]); }, [page.thumbnail, page.id]);
// Poll for cached thumbnails as they're generated // Request thumbnail on-demand using modern service
useEffect(() => { useEffect(() => {
const checkThumbnail = () => { let isCancelled = false;
const cachedThumbnail = getThumbnailFromCache(page.id);
if (cachedThumbnail && cachedThumbnail !== thumbnailUrl) { // If we already have a thumbnail, use it
setThumbnailUrl(cachedThumbnail); if (page.thumbnail) {
setThumbnailUrl(page.thumbnail);
return;
} }
// Check cache first
const cachedThumbnail = getThumbnailFromCache(page.id);
if (cachedThumbnail) {
setThumbnailUrl(cachedThumbnail);
return;
}
// Request thumbnail generation if we have the original file
if (originalFile) {
// Extract page number from page.id (format: fileId-pageNumber)
const pageNumber = parseInt(page.id.split('-').pop() || '1');
requestThumbnail(page.id, originalFile, pageNumber)
.then(thumbnail => {
if (!isCancelled && thumbnail) {
setThumbnailUrl(thumbnail);
}
})
.catch(error => {
console.warn(`Failed to generate thumbnail for ${page.id}:`, error);
});
}
return () => {
isCancelled = true;
}; };
}, [page.id, page.thumbnail, originalFile, getThumbnailFromCache, requestThumbnail]);
// Check immediately
checkThumbnail();
// Poll every 500ms for new thumbnails
const pollInterval = setInterval(checkThumbnail, 500);
return () => clearInterval(pollInterval);
}, [page.id, getThumbnailFromCache, thumbnailUrl]);
const pageElementRef = useCallback((element: HTMLDivElement | null) => { const pageElementRef = useCallback((element: HTMLDivElement | null) => {
if (element) { if (element) {
@ -270,13 +291,11 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
top: 8, top: 8,
right: 8, right: 8,
zIndex: 10, zIndex: 10,
backgroundColor: 'rgba(255, 255, 255, 0.95)', backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: '4px', borderRadius: '4px',
padding: '4px', padding: '2px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)', boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
pointerEvents: 'auto', pointerEvents: 'auto'
cursor: 'pointer'
}} }}
onMouseDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}
onMouseUp={(e) => e.stopPropagation()} onMouseUp={(e) => e.stopPropagation()}
@ -369,6 +388,9 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
alignItems: 'center', alignItems: 'center',
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
}} }}
onMouseDown={(e) => e.stopPropagation()}
onMouseUp={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
> >
<Tooltip label="Move Left"> <Tooltip label="Move Left">
<ActionIcon <ActionIcon