import type jsPDF from 'jspdf'; import type { PdfSettings } from '../services/config'; /** * Render text with basic markdown (**bold**) and line breaks into a jsPDF doc. * Returns the final Y position after rendering. */ export function renderMarkdownText( doc: jsPDF, text: string, x: number, y: number, options?: { fontSize?: number; lineHeight?: number }, ): number { const fontSize = options?.fontSize ?? 9; const lineHeight = options?.lineHeight ?? fontSize * 0.5; doc.setFontSize(fontSize); doc.setTextColor(0, 0, 0); const lines = text.split('\n'); let curY = y; for (const line of lines) { const segments = line.split('**'); let curX = x; for (let i = 0; i < segments.length; i++) { const seg = segments[i]; if (!seg) continue; const isBold = i % 2 === 1; doc.setFont('helvetica', isBold ? 'bold' : 'normal'); doc.text(seg, curX, curY); curX += doc.getTextWidth(seg); } curY += lineHeight; } doc.setFont('helvetica', 'normal'); return curY; } /** Load an image from a data URL and return its natural pixel dimensions. */ function getImageDimensions(src: string): Promise<{ w: number; h: number }> { return new Promise((resolve) => { const img = new Image(); img.onload = () => resolve({ w: img.naturalWidth, h: img.naturalHeight }); img.onerror = () => resolve({ w: 1, h: 1 }); img.src = src; }); } /** * Add a white-background PDF header matching the official template: * - Left: pdf_header text (bold italic, multi-line org hierarchy) * - Right: org name (bold) + logo (aspect-ratio correct) * - Thin dark separator line below * * Does NOT render the document title — callers do that below the returned Y. * Returns Y position where document content should start. */ export async function addPdfHeader( doc: jsPDF, settings: PdfSettings, pageWidth: number, ): Promise { const margin = 6; const rightEdge = pageWidth - margin; const logoMaxH = 16; // ── Right: logo (aspect-ratio-correct) ── const logoSrc = settings.app_logo || settings.pdf_logo; let logoLeftEdge = rightEdge; if (logoSrc) { try { const { w, h } = await getImageDimensions(logoSrc); const ratio = w / h; const logoH = logoMaxH; const logoW = logoH * ratio; const fmt = logoSrc.startsWith('data:image/png') ? 'PNG' : 'JPEG'; const logoX = rightEdge - logoW; doc.addImage(logoSrc, fmt, logoX, 2, logoW, logoH); logoLeftEdge = logoX - 3; } catch { /* ignore invalid image */ } } // ── Right: org name (to the left of logo, vertically centred) ── if (settings.pdf_org_name) { doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.setTextColor(0, 0, 0); const nameW = doc.getTextWidth(settings.pdf_org_name); doc.text(settings.pdf_org_name, logoLeftEdge - nameW, 11); } // ── Left: pdf_header text (bold italic, multi-line) ── let headerEndY = 10; if (settings.pdf_header) { doc.setFontSize(8); doc.setTextColor(60, 60, 60); const headerLines = settings.pdf_header.split('\n'); let hy = 8; for (const line of headerLines) { const segments = line.split('**'); let hx = margin; for (let i = 0; i < segments.length; i++) { const seg = segments[i]; if (!seg) continue; doc.setFont('helvetica', i % 2 === 1 ? 'bolditalic' : 'italic'); doc.text(seg, hx, hy); hx += doc.getTextWidth(seg); } hy += 4; } headerEndY = hy; } // ── Separator line ── const lineY = Math.max(headerEndY, logoMaxH + 4) + 2; doc.setDrawColor(60, 60, 60); doc.setLineWidth(0.5); doc.line(margin, lineY, pageWidth - margin, lineY); // Reset styles doc.setFont('helvetica', 'normal'); doc.setTextColor(0, 0, 0); doc.setFontSize(10); return lineY + 9; } /** * Returns a `didDrawPage` callback that renders pdf_footer (left) and * "Seite X/Y" page numbers (bottom right) on every page. */ export function addPdfFooter( doc: jsPDF, settings: PdfSettings, ): (data: any) => void { return (data: any) => { const pageHeight = doc.internal.pageSize.height; const pageWidth = doc.internal.pageSize.width; doc.setFontSize(8); doc.setFont('helvetica', 'normal'); doc.setTextColor(100, 100, 100); if (settings.pdf_footer) { renderMarkdownText(doc, settings.pdf_footer, 10, pageHeight - 8, { fontSize: 8 }); } const pageNum = data?.pageNumber ?? 1; const pageCount = data?.pageCount ?? 1; const pageText = `Seite ${pageNum}/${pageCount}`; const textWidth = doc.getTextWidth(pageText); doc.text(pageText, pageWidth - 10 - textWidth, pageHeight - 8); doc.setTextColor(0, 0, 0); }; }