- 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
83 lines
2.7 KiB
JavaScript
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 };
|
|
})();
|