import { readFile } from "node:fs/promises"; import sharp from "sharp"; import { getBrandAssets } from "@/lib/brand"; import { resolveUploadPath } from "@/lib/uploads"; /** * Markenfarben fuer das Rechnungs-PDF — dieselben Werte wie die Tokens in * app/globals.css (helles Farbschema). pdfkit kennt keine CSS-Variablen, * deshalb hier als feste Hexwerte; bei einer Aenderung der Markenfarben * beide Stellen anpassen. */ export const INVOICE_COLORS = { forest: "#24352B", honey: "#B88A44", honeyDark: "#8A6531", honeyPale: "#E4CFA4", cream: "#F4EFE5", ink: "#242424", sage: "#788774", hairline: "#E6DDCB", } as const; export interface InvoiceLogo { png: Buffer; width: number; height: number; } /** * Laedt das im Admin hochgeladene Logo (Inhalte > Logo und Favicon) als PNG. * * pdfkit bettet nur PNG und JPEG ein, Uploads duerfen aber auch WebP/AVIF * sein — sharp wandelt deshalb immer nach PNG (behaelt Transparenz). * Der Pfad ist doppelt abgesichert: getBrandAssets laesst nur das feste * Upload-Muster durch, resolveUploadPath verhindert das Verlassen von * UPLOAD_DIR. * * Jeder Fehler (Datei fehlt, kaputtes Bild) fuehrt zu null und damit zum * gezeichneten Waben-Zeichen: eine Rechnung darf nie an einem Logo scheitern. */ export async function loadInvoiceLogo(): Promise { const { logo } = await getBrandAssets(); if (!logo) return null; const filePath = resolveUploadPath([logo.replace(/^\/uploads\//, "")]); if (!filePath) return null; try { const { data, info } = await sharp(await readFile(filePath)).png().toBuffer({ resolveWithObject: true }); return { png: data, width: info.width, height: info.height }; } catch (error) { console.error(`[invoice-pdf] Logo ${logo} konnte nicht geladen werden — es wird das Waben-Zeichen verwendet.`, error); return null; } } /** * Zeichnet das Waben-Zeichen (components/logomark.tsx) als Vektor: Hexagon in * Honiggold, darin drei Tannen in Waldgruen. Gleiche Geometrie wie im * Web-Logo (viewBox 100), skaliert auf `size` Punkt. */ export function drawLogomark(doc: PDFKit.PDFDocument, x: number, y: number, size: number): void { doc.save(); doc.translate(x, y).scale(size / 100); doc .polygon([50, 5], [88.97, 27.5], [88.97, 72.5], [50, 95], [11.03, 72.5], [11.03, 27.5]) .lineWidth(2.5) .strokeColor(INVOICE_COLORS.honey) .stroke(); for (const tree of [ { x: 20, y: 45, scale: 0.7 }, { x: 38, y: 32, scale: 0.95 }, { x: 56, y: 45, scale: 0.7 }, ]) { doc.save(); doc.translate(tree.x, tree.y).scale(tree.scale); doc.lineWidth(1.4).lineJoin("round").strokeColor(INVOICE_COLORS.forest); doc.polygon([10, 2], [5, 13], [15, 13]).stroke(); doc.polygon([10, 10], [2, 22], [18, 22]).stroke(); doc.rect(8, 22, 4, 4).stroke(); doc.restore(); } doc.restore(); } /** * Setzt das Logo in eine Box der Hoehe `height` und gibt die tatsaechlich * belegte Breite zurueck — breite Wortmarken werden bis `maxWidth` breit, * quadratische Zeichen bleiben quadratisch. */ export function drawInvoiceLogo( doc: PDFKit.PDFDocument, logo: InvoiceLogo | null, x: number, y: number, height: number, maxWidth: number ): number { if (!logo) { drawLogomark(doc, x, y, height); return height; } const scale = Math.min(height / logo.height, maxWidth / logo.width); const width = logo.width * scale; doc.image(logo.png, x, y + (height - logo.height * scale) / 2, { width }); return width; }