Dokumente: PDF, ZIP und Word¶
Skripte können Dateien erzeugen und zum Herunterladen anbieten:
pdf— das aktuelle Formular als PDF,zip— mehrere Dateien in ein ZIP-Archiv packen (auch mit Passwort) und ZIP-Archive wieder auspacken,docx— Word-Vorlagen mit Formularwerten füllen, auch als Serienbrief,delivery— beliebige Dateien zum Download anbieten.
Ausführliches dazu: pdf und delivery,
zip und docx.
Bytes — die Sprache der Dateien
Eine Datei ist für das Skript eine Folge von Bytes. pdf.render().bytes,
zip.create(…) und docx.render(…) liefern solche Bytes; delivery,
mail, fs und zip nehmen sie entgegen. So lassen sich die Module
beliebig kombinieren.
Die Word-Vorlagen der Beispiele
Die Word-Beispiele nutzen Vorlagen aus der Datei-Sandbox unter
/vorlagen/. Sie kommen mit den Demo-Formularen „docx-Brief",
„docx-Serienbrief" und „docx-Telefonrechnung" (installierbar unter
System-Administration → Formular-Bausteine → Beispiele).
Einsteiger¶
1. Das Formular als PDF¶
pdf.render() macht aus dem aktuellen Formular ein PDF — so, wie es auch
beim Drucken aussieht.
var dokument = pdf.render();
log.info("PDF mit " + dokument.bytes.length + " Bytes, Vorschlag für den Namen: " + dokument.defaultFilename);
2. Das PDF zum Herunterladen anbieten¶
deliver gibt eine Adresse zurück, unter der die Datei eine Weile abrufbar
ist. view.Link zeigt sie als Link an.
var adresse = pdf.render().deliver("Antrag.pdf");
view.add(new view.Link(adresse, "Antrag als PDF öffnen"));
3. Ein Download-Fenster¶
view.Download zeigt den Download groß in der Mitte an — schwer zu
übersehen.
var adresse = pdf.render().deliver("Bestaetigung.pdf");
view.add(new view.Download(adresse, "Bestätigung herunterladen", "Ihre Bestätigung"));
4. Den Dateinamen aus Feldern bilden¶
Sonderzeichen wie / oder : sind in Dateinamen verboten — sie werden
vorher ersetzt.
function dateiname(text) {
return String(text).replace(/[^A-Za-z0-9ÄÖÜäöüß._-]+/g, "_").replace(/_+/g, "_");
}
var name = dateiname("Antrag " + ctx.form.name + " " + ctx.form.datum) + ".pdf";
log.info(name);
view.add(new view.Link(pdf.render().deliver(name), name));
5. Zwei Dateien in ein ZIP¶
zip.create bekommt eine Liste aus Name und Inhalt.
var archiv = zip.create([
{ name: "hinweis.txt", data: "Bitte bis Freitag zurücksenden." },
{ name: "antrag.pdf", data: pdf.render().bytes }
]);
view.add(new view.Link(delivery.deliver(archiv, "unterlagen.zip"), "unterlagen.zip herunterladen"));
6. Einen Brief aus einer Word-Vorlage¶
In der Vorlage stehen Platzhalter wie {{vorname}}. docx.render setzt
die Werte ein.
var vorlage = fs["/vorlagen/einfacher-brief.docx"].readBytes();
var brief = docx.render(vorlage, {
anrede: "Frau",
vorname: ctx.form.vorname,
nachname: ctx.form.name,
strasse: "Hauptstraße 1",
plz: "58095",
ort: "Hagen",
datum: new Date().format("dd.MM.yyyy"),
betreff: "Ihre Anfrage",
text: "vielen Dank für Ihre Anfrage. Wir melden uns in Kürze.",
sachbearbeiter: ctx.user.firstName + " " + ctx.user.lastName
});
view.add(new view.Download(docx.deliver(brief, "Brief.docx"), "Brief herunterladen"));
Platzhalter ohne Wert werden einfach leer — es bleibt kein {{…}} stehen.
Fortgeschritten¶
7. Das PDF in der Datei-Sandbox ablegen¶
var ziel = fs["/beispiele/dokumente/antrag-" + ctx.form.name + ".pdf"];
ziel.writeBytes(pdf.render().bytes);
log.info("abgelegt: " + ziel.path + " (" + ziel.size + " Bytes)");
8. Einmal erzeugen, zweimal nutzen¶
Das PDF wird nur einmal erzeugt und dann per Mail verschickt und zum Download angeboten.
var dokument = pdf.render();
var mp = new mail.MessageMultipart();
mp.addText("Im Anhang Ihr Antrag.");
mp.addBytes("Antrag.pdf", dokument.bytes, "application/pdf", null);
var msg = new mail.Message();
msg.to = "antrag@beispiel.example";
msg.subject = "Ihr Antrag";
msg.content = mp;
mail.send(msg);
view.add(new view.Link(dokument.deliver("Antrag.pdf"), "Antrag ansehen"));
9. Eine selbst gebaute Datei anbieten¶
delivery.deliver liefert beliebige Bytes aus — hier eine CSV-Tabelle.
Mit mime sagst du dem Browser, was für eine Datei es ist.
var tabelle = new csv.StandardPrinter({ separator: ";", bom: true });
tabelle.println(["Artikel", "Menge"]);
tabelle.println(["Stift", 10]);
tabelle.println(["Block", 5]);
var adresse = delivery.deliver(tabelle.toByteArray(), { name: "Export.csv", mime: "text/csv" });
view.add(new view.Link(adresse, "Export.csv herunterladen"));
10. Ein ZIP Schritt für Schritt füllen¶
Wenn die Dateien in einer Schleife entstehen, ist der Builder bequemer.
var archiv = zip.newArchive();
var posten = ctx.form.positionen || [];
// Indizierte Schleife: die Nummer im Dateinamen braucht den Index, und eine
// Liste AUS DEM FORMULAR gibt ihn per forEach nicht her.
for (var i = 0; i < posten.length; i++) {
archiv.add("positionen/" + (i + 1) + "-" + posten[i].artikel + ".txt",
"Artikel: " + posten[i].artikel);
}
log.info(archiv.size + " Dateien im Archiv");
view.add(new view.Link(archiv.deliver("positionen.zip"), "positionen.zip"));
11. Den Inhalt eines ZIP anzeigen¶
var archiv = zip.create([
{ name: "belege/rechnung.txt", data: "Rechnung" },
{ name: "belege/quittung.txt", data: "Quittung" }
]);
for (var eintrag of zip.list(archiv)) {
log.info("- " + eintrag.name);
}
12. Ein ZIP auspacken¶
zip.read liefert jeden Eintrag samt Inhalt. Bei Textdateien steht der Text
direkt in text.
var archiv = zip.create([{ name: "notiz.txt", data: "Hallo aus dem Archiv" }]);
zip.read(archiv).forEach(function (eintrag) {
log.info(eintrag.name + ": " + eintrag.text);
});
13. Ein ZIP in die Sandbox entpacken¶
var archiv = zip.create([
{ name: "a.txt", data: "A" },
{ name: "unter/b.txt", data: "B" }
]);
fs["/beispiele/dokumente/archiv.zip"] = archiv; // ZIP ablegen
var n = 0;
zip.read(fs["/beispiele/dokumente/archiv.zip"]).forEach(function (e) {
if (e.directory) return; // Ordner-Einträge überspringen
fs["/beispiele/dokumente/entpackt/" + e.name] = e.bytes;
n++;
});
log.info("entpackt: " + n + " Dateien");
14. Eine einzelne Datei herausholen¶
zip.extract liefert die Bytes einer Datei — oder null, wenn es sie im
Archiv nicht gibt.
var archiv = zip.create([{ name: "da.txt", data: "vorhanden" }]);
["da.txt", "fehlt.txt"].forEach(function (name) {
var inhalt = zip.extract(archiv, name);
log.info(inhalt == null ? name + " ist nicht im Archiv" : name + ": " + inhalt.length + " Bytes");
});
15. Ein ZIP mit Passwort¶
encryption: "aes" ist die sichere Verschlüsselung. Die Voreinstellung
("zip") können dafür auch sehr alte Programme öffnen.
var geschuetzt = zip.create(
[{ name: "gehalt.txt", data: "vertraulich" }],
{ password: "Sommer2026!", encryption: "aes" });
var gelesen = zip.read(geschuetzt, { password: "Sommer2026!" });
log.info("Inhalt: " + gelesen[0].text);
16. Ein Serienbrief¶
docx.mailMerge erzeugt für jede Zeile einen Brief — alle zusammen in
einem Dokument, jeweils auf einer neuen Seite.
var vorlage = fs["/vorlagen/einladung.docx"].readBytes();
var gaeste = (ctx.form.positionen || []).map(function (g) {
return {
anrede: "Hallo", vorname: g.vorname, nachname: g.name,
briefdatum: new Date().format("dd.MM.yyyy"),
anlass: "Sommerfest", termin: "12.06.2027", uhrzeit: "18:00",
veranstaltungsort: "Kantine", rueckmeldung: "bis 01.06.2027",
veranstalter: "Das Orga-Team"
};
});
var serienbrief = docx.mailMerge(vorlage, gaeste);
log.info(gaeste.length + " Einladungen in einem Dokument");
view.add(new view.Download(docx.deliver(serienbrief, "Einladungen.docx"), "Einladungen herunterladen"));
17. Eine Tabelle im Word-Dokument¶
Für wiederholte Zeilen steht in der Vorlage {{posten}} in der Zeile
über der Wiederhol-Zeile, die Zellen darunter heißen [datum],
[ziel] … Im Skript gibst du dafür eine Liste mit.
var vorlage = fs["/vorlagen/telefonrechnung.docx"].readBytes();
var posten = [
{ datum: "01.09.", ziel: "0231 123456", dauer: "3:10", betrag: 1.90 },
{ datum: "03.09.", ziel: "030 987654", dauer: "12:00", betrag: 5.50 }
];
var summe = posten.reduce(function (s, p) { return s + p.betrag; }, 0);
var rechnung = docx.render(vorlage, {
kunde: "Anna Meyer", kundennr: "K-1001", rechnungsnr: "R-2026-09", monat: "September 2026",
posten: posten.map(function (p) {
return { datum: p.datum, ziel: p.ziel, dauer: p.dauer, betrag: kc.numFormat(p.betrag, null, null, 0, 2) };
}),
summe: kc.numFormat(summe, null, null, 0, 2)
});
log.info("Summe " + kc.numFormat(summe, null, "€", 0, 2, ",", ".", 3));
view.add(new view.Link(docx.deliver(rechnung, "Rechnung.docx"), "Rechnung öffnen"));
Profi¶
18. Ein Logo in das Word-Dokument¶
Bild-Platzhalter heißen in der Vorlage {{@logo}}. Den Wert baut
docx.image — optional mit Breite und Höhe in Pixeln.
var vorlage = fs["/vorlagen/fortbildungsantrag.docx"].readBytes();
var logo = fs["/vorlagen/uni-logo.png"].readBytes();
var antrag = docx.render(vorlage, {
logo: docx.image(logo, { width: 180, height: 48 }),
name: "Anna Meyer",
titel: "Excel für Fortgeschrittene",
von: "10.10.2026",
bis: "11.10.2026",
kosten: "450,00 €"
});
log.info("Antrag mit Logo: " + antrag.length + " Bytes");
19. Das Word-Dokument per Mail¶
docx.MIME ist der richtige Dateityp für Word-Dateien.
var brief = docx.render(fs["/vorlagen/einfacher-brief.docx"].readBytes(), {
vorname: "Anna", nachname: "Meyer", betreff: "Bestätigung", text: "hiermit bestätigen wir …"
});
var mp = new mail.MessageMultipart();
mp.addText("Ihr Schreiben im Anhang.");
mp.addBytes("Bestaetigung.docx", brief, docx.MIME, null);
var msg = new mail.Message();
msg.to = "anna.meyer@beispiel.example";
msg.subject = "Ihre Bestätigung";
msg.content = mp;
mail.send(msg);
20. Falsches Passwort abfangen¶
var geschuetzt = zip.create([{ name: "a.txt", data: "geheim" }], { password: "richtig", encryption: "aes" });
function oeffne(passwort) {
try {
return zip.read(geschuetzt, { password: passwort })[0].text;
} catch (e) {
return null;
}
}
log.info(oeffne("falsch") == null ? "Passwort falsch – bitte erneut versuchen" : "geöffnet");
log.info("mit richtigem Passwort: " + oeffne("richtig"));
21. Ungültige Dateinamen im Archiv¶
Namen mit .. könnten beim Auspacken aus dem Zielordner „ausbrechen".
KCForms weist sie deshalb ab — auch bei Archiven von außen.
try {
zip.create([{ name: "../../system/datei.txt", data: "x" }]);
} catch (e) {
log.info("abgewiesen: " + e.message);
}
22. Mehrere Serienbriefe in einem ZIP¶
Pro Abteilung ein eigener Serienbrief — zusammen in einem Archiv.
var vorlage = fs["/vorlagen/einladung.docx"].readBytes();
var personen = [
{ nachname: "Meyer", vorname: "Anna", abteilung: "Einkauf" },
{ nachname: "Schulz", vorname: "Ben", abteilung: "Vertrieb" },
{ nachname: "Wagner", vorname: "Cem", abteilung: "Einkauf" }
];
// nach Abteilung gruppieren
var gruppen = {};
personen.forEach(function (p) {
(gruppen[p.abteilung] = gruppen[p.abteilung] || []).push({
anrede: "Hallo", vorname: p.vorname, nachname: p.nachname, anlass: "Jahresgespräch"
});
});
var archiv = zip.newArchive();
Object.keys(gruppen).forEach(function (abteilung) {
archiv.add("Einladungen-" + abteilung + ".docx", docx.mailMerge(vorlage, gruppen[abteilung]));
});
log.info(archiv.size + " Dokumente gepackt");
view.add(new view.Link(archiv.deliver("Einladungen.zip"), "Alle Einladungen (ZIP)"));
23. Was steckt in einer Word-Datei?¶
Eine .docx-Datei ist selbst ein ZIP-Archiv. Der Text steht in
word/document.xml — hilfreich, um zu prüfen, welche Platzhalter eine
Vorlage enthält.
var vorlage = fs["/vorlagen/einfacher-brief.docx"].readBytes();
var inhalt = zip.read(vorlage).filter(function (e) {
return e.name === "word/document.xml";
})[0].text;
var platzhalter = inhalt.match(/\{\{[^}]+\}\}/g) || [];
log.info("Platzhalter: " + platzhalter.join(" "));
Platzhalter mit Formatwechsel
Hat jemand in Word mitten im Platzhalter die Schrift geändert, steht er
intern zerstückelt im XML — dann findet ihn auch docx.render nicht.
Abhilfe: Platzhalter in der Vorlage in einem Zug neu eintippen.
24. Aufräumen¶
Zum Schluss werden die Übungsdateien wieder entfernt.