Files
EnvelopeGenerator/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js
TekH ca3b74f939 Improve PDF rendering quality and user experience
Added `image-rendering` properties and opacity transitions to `.pdf-canvas` for smoother visual effects during rendering. Introduced a `.pdf-canvas.rendering` class to indicate rendering state.

Enhanced `renderPage` method to support HiDPI displays by scaling the viewport based on device pixel ratio (up to 2) and setting both internal resolution and CSS display size for better rendering quality. Enabled high-quality rendering with `imageSmoothingEnabled` and `imageSmoothingQuality`.

Updated scroll container reference to `.pdf-canvas-wrapper` for proper alignment. Added logic to manage the `rendering` class during and after rendering, including error handling to ensure UI consistency. These changes improve both functionality and aesthetics of the PDF viewer.
2026-06-06 11:16:05 +02:00

372 lines
12 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 {
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;
// Add rendering class for smooth transition
this.canvas.classList.add('rendering');
try {
// Get scroll container
const container = this.canvas.closest('.pdf-canvas-wrapper');
// 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);
// HiDPI support for main canvas
const dpr = window.devicePixelRatio || 1;
const viewport = page.getViewport({ scale: this.scale * Math.min(dpr, 2) });
// Set internal canvas resolution (high quality)
this.canvas.width = viewport.width;
this.canvas.height = viewport.height;
// Set CSS display size (visual size)
this.canvas.style.width = `${viewport.width / dpr}px`;
this.canvas.style.height = `${viewport.height / dpr}px`;
const renderContext = {
canvasContext: this.ctx,
viewport: viewport
};
if (this.currentRenderTask) {
this.currentRenderTask.cancel();
}
// Enable high-quality rendering
this.ctx.imageSmoothingEnabled = true;
this.ctx.imageSmoothingQuality = 'high';
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;
}
// Remove rendering class after completion
this.canvas.classList.remove('rendering');
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.canvas.classList.remove('rendering');
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);
}
},
async renderThumbnail(pageNum, canvasId) {
if (!this.pdfDoc) {
console.error('PDF document not loaded for thumbnail:', pageNum);
return;
}
try {
// Wait for canvas to be in DOM
let canvas = document.getElementById(canvasId);
let retries = 0;
while (!canvas && retries < 10) {
await new Promise(resolve => setTimeout(resolve, 100));
canvas = document.getElementById(canvasId);
retries++;
}
if (!canvas) {
console.error('Canvas not found after retries:', canvasId);
return;
}
const page = await this.pdfDoc.getPage(pageNum);
// High-quality rendering with HiDPI support
const dpr = window.devicePixelRatio || 1;
const baseScale = 0.75; // Increased base scale for better quality
const scale = baseScale * Math.min(dpr, 2); // Cap at 2x for performance
const viewport = page.getViewport({ scale: scale });
// Set actual canvas pixel dimensions (internal resolution)
canvas.width = viewport.width;
canvas.height = viewport.height;
// Remove any inline styles - let CSS handle display size
canvas.style.width = '';
canvas.style.height = '';
const ctx = canvas.getContext('2d');
// Enable maximum quality rendering
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
await page.render(renderContext).promise;
} catch (error) {
console.error('Error rendering thumbnail', pageNum, ':', error);
}
},
getCurrentPage() {
return this.pageNum;
},
getTotalPages() {
return this.totalPages;
},
getScale() {
return this.scale;
},
dispose() {
if (this.wheelEventAttached) {
this.wheelEventAttached = false;
this.dotNetReference = null;
}
this.detachResizeListeners();
},
// Resizable splitter functionality
isResizing: false,
resizeMouseMoveHandler: null,
resizeMouseUpHandler: null,
attachResizeListeners(dotNetRef) {
this.dotNetReference = dotNetRef;
this.resizeMouseMoveHandler = (e) => {
if (this.isResizing && this.dotNetReference) {
this.dotNetReference.invokeMethodAsync('OnSplitterMouseMove', e.clientX);
}
};
this.resizeMouseUpHandler = () => {
if (this.isResizing && this.dotNetReference) {
this.isResizing = false;
this.dotNetReference.invokeMethodAsync('OnSplitterMouseUp');
}
};
document.addEventListener('mousemove', this.resizeMouseMoveHandler);
document.addEventListener('mouseup', this.resizeMouseUpHandler);
},
detachResizeListeners() {
if (this.resizeMouseMoveHandler) {
document.removeEventListener('mousemove', this.resizeMouseMoveHandler);
this.resizeMouseMoveHandler = null;
}
if (this.resizeMouseUpHandler) {
document.removeEventListener('mouseup', this.resizeMouseUpHandler);
this.resizeMouseUpHandler = null;
}
this.isResizing = false;
},
startResize() {
this.isResizing = true;
}
};