Add EnvelopeViewer for PDF rendering with pdf.js
Introduced a new Blazor component `EnvelopeViewer.razor` to display PDF documents with a user-friendly interface. The component supports dynamic routing via `/envelope/{EnvelopeKey}` and integrates the `pdf.js` library for efficient PDF rendering in the browser.
Key features include:
- Zoom and navigation controls (Next/Previous page, Zoom In/Out).
- Loading spinner, error handling, and fallback UI for robustness.
- Responsive design with modern styling using CSS.
- JavaScript interop via `pdf-viewer.js` for managing PDF rendering, scaling, and navigation.
Added lifecycle methods (`OnInitializedAsync`, `OnAfterRenderAsync`) for dynamic loading and rendering. Ensured asynchronous operations for smooth user interactions. Included a `DisposeAsync` method for cleanup.
This commit is contained in:
168
EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js
Normal file
168
EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js
Normal file
@@ -0,0 +1,168 @@
|
||||
// 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,
|
||||
|
||||
async initialize(canvasId, pdfDataUrl) {
|
||||
try {
|
||||
console.log('PDF.js initialization started for canvas:', canvasId);
|
||||
|
||||
// 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');
|
||||
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
console.log('Page rendered successfully');
|
||||
|
||||
this.pageRendering = false;
|
||||
|
||||
if (this.pageNumPending !== null) {
|
||||
this.renderPage(this.pageNumPending);
|
||||
this.pageNumPending = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error rendering page:', error);
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user