Zum Inhalt

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.

var ordner = fs["/beispiele/dokumente"];
if (ordner.exists) ordner.remove(true);
log.info("aufgeräumt");