KLEINUILTIES-WEBAPP/js/pdf.js
Slim Uiltjies App 1939bf2ef4 Initial commit: Slim Uiltjies AfterCare invoice system
- Progressive Web App with Express + JSON backend
- PIN-based auth, multi-device sync
- Invoice + Statement PDF generation (branded, Grandstander + Nunito)
- Mobile + Desktop responsive layouts
- n8n backup integration
- Custom logo upload
- Settings for business info, fees, banking
- Bulk invoice generation
- Web Share API integration
2026-08-08 09:25:17 +02:00

83 lines
2.7 KiB
JavaScript

/* Slim Uiltjies — PDF generation
Renders an invoice (or statement) HTML node into a PDF using
html2canvas + jsPDF. Uses JPEG (more reliable than PNG for jsPDF).
*/
(function () {
'use strict';
function nextFrame() {
return new Promise((r) => requestAnimationFrame(() => r()));
}
async function renderNodeToCanvas(node, scale = 2) {
// Wait for fonts to be ready so capture is stable
if (document.fonts && document.fonts.ready) {
try { await document.fonts.ready; } catch {}
}
await nextFrame();
return window.html2canvas(node, {
scale,
backgroundColor: '#FFF8E7',
useCORS: true,
allowTaint: true,
logging: false,
windowWidth: node.scrollWidth,
windowHeight: node.scrollHeight,
});
}
async function nodeToPdfBlob(node, { filename = 'document.pdf', scale = 2 } = {}) {
const { jsPDF } = window.jspdf;
const canvas = await renderNodeToCanvas(node, scale);
// A4 portrait, units in pt
const pdf = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' });
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
// Calculate scaling
const imgW = pageW;
const scaleFactor = imgW / canvas.width;
const pageCanvasH = pageH / scaleFactor; // canvas px that fits one page
let renderedH = 0;
let first = true;
while (renderedH < canvas.height) {
const sliceH = Math.min(pageCanvasH, canvas.height - renderedH);
const slice = document.createElement('canvas');
slice.width = canvas.width;
slice.height = sliceH;
const ctx = slice.getContext('2d');
ctx.fillStyle = '#FFF8E7';
ctx.fillRect(0, 0, slice.width, slice.height);
ctx.drawImage(
canvas,
0, renderedH, canvas.width, sliceH,
0, 0, slice.width, sliceH
);
// Use JPEG for reliability (PNG can fail with large images in jsPDF)
const dataUrl = slice.toDataURL('image/jpeg', 0.92);
if (!first) pdf.addPage();
const sliceImgH = sliceH * scaleFactor;
pdf.addImage(dataUrl, 'JPEG', 0, 0, imgW, sliceImgH, undefined, 'FAST');
renderedH += sliceH;
first = false;
}
const blob = pdf.output('blob');
blob.name = filename;
return blob;
}
async function nodeToPdfDataUri(node, options = {}) {
const blob = await nodeToPdfBlob(node, options);
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
window.SUPDF = { nodeToPdfBlob, nodeToPdfDataUri };
})();