This commit is contained in:
Matthias Hochmeister
2026-03-26 09:29:59 +01:00
parent 884397b520
commit d5e5f2d44e
10 changed files with 428 additions and 154 deletions

View File

@@ -0,0 +1,136 @@
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;
}
/**
* Add a PDF header with white background:
* - Left: pdf_header text (bold italic, multi-line)
* - Right: org name + logo side-by-side
* - Thin dark separator line below
* Returns Y position where content should start.
*/
export function addPdfHeader(
doc: jsPDF,
title: string,
settings: PdfSettings,
pageWidth: number,
): number {
const logoSize = 14;
const margin = 6;
const rightEdge = pageWidth - margin;
// ── Left side: title (bold) ──
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.setTextColor(0, 0, 0);
doc.text(title, 10, 12);
// ── Left side: pdf_header text below title (bold italic, smaller) ──
let headerEndY = 16;
if (settings.pdf_header) {
doc.setFontSize(8);
doc.setTextColor(100, 100, 100);
const headerLines = settings.pdf_header.split('\n');
let hy = 18;
for (const line of headerLines) {
const segments = line.split('**');
let hx = 10;
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (!seg) continue;
const isBold = i % 2 === 1;
doc.setFont('helvetica', isBold ? 'bolditalic' : 'italic');
doc.text(seg, hx, hy);
hx += doc.getTextWidth(seg);
}
hy += 3.5;
}
headerEndY = Math.max(headerEndY, hy);
}
// ── Right side: logo + org name ──
const logoSrc = settings.app_logo || settings.pdf_logo;
let logoLeftEdge = rightEdge;
if (logoSrc) {
try {
const fmt = logoSrc.startsWith('data:image/png') ? 'PNG' : 'JPEG';
const logoX = rightEdge - logoSize;
doc.addImage(logoSrc, fmt, logoX, 3, logoSize, logoSize);
logoLeftEdge = logoX - 3;
} catch { /* ignore invalid image */ }
}
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);
const nameX = logoLeftEdge - nameW;
// Vertically centered with logo area (~y=10)
doc.text(settings.pdf_org_name, nameX, 11);
}
// ── Separator line ──
const lineY = Math.max(headerEndY, 20) + 2;
doc.setDrawColor(60, 60, 60);
doc.setLineWidth(0.5);
doc.line(margin, lineY, pageWidth - margin, lineY);
// Reset
doc.setFont('helvetica', 'normal');
doc.setTextColor(0, 0, 0);
return lineY + 4;
}
/**
* Returns a `didDrawPage` callback that renders pdf_footer at the bottom of each page.
*/
export function addPdfFooter(
doc: jsPDF,
settings: PdfSettings,
): ((data: any) => void) | undefined {
if (!settings.pdf_footer) return undefined;
return () => {
renderMarkdownText(doc, settings.pdf_footer, 10, doc.internal.pageSize.height - 12, {
fontSize: 8,
});
};
}