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 { pdfProcessingService } from "../../services/pdfProcessingService";
import { pdfWorkerManager } from "../../services/pdfWorkerManager";
import { useThumbnailGeneration } from "../../hooks/useThumbnailGeneration";
import { calculateScaleFromFileSize } from "../../utils/thumbnailUtils";
// Thumbnail generation is now handled by individual PageThumbnail components
import { fileStorage } from "../../services/fileStorage";
import { indexedDBManager, DATABASE_CONFIGS } from "../../services/indexedDBManager";
import './PageEditor.module.css';
@ -47,9 +46,12 @@ class RotatePageCommand extends DOMCommand {
if (pageElement) {
const img = pageElement.querySelector('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;
img.style.rotate = `${newRotation}deg`;
img.style.transform = `rotate(${newRotation}deg)`;
}
}
}
@ -60,9 +62,12 @@ class RotatePageCommand extends DOMCommand {
if (pageElement) {
const img = pageElement.querySelector('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;
img.style.rotate = `${previousRotation}deg`;
img.style.transform = `rotate(${previousRotation}deg)`;
}
}
}
@ -297,14 +302,18 @@ class BulkRotateCommand extends DOMCommand {
if (img) {
// Store original rotation for undo (only on first execution)
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);
}
// Apply rotation
const currentRotation = parseInt(img.style.rotate?.replace(/[^\d-]/g, '') || '0');
// Apply rotation using transform to trigger CSS animation
const currentTransform = img.style.transform || '';
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
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) {
const img = pageElement.querySelector('img');
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)
const undoManagerRef = useRef(new UndoManager());
// Thumbnail generation (opt-in for visual tools) - MUST be before mergedPdfDocument
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]);
// Thumbnail generation is now handled on-demand by individual PageThumbnail components using modern services
// Get primary file record outside useMemo to track processedFile changes
@ -617,52 +583,13 @@ const PageEditor = ({
return mergedDoc;
}, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
// Generate missing thumbnails for all loaded files
const generateMissingThumbnails = useCallback(async () => {
if (!mergedPdfDocument || activeFileIds.length === 0) {
return;
}
// Large document detection for smart loading
const isVeryLargeDocument = useMemo(() => {
return mergedPdfDocument ? mergedPdfDocument.totalPages > 2000 : false;
}, [mergedPdfDocument?.totalPages]);
console.log(`📸 PageEditor: Generating thumbnails for ${activeFileIds.length} files with ${mergedPdfDocument.totalPages} total pages`);
// 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]);
// Thumbnails are now generated on-demand by PageThumbnail components
// No bulk generation needed - modern thumbnail service handles this efficiently
// Selection and UI state management
const [selectionMode, setSelectionMode] = useState(false);

View File

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