Refactor PDF reset to restore original document state using a new OriginalPdfBase64 variable and async logic. Redesign app.css with a lighter color palette, CSS variables, and updated styles for buttons, overlays, modals, and inputs for a cleaner, more accessible UI. Adjust signature and text overlay colors in pdfInterop.js for better contrast and consistency.
297 lines
9.8 KiB
JavaScript
297 lines
9.8 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 });
|
|
},
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
})();
|