/** * PDF Export Module * * Generates branded PDF reports from analysis data. * Uses jspdf + jspdf-autotable, loaded on demand via dynamic import * from the calling code so the library is only fetched when needed. * * Usage: * import { exportAnalysisPDF } from '../pdf-export.js'; * await exportAnalysisPDF({ title: 'Circle Analysis', rows: [...] }); */ import { jsPDF } from 'jspdf'; import { applyPlugin } from 'jspdf-autotable'; applyPlugin(jsPDF); // Cached logo data URL — fetched once, reused for subsequent exports let _logoDataUrl = null; /** * Load the LUSPA logo, draw it onto a canvas to flatten alpha and * produce a clean JPEG data URL that jsPDF can embed reliably. * Caches the result so subsequent calls are instant. */ async function getLogoDataUrl() { if (_logoDataUrl) return _logoDataUrl; try { const img = new Image(); img.crossOrigin = 'anonymous'; await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; img.src = './app-icons/luspa-pdf.jpg'; }); // Draw onto a canvas to get a reliable JPEG data URL const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); // White background to flatten any remaining alpha ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); _logoDataUrl = canvas.toDataURL('image/jpeg', 0.92); return _logoDataUrl; } catch (err) { console.warn('[PDF] Could not load logo:', err); return null; } } /** * Generate and open a branded PDF report. * * @param {Object} options * @param {string} options.title - Report title (e.g. "Circle Analysis") * @param {Array<{label: string, value: string}>} options.rows - Table data */ export async function exportAnalysisPDF({ title, rows }) { const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); const pageWidth = doc.internal.pageSize.getWidth(); // Brand colours const navy = [30, 26, 75]; // #1e1a4b // ---- Logo ---- const logo = await getLogoDataUrl(); const logoSize = 28; const marginLeft = 14; let cursorY = 14; if (logo) { doc.addImage(logo, 'JPEG', marginLeft, cursorY, logoSize, logoSize); } // ---- Header text (next to logo) ---- const textX = marginLeft + logoSize + 6; doc.setFont('helvetica', 'bold'); doc.setFontSize(18); doc.setTextColor(...navy); doc.text('LUPMIS', textX, cursorY + 11); doc.setFont('helvetica', 'normal'); doc.setFontSize(12); doc.text(title, textX, cursorY + 19); // ---- Date / time ---- const now = new Date(); const dateStr = now.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric', }); const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); doc.setFontSize(9); doc.setTextColor(120, 120, 120); doc.text(`${dateStr} ${timeStr}`, pageWidth - marginLeft, cursorY + 11, { align: 'right' }); // ---- Divider line ---- cursorY += logoSize + 6; doc.setDrawColor(...navy); doc.setLineWidth(0.5); doc.line(marginLeft, cursorY, pageWidth - marginLeft, cursorY); cursorY += 6; // ---- Analysis table ---- const tableBody = rows.map(r => [r.label, r.value]); doc.autoTable({ startY: cursorY, head: [['Property', 'Value']], body: tableBody, margin: { left: marginLeft, right: marginLeft }, styles: { font: 'helvetica', fontSize: 10, cellPadding: 4, }, headStyles: { fillColor: navy, textColor: [255, 255, 255], fontStyle: 'bold', }, alternateRowStyles: { fillColor: [245, 245, 250], }, columnStyles: { 0: { fontStyle: 'bold', cellWidth: 50 }, }, }); // ---- Footer ---- const finalY = doc.lastAutoTable.finalY + 10; doc.setFontSize(8); doc.setTextColor(160, 160, 160); doc.text('Generated by LUPMIS2 Land Use Planning & Management Information System', marginLeft, finalY); // ---- Open in browser ---- const blob = doc.output('blob'); const url = URL.createObjectURL(blob); const win = window.open(url, '_blank'); if (!win) { // Popup blocked (mobile) — fall back to download const a = document.createElement('a'); a.href = url; a.download = `${title.replace(/\s+/g, '_')}_${now.toISOString().slice(0, 10)}.pdf`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } }