/** * Generates fictitious sample order confirmations for the import lane (L3) as text PDFs — * handwritten minimal PDF writer (Helvetica, WinAnsiEncoding), no dependencies. * * Lauf: npx tsx scripts/make-sample-pdfs.ts → docs/craftvia/samples/*.pdf * All companies, people, addresses and numbers are invented (example.org, 0000-numbers). */ import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const OUT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "docs", "craftvia", "samples"); type Line = { text: string; size?: number; bold?: boolean; x?: number; gap?: number }; /** Latin-1/WinAnsi string literal for a PDF content stream. */ function pdfString(s: string): string { let out = ""; for (const ch of s) { if (ch === "€") out += "\\200"; else if (ch === "–") out += "\\226"; else if (ch === "(" || ch === ")" || ch === "\\") out += `\\${ch}`; else { const code = ch.charCodeAt(0); out += code < 128 ? ch : `\\${code.toString(8).padStart(3, "0")}`; } } return `(${out})`; } export function buildPdf(pages: Line[][]): Buffer { const objects: string[] = []; const add = (body: string) => { objects.push(body); return objects.length; }; const catalogId = add(""); const pagesId = add(""); const fontId = add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>"); const boldId = add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>"); const pageIds: number[] = []; for (const lines of pages) { let y = 800; const ops: string[] = []; for (const l of lines) { const size = l.size ?? 10; y -= l.gap ?? size + 5; ops.push(`BT /${l.bold ? "F2" : "F1"} ${size} Tf ${l.x ?? 56} ${y} Td ${pdfString(l.text)} Tj ET`); } const stream = ops.join("\n"); const contentId = add(`<< /Length ${Buffer.byteLength(stream, "latin1")} >>\nstream\n${stream}\nendstream`); pageIds.push( add(`<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 ${fontId} 0 R /F2 ${boldId} 0 R >> >> /Contents ${contentId} 0 R >>`), ); } objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; objects[pagesId - 1] = `<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] /Count ${pageIds.length} >>`; let pdf = "%PDF-1.4\n%\xe2\xe3\xcf\xd3\n"; const offsets: number[] = []; objects.forEach((body, i) => { offsets.push(Buffer.byteLength(pdf, "latin1")); pdf += `${i + 1} 0 obj\n${body}\nendobj\n`; }); const xref = Buffer.byteLength(pdf, "latin1"); pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; for (const o of offsets) pdf += `${String(o).padStart(10, "0")} 00000 n \n`; pdf += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xref}\n%%EOF\n`; return Buffer.from(pdf, "latin1"); } const letterhead = (): Line[] => [ { text: "Kranich Haustechnik GmbH", size: 16, bold: true, gap: 10 }, { text: "Deichweg 4 · 21029 Hamburg · Tel. 040 0000 1000 · info@kranich-haustechnik.example.org", size: 8 }, ]; const SAMPLES: Record = { "01-musterbau-auftragsbestaetigung.pdf": [ [ ...letterhead(), { text: "Musterbau GmbH", gap: 40 }, { text: "z. Hd. Frau Jana Köhler" }, { text: "Hafenstraße 12" }, { text: "20457 Hamburg" }, { text: "Auftragsbestätigung", size: 14, bold: true, gap: 36 }, { text: "Auftragsnummer: AB-2026-0815 Datum: 02.09.2026", gap: 20 }, { text: "Ihr Angebot: ANG-2026-0342 Kundennummer: K-10042" }, { text: "Bauvorhaben: Neubau Bürogebäude Speicherhof, Am Kaiserkai 30, 20457 Hamburg" }, { text: "Ansprechpartnerin vor Ort: Jana Köhler, Tel. 040 0000 2233, j.koehler@musterbau.example.org" }, { text: "Ausführungszeitraum: 12.10.2026 bis 16.10.2026", bold: true }, { text: "Sehr geehrte Frau Köhler,", gap: 24 }, { text: "vielen Dank für Ihren Auftrag. Wir bestätigen die Ausführung folgender Leistungen:" }, { text: "Pos. Bezeichnung Art.-Nr. Menge Einheit", bold: true, gap: 24 }, { text: "1 Montage Wärmepumpe Luft/Wasser 12 kW 1 Stk" }, { text: "2 Wärmepumpe Aerotherm 12 kW WP-AT-12 1 Stk" }, { text: "3 Pufferspeicher 500 l PS-500 1 Stk" }, { text: "4 Kupferrohr 22 mm CU-22 24 m" }, { text: "5 Inbetriebnahme und Einweisung 4 Std" }, { text: "Gesamtbetrag netto: 18.450,00 € zzgl. 19 % MwSt.: 3.505,50 € Gesamt: 21.955,50 €", bold: true, gap: 24 }, { text: "Hinweise: Zufahrt über Tor 2, Anmeldung beim Bauleiter. Kran ist bauseits zu stellen.", gap: 24 }, { text: "Referenz: Ihre Bestellung BE-7781 vom 28.08.2026" }, { text: "Mit freundlichen Grüßen", gap: 30 }, { text: "Kranich Haustechnik GmbH" }, ], ], "02-elbblick-wartungsauftrag.pdf": [ [ ...letterhead(), { text: "Elbblick Wohnen eG", gap: 40 }, { text: "Hausverwaltung" }, { text: "Neumühlen 7" }, { text: "22763 Hamburg" }, { text: "Auftrag Wartung Heizungsanlagen", size: 14, bold: true, gap: 36 }, { text: "Auftrag Nr. W-26-117 · Kunden-Nr. K-20017 · Hamburg, den 01.09.2026", gap: 20 }, { text: "Objekt: Wohnanlage Elbhang, Övelgönne 45, 22605 Hamburg" }, { text: "Ansprechpartner: Herr Timo Brandt, Hausmeister, Tel. +49 40 0000 4545" }, { text: "Termin: KW 41 nach Absprache mit dem Hausmeister", gap: 20 }, { text: "Leistungsumfang:", bold: true, gap: 22 }, { text: "Jährliche Wartung von 2 Gas-Brennwertkesseln inkl. Abgasmessung und Protokoll." }, { text: "1 Wartung Gas-Brennwertkessel 2 Stk" }, { text: "2 Wartungsset Dichtungen 2 Satz" }, { text: "3 Abgasmessung mit Protokoll 2 Stk" }, { text: "Besondere Hinweise: Heizungsraum im Keller, Schlüssel beim Hausmeister.", gap: 22 }, { text: "Rückfragen bitte an verwaltung@elbblick.example.org" }, ], ], "03-privatkunde-reparatur.pdf": [ [ ...letterhead(), { text: "Frau", gap: 40 }, { text: "Petra Sommer" }, { text: "Lindenallee 9a" }, { text: "2148 Hamburg" }, { text: "Auftragsbestätigung Reparatur", size: 14, bold: true, gap: 36 }, { text: "Auftrag: AB-2026-0901 Datum: 03.09.2026", gap: 20 }, { text: "Einsatzort: wie oben" }, { text: "Telefon: 0170 0000 987 E-Mail: petra.sommer(at)example.org" }, { text: "Geplanter Termin: 18.09.2026, Ende 17.09.2026" }, { text: "Leistung: Austausch defekter Thermostatkopf im Bad, Prüfung Heizkörperventile.", gap: 22 }, { text: "1 Thermostatkopf Standard TK-100 1 Stk" }, { text: "2 Arbeitszeit Monteur 1,5 Std" }, { text: "Gesamt brutto: 189,40 €", bold: true, gap: 22 }, ], ], }; if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { mkdirSync(OUT_DIR, { recursive: true }); for (const [name, pages] of Object.entries(SAMPLES)) { const file = join(OUT_DIR, name); writeFileSync(file, buildPdf(pages)); console.log(`✔ ${file}`); } }