Refactor thumbnail generation code

This commit is contained in:
James Brunton 2025-08-15 17:19:09 +01:00
parent 01a1629aeb
commit 0eee52dedf

View File

@ -224,6 +224,25 @@ function drawLargeLockIcon(ctx: CanvasRenderingContext2D, centerX: number, cente
ctx.fillRect(keyholeX - 2, keyholeY, 4, 8); ctx.fillRect(keyholeX - 2, keyholeY, 4, 8);
} }
/**
* Generate standard PDF thumbnail by rendering first page
*/
async function generateStandardPDFThumbnail(pdf: any, scale: number): Promise<string> {
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;
return canvas.toDataURL();
}
/** /**
* Draw extension badge * Draw extension badge
*/ */
@ -281,98 +300,65 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
console.log('Generating thumbnail for', file.name); console.log('Generating 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 {
// 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();
// 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();
let thumbnail: string;
try {
const pdf = await getDocument({ const pdf = await getDocument({
data: arrayBuffer, data: arrayBuffer,
disableAutoFetch: true, disableAutoFetch: true,
disableStream: true disableStream: true
}).promise; }).promise;
// Check if PDF is encrypted thumbnail = await generateStandardPDFThumbnail(pdf, scale);
if ((pdf as any).isEncrypted) {
console.log('PDF is encrypted, generating encrypted thumbnail:', file.name);
pdf.destroy();
return generateEncryptedPDFThumbnail(file);
}
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 // Immediately clean up memory after thumbnail generation
pdf.destroy(); pdf.destroy();
console.log('Thumbnail generated and PDF destroyed for', file.name);
return thumbnail;
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
// Check if error indicates encrypted PDF // Check if PDF is encrypted
const errorMessage = error.message.toLowerCase(); if (error.name === "PasswordException") {
if (errorMessage.includes('password') || errorMessage.includes('encrypted')) { thumbnail = generateEncryptedPDFThumbnail(file);
console.log('PDF appears to be encrypted based on error, generating encrypted thumbnail:', file.name);
return generateEncryptedPDFThumbnail(file);
}
if (error.name === 'InvalidPDFException') { } else if (error.name === 'InvalidPDFException') {
console.warn(`PDF structure issue for ${file.name} - using fallback thumbnail`); console.warn(`PDF structure issue for ${file.name} - using fallback thumbnail`);
// Return a placeholder or try with full file instead of chunk // Return a placeholder or try with full file instead of chunk
const fullArrayBuffer = await file.arrayBuffer();
try { try {
const fullArrayBuffer = await file.arrayBuffer();
const pdf = await getDocument({ const pdf = await getDocument({
data: fullArrayBuffer, data: fullArrayBuffer,
disableAutoFetch: true, disableAutoFetch: true,
disableStream: true, disableStream: true,
verbosity: 0 // Reduce PDF.js warnings verbosity: 0 // Reduce PDF.js warnings
}).promise; }).promise;
thumbnail = await generateStandardPDFThumbnail(pdf, scale);
// Check if PDF is encrypted in fallback too
if ((pdf as any).isEncrypted) {
console.log('PDF is encrypted in fallback, generating encrypted thumbnail:', file.name);
pdf.destroy();
return generateEncryptedPDFThumbnail(file);
}
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(); pdf.destroy();
return thumbnail;
} catch (fallbackError) { } catch (fallbackError) {
console.warn('Fallback thumbnail generation also failed for', file.name, fallbackError); if (fallbackError instanceof Error) {
return undefined; // Check if PDF is encrypted
if (fallbackError.name === "PasswordException") {
thumbnail = generateEncryptedPDFThumbnail(file);
} else {
console.warn('Fallback thumbnail generation also failed for', file.name, fallbackError);
return undefined;
}
} else {
throw fallbackError; // Re-throw non-Error exceptions
}
} }
} else { } else {
console.warn('Failed to generate thumbnail for', file.name, error); console.warn('Unknown error thrown. Failed to generate thumbnail for', file.name, error);
return undefined; return undefined;
} }
} else {
throw error; // Re-throw non-Error exceptions
} }
console.warn('Unknown error generating thumbnail for', file.name, error);
return undefined;
} }
return thumbnail;
} }