Implement drag-and-drop PDF loading via JS interop and DotNetObjectReference. Refactor file loading logic and UI structure for clarity. Add IAsyncDisposable for resource cleanup. Update pdfInterop.js to handle drop events and send PDF data to Blazor.
330 lines
11 KiB
JavaScript
330 lines
11 KiB
JavaScript
(function () {
|
|
// Stick to pdf.js 3.11 UMD + classic worker for compatibility.
|
|
const PDF_JS_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
|
|
const WORKER_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
|
|
|
|
const state = {
|
|
pdfDoc: null,
|
|
pdfBytes: null,
|
|
lastViewport: null,
|
|
pdfJsReady: null,
|
|
};
|
|
|
|
function base64ToUint8(base64) {
|
|
const binStr = atob(base64);
|
|
const len = binStr.length;
|
|
const bytes = new Uint8Array(len);
|
|
for (let i = 0; i < len; i++) {
|
|
bytes[i] = binStr.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
async function reloadFromBase64(base64) {
|
|
state.pdfBytes = base64ToUint8(base64);
|
|
state.pdfDoc = await pdfjsLib.getDocument({ data: state.pdfBytes }).promise;
|
|
return { pages: state.pdfDoc.numPages };
|
|
}
|
|
|
|
function dataUrlDownload(dataUrl, filename) {
|
|
const a = document.createElement('a');
|
|
a.href = dataUrl;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
|
|
const pointerPads = new Map();
|
|
|
|
function loadScriptOnce(url) {
|
|
return new Promise((resolve, reject) => {
|
|
// If already present, resolve immediately
|
|
const existing = Array.from(document.getElementsByTagName('script')).find(s => s.src === url);
|
|
if (existing && existing.dataset.loaded === "true") {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const script = existing || document.createElement('script');
|
|
script.src = url;
|
|
script.defer = true;
|
|
script.onload = () => {
|
|
script.dataset.loaded = "true";
|
|
resolve();
|
|
};
|
|
script.onerror = (e) => reject(new Error(`Script load failed: ${url}`));
|
|
|
|
if (!existing) {
|
|
document.head.appendChild(script);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function ensurePdfJsLoaded() {
|
|
if (typeof pdfjsLib !== "undefined") {
|
|
return;
|
|
}
|
|
|
|
if (!state.pdfJsReady) {
|
|
state.pdfJsReady = loadScriptOnce(PDF_JS_SRC);
|
|
}
|
|
|
|
await state.pdfJsReady;
|
|
|
|
if (typeof pdfjsLib === "undefined") {
|
|
throw new Error("pdfjsLib could not be loaded");
|
|
}
|
|
}
|
|
|
|
window.pdfInterop = {
|
|
ensureReady: async () => {
|
|
// Ensure pdf.js is present and the worker path is set explicitly.
|
|
await ensurePdfJsLoaded();
|
|
if (pdfjsLib && pdfjsLib.GlobalWorkerOptions) {
|
|
if (pdfjsLib.GlobalWorkerOptions.workerSrc !== WORKER_SRC) {
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_SRC;
|
|
}
|
|
} else {
|
|
throw new Error("pdf.js not available after load");
|
|
}
|
|
},
|
|
loadPdf: async (base64) => {
|
|
await ensurePdfJsLoaded();
|
|
try {
|
|
const result = await reloadFromBase64(base64);
|
|
if (!result || !result.pages) {
|
|
throw new Error("PDF has keine Seiten erkannt");
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
console.error("pdfInterop.loadPdf failed", err);
|
|
throw err;
|
|
}
|
|
},
|
|
renderPage: async (pageIndex, canvasId, targetWidth) => {
|
|
await ensurePdfJsLoaded();
|
|
if (!state.pdfDoc) {
|
|
throw new Error('PDF not loaded');
|
|
}
|
|
const page = await state.pdfDoc.getPage(pageIndex + 1);
|
|
const rawViewport = page.getViewport({ scale: 1 });
|
|
const scale = targetWidth / rawViewport.width;
|
|
const viewport = page.getViewport({ scale });
|
|
|
|
let canvas = document.getElementById(canvasId);
|
|
if (!canvas) {
|
|
// give the UI a tiny delay to render the canvas into the DOM
|
|
await new Promise(r => setTimeout(r, 40));
|
|
canvas = document.getElementById(canvasId);
|
|
}
|
|
if (!canvas) {
|
|
console.error("renderPage: canvas not found", canvasId);
|
|
throw new Error('Canvas not found');
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = viewport.width;
|
|
canvas.height = viewport.height;
|
|
|
|
await page.render({ canvasContext: ctx, viewport }).promise;
|
|
|
|
state.lastViewport = {
|
|
width: viewport.width,
|
|
height: viewport.height,
|
|
pageWidth: rawViewport.width,
|
|
pageHeight: rawViewport.height,
|
|
};
|
|
|
|
return state.lastViewport;
|
|
},
|
|
applySignature: async (payload) => {
|
|
const {
|
|
base64,
|
|
pageIndex,
|
|
left,
|
|
top,
|
|
width,
|
|
height,
|
|
renderWidth,
|
|
renderHeight,
|
|
dataUrl,
|
|
autoDate,
|
|
} = payload;
|
|
|
|
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
|
|
const page = pdfDoc.getPage(pageIndex);
|
|
const scaleX = page.getWidth() / renderWidth;
|
|
const scaleY = page.getHeight() / renderHeight;
|
|
|
|
const pngImage = await pdfDoc.embedPng(dataUrl);
|
|
const drawWidth = width * scaleX;
|
|
const drawHeight = height * scaleY;
|
|
const x = left * scaleX;
|
|
const y = page.getHeight() - (top + height) * scaleY;
|
|
|
|
page.drawImage(pngImage, {
|
|
x,
|
|
y,
|
|
width: drawWidth,
|
|
height: drawHeight,
|
|
});
|
|
|
|
if (autoDate) {
|
|
const text = `Signed ${new Date().toLocaleString()}`;
|
|
page.drawText(text, {
|
|
x,
|
|
y: y - 14 * scaleY,
|
|
size: 14 * scaleX,
|
|
color: PDFLib.rgb(0.11, 0.25, 0.56),
|
|
});
|
|
}
|
|
|
|
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
|
|
await reloadFromBase64(updatedBase64);
|
|
return updatedBase64;
|
|
},
|
|
applyText: async (payload) => {
|
|
const {
|
|
base64,
|
|
pageIndex,
|
|
left,
|
|
top,
|
|
width,
|
|
height,
|
|
renderWidth,
|
|
renderHeight,
|
|
text,
|
|
fontSize,
|
|
} = payload;
|
|
|
|
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
|
|
const page = pdfDoc.getPage(pageIndex);
|
|
const scaleX = page.getWidth() / renderWidth;
|
|
const scaleY = page.getHeight() / renderHeight;
|
|
|
|
const x = left * scaleX;
|
|
const y = page.getHeight() - (top + height) * scaleY;
|
|
|
|
page.drawText(text, {
|
|
x,
|
|
y,
|
|
size: fontSize * scaleX,
|
|
color: PDFLib.rgb(0.2, 0.23, 0.28),
|
|
});
|
|
|
|
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
|
|
await reloadFromBase64(updatedBase64);
|
|
return updatedBase64;
|
|
},
|
|
downloadPdf: (base64, filename) => {
|
|
dataUrlDownload(`data:application/pdf;base64,${base64}`, filename);
|
|
},
|
|
initSignaturePad: (canvasId) => {
|
|
const canvas = document.getElementById(canvasId);
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.lineWidth = 2;
|
|
ctx.lineCap = 'round';
|
|
ctx.strokeStyle = '#1c3d8f';
|
|
|
|
const padState = {
|
|
drawing: false,
|
|
lastX: 0,
|
|
lastY: 0,
|
|
};
|
|
|
|
function start(e) {
|
|
padState.drawing = true;
|
|
const rect = canvas.getBoundingClientRect();
|
|
padState.lastX = e.clientX - rect.left;
|
|
padState.lastY = e.clientY - rect.top;
|
|
}
|
|
|
|
function move(e) {
|
|
if (!padState.drawing) return;
|
|
const rect = canvas.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
ctx.beginPath();
|
|
ctx.moveTo(padState.lastX, padState.lastY);
|
|
ctx.lineTo(x, y);
|
|
ctx.stroke();
|
|
padState.lastX = x;
|
|
padState.lastY = y;
|
|
}
|
|
|
|
function end() {
|
|
padState.drawing = false;
|
|
}
|
|
|
|
canvas.onpointerdown = start;
|
|
canvas.onpointermove = move;
|
|
canvas.onpointerup = end;
|
|
canvas.onpointerleave = end;
|
|
|
|
pointerPads.set(canvasId, { ctx, canvas });
|
|
},
|
|
registerDropHandler: (dotNetRef) => {
|
|
if (window.__pdfDropRegistered) return;
|
|
window.__pdfDropRegistered = true;
|
|
|
|
const prevent = (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
['dragenter', 'dragover', 'dragleave'].forEach(evt => {
|
|
document.addEventListener(evt, prevent, false);
|
|
});
|
|
|
|
document.addEventListener('drop', (e) => {
|
|
prevent(e);
|
|
|
|
const files = e.dataTransfer?.files;
|
|
if (!files || files.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const file = files[0];
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const result = reader.result;
|
|
if (typeof result === 'string') {
|
|
const base64 = result.split(',')[1] || result;
|
|
dotNetRef?.invokeMethodAsync('LoadPdfFromBase64', base64);
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}, false);
|
|
},
|
|
clearSignaturePad: (canvasId) => {
|
|
const pad = pointerPads.get(canvasId);
|
|
if (!pad) return;
|
|
pad.ctx.clearRect(0, 0, pad.canvas.width, pad.canvas.height);
|
|
},
|
|
getSignatureDataUrl: (canvasId) => {
|
|
const pad = pointerPads.get(canvasId);
|
|
if (!pad) return null;
|
|
return pad.canvas.toDataURL('image/png');
|
|
},
|
|
capturePointer: (element, pointerId) => {
|
|
if (element && element.setPointerCapture) {
|
|
try {
|
|
element.setPointerCapture(pointerId);
|
|
} catch (err) {
|
|
console.warn('capturePointer failed', err);
|
|
}
|
|
}
|
|
},
|
|
releasePointer: (element, pointerId) => {
|
|
if (element && element.releasePointerCapture) {
|
|
try {
|
|
element.releasePointerCapture(pointerId);
|
|
} catch (err) {
|
|
console.warn('releasePointer failed', err);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
})();
|