feat: Add support for generating thumbnails with metadata and enhance file processing capabilities

- Introduced `generateThumbnailWithMetadata` function to generate thumbnails and extract page counts for PDF files.
- Updated `FileContextProvider` to handle processed files with pre-existing thumbnails and metadata.
- Enhanced `useToolOperation` to utilize new thumbnail generation method and manage processed files efficiently.
- Refactored `useToolResources` to include the new thumbnail generation method.
- Updated relevant components to improve logging and error handling during thumbnail generation.
This commit is contained in:
Reece Browne 2025-08-14 22:54:43 +01:00
parent f691e690e4
commit c8eb7ac647
10 changed files with 447 additions and 59 deletions

View File

@ -46,6 +46,7 @@ import { EnhancedPDFProcessingService } from '../services/enhancedPDFProcessingS
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
import { fileStorage } from '../services/fileStorage';
import { fileProcessingService } from '../services/fileProcessingService';
import { generateThumbnailWithMetadata } from '../utils/thumbnailUtils';
// Get service instances
const enhancedPDFProcessingService = EnhancedPDFProcessingService.getInstance();
@ -419,7 +420,7 @@ export function FileContextProvider({
// Action implementations
const addFiles = useCallback(async (files: File[]): Promise<File[]> => {
// Three-tier deduplication: UUID (primary key) + quickKey (soft dedupe) + contentHash (hard dedupe)
console.log(`📄 addFiles: Adding ${files.length} files with immediate thumbnail generation`);
const fileRecords: FileRecord[] = [];
const addedFiles: File[] = [];
@ -443,8 +444,41 @@ export function FileContextProvider({
// Store File in ref map
filesRef.current.set(fileId, file);
// Create record
// Generate thumbnail and page count immediately
let thumbnail: string | undefined;
let pageCount: number = 1;
try {
console.log(`📄 Generating immediate thumbnail and metadata for ${file.name}`);
const result = await generateThumbnailWithMetadata(file);
thumbnail = result.thumbnail;
pageCount = result.pageCount;
console.log(`📄 Generated immediate metadata for ${file.name}: ${pageCount} pages, thumbnail: ${!!thumbnail}`);
} catch (error) {
console.warn(`📄 Failed to generate immediate metadata for ${file.name}:`, error);
// Continue with defaults
}
// Create record with immediate thumbnail and page metadata
const record = toFileRecord(file, fileId);
if (thumbnail) {
record.thumbnailUrl = thumbnail;
}
// Create initial processedFile metadata with page count
if (pageCount > 0) {
record.processedFile = {
totalPages: pageCount,
pages: Array.from({ length: pageCount }, (_, index) => ({
pageNumber: index + 1,
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
rotation: 0,
splitBefore: false
})),
thumbnailUrl: thumbnail,
lastProcessed: Date.now()
};
console.log(`📄 addFiles: Created initial processedFile metadata for ${file.name} with ${pageCount} pages`);
}
// Add to deduplication tracking
existingQuickKeys.add(quickKey);
@ -452,29 +486,40 @@ export function FileContextProvider({
fileRecords.push(record);
addedFiles.push(file);
// Start centralized file processing (async, non-blocking) - SINGLE CALL
// Start background processing for validation only (we already have thumbnail and page count)
fileProcessingService.processFile(file, fileId).then(result => {
// Only update if file still exists in context
if (filesRef.current.has(fileId)) {
if (result.success && result.metadata) {
// Update with processed metadata using dispatch directly
dispatch({
type: 'UPDATE_FILE_RECORD',
payload: {
id: fileId,
updates: {
processedFile: result.metadata,
thumbnailUrl: result.metadata.thumbnailUrl
// Only log if page count differs from our immediate calculation
const initialPageCount = pageCount;
if (result.metadata.totalPages !== initialPageCount) {
console.log(`📄 Page count validation: ${file.name} initial=${initialPageCount} → final=${result.metadata.totalPages} pages`);
// Update with the validated page count, but preserve existing thumbnail
dispatch({
type: 'UPDATE_FILE_RECORD',
payload: {
id: fileId,
updates: {
processedFile: {
...result.metadata,
// Preserve our immediate thumbnail if we have one
thumbnailUrl: thumbnail || result.metadata.thumbnailUrl
},
// Keep existing thumbnailUrl if we have one
thumbnailUrl: thumbnail || result.metadata.thumbnailUrl
}
}
}
});
console.log(`✅ File processing complete for ${file.name}: ${result.metadata.totalPages} pages`);
});
} else {
console.log(`✅ Page count validation passed for ${file.name}: ${result.metadata.totalPages} pages (immediate generation was correct)`);
}
// Optional: Persist to IndexedDB if enabled (reuse the same result)
// Optional: Persist to IndexedDB if enabled
if (enablePersistence) {
try {
const thumbnail = result.metadata.thumbnailUrl;
fileStorage.storeFile(file, fileId, thumbnail).then(() => {
const finalThumbnail = thumbnail || result.metadata.thumbnailUrl;
fileStorage.storeFile(file, fileId, finalThumbnail).then(() => {
console.log('File persisted to IndexedDB:', fileId);
}).catch(error => {
console.warn('Failed to persist file to IndexedDB:', error);
@ -484,11 +529,11 @@ export function FileContextProvider({
}
}
} else {
console.warn(`File processing failed for ${file.name}:`, result.error);
console.warn(`Background file processing failed for ${file.name}:`, result.error);
}
}
}).catch(error => {
console.error(`File processing error for ${file.name}:`, error);
console.error(`Background file processing error for ${file.name}:`, error);
});
}
@ -502,7 +547,118 @@ export function FileContextProvider({
return addedFiles;
}, [enablePersistence]); // Remove updateFileRecord dependency
// NEW: Add processed files with pre-existing thumbnails and metadata (for tool outputs)
const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise<File[]> => {
console.log(`📄 addProcessedFiles: Adding ${filesWithThumbnails.length} processed files with pre-existing thumbnails`);
const fileRecords: FileRecord[] = [];
const addedFiles: File[] = [];
// Build quickKey lookup from existing files for deduplication
const existingQuickKeys = new Set<string>();
Object.values(stateRef.current.files.byId).forEach(record => {
existingQuickKeys.add(record.quickKey);
});
for (const { file, thumbnail, pageCount } of filesWithThumbnails) {
const quickKey = createQuickKey(file);
// Soft deduplication: Check if file already exists by metadata
if (existingQuickKeys.has(quickKey)) {
console.log(`📄 Skipping duplicate processed file: ${file.name} (already exists)`);
continue; // Skip duplicate file
}
const fileId = createFileId(); // UUID-based, zero collisions
// Store File in ref map
filesRef.current.set(fileId, file);
// Create record with pre-existing thumbnail and page metadata
const record = toFileRecord(file, fileId);
if (thumbnail) {
record.thumbnailUrl = thumbnail;
}
// If we have page count, create initial processedFile metadata
if (pageCount && pageCount > 0) {
record.processedFile = {
totalPages: pageCount,
pages: Array.from({ length: pageCount }, (_, index) => ({
pageNumber: index + 1,
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
rotation: 0,
splitBefore: false
})),
thumbnailUrl: thumbnail,
lastProcessed: Date.now()
};
console.log(`📄 addProcessedFiles: Created initial processedFile metadata for ${file.name} with ${pageCount} pages`);
}
// Add to deduplication tracking
existingQuickKeys.add(quickKey);
fileRecords.push(record);
addedFiles.push(file);
// Start background processing for page metadata only (thumbnail already provided)
fileProcessingService.processFile(file, fileId).then(result => {
// Only update if file still exists in context
if (filesRef.current.has(fileId)) {
if (result.success && result.metadata) {
// Update with processed metadata but preserve existing thumbnail
dispatch({
type: 'UPDATE_FILE_RECORD',
payload: {
id: fileId,
updates: {
processedFile: result.metadata,
// Keep existing thumbnail if we already have one, otherwise use processed one
thumbnailUrl: thumbnail || result.metadata.thumbnailUrl
}
}
});
// Only log if page count changed (meaning our initial guess was wrong)
const initialPageCount = pageCount || 1;
if (result.metadata.totalPages !== initialPageCount) {
console.log(`📄 Page count updated for ${file.name}: ${initialPageCount}${result.metadata.totalPages} pages`);
} else {
console.log(`✅ Processed file metadata complete for ${file.name}: ${result.metadata.totalPages} pages (thumbnail: ${thumbnail ? 'PRE-EXISTING' : 'GENERATED'})`);
}
// Optional: Persist to IndexedDB if enabled
if (enablePersistence) {
try {
const finalThumbnail = thumbnail || result.metadata.thumbnailUrl;
fileStorage.storeFile(file, fileId, finalThumbnail).then(() => {
console.log('Processed file persisted to IndexedDB:', fileId);
}).catch(error => {
console.warn('Failed to persist processed file to IndexedDB:', error);
});
} catch (error) {
console.warn('Failed to initiate processed file persistence:', error);
}
}
} else {
console.warn(`❌ Processed file background processing failed for ${file.name}:`, result.error);
}
}
}).catch(error => {
console.error(`❌ Processed file background processing error for ${file.name}:`, error);
});
}
// Only dispatch if we have new files
if (fileRecords.length > 0) {
dispatch({ type: 'ADD_FILES', payload: { fileRecords } });
}
console.log(`📄 Added ${fileRecords.length} processed files with pre-existing thumbnails`);
return addedFiles;
}, [enablePersistence]);
// NEW: Add stored files with preserved IDs to prevent duplicates across sessions
// This is the CORRECT way to handle files from IndexedDB storage - no File object mutation
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>): Promise<File[]> => {
const fileRecords: FileRecord[] = [];
const addedFiles: File[] = [];
@ -635,6 +791,7 @@ export function FileContextProvider({
// Memoized actions to prevent re-renders
const actions = useMemo<FileContextActions>(() => ({
addFiles,
addProcessedFiles,
addStoredFiles,
removeFiles,
updateFileRecord,
@ -658,7 +815,7 @@ export function FileContextProvider({
setMode: (mode: ModeType) => dispatch({ type: 'SET_CURRENT_MODE', payload: mode }),
confirmNavigation,
cancelNavigation
}), [addFiles, addStoredFiles, removeFiles, cleanupAllFiles, setHasUnsavedChanges, confirmNavigation, cancelNavigation]);
}), [addFiles, addProcessedFiles, addStoredFiles, removeFiles, cleanupAllFiles, setHasUnsavedChanges, confirmNavigation, cancelNavigation]);
// Split context values to minimize re-renders
const stateValue = useMemo<FileContextStateValue>(() => ({
@ -677,6 +834,7 @@ export function FileContextProvider({
...state.ui,
// Action compatibility layer
addFiles,
addProcessedFiles,
addStoredFiles,
removeFiles,
updateFileRecord,
@ -701,7 +859,7 @@ export function FileContextProvider({
get activeFiles() { return selectors.getFiles(); }, // Getter to avoid creating new arrays on every render
// Selectors
...selectors
}), [state, actions, addFiles, addStoredFiles, removeFiles, updateFileRecord, setHasUnsavedChanges, requestNavigation, confirmNavigation, cancelNavigation, trackBlobUrl, trackPdfDocument, cleanupFile, scheduleCleanup]); // Removed selectors dependency
}), [state, actions, addFiles, addProcessedFiles, addStoredFiles, removeFiles, updateFileRecord, setHasUnsavedChanges, requestNavigation, confirmNavigation, cancelNavigation, trackBlobUrl, trackPdfDocument, cleanupFile, scheduleCleanup]); // Removed selectors dependency
// Cleanup on unmount
useEffect(() => {

View File

@ -121,7 +121,7 @@ export const useToolOperation = <TParams = void>(
// Composed hooks
const { state, actions } = useToolState();
const { processFiles, cancelOperation: cancelApiCalls } = useToolApiCalls<TParams>();
const { generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles } = useToolResources();
const { generateThumbnails, generateThumbnailsWithMetadata, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles } = useToolResources();
const executeOperation = useCallback(async (
params: TParams,
@ -167,23 +167,31 @@ export const useToolOperation = <TParams = void>(
// Use explicit multiFileEndpoint flag to determine processing approach
if (config.multiFileEndpoint) {
// Multi-file processing - single API call with all files
console.log(`🚀 useToolOperation: Multi-file processing for ${config.operationType} with ${validFiles.length} files`);
actions.setStatus('Processing files...');
const formData = (config.buildFormData as (params: TParams, files: File[]) => FormData)(params, validFiles);
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
console.log(`🚀 Calling endpoint: ${endpoint}`);
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
console.log(`🚀 Received response: ${response.data.size} bytes, type: ${response.data.type}`);
// Multi-file responses are typically ZIP files that need extraction
if (config.responseHandler) {
console.log(`🚀 Using custom responseHandler for ${config.operationType}`);
// Use custom responseHandler for multi-file (handles ZIP extraction)
processedFiles = await config.responseHandler(response.data, validFiles);
} else {
console.log(`🚀 Using default ZIP extraction for ${config.operationType}`);
// Default: assume ZIP response for multi-file endpoints
processedFiles = await extractZipFiles(response.data);
console.log(`🚀 Extracted ${processedFiles.length} files from ZIP`);
if (processedFiles.length === 0) {
console.log(`🚀 ZIP extraction failed, trying generic fallback`);
// Try the generic extraction as fallback
processedFiles = await extractAllZipFiles(response.data);
console.log(`🚀 Generic fallback extracted ${processedFiles.length} files`);
}
}
} else {
@ -205,21 +213,35 @@ export const useToolOperation = <TParams = void>(
}
if (processedFiles.length > 0) {
console.log(`🚀 useToolOperation: Processing complete. ${processedFiles.length} files ready for thumbnails:`,
processedFiles.map((f, i) => `[${i}]: ${f.name} (${f.type}, ${f.size} bytes)`));
actions.setFiles(processedFiles);
// Generate thumbnails and download URL concurrently
// Generate thumbnails with metadata and download URL concurrently
actions.setGeneratingThumbnails(true);
const [thumbnails, downloadInfo] = await Promise.all([
generateThumbnails(processedFiles),
const [thumbnailResults, downloadInfo] = await Promise.all([
generateThumbnailsWithMetadata(processedFiles),
createDownloadInfo(processedFiles, config.operationType)
]);
actions.setGeneratingThumbnails(false);
// Extract thumbnails for tool state and page counts for context
const thumbnails = thumbnailResults.map(r => r.thumbnail || '');
const pageCounts = thumbnailResults.map(r => r.pageCount);
console.log(`⚡ useToolOperation: Generated ${thumbnails.length} thumbnails with page counts for ${config.operationType}:`,
thumbnailResults.map((r, i) => `[${i}]: ${r.thumbnail ? 'PRESENT' : 'MISSING'} (${r.pageCount} pages)`));
actions.setThumbnails(thumbnails);
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
// Add to file context
await fileActions.addFiles(processedFiles);
// Add to file context WITH pre-existing thumbnails AND page counts to avoid duplicate processing
const filesWithMetadata = processedFiles.map((file, index) => ({
file,
thumbnail: thumbnails[index] || undefined,
pageCount: pageCounts[index] || undefined
}));
console.log(`📄 useToolOperation: Adding ${filesWithMetadata.length} processed files with pre-existing thumbnails and page counts to context`);
await fileActions.addProcessedFiles(filesWithMetadata);
markOperationApplied(fileId, operationId);
}
@ -233,7 +255,7 @@ export const useToolOperation = <TParams = void>(
actions.setLoading(false);
actions.setProgress(null);
}
}, [t, config, actions, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles, fileActions.addFiles]);
}, [t, config, actions, processFiles, generateThumbnailsWithMetadata, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles, fileActions.addProcessedFiles]);
const cancelOperation = useCallback(() => {
cancelApiCalls();

View File

@ -1,5 +1,5 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
import { generateThumbnailForFile, generateThumbnailWithMetadata, ThumbnailWithMetadata } from '../../../utils/thumbnailUtils';
import { zipFileService } from '../../../services/zipFileService';
@ -43,23 +43,52 @@ export const useToolResources = () => {
}, []); // No dependencies - use ref to access current URLs
const generateThumbnails = useCallback(async (files: File[]): Promise<string[]> => {
console.log(`🖼️ useToolResources.generateThumbnails: Starting for ${files.length} files`);
const thumbnails: string[] = [];
for (const file of files) {
try {
console.log(`🖼️ Generating thumbnail for: ${file.name} (${file.type}, ${file.size} bytes)`);
const thumbnail = await generateThumbnailForFile(file);
console.log(`🖼️ Generated thumbnail for ${file.name}:`, thumbnail ? 'SUCCESS' : 'FAILED (no thumbnail returned)');
if (thumbnail) {
thumbnails.push(thumbnail);
} else {
console.warn(`🖼️ No thumbnail returned for ${file.name}`);
thumbnails.push('');
}
} catch (error) {
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
console.warn(`🖼️ Failed to generate thumbnail for ${file.name}:`, error);
thumbnails.push('');
}
}
console.log(`🖼️ useToolResources.generateThumbnails: Complete. Generated ${thumbnails.filter(t => t).length}/${files.length} thumbnails`);
return thumbnails;
}, []);
const generateThumbnailsWithMetadata = useCallback(async (files: File[]): Promise<ThumbnailWithMetadata[]> => {
console.log(`🖼️ useToolResources.generateThumbnailsWithMetadata: Starting for ${files.length} files`);
const results: ThumbnailWithMetadata[] = [];
for (const file of files) {
try {
console.log(`🖼️ Generating thumbnail with metadata for: ${file.name} (${file.type}, ${file.size} bytes)`);
const result = await generateThumbnailWithMetadata(file);
console.log(`🖼️ Generated thumbnail with metadata for ${file.name}:`,
result.thumbnail ? 'SUCCESS' : 'FAILED (no thumbnail)',
`${result.pageCount} pages`);
results.push(result);
} catch (error) {
console.warn(`🖼️ Failed to generate thumbnail with metadata for ${file.name}:`, error);
results.push({ thumbnail: '', pageCount: 1 });
}
}
console.log(`🖼️ useToolResources.generateThumbnailsWithMetadata: Complete. Generated ${results.filter(r => r.thumbnail).length}/${files.length} thumbnails with metadata`);
return results;
}, []);
const extractZipFiles = useCallback(async (zipBlob: Blob): Promise<File[]> => {
try {
const zipFile = new File([zipBlob], 'temp.zip', { type: 'application/zip' });
@ -116,6 +145,7 @@ export const useToolResources = () => {
return {
generateThumbnails,
generateThumbnailsWithMetadata,
createDownloadInfo,
extractZipFiles,
extractAllZipFiles,

View File

@ -88,6 +88,8 @@ export const useToolState = () => {
}, []);
const setThumbnails = useCallback((thumbnails: string[]) => {
console.log(`🔧 useToolState.setThumbnails: Setting ${thumbnails.length} thumbnails:`,
thumbnails.map((t, i) => `[${i}]: ${t ? 'PRESENT' : 'MISSING'}`));
dispatch({ type: 'SET_THUMBNAILS', payload: thumbnails });
}, []);

View File

@ -47,7 +47,7 @@ export const fileOperationsService = {
}
try {
await fileStorage.init();
// fileStorage.init() no longer needed - using centralized IndexedDB manager
const storedFiles = await fileStorage.getAllFileMetadata();
// Detect if IndexedDB was purged by comparing with current UI state
@ -189,7 +189,7 @@ export const fileOperationsService = {
if (currentFiles.length === 0) return false;
try {
await fileStorage.init();
// fileStorage.init() no longer needed - using centralized IndexedDB manager
const storedFiles = await fileStorage.getAllFileMetadata();
return storedFiles.length === 0; // Purge detected if no files in storage but UI shows files
} catch (error) {

View File

@ -412,7 +412,8 @@ class FileStorageService {
}
/**
* Convert StoredFile back to File object for compatibility
* Convert StoredFile back to pure File object without mutations
* Returns a clean File object - use FileContext.addStoredFiles() for proper metadata handling
*/
createFileFromStored(storedFile: StoredFile): File {
if (!storedFile || !storedFile.data) {
@ -429,13 +430,27 @@ class FileStorageService {
lastModified: storedFile.lastModified
});
// Add custom properties for compatibility
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
Object.defineProperty(file, 'thumbnail', { value: storedFile.thumbnail, writable: false });
// Returns pure File object - no mutations
// Use FileContext.addStoredFiles() to properly associate with metadata
return file;
}
/**
* Convert StoredFile to the format expected by FileContext.addStoredFiles()
* This is the recommended way to load stored files into FileContext
*/
createFileWithMetadata(storedFile: StoredFile): { file: File; originalId: string; metadata: { thumbnail?: string } } {
const file = this.createFileFromStored(storedFile);
return {
file,
originalId: storedFile.id,
metadata: {
thumbnail: storedFile.thumbnail
}
};
}
/**
* Create blob URL for stored file
*/

View File

@ -69,13 +69,17 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const filesCollapsed = hasFiles;
const settingsCollapsed = hasResults;
const previewResults = useMemo(() =>
splitOperation.files?.map((file, index) => ({
const previewResults = useMemo(() => {
const results = splitOperation.files?.map((file, index) => ({
file,
thumbnail: splitOperation.thumbnails[index]
})) || [],
[splitOperation.files, splitOperation.thumbnails]
);
})) || [];
console.log(`🔧 Split tool preview: ${results.length} files, thumbnails:`,
splitOperation.thumbnails.map((t, i) => `[${i}]: ${t ? 'PRESENT' : 'MISSING'}`));
return results;
}, [splitOperation.files, splitOperation.thumbnails]);
return (
<ToolStepContainer>

View File

@ -191,6 +191,7 @@ export type FileContextAction =
export interface FileContextActions {
// File management - lightweight actions only
addFiles: (files: File[]) => Promise<File[]>;
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<File[]>;
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => Promise<File[]>;
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => void;
updateFileRecord: (id: FileId, updates: Partial<FileRecord>) => void;

View File

@ -1,8 +1,34 @@
import { FileWithUrl } from "../types/file";
import { StoredFile, fileStorage } from "../services/fileStorage";
/**
* @deprecated File objects no longer have mutated ID properties.
* The new system maintains pure File objects and tracks IDs separately in FileContext.
* Use FileContext selectors to get file IDs instead.
*/
export function getFileId(file: File): string | null {
return (file as File & { id?: string }).id || null;
const legacyId = (file as File & { id?: string }).id;
if (legacyId) {
console.warn('DEPRECATED: getFileId() found legacy mutated File object. Use FileContext selectors instead.');
}
return legacyId || null;
}
/**
* Get file ID for a File object using FileContext state (new system)
* @param file File object to find ID for
* @param fileState Current FileContext state
* @returns File ID or null if not found
*/
export function getFileIdFromContext(file: File, fileIds: string[], getFile: (id: string) => File | undefined): string | null {
// Find the file ID by comparing File objects
for (const id of fileIds) {
const contextFile = getFile(id);
if (contextFile === file) {
return id;
}
}
return null;
}
/**
@ -81,7 +107,7 @@ export function createEnhancedFileFromStored(storedFile: StoredFile, thumbnail?:
*/
export async function loadFilesFromIndexedDB(): Promise<FileWithUrl[]> {
try {
await fileStorage.init();
// fileStorage.init() no longer needed - using centralized IndexedDB manager
const storedFiles = await fileStorage.getAllFileMetadata();
if (storedFiles.length === 0) {

View File

@ -1,5 +1,10 @@
import { getDocument } from "pdfjs-dist";
export interface ThumbnailWithMetadata {
thumbnail: string | undefined;
pageCount: number;
}
/**
* Calculate thumbnail scale based on file size
* Smaller files get higher quality, larger files get lower quality
@ -166,27 +171,36 @@ function formatFileSize(bytes: number): string {
* Returns base64 data URL or undefined if generation fails
*/
export async function generateThumbnailForFile(file: File): Promise<string | undefined> {
console.log(`🎯 generateThumbnailForFile: Starting for ${file.name} (${file.type}, ${file.size} bytes)`);
// Skip thumbnail generation for very large files to avoid memory issues
if (file.size >= 100 * 1024 * 1024) { // 100MB limit
console.log('Skipping thumbnail generation for large file:', file.name);
return generatePlaceholderThumbnail(file);
console.log('🎯 Skipping thumbnail generation for large file:', file.name);
const placeholder = generatePlaceholderThumbnail(file);
console.log('🎯 Generated placeholder thumbnail for large file:', file.name);
return placeholder;
}
// Handle image files - use original file directly
if (file.type.startsWith('image/')) {
return URL.createObjectURL(file);
console.log('🎯 Creating blob URL for image file:', file.name);
const url = URL.createObjectURL(file);
console.log('🎯 Created image blob URL:', url);
return url;
}
// Handle PDF files
if (!file.type.startsWith('application/pdf')) {
console.log('File is not a PDF or image, generating placeholder:', file.name);
return generatePlaceholderThumbnail(file);
console.log('🎯 File is not a PDF or image, generating placeholder:', file.name);
const placeholder = generatePlaceholderThumbnail(file);
console.log('🎯 Generated placeholder thumbnail for non-PDF file:', file.name);
return placeholder;
}
// Calculate quality scale based on file size
console.log('Generating thumbnail for', file.name);
console.log('🎯 Generating PDF thumbnail for', file.name);
const scale = calculateScaleFromFileSize(file.size);
console.log(`Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
console.log(`🎯 Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
try {
// Only read first 2MB for thumbnail generation to save memory
const chunkSize = 2 * 1024 * 1024; // 2MB
@ -215,13 +229,14 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
// Immediately clean up memory after thumbnail generation
pdf.destroy();
console.log('Thumbnail generated and PDF destroyed for', file.name);
console.log('🎯 PDF thumbnail successfully generated for', file.name, 'size:', thumbnail.length);
return thumbnail;
} catch (error) {
console.warn('🎯 Error generating PDF thumbnail for', file.name, ':', error);
if (error instanceof Error) {
if (error.name === 'InvalidPDFException') {
console.warn(`PDF structure issue for ${file.name} - using fallback thumbnail`);
console.warn(`🎯 PDF structure issue for ${file.name} - trying fallback with full file`);
// Return a placeholder or try with full file instead of chunk
try {
const fullArrayBuffer = await file.arrayBuffer();
@ -247,17 +262,132 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
const thumbnail = canvas.toDataURL();
pdf.destroy();
console.log('🎯 Fallback PDF thumbnail generation succeeded for', file.name);
return thumbnail;
} catch (fallbackError) {
console.warn('Fallback thumbnail generation also failed for', file.name, fallbackError);
return undefined;
console.warn('🎯 Fallback thumbnail generation also failed for', file.name, fallbackError);
console.log('🎯 Using placeholder thumbnail for', file.name);
return generatePlaceholderThumbnail(file);
}
} else {
console.warn('Failed to generate thumbnail for', file.name, error);
return undefined;
console.warn('🎯 Non-PDF error generating thumbnail for', file.name, error);
console.log('🎯 Using placeholder thumbnail for', file.name);
return generatePlaceholderThumbnail(file);
}
}
console.warn('Unknown error generating thumbnail for', file.name, error);
return undefined;
console.warn('🎯 Unknown error generating thumbnail for', file.name, error);
console.log('🎯 Using placeholder thumbnail for', file.name);
return generatePlaceholderThumbnail(file);
}
}
/**
* Generate thumbnail and extract page count for a PDF file
* Returns both thumbnail and metadata in a single pass
*/
export async function generateThumbnailWithMetadata(file: File): Promise<ThumbnailWithMetadata> {
console.log(`🎯 generateThumbnailWithMetadata: Starting for ${file.name} (${file.type}, ${file.size} bytes)`);
// Non-PDF files default to 1 page
if (!file.type.startsWith('application/pdf')) {
console.log('🎯 File is not a PDF, generating placeholder with pageCount=1:', file.name);
const thumbnail = await generateThumbnailForFile(file);
return { thumbnail, pageCount: 1 };
}
// Skip thumbnail generation for very large files to avoid memory issues
if (file.size >= 100 * 1024 * 1024) { // 100MB limit
console.log('🎯 Skipping processing for large PDF file:', file.name);
const thumbnail = generatePlaceholderThumbnail(file);
return { thumbnail, pageCount: 1 }; // Default to 1 for large files
}
// Calculate quality scale based on file size
const scale = calculateScaleFromFileSize(file.size);
console.log(`🎯 Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
try {
// Read file chunk for processing
const chunkSize = 2 * 1024 * 1024; // 2MB
const chunk = file.slice(0, Math.min(chunkSize, file.size));
const arrayBuffer = await chunk.arrayBuffer();
const pdf = await getDocument({
data: arrayBuffer,
disableAutoFetch: true,
disableStream: true,
verbosity: 0
}).promise;
const pageCount = pdf.numPages;
console.log(`🎯 PDF ${file.name} has ${pageCount} pages`);
// Generate thumbnail for first page
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale });
const canvas = document.createElement("canvas");
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext("2d");
if (!context) {
pdf.destroy();
throw new Error('Could not get canvas context');
}
await page.render({ canvasContext: context, viewport }).promise;
const thumbnail = canvas.toDataURL();
// Clean up
pdf.destroy();
console.log('🎯 Successfully generated thumbnail with metadata for', file.name, `${pageCount} pages, thumbnail size:`, thumbnail.length);
return { thumbnail, pageCount };
} catch (error) {
console.warn('🎯 Error generating PDF thumbnail with metadata for', file.name, ':', error);
// Try fallback with full file if chunk approach failed
if (error instanceof Error && error.name === 'InvalidPDFException') {
try {
console.warn(`🎯 Trying fallback with full file for ${file.name}`);
const fullArrayBuffer = await file.arrayBuffer();
const pdf = await getDocument({
data: fullArrayBuffer,
disableAutoFetch: true,
disableStream: true,
verbosity: 0
}).promise;
const pageCount = pdf.numPages;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale });
const canvas = document.createElement("canvas");
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext("2d");
if (!context) {
pdf.destroy();
throw new Error('Could not get canvas context');
}
await page.render({ canvasContext: context, viewport }).promise;
const thumbnail = canvas.toDataURL();
pdf.destroy();
console.log('🎯 Fallback successful for', file.name, `${pageCount} pages`);
return { thumbnail, pageCount };
} catch (fallbackError) {
console.warn('🎯 Fallback also failed for', file.name, fallbackError);
}
}
// Final fallback: placeholder thumbnail with default page count
console.log('🎯 Using placeholder thumbnail with default pageCount=1 for', file.name);
const thumbnail = generatePlaceholderThumbnail(file);
return { thumbnail, pageCount: 1 };
}
}