// PDF.js Viewer for Blazor WASM window.pdfViewer = { pdfDoc: null, pageNum: 1, pageRendering: false, pageNumPending: null, scale: 1.5, canvas: null, ctx: null, totalPages: 0, currentRenderTask: null, dotNetReference: null, wheelEventAttached: false, async initialize(canvasId, pdfDataUrl, dotNetRef) { try { this.dotNetReference = dotNetRef; if (typeof window.pdfjsLib === 'undefined') { await this.waitForPdfJs(); } const pdfjsLib = window.pdfjsLib; pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; this.canvas = document.getElementById(canvasId); if (!this.canvas) { return false; } this.ctx = this.canvas.getContext('2d'); this.attachWheelEvent(); const uint8Array = this.base64ToUint8Array(pdfDataUrl); const loadingTask = pdfjsLib.getDocument({ data: uint8Array }); this.pdfDoc = await loadingTask.promise; this.totalPages = this.pdfDoc.numPages; await this.renderPage(this.pageNum); return true; } catch (error) { console.error('PDF viewer initialization failed:', error); return false; } }, attachWheelEvent() { if (this.wheelEventAttached) return; // Attach to the entire document body for global zoom control document.body.addEventListener('wheel', (e) => { // Check if Ctrl key is pressed (zoom gesture) if (e.ctrlKey || e.metaKey) { e.preventDefault(); if (e.deltaY < 0) { // Scroll up = Zoom In (1% ad?m) this.scale = Math.min(this.scale + 0.01, 3.0); this.queueRenderPage(this.pageNum); if (this.dotNetReference) { this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale); } } else { // Scroll down = Zoom Out (1% ad?m) this.scale = Math.max(this.scale - 0.01, 0.5); this.queueRenderPage(this.pageNum); if (this.dotNetReference) { this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale); } } } }, { passive: false }); this.wheelEventAttached = true; }, async waitForPdfJs() { for (let i = 0; i < 50; i++) { if (typeof window.pdfjsLib !== 'undefined') { return; } await new Promise(resolve => setTimeout(resolve, 100)); } throw new Error('PDF.js failed to load after 5 seconds'); }, base64ToUint8Array(base64) { // Remove data URL prefix if present const base64String = base64.includes(',') ? base64.split(',')[1] : base64; const raw = atob(base64String); const uint8Array = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) { uint8Array[i] = raw.charCodeAt(i); } return uint8Array; }, async renderPage(num) { this.pageRendering = true; try { // Get scroll container const container = this.canvas.closest('.pdf-frame'); // Store scroll position and viewport center BEFORE rendering let scrollLeft = 0, scrollTop = 0; let centerX = 0, centerY = 0; let oldWidth = this.canvas.width; let oldHeight = this.canvas.height; if (container) { scrollLeft = container.scrollLeft; scrollTop = container.scrollTop; centerX = scrollLeft + container.clientWidth / 2; centerY = scrollTop + container.clientHeight / 2; } const page = await this.pdfDoc.getPage(num); const viewport = page.getViewport({ scale: this.scale }); this.canvas.height = viewport.height; this.canvas.width = viewport.width; const renderContext = { canvasContext: this.ctx, viewport: viewport }; if (this.currentRenderTask) { this.currentRenderTask.cancel(); } this.currentRenderTask = page.render(renderContext); await this.currentRenderTask.promise; // Restore viewport center position AFTER rendering if (container && oldWidth > 0 && oldHeight > 0) { const scaleX = this.canvas.width / oldWidth; const scaleY = this.canvas.height / oldHeight; const newCenterX = centerX * scaleX; const newCenterY = centerY * scaleY; container.scrollLeft = newCenterX - container.clientWidth / 2; container.scrollTop = newCenterY - container.clientHeight / 2; } this.currentRenderTask = null; this.pageRendering = false; if (this.pageNumPending !== null) { this.renderPage(this.pageNumPending); this.pageNumPending = null; } } catch (error) { if (error.name !== 'RenderingCancelledException') { console.error('Render error:', error); } this.currentRenderTask = null; this.pageRendering = false; } }, queueRenderPage(num) { if (this.pageRendering) { this.pageNumPending = num; } else { this.renderPage(num); } }, nextPage() { if (this.pageNum >= this.totalPages) { return false; } this.pageNum++; this.queueRenderPage(this.pageNum); return true; }, previousPage() { if (this.pageNum <= 1) { return false; } this.pageNum--; this.queueRenderPage(this.pageNum); return true; }, goToPage(num) { if (num < 1 || num > this.totalPages) { return false; } this.pageNum = num; this.queueRenderPage(this.pageNum); return true; }, zoomIn() { // 1% art?? this.scale = Math.min(this.scale + 0.01, 3.0); this.queueRenderPage(this.pageNum); }, zoomOut() { // 1% azal?? this.scale = Math.max(this.scale - 0.01, 0.5); this.queueRenderPage(this.pageNum); }, setScale(scale) { if (scale >= 0.5 && scale <= 3.0) { this.scale = scale; this.queueRenderPage(this.pageNum); } }, async fitToWidth() { const container = this.canvas.closest('.pdf-frame'); if (!container || !this.pdfDoc) return; try { const page = await this.pdfDoc.getPage(this.pageNum); const viewport = page.getViewport({ scale: 1.0 }); const containerWidth = container.clientWidth - 80; const optimalScale = containerWidth / viewport.width; this.scale = Math.min(Math.max(optimalScale, 0.5), 3.0); this.queueRenderPage(this.pageNum); } catch (error) { console.error('Error fitting to width:', error); } }, getCurrentPage() { return this.pageNum; }, getTotalPages() { return this.totalPages; }, getScale() { return this.scale; }, dispose() { if (this.wheelEventAttached) { this.wheelEventAttached = false; this.dotNetReference = null; } } };