From 33e6a658f471bffc9cbdfd6f9dd37b64f17afe83 Mon Sep 17 00:00:00 2001 From: kenilkb Date: Thu, 16 Jul 2026 09:25:11 +0530 Subject: [PATCH] Feat: Add dynamic client-side PDF first-page thumbnail rendering using PDF.js --- .../features/assets/components/AssetCard.tsx | 178 +++++++++--------- .../assets/components/PdfThumbnail.tsx | 129 +++++++++++++ 2 files changed, 221 insertions(+), 86 deletions(-) create mode 100644 Channel-Frontend/src/features/assets/components/PdfThumbnail.tsx diff --git a/Channel-Frontend/src/features/assets/components/AssetCard.tsx b/Channel-Frontend/src/features/assets/components/AssetCard.tsx index 758ed36..fa47ec2 100644 --- a/Channel-Frontend/src/features/assets/components/AssetCard.tsx +++ b/Channel-Frontend/src/features/assets/components/AssetCard.tsx @@ -17,6 +17,7 @@ import { import type { Asset } from '../../../types/assets'; import type { User } from '../../../types/auth'; import { axiosInstance } from '../../../services/axios'; +import { PdfThumbnail } from './PdfThumbnail'; interface AssetCardProps { asset: Asset; @@ -223,102 +224,107 @@ export const AssetCard: React.FC = ({ if (placeholder) placeholder.style.display = 'flex'; }} /> - ) : null} - -
- {isPdf ? ( -
-
-
- PDF Document -
-
-
-
-
-
-
-
-
- PDF RESOURCE - + ) : isPdf ? ( + +
+
+ PDF Document +
+
+
+
+
+
+
+
+
+ PDF RESOURCE + +
-
- ) : isPresentation ? ( -
-
-
-
-
-
-
-
-
- PRESENTATION - PPTX + } + /> + ) : ( +
+ {isPresentation ? ( +
+
+
+
+
+
+
+
+
+ PRESENTATION + PPTX +
-
- ) : isWord ? ( -
-
-
- Word Doc -
-
-
-
-
-
-
-
- DOCX RESOURCE - + ) : isWord ? ( +
+
+
+ Word Doc +
+
+
+
+
+
+
+
+ DOCX RESOURCE + +
-
- ) : isSpreadsheet ? ( -
-
-
- Spreadsheet -
-
-
-
-
-
-
-
-
-
- XLSX SHEET - EXCEL + ) : isSpreadsheet ? ( +
+
+
+ Spreadsheet +
+
+
+
+
+
+
+
+
+
+ XLSX SHEET + EXCEL +
-
- ) : ( -
-
-
- Resource File -
-
- -
-
- BINARY - {asset.type.split('/').pop() || 'FILE'} + ) : ( +
+
+
+ Resource File +
+
+ +
+
+ BINARY + {asset.type.split('/').pop() || 'FILE'} +
-
- )} -
+ )} +
+ )}
diff --git a/Channel-Frontend/src/features/assets/components/PdfThumbnail.tsx b/Channel-Frontend/src/features/assets/components/PdfThumbnail.tsx new file mode 100644 index 0000000..7229189 --- /dev/null +++ b/Channel-Frontend/src/features/assets/components/PdfThumbnail.tsx @@ -0,0 +1,129 @@ +import React, { useState, useEffect, useRef } from 'react'; + +interface PdfThumbnailProps { + url: string; + title: string; + fallback: React.ReactNode; +} + +// Simple in-memory cache for rendered thumbnails to avoid reprocessing the same PDFs during a session +const thumbnailCache: Record = {}; + +export const PdfThumbnail: React.FC = ({ url, title, fallback }) => { + const [thumbnailUrl, setThumbnailUrl] = useState(thumbnailCache[url] || null); + const [error, setError] = useState(false); + const [loading, setLoading] = useState(!thumbnailCache[url]); + const canvasRef = useRef(null); + + useEffect(() => { + if (thumbnailUrl) { + setLoading(false); + return; + } + + let isMounted = true; + + const loadPdfAndRender = async () => { + try { + // 1. Ensure PDF.js script is loaded dynamically + if (!(window as any).pdfjsLib) { + await new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js'; + script.async = true; + script.onload = () => resolve(); + script.onerror = () => reject(new Error('Failed to load PDF.js library')); + document.body.appendChild(script); + }); + } + + const pdfjs = (window as any).pdfjsLib; + if (!pdfjs) throw new Error('PDF.js lib not available'); + + // Configure PDF.js Worker + pdfjs.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js'; + + // 2. Fetch and load the PDF document + const loadingTask = pdfjs.getDocument(url); + const pdf = await loadingTask.promise; + + if (!isMounted) return; + + // 3. Load the first page of the PDF + const page = await pdf.getPage(1); + + if (!isMounted) return; + + // Create a temporary canvas if not in DOM yet + const canvas = canvasRef.current || document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) throw new Error('Could not get 2D context'); + + // We want a thumbnail width of around 220px to fit the card well + const originalViewport = page.getViewport({ scale: 1.0 }); + const scale = 220 / originalViewport.width; + const viewport = page.getViewport({ scale }); + + canvas.width = viewport.width; + canvas.height = viewport.height; + + // Render PDF page to canvas + const renderContext = { + canvasContext: context, + viewport: viewport, + }; + + await page.render(renderContext).promise; + + if (!isMounted) return; + + // Convert canvas rendering to a base64 image URL for high performance image display + const dataUrl = canvas.toDataURL('image/jpeg', 0.8); + thumbnailCache[url] = dataUrl; + + if (isMounted) { + setThumbnailUrl(dataUrl); + setLoading(false); + } + } catch (err) { + console.error('[PdfThumbnail] Failed to render first page preview:', err); + if (isMounted) { + setError(true); + setLoading(false); + } + } + }; + + loadPdfAndRender(); + + return () => { + isMounted = false; + }; + }, [url]); + + if (error) { + return <>{fallback}; + } + + return ( +
+ {/* Hidden canvas for offscreen rendering */} + + + {loading ? ( +
+
+ Generating preview... +
+ ) : thumbnailUrl ? ( + {title} + ) : ( + fallback + )} +
+ ); +};