103 lines
4.9 KiB
JavaScript
103 lines
4.9 KiB
JavaScript
import { inflateSync } from 'node:zlib'
|
|
|
|
/** Beispiel-Auftrag (V3-Form) für die PDF-Tests; `extra` überschreibt einzelne Felder. */
|
|
export function beispielAuftrag(extra = {}) {
|
|
return {
|
|
id: 'ab1', nummer: 'AB-2026-0007', jahr: 2026, laufnummer: 7, status: 'versendet', kundenId: 'k1',
|
|
kunde: {
|
|
name: 'Anna Müller', firma: 'Müller Bäckerei GmbH', strasse: 'Hauptstraße 12', plz: '67722', ort: 'Winnweiler',
|
|
land: 'Deutschland', email: 'anna@example.test', kundenart: 'unternehmen', kundennummer: 'K-1042',
|
|
},
|
|
projektId: 'p1', titel: 'Neue Website Müller Bäckerei', zeitquelle: ['t1'], art: 'festpreis',
|
|
positionen: [
|
|
{ beschreibung: 'Gestaltung und Umsetzung der Website (5 Seiten)', menge: 1, einzelpreisCents: 60000 },
|
|
{ beschreibung: 'Kontaktformular mit E-Mail-Versand', menge: 2, einzelpreisCents: 10000 },
|
|
],
|
|
stundensatzCents: null, schaetzungMinuten: null, summeCents: 80000,
|
|
zahlungsplan: { art: 'anzahlung', prozent: 50, text: '' }, zahlungszielTage: 14,
|
|
auftragVom: '2026-10-05', auftragWeg: 'telefon', startAm: '2026-10-12', fertigBis: '2026-11-15',
|
|
ersetztId: '', ersetztDurchId: '', texteFassung: '2026-10-06', agbFassung: '2026-09-30', pdfDatei: '', pdfSha256: '',
|
|
gesendetAm: '2026-10-06T08:30:00.000Z', gesendetAn: 'anna@example.test', mailStatus: '', mailFehler: '',
|
|
tokenHash: '', tokenGueltigBis: null, bestaetigtAm: null, bestaetigtUeber: '', bestaetigtPdfSha256: '',
|
|
fruehbeginn: false, fruehbeginnFassung: '', storniertAm: null, widerrufenAm: null,
|
|
erstelltVon: 'kenso', erstelltAm: '2026-10-06T08:00:00.000Z', geaendertAm: '2026-10-06T08:00:00.000Z',
|
|
...extra,
|
|
}
|
|
}
|
|
|
|
export const beispielSettings = { steuernummer: '12/345/67890', bankDetailsText: 'Finom Bank\nIBAN DE12 3456 7890 1234 5678 90\nBIC FNOMDEB2' }
|
|
|
|
/** Alle Zeichenketten in verschachtelten Objekten und Listen (flach gesammelt); Werte unter den Schlüsseln id und art sind keine Texte. */
|
|
export function alleTexte(wert, aus = []) {
|
|
if (typeof wert === 'string') aus.push(wert)
|
|
else if (Array.isArray(wert)) wert.forEach((e) => alleTexte(e, aus))
|
|
else if (wert && typeof wert === 'object') {
|
|
for (const [schluessel, e] of Object.entries(wert)) if (schluessel !== 'id' && schluessel !== 'art') alleTexte(e, aus)
|
|
}
|
|
return aus
|
|
}
|
|
|
|
/** Anzahl Seiten eines PDFs (Seitenobjekte, nicht der Seitenbaum). */
|
|
export const seitenZahl = (pdf) => (pdf.toString('latin1').match(/\/Type \/Page(?![s\w])/g) || []).length
|
|
|
|
/**
|
|
* Text aus einem mit pdfkit erzeugten PDF (eingebettete Schriften, Identity-H, ToUnicode), je Seite eine Liste
|
|
* von Textzeilen in Zeichenreihenfolge. Nur für Tests: kennt genau das, was pdfkit schreibt.
|
|
*/
|
|
export function leseSeiten(pdf) {
|
|
const s = pdf.toString('latin1')
|
|
const objekte = new Map()
|
|
const re = /(\d+) 0 obj\n/g
|
|
let m
|
|
while ((m = re.exec(s))) {
|
|
const start = m.index + m[0].length
|
|
const ende = s.indexOf('\nendobj', start)
|
|
const koerper = s.slice(start, ende)
|
|
const strom = koerper.indexOf('\nstream\n')
|
|
let dict = koerper
|
|
let daten = null
|
|
if (strom >= 0) {
|
|
dict = koerper.slice(0, strom)
|
|
const laenge = Number(/\/Length (\d+)/.exec(dict)[1])
|
|
daten = pdf.subarray(start + strom + 8, start + strom + 8 + laenge)
|
|
if (/\/Filter \/FlateDecode/.test(dict)) daten = inflateSync(daten)
|
|
}
|
|
objekte.set(Number(m[1]), { dict, daten })
|
|
re.lastIndex = ende
|
|
}
|
|
const tabellen = new Map()
|
|
for (const [id, o] of objekte) {
|
|
const tu = /\/ToUnicode (\d+) 0 R/.exec(o.dict)
|
|
if (!/\/Subtype \/Type0/.test(o.dict) || !tu) continue
|
|
const tabelle = []
|
|
for (const r of objekte.get(Number(tu[1])).daten.toString('latin1').matchAll(/<([0-9a-f]{4})> <([0-9a-f]{4})> \[([^\]]*)\]/g)) {
|
|
let i = parseInt(r[1], 16)
|
|
for (const e of r[3].matchAll(/<([0-9a-f ]+)>/g)) tabelle[i++] = e[1].split(' ').map((h) => String.fromCharCode(parseInt(h, 16))).join('')
|
|
}
|
|
tabellen.set(id, tabelle)
|
|
}
|
|
const schriftNachName = new Map()
|
|
for (const o of objekte.values()) {
|
|
if (!/\/Font <</.test(o.dict)) continue
|
|
for (const f of o.dict.matchAll(/\/(F\d+) (\d+) 0 R/g)) schriftNachName.set(f[1], tabellen.get(Number(f[2])))
|
|
}
|
|
const seiten = []
|
|
for (const o of objekte.values()) {
|
|
if (!/\/Type \/Page(?![s\w])/.test(o.dict)) continue
|
|
const inhalt = objekte.get(Number(/\/Contents (\d+) 0 R/.exec(o.dict)[1])).daten.toString('latin1')
|
|
const zeilen = []
|
|
for (const bt of inhalt.matchAll(/BT\n([\s\S]*?)\nET/g)) {
|
|
const tabelle = schriftNachName.get(/\/(F\d+) [\d.]+ Tf/.exec(bt[1])?.[1])
|
|
let text = ''
|
|
for (const tj of bt[1].matchAll(/\[([^\]]*)\] TJ/g)) {
|
|
for (const h of tj[1].matchAll(/<([0-9a-f]+)>/g)) {
|
|
for (let i = 0; i < h[1].length; i += 4) text += tabelle[parseInt(h[1].slice(i, i + 4), 16)] ?? '\u{FFFD}'
|
|
}
|
|
}
|
|
zeilen.push(text)
|
|
}
|
|
seiten.push(zeilen)
|
|
}
|
|
return seiten
|
|
}
|