Auf Knopfdruck: PDFs in Anwendungen erstellen mit HTML und CSS-Modul

Seite 2: Den Seitenumbruch schützen gegen typografische Schönheitsfehler

Inhaltsverzeichnis

Im Gegensatz zu Inhalten im Browser, die kontinuierlich scrollbar sein können, benötigen die seitenbasierten Layouts Steueranweisungen für Umbrüche. Damit die oberste Überschriftenebene eines Kapitels immer auf einer neuen Seite beginnt, ist mit der Regel page-break-before: always ein Seitenumbruch zu forcieren. Zusätzlich sollen Zwischenüberschriften nicht am Ende einer Seite stehen, wofür sich mit page-break-after: avoid der Seitenumbruch direkt danach unterbinden lässt (Listing 3).

.chapter h1 {
    page-break-before: always;
    string-set: chapter-title content()
}

h2, h3 {
    page-break-after: avoid;
}

p {
    orphans: 3;
    widows: 3;
}

Listing 3: Seitenumbruch, Witwen und Waisen

Innerhalb eines Textabsatzes können durch den Seitenumbruch ebenfalls unschöne Satzfehler entstehen, die in der Typografie als Witwen und Waisenkinder bezeichnet werden.

Befindet sich die letzte Zeile eines Absatzes auf einer neuen Seite, sprechen Layouter von einer Witwe (widow) – dafür ist weiterhin auch noch der ältere Fachbegriff "Hurenkind" gängig. Steht die erste Zeile eines Absatzes auf einer Seite und der Rest auf einer neuen Seite, heißt dieser typografische Schönheitsfehler im Druckereijargon "Schusterjunge" beziehungsweise Waise (orphan).

Diese Fehler beeinträchtigen den Lesefluss und sehen im Layout nicht ästhetisch aus, daher sind sie zu vermeiden. In Listing 3 ist die Einstellung so gewählt, dass mindestens drei Zeilen eines Absatzes am Beginn oder am Ende einer Seite stehen müssen.

Zur Orientierung in einem mehrseitigen Druckwerk ist ein Inhaltsverzeichnis hilfreich.

Mit den Funktionen aus dem CSS Generated Content for Paged Media Module lässt sich das Inhaltsverzeichnis erstellen. Der HTML-Code dafür besteht aus einfachen Listen mit Links, wie es auch im Web für die Navigation üblich ist. Damit die Links im PDF-Dokument auf die richtige Seite verweisen, gilt es, die Funktion target-counter(attr(href), page) aufzurufen und als Parameter das Attribut href des Links sowie den Counter page zu verwenden (Listing 4).

<ul id="index">
   <li>
      <a href="#c1">Kapitel 1</a>
   </li>
       . . . 
</ul>
#index a:after {
    content: leader('.') "S. " target-counter(attr(href), page);
}

Listing 4: Inhaltsverzeichnis

Für die automatisierte Erstellung mit einer Anwendung als Webservice lassen sich noch weitere Techniken kombinieren, so ist eine Kombination aus Java-Code und Template-Engines machbar. Zur Verwendung unternehmensweiter Designs für die Kopf- und Fußzeile können diese in HTML Templates ausgelagert und mittels Template-Engines verarbeitet werden. Template-Engines sind Programme, die Vorlage-Dateien verarbeiten und definierte Platzhalter mit Daten befüllen – für diese Aufgabe sind Bibliotheken in verschiedenen Programmiersprachen verfügbar. Im Umfeld von Java und Spring Boot bietet sich die Java-basierte Engine Thymeleaf an.

Das folgende Beispiel liest Daten über E-Ladestellen in Wien aus einem Open-Data-Datensatz aus und bereitet sie als Report in HTML und als PDF-Dokument auf. Das Programm transformiert die Daten und listet sie als Tabelle mit E-Ladestellen aufgeteilt in Bezirke auf. Zur Vereinfachung liefert die Anwendung den Datensatz als JSON-Datei direkt mit. Das vollständige Code-Beispiel ist im GitHub-Verzeichnis des Autors unter "PDF CSS Paged Media" zu finden, es beinhaltet zusätzlich noch Beispiele zu den erwähnten Technologien Apache FOP und Apache PDFBox.

Durch die reibungslose Integration von Spring Boot und Thymeleaf ist die Konfiguration (Listing 5) der Template-Engine schnell erledigt.

@Bean
    public ClassLoaderTemplateResolver templateResolver() {
        ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
        templateResolver.setPrefix("templates/");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setSuffix(".template.html");
        templateResolver.setCharacterEncoding("UTF-8");

        return templateResolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver());
        return engine;
    }

Listing 5: Konfiguration Template-Engine in TemplateConfiguration.java

Die Vorlagen für den Report sind aufgeteilt in eine Gesamtdatei (report.template.html) und in eine Kapitelvorlage (chapter.template.html). Das Report-Template (Listing 6) erzeugt eine Coverseite mit dem div-Container mit der ID "cover" und eine Seite für das Inhaltsverzeichnis mit der ID "toc". Danach beginnt der Inhalt des Reports und für jedes Kapitel bindet das Programm das Kapitel-Template ein. Im HEAD-Bereich lassen sich die Metadaten für das PDF-Dokument angeben.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.thymeleaf.org" lang="de">
<head>
    <meta charset="UTF-8" />
    <title th:text="${title}"></title>
</head>
<body>
    <div id="cover">
        <h1 th:text="${title}"></h1>
        <p>Das ist ein Test Report Template.</p>
    </div>
    <div id="toc">
        <h1>Inhaltsverzeichnis</h1>
        <div>
            <ul id="index">
                <li th:each="idx : ${index}">
                    <a th:href="'#'+${idx.target}"><span th:text="${idx.title}"></span></a>
                </li>
            </ul>
        </div>
    </div>
    <div id="content" th:each="chapter : ${chapters}">
    <div th:replace="chapter.template.html :: chapter-fragment" />
    </div>
</body>
</body>
</html>

Listing 6: Report-Template

Im Kapitel-Template erzeugt das Programm eine Tabelle mit allen E-Ladestellen des jeweiligen Bezirks.

<body>
    <div th:id="${chapter.bezirk}" class="chapter" th:fragment="chapter-fragment">
        <h1 th:text="${chapter.title}"></h1>
        <p th:text="'Im ' + ${chapter.bezirk} + '. Bezirk gibt es ' + ${#lists.size(chapter.ladestellen)} +' E-Ladestellen.'">
        </p>
        <table>
            <tr>
                <th>Objekt-ID</th>
                <th>Adresse</th>
                <th>Bezeichnung</th>
                <th>Betreiber</th>
                <th>EVSEID</th>
            </tr>
            <tr th:each="ladestelle : ${chapter.ladestellen}">
                <td th:text="${ladestelle.objectId}"></td>
                <td th:text="${ladestelle.address}"></td>
                <td th:text="${ladestelle.designation}"></td>
                <td th:text="${ladestelle.operatorName}"></td>
                <td th:text="${ladestelle.evseId}"></td>
            </tr>
        </table>

    </div>
</body>

Listing 7: Kapitel-Template (Ausschnitt aus dem HTML-Body)

Zum Befüllen der Templates werden die Daten in einfache Java Objekte abgebildet, so gibt es zum Chapter-Template eine Chapter-Klasse in Java mit der Liste der E-Ladestellen zum Befüllen der Tabelle (Listing 8).

public class Chapter {
    private String title;    
    private Long bezirk;
    private List<ELadestelle> ladestellen;
    // getter and setter 
}

Listing 8: Chapter.java

Das Einlesen und Aufbereiten der Daten von der Datenquelle erfolgt im Beispielcode mit der Implementierung im OpenDataService. Für das Erstellen des Berichts werden die Daten als Map im Key-Value-Format aufbereitet (Listing 9).

public Map<String,Object> generateValuesForReport() {
        Map<String,Object> values = new HashMap<>();
        List<Chapter> chapters = new ArrayList<>();
        List<IndexEntry> indexEntries = new ArrayList<>();

        values.put("title", "E-Ladestellen in Wien");
        values.put("chapters", chapters);
        values.put("index", indexEntries);
        List<ELadestelle> loadingPoints;
        try {
            loadingPoints = getELadeStelleData();
            Map<Long, List<ELadestelle>> loadingPointsForDistrict = groupLoadingPointByDistrict(loadingPoints);

            for(var entry : loadingPointsForDistrict.entrySet()) {
                Chapter c = new Chapter();
                IndexEntry index = new IndexEntry();
                String title = "Ladestellen im "+entry.getKey()+". Bezirk";

                c.setBezirk(entry.getKey());
                c.setTitle(title);
                c.setLadestellen(entry.getValue());
                chapters.add(c);
                index.setTitle(title);
                index.setTarget(entry.getKey().toString());
                indexEntries.add(index);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return values;
    }

Listing 9: Die Kapitel und das Inhaltsverzeichnis aufbereiten

Jedes Kapitel erhält dabei einen Eintrag in der Liste "chapters" und zusätzlich entsteht eine Liste "index", die die Einträge für das Inhaltsverzeichnis aufnimmt. Damit das Inhaltsverzeichnis im PDF-Dokument und als HTML-Code funktioniert, kommt als ID-Element die Nummer des Bezirks zum Tragen (entry.getKey()), die auch in der gruppierten Liste an E-Ladestellen als Key Verwendung findet. Somit ist die Referenz beim HTML-Attribut href richtig gesetzt und die PDF Render Engine kann die Seitennummer dank dieser eindeutigen Konkordanz richtig zuordnen.

Die Implementierung im PDFGenerationService erstellt zunächst aus den Templates die HTML-Datei und damit anschließend das PDF-Dokument. Diese einfache Reporting-Anwendung liefert als Ergebnis eine HTML-Datei ohne Styling und ein PDF-Dokument. Soll die HTML-Datei auch als Webansicht verwendet werden, kann zusätzlich noch ein eigenes CSS für den Browser erstellt werden. Somit erstellt die Anwendung ein semantisch korrektes HTML-Dokument aus den Ausgangsdaten, welches durch die saubere Trennung der Darstellungsvorgaben in verschiedene Formate überführt werden kann.

Mit der Einbindung eines CSS-Präprozessors in den Workflow können auch noch weitere Features wie die dynamische Festlegung der Schriftgröße für das PDF-Dokument umgesetzt werden. Ein CSS-Präprozessor ist ein Programm zur Erstellung von CSS aus einer eigenen Syntax, die den Funktionsumfang erweitert. Beispiele für CSS-Präprozessoren sind die Stylesheet-Sprachen SASS und LESS.

Im Beispiel mit den E-Ladestellen würde sich auch eine Darstellung als Kartenansicht anstatt einer Tabelle anbieten. Die Render Engines unterstützen grundsätzlich die Ausführung von eingebundenem JavaScript, aber inwiefern Kartenansichten oder auch Diagramme mit externen Bibliotheken funktionieren, kann beim jeweiligen Hersteller nachgesehen werden. Die Einbindung einer Karte oder eines Diagramms als SVG (Scalable Vector Graphics) sollte immer funktionieren.