Refactored mouse event handling in pdfInterop.js to use a new getPos helper, ensuring accurate coordinate mapping on scaled or resized canvases. Updated start and move functions to use this helper. Added an empty settings.json file.
340 lines
11 KiB
JavaScript
340 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 getPos(evt) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const scaleX = rect.width ? canvas.width / rect.width : 1;
|
|
const scaleY = rect.height ? canvas.height / rect.height : 1;
|
|
return {
|
|
x: (evt.clientX - rect.left) * scaleX,
|
|
y: (evt.clientY - rect.top) * scaleY,
|
|
};
|
|
}
|
|
|
|
function start(e) {
|
|
padState.drawing = true;
|
|
const pos = getPos(e);
|
|
padState.lastX = pos.x;
|
|
padState.lastY = pos.y;
|
|
}
|
|
|
|
function move(e) {
|
|
if (!padState.drawing) return;
|
|
const pos = getPos(e);
|
|
const x = pos.x;
|
|
const y = pos.y;
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
})();
|