mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-27 06:39:24 +00:00
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:
parent
f691e690e4
commit
c8eb7ac647
@ -46,6 +46,7 @@ import { EnhancedPDFProcessingService } from '../services/enhancedPDFProcessingS
|
|||||||
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
|
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
|
||||||
import { fileStorage } from '../services/fileStorage';
|
import { fileStorage } from '../services/fileStorage';
|
||||||
import { fileProcessingService } from '../services/fileProcessingService';
|
import { fileProcessingService } from '../services/fileProcessingService';
|
||||||
|
import { generateThumbnailWithMetadata } from '../utils/thumbnailUtils';
|
||||||
|
|
||||||
// Get service instances
|
// Get service instances
|
||||||
const enhancedPDFProcessingService = EnhancedPDFProcessingService.getInstance();
|
const enhancedPDFProcessingService = EnhancedPDFProcessingService.getInstance();
|
||||||
@ -419,7 +420,7 @@ export function FileContextProvider({
|
|||||||
|
|
||||||
// Action implementations
|
// Action implementations
|
||||||
const addFiles = useCallback(async (files: File[]): Promise<File[]> => {
|
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 fileRecords: FileRecord[] = [];
|
||||||
const addedFiles: File[] = [];
|
const addedFiles: File[] = [];
|
||||||
|
|
||||||
@ -443,8 +444,41 @@ export function FileContextProvider({
|
|||||||
// Store File in ref map
|
// Store File in ref map
|
||||||
filesRef.current.set(fileId, file);
|
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);
|
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
|
// Add to deduplication tracking
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
@ -452,29 +486,40 @@ export function FileContextProvider({
|
|||||||
fileRecords.push(record);
|
fileRecords.push(record);
|
||||||
addedFiles.push(file);
|
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 => {
|
fileProcessingService.processFile(file, fileId).then(result => {
|
||||||
// Only update if file still exists in context
|
// Only update if file still exists in context
|
||||||
if (filesRef.current.has(fileId)) {
|
if (filesRef.current.has(fileId)) {
|
||||||
if (result.success && result.metadata) {
|
if (result.success && result.metadata) {
|
||||||
// Update with processed metadata using dispatch directly
|
// Only log if page count differs from our immediate calculation
|
||||||
dispatch({
|
const initialPageCount = pageCount;
|
||||||
type: 'UPDATE_FILE_RECORD',
|
if (result.metadata.totalPages !== initialPageCount) {
|
||||||
payload: {
|
console.log(`📄 Page count validation: ${file.name} initial=${initialPageCount} → final=${result.metadata.totalPages} pages`);
|
||||||
id: fileId,
|
// Update with the validated page count, but preserve existing thumbnail
|
||||||
updates: {
|
dispatch({
|
||||||
processedFile: result.metadata,
|
type: 'UPDATE_FILE_RECORD',
|
||||||
thumbnailUrl: result.metadata.thumbnailUrl
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
} else {
|
||||||
console.log(`✅ File processing complete for ${file.name}: ${result.metadata.totalPages} pages`);
|
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) {
|
if (enablePersistence) {
|
||||||
try {
|
try {
|
||||||
const thumbnail = result.metadata.thumbnailUrl;
|
const finalThumbnail = thumbnail || result.metadata.thumbnailUrl;
|
||||||
fileStorage.storeFile(file, fileId, thumbnail).then(() => {
|
fileStorage.storeFile(file, fileId, finalThumbnail).then(() => {
|
||||||
console.log('File persisted to IndexedDB:', fileId);
|
console.log('File persisted to IndexedDB:', fileId);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.warn('Failed to persist file to IndexedDB:', error);
|
console.warn('Failed to persist file to IndexedDB:', error);
|
||||||
@ -484,11 +529,11 @@ export function FileContextProvider({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`❌ File processing failed for ${file.name}:`, result.error);
|
console.warn(`❌ Background file processing failed for ${file.name}:`, result.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch(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;
|
return addedFiles;
|
||||||
}, [enablePersistence]); // Remove updateFileRecord dependency
|
}, [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
|
// 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 addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>): Promise<File[]> => {
|
||||||
const fileRecords: FileRecord[] = [];
|
const fileRecords: FileRecord[] = [];
|
||||||
const addedFiles: File[] = [];
|
const addedFiles: File[] = [];
|
||||||
@ -635,6 +791,7 @@ export function FileContextProvider({
|
|||||||
// Memoized actions to prevent re-renders
|
// Memoized actions to prevent re-renders
|
||||||
const actions = useMemo<FileContextActions>(() => ({
|
const actions = useMemo<FileContextActions>(() => ({
|
||||||
addFiles,
|
addFiles,
|
||||||
|
addProcessedFiles,
|
||||||
addStoredFiles,
|
addStoredFiles,
|
||||||
removeFiles,
|
removeFiles,
|
||||||
updateFileRecord,
|
updateFileRecord,
|
||||||
@ -658,7 +815,7 @@ export function FileContextProvider({
|
|||||||
setMode: (mode: ModeType) => dispatch({ type: 'SET_CURRENT_MODE', payload: mode }),
|
setMode: (mode: ModeType) => dispatch({ type: 'SET_CURRENT_MODE', payload: mode }),
|
||||||
confirmNavigation,
|
confirmNavigation,
|
||||||
cancelNavigation
|
cancelNavigation
|
||||||
}), [addFiles, addStoredFiles, removeFiles, cleanupAllFiles, setHasUnsavedChanges, confirmNavigation, cancelNavigation]);
|
}), [addFiles, addProcessedFiles, addStoredFiles, removeFiles, cleanupAllFiles, setHasUnsavedChanges, confirmNavigation, cancelNavigation]);
|
||||||
|
|
||||||
// Split context values to minimize re-renders
|
// Split context values to minimize re-renders
|
||||||
const stateValue = useMemo<FileContextStateValue>(() => ({
|
const stateValue = useMemo<FileContextStateValue>(() => ({
|
||||||
@ -677,6 +834,7 @@ export function FileContextProvider({
|
|||||||
...state.ui,
|
...state.ui,
|
||||||
// Action compatibility layer
|
// Action compatibility layer
|
||||||
addFiles,
|
addFiles,
|
||||||
|
addProcessedFiles,
|
||||||
addStoredFiles,
|
addStoredFiles,
|
||||||
removeFiles,
|
removeFiles,
|
||||||
updateFileRecord,
|
updateFileRecord,
|
||||||
@ -701,7 +859,7 @@ export function FileContextProvider({
|
|||||||
get activeFiles() { return selectors.getFiles(); }, // Getter to avoid creating new arrays on every render
|
get activeFiles() { return selectors.getFiles(); }, // Getter to avoid creating new arrays on every render
|
||||||
// Selectors
|
// Selectors
|
||||||
...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
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -121,7 +121,7 @@ export const useToolOperation = <TParams = void>(
|
|||||||
// Composed hooks
|
// Composed hooks
|
||||||
const { state, actions } = useToolState();
|
const { state, actions } = useToolState();
|
||||||
const { processFiles, cancelOperation: cancelApiCalls } = useToolApiCalls<TParams>();
|
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 (
|
const executeOperation = useCallback(async (
|
||||||
params: TParams,
|
params: TParams,
|
||||||
@ -167,23 +167,31 @@ export const useToolOperation = <TParams = void>(
|
|||||||
// Use explicit multiFileEndpoint flag to determine processing approach
|
// Use explicit multiFileEndpoint flag to determine processing approach
|
||||||
if (config.multiFileEndpoint) {
|
if (config.multiFileEndpoint) {
|
||||||
// Multi-file processing - single API call with all files
|
// 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...');
|
actions.setStatus('Processing files...');
|
||||||
const formData = (config.buildFormData as (params: TParams, files: File[]) => FormData)(params, validFiles);
|
const formData = (config.buildFormData as (params: TParams, files: File[]) => FormData)(params, validFiles);
|
||||||
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
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' });
|
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
|
// Multi-file responses are typically ZIP files that need extraction
|
||||||
if (config.responseHandler) {
|
if (config.responseHandler) {
|
||||||
|
console.log(`🚀 Using custom responseHandler for ${config.operationType}`);
|
||||||
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
||||||
processedFiles = await config.responseHandler(response.data, validFiles);
|
processedFiles = await config.responseHandler(response.data, validFiles);
|
||||||
} else {
|
} else {
|
||||||
|
console.log(`🚀 Using default ZIP extraction for ${config.operationType}`);
|
||||||
// Default: assume ZIP response for multi-file endpoints
|
// Default: assume ZIP response for multi-file endpoints
|
||||||
processedFiles = await extractZipFiles(response.data);
|
processedFiles = await extractZipFiles(response.data);
|
||||||
|
console.log(`🚀 Extracted ${processedFiles.length} files from ZIP`);
|
||||||
|
|
||||||
if (processedFiles.length === 0) {
|
if (processedFiles.length === 0) {
|
||||||
|
console.log(`🚀 ZIP extraction failed, trying generic fallback`);
|
||||||
// Try the generic extraction as fallback
|
// Try the generic extraction as fallback
|
||||||
processedFiles = await extractAllZipFiles(response.data);
|
processedFiles = await extractAllZipFiles(response.data);
|
||||||
|
console.log(`🚀 Generic fallback extracted ${processedFiles.length} files`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -205,21 +213,35 @@ export const useToolOperation = <TParams = void>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (processedFiles.length > 0) {
|
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);
|
actions.setFiles(processedFiles);
|
||||||
|
|
||||||
// Generate thumbnails and download URL concurrently
|
// Generate thumbnails with metadata and download URL concurrently
|
||||||
actions.setGeneratingThumbnails(true);
|
actions.setGeneratingThumbnails(true);
|
||||||
const [thumbnails, downloadInfo] = await Promise.all([
|
const [thumbnailResults, downloadInfo] = await Promise.all([
|
||||||
generateThumbnails(processedFiles),
|
generateThumbnailsWithMetadata(processedFiles),
|
||||||
createDownloadInfo(processedFiles, config.operationType)
|
createDownloadInfo(processedFiles, config.operationType)
|
||||||
]);
|
]);
|
||||||
actions.setGeneratingThumbnails(false);
|
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.setThumbnails(thumbnails);
|
||||||
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
|
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
|
||||||
|
|
||||||
// Add to file context
|
// Add to file context WITH pre-existing thumbnails AND page counts to avoid duplicate processing
|
||||||
await fileActions.addFiles(processedFiles);
|
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);
|
markOperationApplied(fileId, operationId);
|
||||||
}
|
}
|
||||||
@ -233,7 +255,7 @@ export const useToolOperation = <TParams = void>(
|
|||||||
actions.setLoading(false);
|
actions.setLoading(false);
|
||||||
actions.setProgress(null);
|
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(() => {
|
const cancelOperation = useCallback(() => {
|
||||||
cancelApiCalls();
|
cancelApiCalls();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
import { generateThumbnailForFile, generateThumbnailWithMetadata, ThumbnailWithMetadata } from '../../../utils/thumbnailUtils';
|
||||||
import { zipFileService } from '../../../services/zipFileService';
|
import { zipFileService } from '../../../services/zipFileService';
|
||||||
|
|
||||||
|
|
||||||
@ -43,23 +43,52 @@ export const useToolResources = () => {
|
|||||||
}, []); // No dependencies - use ref to access current URLs
|
}, []); // No dependencies - use ref to access current URLs
|
||||||
|
|
||||||
const generateThumbnails = useCallback(async (files: File[]): Promise<string[]> => {
|
const generateThumbnails = useCallback(async (files: File[]): Promise<string[]> => {
|
||||||
|
console.log(`🖼️ useToolResources.generateThumbnails: Starting for ${files.length} files`);
|
||||||
const thumbnails: string[] = [];
|
const thumbnails: string[] = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
|
console.log(`🖼️ Generating thumbnail for: ${file.name} (${file.type}, ${file.size} bytes)`);
|
||||||
const thumbnail = await generateThumbnailForFile(file);
|
const thumbnail = await generateThumbnailForFile(file);
|
||||||
|
console.log(`🖼️ Generated thumbnail for ${file.name}:`, thumbnail ? 'SUCCESS' : 'FAILED (no thumbnail returned)');
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
thumbnails.push(thumbnail);
|
thumbnails.push(thumbnail);
|
||||||
|
} else {
|
||||||
|
console.warn(`🖼️ No thumbnail returned for ${file.name}`);
|
||||||
|
thumbnails.push('');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
|
console.warn(`🖼️ Failed to generate thumbnail for ${file.name}:`, error);
|
||||||
thumbnails.push('');
|
thumbnails.push('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`🖼️ useToolResources.generateThumbnails: Complete. Generated ${thumbnails.filter(t => t).length}/${files.length} thumbnails`);
|
||||||
return 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[]> => {
|
const extractZipFiles = useCallback(async (zipBlob: Blob): Promise<File[]> => {
|
||||||
try {
|
try {
|
||||||
const zipFile = new File([zipBlob], 'temp.zip', { type: 'application/zip' });
|
const zipFile = new File([zipBlob], 'temp.zip', { type: 'application/zip' });
|
||||||
@ -116,6 +145,7 @@ export const useToolResources = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
generateThumbnails,
|
generateThumbnails,
|
||||||
|
generateThumbnailsWithMetadata,
|
||||||
createDownloadInfo,
|
createDownloadInfo,
|
||||||
extractZipFiles,
|
extractZipFiles,
|
||||||
extractAllZipFiles,
|
extractAllZipFiles,
|
||||||
|
@ -88,6 +88,8 @@ export const useToolState = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setThumbnails = useCallback((thumbnails: string[]) => {
|
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 });
|
dispatch({ type: 'SET_THUMBNAILS', payload: thumbnails });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export const fileOperationsService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fileStorage.init();
|
// fileStorage.init() no longer needed - using centralized IndexedDB manager
|
||||||
const storedFiles = await fileStorage.getAllFileMetadata();
|
const storedFiles = await fileStorage.getAllFileMetadata();
|
||||||
|
|
||||||
// Detect if IndexedDB was purged by comparing with current UI state
|
// Detect if IndexedDB was purged by comparing with current UI state
|
||||||
@ -189,7 +189,7 @@ export const fileOperationsService = {
|
|||||||
if (currentFiles.length === 0) return false;
|
if (currentFiles.length === 0) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fileStorage.init();
|
// fileStorage.init() no longer needed - using centralized IndexedDB manager
|
||||||
const storedFiles = await fileStorage.getAllFileMetadata();
|
const storedFiles = await fileStorage.getAllFileMetadata();
|
||||||
return storedFiles.length === 0; // Purge detected if no files in storage but UI shows files
|
return storedFiles.length === 0; // Purge detected if no files in storage but UI shows files
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -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 {
|
createFileFromStored(storedFile: StoredFile): File {
|
||||||
if (!storedFile || !storedFile.data) {
|
if (!storedFile || !storedFile.data) {
|
||||||
@ -429,13 +430,27 @@ class FileStorageService {
|
|||||||
lastModified: storedFile.lastModified
|
lastModified: storedFile.lastModified
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add custom properties for compatibility
|
// Returns pure File object - no mutations
|
||||||
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
// Use FileContext.addStoredFiles() to properly associate with metadata
|
||||||
Object.defineProperty(file, 'thumbnail', { value: storedFile.thumbnail, writable: false });
|
|
||||||
|
|
||||||
return file;
|
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
|
* Create blob URL for stored file
|
||||||
*/
|
*/
|
||||||
|
@ -69,13 +69,17 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const filesCollapsed = hasFiles;
|
const filesCollapsed = hasFiles;
|
||||||
const settingsCollapsed = hasResults;
|
const settingsCollapsed = hasResults;
|
||||||
|
|
||||||
const previewResults = useMemo(() =>
|
const previewResults = useMemo(() => {
|
||||||
splitOperation.files?.map((file, index) => ({
|
const results = splitOperation.files?.map((file, index) => ({
|
||||||
file,
|
file,
|
||||||
thumbnail: splitOperation.thumbnails[index]
|
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 (
|
return (
|
||||||
<ToolStepContainer>
|
<ToolStepContainer>
|
||||||
|
@ -191,6 +191,7 @@ export type FileContextAction =
|
|||||||
export interface FileContextActions {
|
export interface FileContextActions {
|
||||||
// File management - lightweight actions only
|
// File management - lightweight actions only
|
||||||
addFiles: (files: File[]) => Promise<File[]>;
|
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[]>;
|
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => Promise<File[]>;
|
||||||
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => void;
|
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => void;
|
||||||
updateFileRecord: (id: FileId, updates: Partial<FileRecord>) => void;
|
updateFileRecord: (id: FileId, updates: Partial<FileRecord>) => void;
|
||||||
|
@ -1,8 +1,34 @@
|
|||||||
import { FileWithUrl } from "../types/file";
|
import { FileWithUrl } from "../types/file";
|
||||||
import { StoredFile, fileStorage } from "../services/fileStorage";
|
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 {
|
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[]> {
|
export async function loadFilesFromIndexedDB(): Promise<FileWithUrl[]> {
|
||||||
try {
|
try {
|
||||||
await fileStorage.init();
|
// fileStorage.init() no longer needed - using centralized IndexedDB manager
|
||||||
const storedFiles = await fileStorage.getAllFileMetadata();
|
const storedFiles = await fileStorage.getAllFileMetadata();
|
||||||
|
|
||||||
if (storedFiles.length === 0) {
|
if (storedFiles.length === 0) {
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { getDocument } from "pdfjs-dist";
|
import { getDocument } from "pdfjs-dist";
|
||||||
|
|
||||||
|
export interface ThumbnailWithMetadata {
|
||||||
|
thumbnail: string | undefined;
|
||||||
|
pageCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate thumbnail scale based on file size
|
* Calculate thumbnail scale based on file size
|
||||||
* Smaller files get higher quality, larger files get lower quality
|
* 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
|
* Returns base64 data URL or undefined if generation fails
|
||||||
*/
|
*/
|
||||||
export async function generateThumbnailForFile(file: File): Promise<string | undefined> {
|
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
|
// Skip thumbnail generation for very large files to avoid memory issues
|
||||||
if (file.size >= 100 * 1024 * 1024) { // 100MB limit
|
if (file.size >= 100 * 1024 * 1024) { // 100MB limit
|
||||||
console.log('Skipping thumbnail generation for large file:', file.name);
|
console.log('🎯 Skipping thumbnail generation for large file:', file.name);
|
||||||
return generatePlaceholderThumbnail(file);
|
const placeholder = generatePlaceholderThumbnail(file);
|
||||||
|
console.log('🎯 Generated placeholder thumbnail for large file:', file.name);
|
||||||
|
return placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle image files - use original file directly
|
// Handle image files - use original file directly
|
||||||
if (file.type.startsWith('image/')) {
|
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
|
// Handle PDF files
|
||||||
if (!file.type.startsWith('application/pdf')) {
|
if (!file.type.startsWith('application/pdf')) {
|
||||||
console.log('File is not a PDF or image, generating placeholder:', file.name);
|
console.log('🎯 File is not a PDF or image, generating placeholder:', file.name);
|
||||||
return generatePlaceholderThumbnail(file);
|
const placeholder = generatePlaceholderThumbnail(file);
|
||||||
|
console.log('🎯 Generated placeholder thumbnail for non-PDF file:', file.name);
|
||||||
|
return placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate quality scale based on file size
|
// 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);
|
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 {
|
try {
|
||||||
// Only read first 2MB for thumbnail generation to save memory
|
// Only read first 2MB for thumbnail generation to save memory
|
||||||
const chunkSize = 2 * 1024 * 1024; // 2MB
|
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
|
// Immediately clean up memory after thumbnail generation
|
||||||
pdf.destroy();
|
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;
|
return thumbnail;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.warn('🎯 Error generating PDF thumbnail for', file.name, ':', error);
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.name === 'InvalidPDFException') {
|
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
|
// Return a placeholder or try with full file instead of chunk
|
||||||
try {
|
try {
|
||||||
const fullArrayBuffer = await file.arrayBuffer();
|
const fullArrayBuffer = await file.arrayBuffer();
|
||||||
@ -247,17 +262,132 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
|
|||||||
const thumbnail = canvas.toDataURL();
|
const thumbnail = canvas.toDataURL();
|
||||||
|
|
||||||
pdf.destroy();
|
pdf.destroy();
|
||||||
|
console.log('🎯 Fallback PDF thumbnail generation succeeded for', file.name);
|
||||||
return thumbnail;
|
return thumbnail;
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.warn('Fallback thumbnail generation also failed for', file.name, fallbackError);
|
console.warn('🎯 Fallback thumbnail generation also failed for', file.name, fallbackError);
|
||||||
return undefined;
|
console.log('🎯 Using placeholder thumbnail for', file.name);
|
||||||
|
return generatePlaceholderThumbnail(file);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('Failed to generate thumbnail for', file.name, error);
|
console.warn('🎯 Non-PDF error generating thumbnail for', file.name, error);
|
||||||
return undefined;
|
console.log('🎯 Using placeholder thumbnail for', file.name);
|
||||||
|
return generatePlaceholderThumbnail(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.warn('Unknown error generating thumbnail for', file.name, error);
|
console.warn('🎯 Unknown error generating thumbnail for', file.name, error);
|
||||||
return undefined;
|
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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user