Added `DotNetObjectReference` in `EnvelopeViewer.razor` to enable Blazor-JS communication. Updated `OnAfterRenderAsync` to pass the reference to `pdfViewer.initialize`. Introduced `[JSInvokable]` method `OnZoomChanged` to handle zoom updates from JavaScript. Enhanced `DisposeAsync` to clean up resources, including disposing of the `.NET reference` and invoking a JavaScript `dispose` method. In `pdf-viewer.js`, modified `initialize` to accept a `.NET reference` and added `attachWheelEvent` to handle zooming via mouse wheel with `Ctrl`/`Meta` key. Updated `dispose` to clean up event listeners and reset the `.NET reference`. Added `getScale` to retrieve the current zoom scale. Improved resource cleanup and event handling to prevent memory leaks and ensure stability.
230 lines
6.8 KiB
JavaScript
230 lines
6.8 KiB
JavaScript
// 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 {
|
|
console.log('PDF.js initialization started for canvas:', canvasId);
|
|
|
|
// Store .NET reference for callbacks
|
|
this.dotNetReference = dotNetRef;
|
|
|
|
// Wait for PDF.js to load
|
|
if (typeof window.pdfjsLib === 'undefined') {
|
|
console.error('PDF.js library not loaded, waiting...');
|
|
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) {
|
|
console.error('Canvas element not found:', canvasId);
|
|
return false;
|
|
}
|
|
|
|
console.log('Canvas element found, loading PDF...');
|
|
|
|
this.ctx = this.canvas.getContext('2d');
|
|
|
|
// Attach mouse wheel event listener
|
|
this.attachWheelEvent();
|
|
|
|
// Load PDF from data URL
|
|
const uint8Array = this.base64ToUint8Array(pdfDataUrl);
|
|
console.log('PDF data converted to Uint8Array, size:', uint8Array.length);
|
|
|
|
const loadingTask = pdfjsLib.getDocument({ data: uint8Array });
|
|
this.pdfDoc = await loadingTask.promise;
|
|
this.totalPages = this.pdfDoc.numPages;
|
|
|
|
console.log('PDF loaded successfully, total pages:', this.totalPages);
|
|
|
|
// Render first page
|
|
await this.renderPage(this.pageNum);
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error initializing PDF viewer:', 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
|
|
this.zoomIn();
|
|
if (this.dotNetReference) {
|
|
this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale);
|
|
}
|
|
} else {
|
|
// Scroll down = Zoom Out
|
|
this.zoomOut();
|
|
if (this.dotNetReference) {
|
|
this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale);
|
|
}
|
|
}
|
|
}
|
|
}, { passive: false });
|
|
|
|
this.wheelEventAttached = true;
|
|
console.log('Wheel event listener attached to document body');
|
|
},
|
|
|
|
async waitForPdfJs() {
|
|
for (let i = 0; i < 50; i++) {
|
|
if (typeof window.pdfjsLib !== 'undefined') {
|
|
console.log('PDF.js loaded after', i * 100, 'ms');
|
|
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 {
|
|
const page = await this.pdfDoc.getPage(num);
|
|
const viewport = page.getViewport({ scale: this.scale });
|
|
|
|
console.log('Rendering page:', num, 'Viewport:', viewport.width, 'x', viewport.height);
|
|
|
|
this.canvas.height = viewport.height;
|
|
this.canvas.width = viewport.width;
|
|
|
|
const renderContext = {
|
|
canvasContext: this.ctx,
|
|
viewport: viewport
|
|
};
|
|
|
|
if (this.currentRenderTask) {
|
|
console.log('Cancelling previous render task');
|
|
this.currentRenderTask.cancel();
|
|
}
|
|
|
|
this.currentRenderTask = page.render(renderContext);
|
|
await this.currentRenderTask.promise;
|
|
|
|
console.log('Page rendered successfully');
|
|
|
|
this.currentRenderTask = null;
|
|
this.pageRendering = false;
|
|
|
|
if (this.pageNumPending !== null) {
|
|
this.renderPage(this.pageNumPending);
|
|
this.pageNumPending = null;
|
|
}
|
|
} catch (error) {
|
|
if (error.name === 'RenderingCancelledException') {
|
|
console.log('Rendering cancelled, will render pending page');
|
|
} else {
|
|
console.error('Error rendering page:', 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() {
|
|
this.scale += 0.25;
|
|
this.queueRenderPage(this.pageNum);
|
|
},
|
|
|
|
zoomOut() {
|
|
if (this.scale > 0.5) {
|
|
this.scale -= 0.25;
|
|
this.queueRenderPage(this.pageNum);
|
|
}
|
|
},
|
|
|
|
getCurrentPage() {
|
|
return this.pageNum;
|
|
},
|
|
|
|
getTotalPages() {
|
|
return this.totalPages;
|
|
},
|
|
|
|
getScale() {
|
|
return this.scale;
|
|
},
|
|
|
|
dispose() {
|
|
// Clean up event listeners
|
|
if (this.wheelEventAttached) {
|
|
// Note: We can't remove the exact listener without keeping a reference
|
|
// but we can at least mark it as disposed
|
|
this.wheelEventAttached = false;
|
|
this.dotNetReference = null;
|
|
console.log('PDF viewer disposed');
|
|
}
|
|
}
|
|
};
|
|
|