Added functionality to maintain the scroll position and viewport center of the `.pdf-frame` container during PDF page rendering. This ensures the user's view remains centered on the same content after re-rendering, even if the canvas dimensions change. Implemented logic to store the scroll position and viewport center before rendering and restore them afterward using scaling factors calculated from the canvas's old and new dimensions.
258 lines
8.0 KiB
JavaScript
258 lines
8.0 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 {
|
|
// 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 });
|
|
|
|
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');
|
|
|
|
// 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.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');
|
|
}
|
|
}
|
|
};
|
|
|