Nach fast fünf Jahren bei codecentric ist es nun endlich so weit, dass ich auf meine Zeit vor codecentric zurückblicke und ein Thema betrachten möchte, das immer noch viele Menschen im Rahmen von Softwareentwicklungsprojekten bewegt: die Generierung von PDFs. Noch immer versuchen wir, mithilfe von Bibliotheken wie iText oder PDFBox auf programmatische Weise diese portablen Dokumente zu erzeugen. Was mit einfachen Layouts auch einwandfrei funktioniert. Aber was passiert, wenn die Layouts und Datenlagen komplexer erscheinen und die Geschwindigkeit bei der Erstellung entscheidend ist?
Dazu möchte ich ein wenig in der Zeit zurückgehen. Während meines Studiums Anfang der 2000er und auch danach habe ich immer wieder versucht, die Erstellung von PDFs auf Basis von LaTeX und einiger Derivate wie Lua(La)TeX und ConTeXt zu automatisieren, wobei die Datenlagen grundsätzlich unterschiedlich waren. Und genau diese erfolgreichen Experimente führten dazu, dass ich mich Anfang der 2010er in die Welt des Consulting und der Entwicklung begab und in einem Team versuchte, einen TeX-Renderer in den Markt des Database Publishings zu bringen. Hierbei ging es konkret darum, Kataloge und Datenblätter in hoher Stückzahl und gleichzeitig hoher Geschwindigkeit zu erstellen. Es ergaben sich eine Menge Projekte mit vielen Learnings und vielen interessanten Einblicken in komplexe Datenstrukturen sowie entsprechenden Datenbanken und ersten Schritten hin zu Integrationen. Mitte der 2010er wurde ich dann auf ein Produkt aufmerksam, das zumindest den Aufwand hinsichtlich der Erstellung von Layouts zu reduzieren versprach: speedata Publisher . Bei diesem Renderer bilden XML-Dateien die Grundlage sowohl bezogen auf die Daten als auch auf ein mögliches Layout. Das Tool basiert ansonsten technologisch auf LuaTeX, Lua und Go. Nun aber genug der Theorie und Geschichte, hinein in die Praxis.
Installation und sonstige Voraussetzungen
Zuallererst wollen wir den Publisher auf unserem jeweiligen System installieren. Hierzu werden für das jeweilige Betriebssystem Pakete zur Verfügung gestellt. Für die nachfolgenden Betrachtungen verwende ich den Development-Release (bei Veröffentlichung des Blog-Posts in der Version 4.3.7 ), Visual Code Studio mit XML-Extension von Red Hat. Der speedata Publisher wird mit sp
im Terminal aufgerufen, dazu muss in der Path-Variable das bin
-Verzeichnis des Publishers hinterlegt werden. Um die Autovervollständigung in Visual Studio Code nutzen zu können, hinterlegen wir in den Einstellungen der XML-Extension das XML-Schema des Publishers.
Das obligatorische Beispiel
Beim Database Publishing geht es ja, wie der Begriff schon erkennen lässt, um die Veröffentlichung von Daten aus einer entsprechenden Quelle. Für den speedata Publisher müssen die Daten in Form einer XML bereitstehen. Wobei das Produkt auch in der Lage ist, entsprechende Datenlage mit vorgelagerten Filtern in XML umzuwandeln. Dies würde diesen Einführungspost inhaltlich deutlich sprengen. Für das Beispiel verwenden wir Produktdaten eines fiktiven Baumarktes, die sich in Zeiten von Corona großer Beliebtheit erfreuen.
1<products> 2 <product name="Schnellbauschraube"> 3 <image>screw.pdf</image> 4 <description> 5 <diameter>3,9mm</diameter> 6 <details>Senkkopf, Grobgewinde</details> 7 <dash>Ideal für Gipskartonplatten auf Holz</dash> 8 <dash>Phosphatierte Oberfläche</dash> 9 <dash>Gute Anhaftung von Gips auf dem Schraubkopf</dash> 10 <dash>Vor Rost geschützt</dash> 11 </description> 12 <attribute type="length" value="25mm"> 13 <quantity unit="Stk" price="2" currency="€">200</quantity> 14 <quantity unit="Stk" price="5" currency="€">500</quantity> 15 <quantity unit="Stk" price="7" currency="€">1000</quantity> 16 </attribute> 17 <attribute type="length" value="35mm"> 18 <quantity unit="Stk" price="3" currency="€">200</quantity> 19 <quantity unit="Stk" price="6" currency="€">500</quantity> 20 <quantity unit="Stk" price="8" currency="€">1000</quantity> 21 </attribute> 22 <attribute type="length" value="45mm"> 23 <quantity unit="Stk" price="4" currency="€">200</quantity> 24 <quantity unit="Stk" price="7" currency="€">500</quantity> 25 <quantity unit="Stk" price="9" currency="€">1000</quantity> 26 </attribute> 27 </product> 28 <product name="Schraubendreher-Set"> 29 <image>screwdrivers.pdf</image> 30 <description> 31 <details>Geeignet für: Schlitz und Kreuzschlitz PH, 6-tlg.</details> 32 <dash>Isolierte VDE Klingen für sicheres Arbeiten</dash> 33 <dash>Mehrkomponentiger Griff</dash> 34 <dash>Mit Griffkennzeichnung zum leichteren Finden</dash> 35 <dash>Erhöhter Korrosionsschutz</dash> 36 </description> 37 <attribute type="package" value="6"> 38 <quantity unit="Verpackung" price="11" currency="€">1</quantity> 39 </attribute> 40 </product> 41 <product name="Schraubendreher-Set"> 42 <image>screwdrivers.pdf</image> 43 <description> 44 <details>Geeignet für: Schlitz und Kreuzschlitz PH, 6-tlg.</details> 45 <dash>Isolierte VDE Klingen für sicheres Arbeiten</dash> 46 <dash>Mehrkomponentiger Griff</dash> 47 <dash>Mit Griffkennzeichnung zum leichteren Finden</dash> 48 <dash>Erhöhter Korrosionsschutz</dash> 49 </description> 50 <attribute type="package" value="6"> 51 <quantity unit="Verpackung" price="11" currency="€">1</quantity> 52 </attribute> 53 </product> 54 <product name="Schraubendreher-Set"> 55 <image>screwdrivers.pdf</image> 56 <description> 57 <details>Geeignet für: Schlitz und Kreuzschlitz PH, 8-tlg.</details> 58 <dash>Isolierte VDE Klingen für sicheres Arbeiten</dash> 59 <dash>Mehrkomponentiger Griff</dash> 60 <dash>Mit Griffkennzeichnung zum leichteren Finden</dash> 61 <dash>Erhöhter Korrosionsschutz</dash> 62 </description> 63 <attribute type="package" value="8"> 64 <quantity unit="Verpackung" price="13" currency="€">1</quantity> 65 </attribute> 66 </product> 67 <product name="Schraubendreher-Set"> 68 <image>screwdrivers.pdf</image> 69 <description> 70 <details>Geeignet für: Schlitz und Kreuzschlitz PH, 10-tlg.</details> 71 <dash>Isolierte VDE Klingen für sicheres Arbeiten</dash> 72 <dash>Mehrkomponentiger Griff</dash> 73 <dash>Mit Griffkennzeichnung zum leichteren Finden</dash> 74 <dash>Erhöhter Korrosionsschutz</dash> 75 </description> 76 <attribute type="package" value="10"> 77 <quantity unit="Verpackung" price="15" currency="€">1</quantity> 78 </attribute> 79 </product> 80 <product name="Schraubendreher-Set"> 81 <image>screwdrivers.pdf</image> 82 <description> 83 <details>Geeignet für: Schlitz und Kreuzschlitz PH, 12-tlg.</details> 84 <dash>Isolierte VDE Klingen für sicheres Arbeiten</dash> 85 <dash>Mehrkomponentiger Griff</dash> 86 <dash>Mit Griffkennzeichnung zum leichteren Finden</dash> 87 <dash>Erhöhter Korrosionsschutz</dash> 88 </description> 89 <attribute type="package" value="12"> 90 <quantity unit="Verpackung" price="17" currency="€">1</quantity> 91 </attribute> 92 </product> 93</products>
Der Erstellungsprozess
Nun gilt es, diese Daten in ein Layout zu setzen. Hierzu wird eine layout.xml erstellt.
1<Layout 2 xmlns="urn:speedata.de:2009/publisher/en" 3 xmlns:sd="urn:speedata:2009/publisher/functions/en"> 4</Layout>
Ein Layout beginnt grundsätzlich auf Basis eines Layout
-Nodes mit dem entsprechendem Namespace. Im Folgenden werden die Sprache des PDFs und die Schriftarten mit ihren einzelnen Definitionen beschrieben.
1<Options mainlanguage="German" imagenotfound="warning"/> 2 <LoadFontfile name="RubikRegular" filename="https://github.com/googlefonts/rubik/raw/main/fonts/otf/Rubik-Regular.otf"/> 3 <LoadFontfile name="RubikBold" filename="https://github.com/googlefonts/rubik/raw/main/fonts/otf/Rubik-Bold.otf"/> 4 <LoadFontfile name="RubikItalic" filename="https://github.com/googlefonts/rubik/raw/main/fonts/otf/Rubik-Italic.otf"/> 5 <LoadFontfile name="RubikMedium" filename="https://github.com/googlefonts/rubik/raw/masin/fonts/otf/Rubik-Medium.otf"/> 6 <LoadFontfile name="RubikLight" filename="https://github.com/googlefonts/rubik/raw/main/fonts/otf/Rubik-Light.otf"/> 7 <LoadFontfile name="RubikBoldItalic" filename="https://github.com/googlefonts/rubik/raw/main/fonts/otf/Rubik-BoldItalic.otf"/> 8 <LoadFontfile name="RubikMediumItalic" filename="https://github.com/googlefonts/rubik/raw/main/fonts/otf/Rubik-MediumItalic.otf"/> 9 <LoadFontfile name="RubikLightItalic" filename="https://github.com/googlefonts/rubik/raw/main/fonts/otf/Rubik-LightItalic.otf"/> 10 <DefineFontfamily fontsize="11" leading="12" name="description"> 11 <Regular fontface="RubikRegular"/> 12 <Bold fontface="RubikBold"/> 13 <Italic fontface="RubikItalic"/> 14 <BoldItalic fontface="RubikBoldItalic"/> 15 </DefineFontfamily> 16 <DefineFontfamily fontsize="12" leading="13" name="product"> 17 <Regular fontface="RubikRegular"/> 18 <Bold fontface="RubikBold"/> 19 <Italic fontface="RubikItalic"/> 20 <BoldItalic fontface="RubikBoldItalic"/> 21 </DefineFontfamily> 22 <DefineFontfamily fontsize="17" leading="19" name="price"> 23 <Regular fontface="RubikRegular"/> 24 <Bold fontface="RubikBold"/> 25 <Italic fontface="RubikItalic"/> 26 <BoldItalic fontface="RubikBoldItalic"/> 27 </DefineFontfamily>
Nach diesen Fontregeln gilt es, ein einfaches Tabellenlayout in das PDF zu schreiben. Hierzu werden mittels die einzelnen Nodes der Daten-XML angesprochen, wobei eine erste Tabelle erst auf Basis des
-Node erstellt wird. Mittels
wird auf Basis von Variablen ein dynamisches Layout in Bezug auf die Produktbeschreibung erzeugt. Für die Nodes
,
und
werden die Elemente im PDF per Loops über
erzeugt.
1<Record element="products"> 2 <ProcessNode select="product"/> 3 </Record> 4 <Record element="product"> 5 <SetVariable variable="diameter" select="description/diameter"/> 6 <SetVariable variable="type" select="attribute/@type"/> 7 <PlaceObject> 8 <Table stretch="max"> 9 <Columns> 10 <Column width="1*"/> 11 <Column width="2*"/> 12 </Columns> 13 <Tr> 14 <Td> 15 <Image file="{image}"/> 16 </Td> 17 <Td align="left" valign="top"> 18 <Paragraph fontfamily="product"><B><Value select="@name"/></B></Paragraph> 19 <Paragraph fontfamily="description"> 20 <Switch> 21 <Case test="not(empty($diameter))"> 22 <Value select="$diameter"/> 23 <Value> Durchmesser, </Value> 24 <Value select="description/details"/> 25 </Case> 26 <Otherwise> 27 <Value select="description/details"/> 28 </Otherwise> 29 </Switch> 30 </Paragraph> 31 <ForAll select="description/dash"> 32 <Table max="stretch" fontfamily="description"> 33 <Columns> 34 <Column width="5mm"/> 35 <Column width="5mm"/> 36 <Column width="1*"/> 37 </Columns> 38 <Tr valign="top"> 39 <Td></Td> 40 <Td> 41 <Paragraph> 42 <Value>• </Value> 43 </Paragraph> 44 </Td> 45 <Td> 46 <Paragraph textformat="justified"> 47 <Value select="."/> 48 </Paragraph> 49 </Td> 50 </Tr> 51 </Table> 52 </ForAll> 53 <ForAll select="attribute"> 54 <Switch> 55 <Case test="$type != 'package'"> 56 <Table> 57 <Tr> 58 <Td></Td> 59 <Td><Paragraph><Value select="@value"/>:</Paragraph></Td> 60 </Tr> 61 </Table> 62 <Table width="5" stretch="max"> 63 <Tablehead> 64 <Tr backgroundcolor="gray"> 65 <Td><Paragraph><Value>Menge</Value></Paragraph></Td> 66 <Td><Paragraph><Value>Einheit</Value></Paragraph></Td> 67 <Td><Paragraph><Value>Preis</Value></Paragraph></Td> 68 </Tr> 69 </Tablehead> 70 <ForAll select="quantity"> 71 <Tr> 72 <Td><Paragraph><Value select="."/></Paragraph></Td> 73 <Td><Paragraph><Value select="@unit"/></Paragraph></Td> 74 <Td><Paragraph><Value select="concat(@price,@currency"/></Paragraph></Td> 75 </Tr> 76 </ForAll> 77 </Table> 78 </Case> 79 <Otherwise> 80 <Table width="10" stretch="max"> 81 <Tr> 82 <Td align="right" height="30"><Paragraph fontface="price"><Value select="concat(quantity/@price,quantity/@currency)"/></Paragraph></Td> 83 </Tr> 84 </Table> 85 </Otherwise> 86 </Switch> 87 </ForAll> 88 </Td> 89 </Tr> 90 </Table> 91 </PlaceObject> 92 </Record>
Das Ergebnis stellt nun einen Produktkatalog mit einem einfachen Tabellenlayout, inklusive Logik, dar. Da bei Erstellung des PDFs die entsprechenden Produktbilder nicht vorhanden waren, wurde diese direkt durch einen Platzhalter ersetzt. Die Erstellung des PDFs dauerte auf einem MacBook Pro (2,3 GHz 8-Core Intel Core i9, 32 GB RAM) genau 0.579982 Sekunden.
Wie wir sehen konnten, ist es mithilfe des speedata Publishers recht einfach, auf Basis von Datenstrukturen ein entsprechendes PDF in hoher Geschwindigkeit zu erstellen. Zum Abschluss gilt es nun, die lokale Umgebung in einen Container zu packen.
PDFs aus dem Container
Leider gibt es noch keinen fertigen Docker-Container, sodass wir direkt loslegen könnten. Daher gilt es, diesen nun zu erstellen.
1FROM ubuntu:latest 2 3ENV DEBIAN_FRONTEND=noninteractive 4RUN apt update && apt install -y gnupg openjdk-13-jre-headless inkscape 5 6COPY files/speedata.list /etc/apt/sources.list.d 7COPY files/gpgkey-speedata.txt /tmp/gpgkey-speedata.txt 8RUN APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 apt-key add /tmp/gpgkey-speedata.txt 9 10RUN apt update 11RUN apt install -y speedata-publisher 12 13WORKDIR /server 14ADD files/publisher.cfg /server 15 16ENTRYPOINT [ "sp", "server" ]
Im Ordner files befindet sich neben speedata.list
und gpgkey-speedata.txt
noch die Datei publisher.cfg
. Hier wird über Key-/Value-Parameter die Konfiguration des speedata Publishers vorgenommen. Wenn das Image nun mit docker build -t pdf-container
und der Container anschließend mittelsdocker run --rm -p 5266:5266 pdf-container
gestartet wurde, verfügt dieser über die nachfolgenden Endpunkte.
Methode | Endpunkt | Beschreibung |
---|---|---|
GET | /available | Es wird ein Statuscode 200 zurückgegeben, wenn der Publishing Server verfügbar ist. |
POST | /v0/publish | Es werden Daten an den Server gesendet, um einen Publishing-Lauf zu starten. Eine ID des Publishing-Laufs wird als Rückgabewert geliefert. |
GET | /v0/publish/ | Es wird geprüft, ob ein Publishing Prozess beendet ist. |
GET | /v0/pdf/ | Es wird auf die Fertigstellung eines PDFs gewartet und das PDF wird heruntergeladen. |
GET | GET/v0/data/ | Es wird die data.xml des jeweiligen Publishing-Laufs in Referenz zur angegebenen ID geladen. |
GET | /v0/layout/ | Es wird die layout.xml des jeweiligen Publishing-Laufs in Referenz zur angegebenen ID geladen. |
GET | /v0/statusfile/ | Es wird die Statusdatei (publisher.status) des jeweiligen Publishing-Laufs in Referenz zur angegebenen ID geladen. |
GET | /v0/status | Es wird eine Übersicht über die laufenden Publishing-Prozesse zurückgegeben. |
GET | /v0/status/ | Es wird eine Übersicht über einen laufenden Publishing-Prozess in Referenz zur angegebenen ID zurückgegeben. |
POST | /v0/delete/ | Es wird ein Publishing-Lauf in Referenz zur angegebenen ID gelöscht. |
Mit /v0/publish
können wir nun Daten und das Layout an den „PDF-Container“ senden. Diese müssen in einer JSON-Datei base64-kodiert hinterlegt werden. Was bezogen auf unser Beispiel folgendermaßen aussehen würde.
1{ 2 "layout.xml": "...", 3 "data.xml": "..." 4}
Als Rückgabe erhalten wir eine ID mit Statuscode 201 zurück. Mithilfe dieser ID können wir nun dem GET-Request v0/pdf/
das PDF vom Server laden. Durch Setzen des Parameters delete
auf den Wert false
wird das PDF nicht vom Server gelöscht.
Fazit
Mit dem speedata Publisher können wir auf recht einfache Art und Weise PDFs erzeugen und diesen Erstellungsprozess, vor allem aufgrund eines Containers, in bestehende Entwicklungsprojekte einbinden. Das Beispiel und das Dockerfile sind in GitHub zu finden.
Weitere Beiträge
von Daniel Kocot
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Daniel Kocot
Senior Solution Architect / Head of API Consulting
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.