mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Clean up
This commit is contained in:
parent
646cedfb0f
commit
aff610448b
@ -1,26 +0,0 @@
|
||||
// Web Worker for lightweight data processing (not PDF rendering)
|
||||
// PDF rendering must stay on main thread due to DOM dependencies
|
||||
|
||||
self.onmessage = async function(e) {
|
||||
const { type, data, jobId } = e.data;
|
||||
|
||||
try {
|
||||
// Handle PING for worker health check
|
||||
if (type === 'PING') {
|
||||
self.postMessage({ type: 'PONG', jobId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'GENERATE_THUMBNAILS') {
|
||||
// Web Workers cannot do PDF rendering due to DOM dependencies
|
||||
// This is expected to fail and trigger main thread fallback
|
||||
throw new Error('PDF rendering requires main thread (DOM access needed)');
|
||||
}
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
type: 'ERROR',
|
||||
jobId,
|
||||
data: { error: error.message }
|
||||
});
|
||||
}
|
||||
};
|
@ -154,6 +154,7 @@ const PageThumbnail = React.memo(({
|
||||
: undefined;
|
||||
onReorderPages(page.pageNumber, targetIndex, pagesToMove);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* High-performance thumbnail generation service using Web Workers
|
||||
* High-performance thumbnail generation service using main thread processing
|
||||
*/
|
||||
|
||||
interface ThumbnailResult {
|
||||
@ -23,108 +23,17 @@ interface CachedThumbnail {
|
||||
}
|
||||
|
||||
export class ThumbnailGenerationService {
|
||||
private workers: Worker[] = [];
|
||||
private activeJobs = new Map<string, { resolve: Function; reject: Function; onProgress?: Function }>();
|
||||
private jobCounter = 0;
|
||||
|
||||
// Session-based thumbnail cache
|
||||
private thumbnailCache = new Map<string, CachedThumbnail>();
|
||||
private maxCacheSizeBytes = 1024 * 1024 * 1024; // 1GB cache limit
|
||||
private currentCacheSize = 0;
|
||||
|
||||
constructor(private maxWorkers: number = 3) {
|
||||
/**
|
||||
* NOTE: PDF rendering requires DOM access (document, canvas, etc.) which isn't
|
||||
* available in Web Workers. This service attempts Web Worker setup but will
|
||||
* gracefully fallback to optimized main thread processing when Workers fail.
|
||||
* This is expected behavior, not an error.
|
||||
*/
|
||||
this.initializeWorkers();
|
||||
}
|
||||
|
||||
private initializeWorkers(): void {
|
||||
const workerPromises: Promise<Worker | null>[] = [];
|
||||
|
||||
for (let i = 0; i < this.maxWorkers; i++) {
|
||||
const workerPromise = new Promise<Worker | null>((resolve) => {
|
||||
try {
|
||||
const worker = new Worker('/thumbnailWorker.js');
|
||||
let workerReady = false;
|
||||
let pingTimeout: NodeJS.Timeout;
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
const { type, data, jobId } = e.data;
|
||||
|
||||
// Handle PONG response to confirm worker is ready
|
||||
if (type === 'PONG') {
|
||||
workerReady = true;
|
||||
clearTimeout(pingTimeout);
|
||||
resolve(worker);
|
||||
return;
|
||||
}
|
||||
|
||||
const job = this.activeJobs.get(jobId);
|
||||
if (!job) return;
|
||||
|
||||
switch (type) {
|
||||
case 'PROGRESS':
|
||||
if (job.onProgress) {
|
||||
job.onProgress(data);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'COMPLETE':
|
||||
job.resolve(data.thumbnails);
|
||||
this.activeJobs.delete(jobId);
|
||||
break;
|
||||
|
||||
case 'ERROR':
|
||||
job.reject(new Error(data.error));
|
||||
this.activeJobs.delete(jobId);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (error) => {
|
||||
clearTimeout(pingTimeout);
|
||||
worker.terminate();
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
// Test worker with timeout
|
||||
pingTimeout = setTimeout(() => {
|
||||
if (!workerReady) {
|
||||
worker.terminate();
|
||||
resolve(null);
|
||||
}
|
||||
}, 1000); // Quick timeout since we expect failure
|
||||
|
||||
// Send PING to test worker
|
||||
try {
|
||||
worker.postMessage({ type: 'PING' });
|
||||
} catch (pingError) {
|
||||
clearTimeout(pingTimeout);
|
||||
worker.terminate();
|
||||
resolve(null);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
workerPromises.push(workerPromise);
|
||||
}
|
||||
|
||||
// Wait for all workers to initialize or fail
|
||||
Promise.all(workerPromises).then((workers) => {
|
||||
this.workers = workers.filter((w): w is Worker => w !== null);
|
||||
// Workers expected to fail due to PDF.js DOM requirements - no logging needed
|
||||
});
|
||||
// PDF rendering requires DOM access, so we use optimized main thread processing
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate thumbnails for multiple pages using Web Workers
|
||||
* Generate thumbnails for multiple pages using main thread processing
|
||||
*/
|
||||
async generateThumbnails(
|
||||
pdfArrayBuffer: ArrayBuffer,
|
||||
@ -132,86 +41,16 @@ export class ThumbnailGenerationService {
|
||||
options: ThumbnailGenerationOptions = {},
|
||||
onProgress?: (progress: { completed: number; total: number; thumbnails: ThumbnailResult[] }) => void
|
||||
): Promise<ThumbnailResult[]> {
|
||||
// Create unique job ID to track this specific generation request
|
||||
const jobId = `thumbnails-${++this.jobCounter}`;
|
||||
|
||||
// Instead of blocking globally, we'll track individual generation jobs
|
||||
// This allows multiple thumbnail generation requests to run concurrently
|
||||
|
||||
const {
|
||||
scale = 0.2,
|
||||
quality = 0.8,
|
||||
batchSize = 20, // Pages per worker
|
||||
parallelBatches = this.maxWorkers
|
||||
quality = 0.8
|
||||
} = options;
|
||||
|
||||
try {
|
||||
// Check if workers are available, fallback to main thread if not
|
||||
if (this.workers.length === 0) {
|
||||
return await this.generateThumbnailsMainThread(pdfArrayBuffer, pageNumbers, scale, quality, onProgress);
|
||||
}
|
||||
|
||||
// Split pages across workers
|
||||
const workerBatches = this.distributeWork(pageNumbers, this.workers.length);
|
||||
const jobPromises: Promise<ThumbnailResult[]>[] = [];
|
||||
|
||||
for (let i = 0; i < workerBatches.length; i++) {
|
||||
const batch = workerBatches[i];
|
||||
if (batch.length === 0) continue;
|
||||
|
||||
const worker = this.workers[i % this.workers.length];
|
||||
const jobId = `job-${++this.jobCounter}`;
|
||||
|
||||
const promise = new Promise<ThumbnailResult[]>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.activeJobs.delete(jobId);
|
||||
reject(new Error(`Worker job ${jobId} timed out`));
|
||||
}, 60000); // 1 minute timeout
|
||||
|
||||
// Create job with timeout handling
|
||||
this.activeJobs.set(jobId, {
|
||||
resolve: (result: any) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(result);
|
||||
},
|
||||
reject: (error: any) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
},
|
||||
onProgress: onProgress
|
||||
});
|
||||
|
||||
worker.postMessage({
|
||||
type: 'GENERATE_THUMBNAILS',
|
||||
jobId,
|
||||
data: {
|
||||
pdfArrayBuffer,
|
||||
pageNumbers: batch,
|
||||
scale,
|
||||
quality
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
jobPromises.push(promise);
|
||||
}
|
||||
|
||||
// Wait for all workers to complete
|
||||
const results = await Promise.all(jobPromises);
|
||||
|
||||
// Flatten and sort results by page number
|
||||
const allThumbnails = results.flat().sort((a, b) => a.pageNumber - b.pageNumber);
|
||||
return allThumbnails;
|
||||
|
||||
} catch (error) {
|
||||
return await this.generateThumbnailsMainThread(pdfArrayBuffer, pageNumbers, scale, quality, onProgress);
|
||||
} finally {
|
||||
// Individual job completed, no need to reset global flag
|
||||
}
|
||||
return await this.generateThumbnailsMainThread(pdfArrayBuffer, pageNumbers, scale, quality, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback thumbnail generation on main thread
|
||||
* Main thread thumbnail generation with batching for UI responsiveness
|
||||
*/
|
||||
private async generateThumbnailsMainThread(
|
||||
pdfArrayBuffer: ArrayBuffer,
|
||||
@ -220,13 +59,9 @@ export class ThumbnailGenerationService {
|
||||
quality: number,
|
||||
onProgress?: (progress: { completed: number; total: number; thumbnails: ThumbnailResult[] }) => void
|
||||
): Promise<ThumbnailResult[]> {
|
||||
// Import PDF.js dynamically for main thread
|
||||
const { getDocument } = await import('pdfjs-dist');
|
||||
|
||||
// Load PDF once
|
||||
const pdf = await getDocument({ data: pdfArrayBuffer }).promise;
|
||||
|
||||
|
||||
const allResults: ThumbnailResult[] = [];
|
||||
let completed = 0;
|
||||
const batchSize = 5; // Small batches for UI responsiveness
|
||||
@ -277,141 +112,81 @@ export class ThumbnailGenerationService {
|
||||
});
|
||||
}
|
||||
|
||||
// Small delay to keep UI responsive
|
||||
if (i + batchSize < pageNumbers.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
// Yield control to prevent UI blocking
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
}
|
||||
|
||||
// Clean up
|
||||
pdf.destroy();
|
||||
|
||||
return allResults.filter(r => r.success);
|
||||
await pdf.destroy();
|
||||
return allResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute work evenly across workers
|
||||
*/
|
||||
private distributeWork(pageNumbers: number[], numWorkers: number): number[][] {
|
||||
const batches: number[][] = Array(numWorkers).fill(null).map(() => []);
|
||||
|
||||
pageNumbers.forEach((pageNum, index) => {
|
||||
const workerIndex = index % numWorkers;
|
||||
batches[workerIndex].push(pageNum);
|
||||
});
|
||||
|
||||
return batches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single thumbnail (fallback for individual pages)
|
||||
*/
|
||||
async generateSingleThumbnail(
|
||||
pdfArrayBuffer: ArrayBuffer,
|
||||
pageNumber: number,
|
||||
options: ThumbnailGenerationOptions = {}
|
||||
): Promise<string> {
|
||||
const results = await this.generateThumbnails(pdfArrayBuffer, [pageNumber], options);
|
||||
|
||||
if (results.length === 0 || !results[0].success) {
|
||||
throw new Error(`Failed to generate thumbnail for page ${pageNumber}`);
|
||||
}
|
||||
|
||||
return results[0].thumbnail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add thumbnail to cache with size management
|
||||
*/
|
||||
addThumbnailToCache(pageId: string, thumbnail: string): void {
|
||||
const thumbnailSizeBytes = thumbnail.length * 0.75; // Rough base64 size estimate
|
||||
const now = Date.now();
|
||||
|
||||
// Add new thumbnail
|
||||
this.thumbnailCache.set(pageId, {
|
||||
thumbnail,
|
||||
lastUsed: now,
|
||||
sizeBytes: thumbnailSizeBytes
|
||||
});
|
||||
|
||||
this.currentCacheSize += thumbnailSizeBytes;
|
||||
|
||||
// If we exceed 1GB, trigger cleanup
|
||||
if (this.currentCacheSize > this.maxCacheSizeBytes) {
|
||||
this.cleanupThumbnailCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get thumbnail from cache and update last used timestamp
|
||||
* Cache management
|
||||
*/
|
||||
getThumbnailFromCache(pageId: string): string | null {
|
||||
const cached = this.thumbnailCache.get(pageId);
|
||||
if (!cached) return null;
|
||||
|
||||
// Update last used timestamp
|
||||
cached.lastUsed = Date.now();
|
||||
|
||||
return cached.thumbnail;
|
||||
if (cached) {
|
||||
cached.lastUsed = Date.now();
|
||||
return cached.thumbnail;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up cache using LRU eviction
|
||||
*/
|
||||
private cleanupThumbnailCache(): void {
|
||||
const entries = Array.from(this.thumbnailCache.entries());
|
||||
addThumbnailToCache(pageId: string, thumbnail: string): void {
|
||||
const sizeBytes = thumbnail.length * 2; // Rough estimate for base64 string
|
||||
|
||||
// Sort by last used (oldest first)
|
||||
entries.sort(([, a], [, b]) => a.lastUsed - b.lastUsed);
|
||||
// Enforce cache size limits
|
||||
while (this.currentCacheSize + sizeBytes > this.maxCacheSizeBytes && this.thumbnailCache.size > 0) {
|
||||
this.evictLeastRecentlyUsed();
|
||||
}
|
||||
|
||||
this.thumbnailCache.clear();
|
||||
this.currentCacheSize = 0;
|
||||
const targetSize = this.maxCacheSizeBytes * 0.8; // Clean to 80% of limit
|
||||
this.thumbnailCache.set(pageId, {
|
||||
thumbnail,
|
||||
lastUsed: Date.now(),
|
||||
sizeBytes
|
||||
});
|
||||
|
||||
// Keep most recently used entries until we hit target size
|
||||
for (let i = entries.length - 1; i >= 0 && this.currentCacheSize < targetSize; i--) {
|
||||
const [key, value] = entries[i];
|
||||
this.thumbnailCache.set(key, value);
|
||||
this.currentCacheSize += value.sizeBytes;
|
||||
this.currentCacheSize += sizeBytes;
|
||||
}
|
||||
|
||||
private evictLeastRecentlyUsed(): void {
|
||||
let oldestEntry: [string, CachedThumbnail] | null = null;
|
||||
let oldestTime = Date.now();
|
||||
|
||||
for (const [key, value] of this.thumbnailCache.entries()) {
|
||||
if (value.lastUsed < oldestTime) {
|
||||
oldestTime = value.lastUsed;
|
||||
oldestEntry = [key, value];
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestEntry) {
|
||||
this.thumbnailCache.delete(oldestEntry[0]);
|
||||
this.currentCacheSize -= oldestEntry[1].sizeBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached thumbnails
|
||||
*/
|
||||
clearThumbnailCache(): void {
|
||||
this.thumbnailCache.clear();
|
||||
this.currentCacheSize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
getCacheStats() {
|
||||
return {
|
||||
entries: this.thumbnailCache.size,
|
||||
totalSizeBytes: this.currentCacheSize,
|
||||
size: this.thumbnailCache.size,
|
||||
sizeBytes: this.currentCacheSize,
|
||||
maxSizeBytes: this.maxCacheSizeBytes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop generation but keep cache and workers alive
|
||||
*/
|
||||
stopGeneration(): void {
|
||||
this.activeJobs.clear();
|
||||
// No-op since we removed workers
|
||||
}
|
||||
|
||||
clearCache(): void {
|
||||
this.thumbnailCache.clear();
|
||||
this.currentCacheSize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate all workers and clear cache (only on explicit cleanup)
|
||||
*/
|
||||
destroy(): void {
|
||||
this.workers.forEach(worker => worker.terminate());
|
||||
this.workers = [];
|
||||
this.activeJobs.clear();
|
||||
this.clearThumbnailCache();
|
||||
this.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
// Global singleton instance
|
||||
export const thumbnailGenerationService = new ThumbnailGenerationService();
|
@ -1,292 +0,0 @@
|
||||
/**
|
||||
* Typed operation model with discriminated unions
|
||||
* Centralizes all PDF operations with proper type safety
|
||||
*/
|
||||
|
||||
import { FileId } from './fileContext';
|
||||
|
||||
export type OperationId = string;
|
||||
|
||||
export type OperationStatus =
|
||||
| 'idle'
|
||||
| 'preparing'
|
||||
| 'uploading'
|
||||
| 'processing'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'canceled';
|
||||
|
||||
// Base operation interface
|
||||
export interface BaseOperation {
|
||||
id: OperationId;
|
||||
type: string;
|
||||
status: OperationStatus;
|
||||
progress: number;
|
||||
error?: string | null;
|
||||
createdAt: number;
|
||||
startedAt?: number;
|
||||
completedAt?: number;
|
||||
abortController?: AbortController;
|
||||
}
|
||||
|
||||
// Split operations
|
||||
export type SplitMode =
|
||||
| 'pages'
|
||||
| 'size'
|
||||
| 'duplicates'
|
||||
| 'bookmarks'
|
||||
| 'sections';
|
||||
|
||||
export interface SplitPagesParams {
|
||||
mode: 'pages';
|
||||
pages: number[];
|
||||
}
|
||||
|
||||
export interface SplitSizeParams {
|
||||
mode: 'size';
|
||||
maxSizeBytes: number;
|
||||
}
|
||||
|
||||
export interface SplitDuplicatesParams {
|
||||
mode: 'duplicates';
|
||||
tolerance?: number;
|
||||
}
|
||||
|
||||
export interface SplitBookmarksParams {
|
||||
mode: 'bookmarks';
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export interface SplitSectionsParams {
|
||||
mode: 'sections';
|
||||
sectionCount: number;
|
||||
}
|
||||
|
||||
export type SplitParams =
|
||||
| SplitPagesParams
|
||||
| SplitSizeParams
|
||||
| SplitDuplicatesParams
|
||||
| SplitBookmarksParams
|
||||
| SplitSectionsParams;
|
||||
|
||||
export interface SplitOperation extends BaseOperation {
|
||||
type: 'split';
|
||||
inputFileId: FileId;
|
||||
params: SplitParams;
|
||||
outputFileIds?: FileId[];
|
||||
}
|
||||
|
||||
// Merge operations
|
||||
export interface MergeOperation extends BaseOperation {
|
||||
type: 'merge';
|
||||
inputFileIds: FileId[];
|
||||
params: {
|
||||
sortBy?: 'name' | 'size' | 'date' | 'custom';
|
||||
customOrder?: FileId[];
|
||||
bookmarks?: boolean;
|
||||
};
|
||||
outputFileId?: FileId;
|
||||
}
|
||||
|
||||
// Compress operations
|
||||
export interface CompressOperation extends BaseOperation {
|
||||
type: 'compress';
|
||||
inputFileId: FileId;
|
||||
params: {
|
||||
level: 'low' | 'medium' | 'high' | 'extreme';
|
||||
imageQuality?: number; // 0-100
|
||||
grayscale?: boolean;
|
||||
removeAnnotations?: boolean;
|
||||
};
|
||||
outputFileId?: FileId;
|
||||
}
|
||||
|
||||
// Convert operations
|
||||
export type ConvertFormat =
|
||||
| 'pdf'
|
||||
| 'docx'
|
||||
| 'pptx'
|
||||
| 'xlsx'
|
||||
| 'html'
|
||||
| 'txt'
|
||||
| 'jpg'
|
||||
| 'png';
|
||||
|
||||
export interface ConvertOperation extends BaseOperation {
|
||||
type: 'convert';
|
||||
inputFileIds: FileId[];
|
||||
params: {
|
||||
targetFormat: ConvertFormat;
|
||||
imageSettings?: {
|
||||
quality?: number;
|
||||
dpi?: number;
|
||||
colorSpace?: 'rgb' | 'grayscale' | 'cmyk';
|
||||
};
|
||||
pdfSettings?: {
|
||||
pdfStandard?: 'PDF/A-1' | 'PDF/A-2' | 'PDF/A-3';
|
||||
compliance?: boolean;
|
||||
};
|
||||
};
|
||||
outputFileIds?: FileId[];
|
||||
}
|
||||
|
||||
// OCR operations
|
||||
export interface OcrOperation extends BaseOperation {
|
||||
type: 'ocr';
|
||||
inputFileId: FileId;
|
||||
params: {
|
||||
languages: string[];
|
||||
mode: 'searchable' | 'text-only' | 'overlay';
|
||||
preprocess?: boolean;
|
||||
deskew?: boolean;
|
||||
};
|
||||
outputFileId?: FileId;
|
||||
}
|
||||
|
||||
// Security operations
|
||||
export interface SecurityOperation extends BaseOperation {
|
||||
type: 'security';
|
||||
inputFileId: FileId;
|
||||
params: {
|
||||
action: 'encrypt' | 'decrypt' | 'sign' | 'watermark';
|
||||
password?: string;
|
||||
permissions?: {
|
||||
printing?: boolean;
|
||||
copying?: boolean;
|
||||
editing?: boolean;
|
||||
annotations?: boolean;
|
||||
};
|
||||
watermark?: {
|
||||
text: string;
|
||||
position: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||
opacity: number;
|
||||
};
|
||||
};
|
||||
outputFileId?: FileId;
|
||||
}
|
||||
|
||||
// Union type for all operations
|
||||
export type Operation =
|
||||
| SplitOperation
|
||||
| MergeOperation
|
||||
| CompressOperation
|
||||
| ConvertOperation
|
||||
| OcrOperation
|
||||
| SecurityOperation;
|
||||
|
||||
// Operation state management
|
||||
export interface OperationState {
|
||||
operations: Record<OperationId, Operation>;
|
||||
queue: OperationId[];
|
||||
active: OperationId[];
|
||||
history: OperationId[];
|
||||
}
|
||||
|
||||
// Operation creation helpers
|
||||
export function createOperationId(): OperationId {
|
||||
return `op-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
||||
}
|
||||
|
||||
export function createBaseOperation(type: string): BaseOperation {
|
||||
return {
|
||||
id: createOperationId(),
|
||||
type,
|
||||
status: 'idle',
|
||||
progress: 0,
|
||||
error: null,
|
||||
createdAt: Date.now(),
|
||||
abortController: new AbortController()
|
||||
};
|
||||
}
|
||||
|
||||
// Type guards for operations
|
||||
export function isSplitOperation(op: Operation): op is SplitOperation {
|
||||
return op.type === 'split';
|
||||
}
|
||||
|
||||
export function isMergeOperation(op: Operation): op is MergeOperation {
|
||||
return op.type === 'merge';
|
||||
}
|
||||
|
||||
export function isCompressOperation(op: Operation): op is CompressOperation {
|
||||
return op.type === 'compress';
|
||||
}
|
||||
|
||||
export function isConvertOperation(op: Operation): op is ConvertOperation {
|
||||
return op.type === 'convert';
|
||||
}
|
||||
|
||||
export function isOcrOperation(op: Operation): op is OcrOperation {
|
||||
return op.type === 'ocr';
|
||||
}
|
||||
|
||||
export function isSecurityOperation(op: Operation): op is SecurityOperation {
|
||||
return op.type === 'security';
|
||||
}
|
||||
|
||||
// Operation status helpers
|
||||
export function isOperationActive(op: Operation): boolean {
|
||||
return ['preparing', 'uploading', 'processing'].includes(op.status);
|
||||
}
|
||||
|
||||
export function isOperationComplete(op: Operation): boolean {
|
||||
return op.status === 'completed';
|
||||
}
|
||||
|
||||
export function isOperationFailed(op: Operation): boolean {
|
||||
return op.status === 'failed';
|
||||
}
|
||||
|
||||
export function canRetryOperation(op: Operation): boolean {
|
||||
return op.status === 'failed' && !!op.abortController && !op.abortController.signal.aborted;
|
||||
}
|
||||
|
||||
// Operation validation
|
||||
export function validateSplitParams(params: SplitParams): string | null {
|
||||
switch (params.mode) {
|
||||
case 'pages':
|
||||
if (!params.pages.length) return 'No pages specified';
|
||||
if (params.pages.some(p => p < 1)) return 'Invalid page numbers';
|
||||
break;
|
||||
case 'size':
|
||||
if (params.maxSizeBytes <= 0) return 'Invalid size limit';
|
||||
break;
|
||||
case 'sections':
|
||||
if (params.sectionCount < 2) return 'Section count must be at least 2';
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateMergeParams(params: MergeOperation['params'], fileIds: FileId[]): string | null {
|
||||
if (fileIds.length < 2) return 'At least 2 files required for merge';
|
||||
if (params.sortBy === 'custom' && !params.customOrder?.length) {
|
||||
return 'Custom order required when sort by custom is selected';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateCompressParams(params: CompressOperation['params']): string | null {
|
||||
if (params.imageQuality !== undefined && (params.imageQuality < 0 || params.imageQuality > 100)) {
|
||||
return 'Image quality must be between 0-100';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Operation result types
|
||||
export interface OperationResult {
|
||||
operationId: OperationId;
|
||||
success: boolean;
|
||||
outputFileIds: FileId[];
|
||||
error?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Operation events for pub/sub
|
||||
export type OperationEvent =
|
||||
| { type: 'operation:created'; operation: Operation }
|
||||
| { type: 'operation:started'; operationId: OperationId }
|
||||
| { type: 'operation:progress'; operationId: OperationId; progress: number }
|
||||
| { type: 'operation:completed'; operationId: OperationId; result: OperationResult }
|
||||
| { type: 'operation:failed'; operationId: OperationId; error: string }
|
||||
| { type: 'operation:canceled'; operationId: OperationId };
|
@ -7,20 +7,42 @@ export interface ThumbnailWithMetadata {
|
||||
|
||||
/**
|
||||
* Calculate thumbnail scale based on file size
|
||||
* Smaller files get higher quality, larger files get lower quality
|
||||
*/
|
||||
export function calculateScaleFromFileSize(fileSize: number): number {
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
if (fileSize < 1 * MB) return 0.6; // < 1MB: High quality
|
||||
if (fileSize < 5 * MB) return 0.4; // 1-5MB: Medium-high quality
|
||||
if (fileSize < 15 * MB) return 0.3; // 5-15MB: Medium quality
|
||||
if (fileSize < 30 * MB) return 0.2; // 15-30MB: Low-medium quality
|
||||
return 0.15; // 30MB+: Low quality
|
||||
if (fileSize < 1 * MB) return 0.6;
|
||||
if (fileSize < 5 * MB) return 0.4;
|
||||
if (fileSize < 15 * MB) return 0.3;
|
||||
if (fileSize < 30 * MB) return 0.2;
|
||||
return 0.15;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate modern placeholder thumbnail with file extension
|
||||
* Get file type color scheme
|
||||
*/
|
||||
function getFileTypeColor(extension: string): { bg: string; text: string; icon: string } {
|
||||
const ext = extension.toLowerCase();
|
||||
|
||||
const colorMap: Record<string, { bg: string; text: string; icon: string }> = {
|
||||
pdf: { bg: '#ff4444', text: '#ffffff', icon: '📄' },
|
||||
doc: { bg: '#2196f3', text: '#ffffff', icon: '📝' },
|
||||
docx: { bg: '#2196f3', text: '#ffffff', icon: '📝' },
|
||||
xls: { bg: '#4caf50', text: '#ffffff', icon: '📊' },
|
||||
xlsx: { bg: '#4caf50', text: '#ffffff', icon: '📊' },
|
||||
ppt: { bg: '#ff9800', text: '#ffffff', icon: '📈' },
|
||||
pptx: { bg: '#ff9800', text: '#ffffff', icon: '📈' },
|
||||
txt: { bg: '#607d8b', text: '#ffffff', icon: '📃' },
|
||||
rtf: { bg: '#795548', text: '#ffffff', icon: '📃' },
|
||||
odt: { bg: '#3f51b5', text: '#ffffff', icon: '📝' },
|
||||
ods: { bg: '#009688', text: '#ffffff', icon: '📊' },
|
||||
odp: { bg: '#e91e63', text: '#ffffff', icon: '📈' }
|
||||
};
|
||||
|
||||
return colorMap[ext] || { bg: '#9e9e9e', text: '#ffffff', icon: '📄' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate simple placeholder thumbnail
|
||||
*/
|
||||
function generatePlaceholderThumbnail(file: File): string {
|
||||
const canvas = document.createElement('canvas');
|
||||
@ -28,301 +50,103 @@ function generatePlaceholderThumbnail(file: File): string {
|
||||
canvas.height = 150;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
// Get file extension for color theming
|
||||
const extension = file.name.split('.').pop()?.toUpperCase() || 'FILE';
|
||||
const colorScheme = getFileTypeColorScheme(extension);
|
||||
const extension = file.name.split('.').pop() || 'file';
|
||||
const colors = getFileTypeColor(extension);
|
||||
|
||||
// Create gradient background
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||
gradient.addColorStop(0, colorScheme.bgTop);
|
||||
gradient.addColorStop(1, colorScheme.bgBottom);
|
||||
// Colored background
|
||||
ctx.fillStyle = colors.bg;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Rounded rectangle background
|
||||
drawRoundedRect(ctx, 8, 8, canvas.width - 16, canvas.height - 16, 8);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
|
||||
// Subtle shadow/border
|
||||
ctx.strokeStyle = colorScheme.border;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Modern document icon
|
||||
drawModernDocumentIcon(ctx, canvas.width / 2, 45, colorScheme.icon);
|
||||
|
||||
// Extension badge
|
||||
drawExtensionBadge(ctx, canvas.width / 2, canvas.height / 2 + 15, extension, colorScheme);
|
||||
|
||||
// File size with subtle styling
|
||||
const sizeText = formatFileSize(file.size);
|
||||
ctx.font = '11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||
ctx.fillStyle = colorScheme.textSecondary;
|
||||
// File icon
|
||||
ctx.font = '48px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(sizeText, canvas.width / 2, canvas.height - 15);
|
||||
ctx.fillStyle = colors.text;
|
||||
ctx.fillText(colors.icon, canvas.width / 2, canvas.height / 2);
|
||||
|
||||
// File extension
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillStyle = colors.text;
|
||||
ctx.fillText(extension.toUpperCase(), canvas.width / 2, canvas.height - 20);
|
||||
|
||||
return canvas.toDataURL();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color scheme based on file extension
|
||||
* Generate PDF thumbnail from first page
|
||||
*/
|
||||
function getFileTypeColorScheme(extension: string) {
|
||||
const schemes: Record<string, any> = {
|
||||
// Documents
|
||||
'PDF': { bgTop: '#FF6B6B20', bgBottom: '#FF6B6B10', border: '#FF6B6B40', icon: '#FF6B6B', badge: '#FF6B6B', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
'DOC': { bgTop: '#4ECDC420', bgBottom: '#4ECDC410', border: '#4ECDC440', icon: '#4ECDC4', badge: '#4ECDC4', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
'DOCX': { bgTop: '#4ECDC420', bgBottom: '#4ECDC410', border: '#4ECDC440', icon: '#4ECDC4', badge: '#4ECDC4', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
'TXT': { bgTop: '#95A5A620', bgBottom: '#95A5A610', border: '#95A5A640', icon: '#95A5A6', badge: '#95A5A6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
async function generatePdfThumbnail(file: File, scale: number): Promise<string> {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await getDocument({ data: arrayBuffer }).promise;
|
||||
|
||||
// Spreadsheets
|
||||
'XLS': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
'XLSX': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
'CSV': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
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");
|
||||
|
||||
// Presentations
|
||||
'PPT': { bgTop: '#E67E2220', bgBottom: '#E67E2210', border: '#E67E2240', icon: '#E67E22', badge: '#E67E22', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
'PPTX': { bgTop: '#E67E2220', bgBottom: '#E67E2210', border: '#E67E2240', icon: '#E67E22', badge: '#E67E22', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
if (!context) {
|
||||
throw new Error('Could not get canvas context');
|
||||
}
|
||||
|
||||
// Archives
|
||||
'ZIP': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
'RAR': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
'7Z': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
const thumbnail = canvas.toDataURL();
|
||||
|
||||
// Default
|
||||
'DEFAULT': { bgTop: '#74B9FF20', bgBottom: '#74B9FF10', border: '#74B9FF40', icon: '#74B9FF', badge: '#74B9FF', textPrimary: '#FFFFFF', textSecondary: '#666666' }
|
||||
};
|
||||
|
||||
return schemes[extension] || schemes['DEFAULT'];
|
||||
pdf.destroy();
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw rounded rectangle
|
||||
*/
|
||||
function drawRoundedRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + width - radius, y);
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
||||
ctx.lineTo(x + width, y + height - radius);
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
||||
ctx.lineTo(x + radius, y + height);
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
||||
ctx.lineTo(x, y + radius);
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw modern document icon
|
||||
*/
|
||||
function drawModernDocumentIcon(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, color: string) {
|
||||
const size = 24;
|
||||
ctx.fillStyle = color;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
// Document body
|
||||
drawRoundedRect(ctx, centerX - size/2, centerY - size/2, size, size * 1.2, 3);
|
||||
ctx.fill();
|
||||
|
||||
// Folded corner
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX + size/2 - 6, centerY - size/2);
|
||||
ctx.lineTo(centerX + size/2, centerY - size/2 + 6);
|
||||
ctx.lineTo(centerX + size/2 - 6, centerY - size/2 + 6);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = '#FFFFFF40';
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw extension badge
|
||||
*/
|
||||
function drawExtensionBadge(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, extension: string, colorScheme: any) {
|
||||
const badgeWidth = Math.max(extension.length * 8 + 16, 40);
|
||||
const badgeHeight = 22;
|
||||
|
||||
// Badge background
|
||||
drawRoundedRect(ctx, centerX - badgeWidth/2, centerY - badgeHeight/2, badgeWidth, badgeHeight, 11);
|
||||
ctx.fillStyle = colorScheme.badge;
|
||||
ctx.fill();
|
||||
|
||||
// Badge text
|
||||
ctx.font = 'bold 11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||
ctx.fillStyle = colorScheme.textPrimary;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(extension, centerX, centerY + 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size for display
|
||||
*/
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate thumbnail for any file type
|
||||
* 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);
|
||||
const placeholder = generatePlaceholderThumbnail(file);
|
||||
console.log('🎯 Generated placeholder thumbnail for large file:', file.name);
|
||||
return placeholder;
|
||||
// Skip very large files
|
||||
if (file.size >= 100 * 1024 * 1024) {
|
||||
return generatePlaceholderThumbnail(file);
|
||||
}
|
||||
|
||||
// Handle image files - use original file directly
|
||||
// Handle image files
|
||||
if (file.type.startsWith('image/')) {
|
||||
console.log('🎯 Creating blob URL for image file:', file.name);
|
||||
const url = URL.createObjectURL(file);
|
||||
console.log('🎯 Created image blob URL:', url);
|
||||
return url;
|
||||
return URL.createObjectURL(file);
|
||||
}
|
||||
|
||||
// Handle PDF files
|
||||
if (!file.type.startsWith('application/pdf')) {
|
||||
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;
|
||||
if (file.type.startsWith('application/pdf')) {
|
||||
const scale = calculateScaleFromFileSize(file.size);
|
||||
try {
|
||||
return await generatePdfThumbnail(file, scale);
|
||||
} catch (error) {
|
||||
return generatePlaceholderThumbnail(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate quality scale based on file size
|
||||
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)`);
|
||||
try {
|
||||
// Only read first 2MB for thumbnail generation to save memory
|
||||
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
|
||||
}).promise;
|
||||
|
||||
const page = await pdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale }); // Dynamic scale based on file size
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Could not get canvas context');
|
||||
}
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
const thumbnail = canvas.toDataURL();
|
||||
|
||||
// Immediately clean up memory after thumbnail generation
|
||||
pdf.destroy();
|
||||
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} - trying fallback with full file`);
|
||||
// Return a placeholder or try with full file instead of chunk
|
||||
try {
|
||||
const fullArrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await getDocument({
|
||||
data: fullArrayBuffer,
|
||||
disableAutoFetch: true,
|
||||
disableStream: true,
|
||||
verbosity: 0 // Reduce PDF.js warnings
|
||||
}).promise;
|
||||
|
||||
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) {
|
||||
throw new Error('Could not get canvas context');
|
||||
}
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
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);
|
||||
console.log('🎯 Using placeholder thumbnail for', file.name);
|
||||
return generatePlaceholderThumbnail(file);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
console.log('🎯 Using placeholder thumbnail for', file.name);
|
||||
return generatePlaceholderThumbnail(file);
|
||||
}
|
||||
// All other files get placeholder
|
||||
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);
|
||||
// Skip very large files
|
||||
if (file.size >= 100 * 1024 * 1024) {
|
||||
const thumbnail = generatePlaceholderThumbnail(file);
|
||||
return { thumbnail, pageCount: 1 }; // Default to 1 for large files
|
||||
return { thumbnail, pageCount: 1 };
|
||||
}
|
||||
|
||||
// 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 arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await getDocument({ data: arrayBuffer }).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");
|
||||
@ -338,55 +162,10 @@ export async function generateThumbnailWithMetadata(file: File): Promise<Thumbna
|
||||
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