AB-PDF: erzeugeAuftragPdf zeichnet Briefkopf, Tabelle über mehrere Seiten, Belehrung und Fuß mit pdfkit
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
01406c82e5
commit
6f213dca25
2 files changed
+281
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as fontkitModul from 'fontkit'
|
||||
import PDFDocument from 'pdfkit'
|
||||
import { euroDe } from '../finance/format.js'
|
||||
import { berlinDate, formatDe } from '../finance/dates.js'
|
||||
import { AuftragError } from './fehler.js'
|
||||
@@ -299,3 +300,153 @@ export function paginiere(atome, bereiche = BEREICHE) {
|
||||
}
|
||||
return seiten
|
||||
}
|
||||
|
||||
// ===== 4. Zeichnen mit pdfkit =====
|
||||
|
||||
/** Zeichnen mit pdfkit. Alle Texte stehen fertig in `daten`; Höhen und Seiten kommen aus dem Abschnitt Layout. */
|
||||
const SCHRIFT_NAMEN = { regular: 'Figtree', semibold: 'Figtree-SemiBold' }
|
||||
const RECHTS = SEITE.breite - SEITE.rechts
|
||||
const SPALTEN_X = SPALTEN.reduce((liste, breite) => [...liste, liste[liste.length - 1] + breite], [SEITE.links]).slice(0, SPALTEN.length)
|
||||
|
||||
function setzeStil(doc, stil) {
|
||||
return doc.font(SCHRIFT_NAMEN[stil.schrift]).fontSize(stil.groesse).fillColor(stil.farbe)
|
||||
}
|
||||
|
||||
function schreibe(doc, text, x, y, stil, optionen = {}) {
|
||||
setzeStil(doc, stil).text(text, x, y, { lineGap: stil.lineGap, ...optionen })
|
||||
}
|
||||
|
||||
const linie = (doc, x1, y, x2, farbe = FARBEN.linie, breite = 0.5) => {
|
||||
doc.save().lineWidth(breite).strokeColor(farbe).moveTo(x1, y).lineTo(x2, y).stroke().restore()
|
||||
}
|
||||
|
||||
function zeichneBriefkopf(doc, daten, atom, y0, assets) {
|
||||
const b = daten.beschriftung
|
||||
const m = atom.masse
|
||||
doc.image(assets.logo, SEITE.links, y0, { width: 34 })
|
||||
schreibe(doc, 'WEBklar', SEITE.links + 43, y0 + 6, { schrift: 'semibold', groesse: 20, lineGap: 0, farbe: FARBEN.tinte }, { characterSpacing: -0.4, lineBreak: false })
|
||||
const [name, ...rest] = daten.absender.zeilen
|
||||
const blockX = RECHTS - 190
|
||||
schreibe(doc, name, blockX, y0, { ...STIL.klein, schrift: 'semibold', farbe: FARBEN.tinte }, { width: 190, align: 'right' })
|
||||
schreibe(doc, rest.join('\n'), blockX, y0 + 13, STIL.klein, { width: 190, align: 'right' })
|
||||
schreibe(doc, daten.absender.rueckzeile, SEITE.links, y0 + MASSE.rueckY, { ...STIL.klein, groesse: 7 }, { width: MASSE.adresseBreite, lineBreak: false, underline: true })
|
||||
schreibe(doc, daten.empfaenger.join('\n'), SEITE.links, y0 + MASSE.adresseY, STIL.adresse, { width: MASSE.adresseBreite })
|
||||
m.infoZeilen.forEach(([label, wert], i) => {
|
||||
const zeileY = y0 + MASSE.adresseY + i * MASSE.infoZeile
|
||||
schreibe(doc, label, MASSE.infoX, zeileY + 1, STIL.klein, { width: 90, lineBreak: false })
|
||||
schreibe(doc, wert, MASSE.infoX + 92, zeileY, { ...STIL.text, schrift: 'semibold' }, { width: RECHTS - MASSE.infoX - 92, align: 'right' })
|
||||
})
|
||||
schreibe(doc, daten.titel, SEITE.links, y0 + m.titelY, STIL.titel, { width: INHALT_BREITE })
|
||||
schreibe(doc, `${b.projekt}: ${daten.projekt}`, SEITE.links, y0 + m.projektY, STIL.projekt, { width: INHALT_BREITE })
|
||||
}
|
||||
|
||||
function zeichneAtom(doc, daten, atom, y, assets) {
|
||||
switch (atom.typ) {
|
||||
case 'briefkopf':
|
||||
return zeichneBriefkopf(doc, daten, atom, y, assets)
|
||||
case 'ueberschrift':
|
||||
return schreibe(doc, atom.text, SEITE.links, y, atom.stil === 'titel' ? STIL.titel : STIL.ueberschrift, { width: INHALT_BREITE })
|
||||
case 'absatz': {
|
||||
const stil = STIL[atom.stil]
|
||||
if (atom.kasten) {
|
||||
doc.save().roundedRect(SEITE.links, y, INHALT_BREITE, atom.hoehe, 4).lineWidth(0.5).fillAndStroke(FARBEN.flaeche, FARBEN.linie).restore()
|
||||
return schreibe(doc, atom.text, SEITE.links + MASSE.kastenRand, y + MASSE.kastenRand, stil, { width: INHALT_BREITE - 2 * MASSE.kastenRand })
|
||||
}
|
||||
return schreibe(doc, atom.text, SEITE.links, y, stil, { width: INHALT_BREITE })
|
||||
}
|
||||
case 'tabellenkopf': {
|
||||
doc.save().rect(SEITE.links, y, INHALT_BREITE, atom.hoehe).fill(FARBEN.flaeche).restore()
|
||||
atom.spalten.forEach((text, i) => {
|
||||
const rechts = i >= 2
|
||||
schreibe(doc, text, SPALTEN_X[i] + (i === 0 ? 0 : ZELLEN_RAND), y + 7, STIL.tabellenkopf, { width: SPALTEN[i] - (i === 0 ? ZELLEN_RAND : 2 * ZELLEN_RAND), align: rechts ? 'right' : 'left', lineBreak: false })
|
||||
})
|
||||
return linie(doc, SEITE.links, y + atom.hoehe, RECHTS, FARBEN.linieDunkel, 0.75)
|
||||
}
|
||||
case 'zeile': {
|
||||
const z = atom.zelle
|
||||
const oben = y + ZELLEN_RAND
|
||||
schreibe(doc, z.nr, SPALTEN_X[0], oben, STIL.text, { width: SPALTEN[0] - ZELLEN_RAND })
|
||||
schreibe(doc, z.beschreibung, SPALTEN_X[1] + ZELLEN_RAND, oben, STIL.text, { width: SPALTEN[1] - 2 * ZELLEN_RAND })
|
||||
schreibe(doc, z.menge, SPALTEN_X[2], oben, STIL.text, { width: SPALTEN[2] - ZELLEN_RAND, align: 'right' })
|
||||
schreibe(doc, z.einzelpreis, SPALTEN_X[3], oben, STIL.text, { width: SPALTEN[3] - ZELLEN_RAND, align: 'right' })
|
||||
schreibe(doc, z.summe, SPALTEN_X[4], oben, STIL.text, { width: SPALTEN[4] - ZELLEN_RAND, align: 'right' })
|
||||
return linie(doc, SEITE.links, y + atom.hoehe, RECHTS)
|
||||
}
|
||||
case 'summe': {
|
||||
linie(doc, SEITE.links, y, RECHTS, FARBEN.tinte, 0.75)
|
||||
const labelBreite = SPALTEN_X[4] - SEITE.links - ZELLEN_RAND
|
||||
schreibe(doc, atom.label, SEITE.links, y + 9, STIL.summe, { width: labelBreite, align: 'right', lineBreak: false })
|
||||
return schreibe(doc, atom.betrag, SPALTEN_X[4], y + 9, STIL.summe, { width: SPALTEN[4] - ZELLEN_RAND, align: 'right', lineBreak: false })
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unbekannter Baustein: ${atom.typ}`)
|
||||
}
|
||||
}
|
||||
|
||||
function zeichneFolgeKopf(doc, daten, assets) {
|
||||
const y = SEITE.oben
|
||||
doc.image(assets.logo, SEITE.links, y, { width: 16 })
|
||||
schreibe(doc, 'WEBklar', SEITE.links + 22, y + 2, { schrift: 'semibold', groesse: 11, lineGap: 0, farbe: FARBEN.tinte }, { characterSpacing: -0.2, lineBreak: false })
|
||||
schreibe(doc, `${daten.titel} ${daten.nummer}`, RECHTS - 250, y + 4, STIL.klein, { width: 250, align: 'right', lineBreak: false })
|
||||
linie(doc, SEITE.links, y + 24, RECHTS)
|
||||
}
|
||||
|
||||
function zeichneFuss(doc, daten, nummer, gesamt) {
|
||||
const b = daten.beschriftung
|
||||
const y = SEITE.hoehe - MASSE.fussHoehe
|
||||
linie(doc, SEITE.links, y, RECHTS)
|
||||
const spalte = (x, breite, label, zeilen) => {
|
||||
schreibe(doc, label, x, y + 8, STIL.fussLabel, { width: breite, lineBreak: false })
|
||||
schreibe(doc, zeilen.join('\n'), x, y + 19, STIL.fuss, { width: breite, height: 46, ellipsis: true })
|
||||
}
|
||||
if (daten.fuss.bank.length) spalte(SEITE.links, 175, b.bank, daten.fuss.bank)
|
||||
spalte(SEITE.links + 190, 225, b.kontakt, daten.fuss.kontakt)
|
||||
schreibe(doc, `${b.seite} ${nummer} ${b.von} ${gesamt}`, RECHTS - 70, y + 8, STIL.fussLabel, { width: 70, align: 'right', lineBreak: false })
|
||||
}
|
||||
|
||||
/** Zeichnet alle Seiten; die erste Seite legt pdfkit selbst an. */
|
||||
function zeichneAuftrag(doc, daten, seiten, assets) {
|
||||
seiten.forEach((seite, index) => {
|
||||
if (index > 0) doc.addPage()
|
||||
if (index > 0) zeichneFolgeKopf(doc, daten, assets)
|
||||
for (const posten of seite.posten) zeichneAtom(doc, daten, posten.atom, posten.y, assets)
|
||||
zeichneFuss(doc, daten, seite.nummer, seiten.length)
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 5. Ablauf =====
|
||||
|
||||
/** Das AB-PDF (A4) als Buffer. */
|
||||
export async function erzeugeAuftragPdf(auftrag, { settings, vorschau = false, jetzt = new Date() } = {}) {
|
||||
const daten = bereinigeTexte(auftragPdfDaten(auftrag, { settings, vorschau, jetzt }))
|
||||
const assets = ladeSchriften()
|
||||
const doc = new PDFDocument({
|
||||
size: 'A4', margins: { top: 0, bottom: 0, left: 0, right: 0 }, lang: 'de-DE', displayTitle: true,
|
||||
info: {
|
||||
Title: `${daten.titel} ${daten.nummer}`, Author: 'WEBklar GbR', Subject: daten.projekt, Producer: 'WEBklar Kundenportal',
|
||||
CreationDate: new Date(auftrag.gesendetAm || jetzt),
|
||||
},
|
||||
})
|
||||
doc.registerFont('Figtree', assets.regular)
|
||||
doc.registerFont('Figtree-SemiBold', assets.semibold)
|
||||
const teile = []
|
||||
doc.on('data', (teil) => teile.push(teil))
|
||||
const fertig = new Promise((resolve, reject) => {
|
||||
doc.on('end', () => resolve(Buffer.concat(teile)))
|
||||
doc.on('error', reject)
|
||||
})
|
||||
fertig.catch(() => {}) // scheitert das Zeichnen zuerst, soll das verworfene Versprechen nichts auslösen
|
||||
try {
|
||||
const messe = (text, stil, breite) => {
|
||||
setzeStil(doc, stil)
|
||||
return doc.heightOfString(text, { width: breite, lineGap: stil.lineGap })
|
||||
}
|
||||
const seiten = paginiere(baueAtome(daten, messe), BEREICHE)
|
||||
zeichneAuftrag(doc, daten, seiten, assets)
|
||||
doc.end()
|
||||
} catch (fehler) {
|
||||
doc.destroy()
|
||||
throw fehler
|
||||
}
|
||||
return fertig
|
||||
}
|
||||
Reference in new issue
Block a user