update
This commit is contained in:
136
frontend/src/utils/pdfExport.ts
Normal file
136
frontend/src/utils/pdfExport.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user