Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 9 Min.

Chrome als PDF-Drucker

Mit Puppeteer Sharp und Paged.js lassen sich druckreife Dokumente erzeugen.
© dotnetpro
Aus einem Nachrichtenartikel oder einem Blog-Artikel mal eben ein PDF erzeugen: Das funktioniert in Chromium-basierten Browsern, ohne einen zusätzlichen PDF-Drucker im Rechner einrichten zu müssen. Das kann man sich zum ­Beispiel mit der .NET-Bibliothek Puppeteer Sharp zunutze machen, um aus HTML-Templates PDF-Dokumente zu erzeugen [1].Dass Puppeteer Sharp sich als einfach zu verwendendes Tool für Web-UI-Tests mittels Chromium-basierten Browsern verwenden lässt (insbesondere mit Google Chrome und Microsoft Edge), hat die vorangegangene Ausgabe der dotnetpro gezeigt. Der Artikel unter [2] erläuterte das Zusammenspiel der Bibliothek mit dem Browser und es lohnt sich, dies zum besseren Verständnis zu lesen, doch es ist kein Muss. Kurz: Puppeteer Sharp ist eine Portierung des Node.js-Programms Puppeteer, mit dessen Hilfe sich Chromium-basierte Browser per API bequem aus .NET heraus steuern lassen.

Mobile first – auch für Druckerzeugnisse

Das Schlagwort „mobile first“ ist bei der Webseiten-Entwicklung mittlerweile Standard, und auch das papierlose Büro rückt langsam anscheinend doch näher. Spätestens bei Auftragsbestätigungen oder Rechnungen muss es in den meisten Fällen zumindest ein PDF sein, das den Kunden zugeschickt wird. Wieso also nicht gleich die eigene Firmen-Website und die dort umgesetzten Design-Entscheidungen (Schriftarten und -größen, Farbschema) als Vorlage verwenden?Puppeteer Sharp bietet für das Erzeugen von PDF-Dateien aus HTML-Seiten ein einfaches API. Im Vergleich zu anderen Template-Lösungen auf Basis von Word kann der (UI-)Entwickler hier mit den gewohnten Werkzeugen HTML und CSS arbeiten. Die Einarbeitung in eine eigene Template-Sprache und die Kosten einer Lizenz für MS Office oder eine Drittanbieter-Komponente entfallen.

Ein erstes PDF

Listing 1 zeigt das vollständige Programm für die Ausgabe der dotnetpro-Homepage als PDF. Der Code startet zuerst eine Chromium-Instanz, erzeugt in ihr eine neue Karteikarte und öffnet die Homepage der dotnetpro, bevor die Seite als PDF gespeichert wird. Das Ergebnis ist das gleiche wie bei der Auswahl der Option Als PDF speichern im Drucken …-Menü von Chromium. Das Drucken funktioniert mit Puppeteer allerdings nur im Headless-Modus. Setzt man Headless = false, schlägt das Drucken fehl. Die Optionen von Chromes Drucken-Dialog, der in Bild 1 zu sehen ist, lassen sich per PdfOptions-Objekt auch via Puppeteer übergeben.
Listing 1: Ein „Hello World“-PDF mit Puppeteer Sharp
<span class="hljs-keyword">using</span> PuppeteerSharp; <br/><br/><span class="hljs-keyword">var</span> browser = <span class="hljs-keyword">await</span> Puppeteer.LaunchAsync(<br/>    <span class="hljs-keyword">new</span> LaunchOptions <br/>{ <br/>  Headless = <span class="hljs-literal">true</span>, <br/>  ExecutablePath = <span class="hljs-string">@"C:\Program Files (x86)\"</span><br/>    + <span class="hljs-string">@"Microsoft\Edge\Application\msedge.exe"</span> <br/>}); <br/><span class="hljs-keyword">var</span> page = <span class="hljs-keyword">await</span> browser.NewPageAsync(); <br/><span class="hljs-keyword">await</span> page.GoToAsync(<span class="hljs-string">"https://www.dotnetpro.de"</span>); <br/><span class="hljs-keyword">await</span> page.PdfAsync(<span class="hljs-string">@"c:\temp\dotnetpro.pdf"</span>); 
Der Drucken-Dialogvon Chrome lässt sich mit einem PdfOptions-Objekt konfigurieren(Bild 1) © Autor

Einfache Kopf- und Fußzeilen

Die Optionen des Druckmenüs lassen sich in einem PdfOptions-Objekt als zweiter Parameter an PdfAsync() übergeben. Listing 2 zeigt dafür ein Beispiel. Hier wird das Papierformat auf A4 gesetzt; die Seitenränder werden für den Druck zuerst festgelegt, bevor Kopf- und Fußzeile angezeigt und mit Inhalten gefüllt werden.
Listing 2: Die Seite mittels PdfOptions-Parameter layouten
PdfOptions <span class="hljs-attr">pdfOptions</span> = new() { <br/>  <span class="hljs-attr">Format</span> = PuppeteerSharp.Media.PaperFormat.A4,, <br/>    <span class="hljs-attr">MarginOptions</span> = new() { <span class="hljs-attr">Top</span> = <span class="hljs-string">"20mm"</span>,<br/>    <span class="hljs-attr">Bottom</span> = <span class="hljs-string">"20mm"</span>, <span class="hljs-attr">Left="14mm",</span> <span class="hljs-attr">Right="14mm"</span> }, <br/>    <span class="hljs-attr">DisplayHeaderFooter</span> = <span class="hljs-literal">true</span>, <br/>    <span class="hljs-attr">HeaderTemplate</span> = <span class="hljs-string">"<div style=\"</span>font-size:<span class="hljs-number">10</span>px;<span class="hljs-string">"</span><br/><span class="hljs-string">      + "</span>margin: <span class="hljs-number">0</span> <span class="hljs-number">14</span>mm;\<span class="hljs-string">"<span class='title'/></"</span><br/>      + <span class="hljs-string">"div>"</span>, <span class="hljs-attr">FooterTemplate</span> = <span class="hljs-string">"<div style=\"</span><span class="hljs-string">"</span><br/><span class="hljs-string">      + "</span>font-size:<span class="hljs-number">10</span>px; margin: <span class="hljs-number">0</span> <span class="hljs-number">14</span>mm;\<span class="hljs-string">">Seite "</span><br/>      + <span class="hljs-string">"<span class=\"</span>pageNumber\<span class="hljs-string">"></span> von <span"</span><br/>      + <span class="hljs-string">" class=\"</span>totalPages\<span class="hljs-string">"></span></div>"</span>, <br/>    <span class="hljs-attr">PrintBackground</span> = <span class="hljs-literal">true</span>, <br/>}; <br/><br/>await page.PdfAsync(@<span class="hljs-string">"c:\temp\dotnetpro.pdf"</span>,<br/>  pdfOptions); 
Der Renderer kennt die folgenden Klassen und füllt das innere HTML mit den entsprechenden Werten:
  • date: das Druckdatum (ohne Uhrzeit),
  • title: der Titel der Website,
  • url: die Webadresse der Seite,
  • pageNumber: die aktuelle Seitenzahl,
  • totalPages: die Gesamtzahl der Seiten.
Das Formatieren von Kopf- und Fußzeile muss (leider) „in­line“, also direkt im jeweiligen Element erfolgen, ein Zugriff auf CSS-Dateien ist nicht möglich.Möchte man nur die Kopf- oder die Fußzeile anzeigen, ist der jeweils andere Wert mit einem leeren <span /> zu füllen, damit nicht der Standardwert angezeigt wird. PrintBackground = true sorgt dafür, dass auch via CSS mit background-image gesetzte Bilder gedruckt werden.

Vorlagen mit Puppeteer-Bordmitteln

Jetzt wird es Zeit für eine erste echte Vorlage. Folgendes Beispiel zeigt die Anschrift als Teil einer Rechnungsvorlage. Die zu ersetzenden Werte sind mit id-Tags versehen. Die Beispiel­inhalte können somit realistisch gewählt sein, was den Entwurf und die fachliche Abstimmung mit Endanwendern erheblich erleichtert:
&lt;<span class="hljs-keyword">div</span> <span class="hljs-built_in">class</span>=<span class="hljs-string">"Mahnung"</span> <span class="hljs-built_in">id</span>=<span class="hljs-string">"mahnung"</span>&gt;Mahnung&lt;/<span class="hljs-keyword">div</span>&gt; 
&lt;<span class="hljs-keyword">div</span> <span class="hljs-built_in">class</span>=<span class="hljs-string">"anschrift"</span>&gt; 
  &lt;p <span class="hljs-built_in">id</span>=<span class="hljs-string">"name"</span>&gt;Max Mustermann&lt;/p&gt; 
  &lt;p <span class="hljs-built_in">id</span>=<span class="hljs-string">"strasseHausnummer"</span>&gt;Hauptstraße <span class="hljs-number">1</span>&lt;/p&gt; 
  &lt;p <span class="hljs-built_in">id</span>=<span class="hljs-string">"plzOrt"</span>&gt;<span class="hljs-number">12345</span> Musterstadt&lt;/p&gt; 
&lt;/<span class="hljs-keyword">div</span>&gt; 
Der Code in Listing 3 zeigt das Laden der Vorlage und das Füllen mit echten Werten in den beiden Hilfsmethoden ReplaceMarker() und RemoveMarker(). Die Methoden manipulieren das DOM, um die gewünschten Werte zu schreiben – nicht ganz elegant, dafür lassen sich Referenzen auf verlinkte CSS-Dateien und eingebundene Schriftarten aber korrekt auflösen. Würde man stattdessen das HTML per page.Set­Con­tent(“<html>…“) injizieren, wäre die Auflösung relativer Referenzen deutlich aufwendiger.
Listing 3: Die Vorlage laden und Marker ersetzen
var templatePath = Path.Combine(AppDomain.&lt;br/&gt;  CurrentDomain.BaseDirectory,&lt;br/&gt;  &lt;span class="hljs-string"&gt;"RechnungTemplate.html"&lt;/span&gt;); &lt;br/&gt;&lt;span class="hljs-regexp"&gt;//&lt;/span&gt; sicherstellen, dass die Seite vollständig geladen&lt;br/&gt;&lt;span class="hljs-regexp"&gt;//&lt;/span&gt; wurde &lt;br/&gt;WaitUntilNavigation[] arg = { WaitUntilNavigation.&lt;br/&gt;  Networkidle0, WaitUntilNavigation.Load }; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;await&lt;/span&gt; page.GoToAsync(templatePath, waitUntil: arg); &lt;br/&gt;&lt;span class="hljs-keyword"&gt;await&lt;/span&gt; ReplaceMarker(&lt;span class="hljs-string"&gt;"name"&lt;/span&gt;, &lt;span class="hljs-string"&gt;"Michael Meyer"&lt;/span&gt;); &lt;br/&gt;&lt;span class="hljs-keyword"&gt;await&lt;/span&gt; ReplaceMarker(&lt;span class="hljs-string"&gt;"strasseHausnummer"&lt;/span&gt;,&lt;br/&gt;  &lt;span class="hljs-string"&gt;"Maximiliansstraße 1"&lt;/span&gt;); &lt;br/&gt;&lt;span class="hljs-keyword"&gt;await&lt;/span&gt; ReplaceMarker(&lt;span class="hljs-string"&gt;"plzOrt"&lt;/span&gt;, &lt;span class="hljs-string"&gt;"80434 München"&lt;/span&gt;); &lt;br/&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;await&lt;/span&gt; RemoveMarker(&lt;span class="hljs-string"&gt;"mahnung"&lt;/span&gt;); &lt;br/&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;await&lt;/span&gt; page.PdfAsync(@&lt;span class="hljs-string"&gt;"c:\temp\Rechnung.pdf"&lt;/span&gt;,&lt;br/&gt;  pdfOptions); &lt;br/&gt;&lt;br/&gt;async Task ReplaceMarker(string markerId,&lt;br/&gt;    string content) &lt;br/&gt;{ &lt;br/&gt;  var jSQuery = @$&lt;span class="hljs-string"&gt;"document.getElementById(&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-string"&gt;    "&lt;/span&gt;&lt;span class="hljs-string"&gt;"{markerId}"&lt;/span&gt;&lt;span class="hljs-string"&gt;").innerHTML = "&lt;/span&gt;&lt;span class="hljs-string"&gt;"{content}"&lt;/span&gt;&lt;span class="hljs-string"&gt;""&lt;/span&gt;; &lt;br/&gt;  &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; page.EvaluateExpressionAsync(jSQuery); &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;async Task RemoveMarker(string markerId) &lt;br/&gt;{ &lt;br/&gt;  var jSQuery = @$&lt;span class="hljs-string"&gt;"document.getElementById(&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-string"&gt;    "&lt;/span&gt;&lt;span class="hljs-string"&gt;"{markerId}"&lt;/span&gt;&lt;span class="hljs-string"&gt;").remove()"&lt;/span&gt;; &lt;br/&gt;  &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; page.EvaluateExpressionAsync(jSQuery); &lt;br/&gt;} 
Schriftarten bindet Chromium übrigens auch automatisch in das PDF ein. Was es dabei zu beachten gilt, erläutert der Kasten Große Dateien vermeiden und Schriftarten richtig referenzieren.

Große Dateien vermeiden und Schriftarten richtig referenzieren

Das Projekt war abgeschlossen, der Kunde davon überzeugt, dass verlorene Flexibilität durch die Umstellung von Word auf ein generiertes PDF durch eingesparte Zeit aufgewogen wird. Ist nun ­alles gut?
Mit diesem Rüstzeug ausgestattet lassen sich einfache PDF-Seiten erstellen. Was aber, wenn es doch ein längerer Bericht sein soll und die Anforderungen etwas anspruchsvoller sind? Wenn etwa unterschiedliche Kopf- und Fußzeilen für gerade und ungerade Seiten oder eine bessere Steuerung von Seitenwechseln, Fußnoten oder Tabellen erzielt werden sollen? Hier stößt Puppeteer (Sharp) an seine Grenzen.

Komplexere Elemente einfügen

Die direkte Manipulation des DOM mit JavaScript funktioniert. Aber spätestens, wenn der einzufügende Inhalt Anführungszeichen oder eventuell auch dynamisch lange Tabellen enthält, wird diese Variante unnötig komplex: Erst müssen bestimmte Zeichen in C# maskiert und sie dann für JavaScript entsprechend umgewandelt werden. Weil im nächsten Schritt das vollständige Dokument mit allen Inhalten benötigt wird, ist an dieser Stelle der Einsatz des Html Agility Pack [3] und das Erzeugen einer temporären HTML-Datei sinnvoll; das erspart die Arbeit mit JavaScript.Dazu wird zuerst eine Datenstruktur erzeugt, die alle nötigen Inhalte enthält, die es zu ersetzen gilt (Listing 4). Die Methode ReplaceMarkers() nutzt das Html Agility Pack, um das Template zu laden. Im foreach-Loop wird per XPath-Ausdruck das HTML-Element mit der entsprechenden ID ausgewählt und dieses Element entweder gelöscht oder der Wert für InnerHtml durch den Wert des replacement ersetzt. Anschließend wird die temporäre HTML-Datei gespeichert; entweder im selben Verzeichnis wie die Vorlage, oder es muss sichergestellt werden, dass die in der temporären Datei referenzierten Dateien korrekt verlinkt sind. Wie ein solches Replacement aufgebaut ist, zeigt der folgende Code:
Listing 4: Eine temporäre HTML-Datei mit ersetzten Markern erzeugen
void ReplaceMarkers(&lt;span class="hljs-keyword"&gt;string&lt;/span&gt; templatePath, &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;&lt;br/&gt;    tempFile, IEnumerable&lt;span class="hljs-tag"&gt;&amp;lt;Replacement&amp;gt;&lt;/span&gt; replacements) &lt;br/&gt;{ &lt;br/&gt;  var htmlDoc = new HtmlDocument(); &lt;br/&gt;  htmlDoc.Load(templatePath); &lt;br/&gt;&lt;br/&gt;  foreach (var replacement &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; replacements) &lt;br/&gt;  { &lt;br/&gt;    var id = replacement.Id; &lt;br/&gt;    var &lt;span class="hljs-keyword"&gt;node&lt;/span&gt; &lt;span class="hljs-title"&gt;= htmlDoc&lt;/span&gt;.DocumentNode.&lt;br/&gt;      SelectSingleNode($&lt;span class="hljs-string"&gt;"//*[@id='{id}']"&lt;/span&gt;); &lt;br/&gt;    if (&lt;span class="hljs-keyword"&gt;node&lt;/span&gt; &lt;span class="hljs-title"&gt;== null&lt;/span&gt;) &lt;br/&gt;      continue; // alterantiv: ErrorHandling &lt;br/&gt;&lt;br/&gt;    if (replacement.RemoveElement) &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;node&lt;/span&gt;.&lt;span class="hljs-title"&gt;Remove&lt;/span&gt;(); &lt;br/&gt;    else &lt;br/&gt;      &lt;span class="hljs-keyword"&gt;node&lt;/span&gt;.&lt;span class="hljs-title"&gt;InnerHtml&lt;/span&gt; = replacement.InsertValue; &lt;br/&gt;  } &lt;br/&gt;&lt;br/&gt;  htmlDoc.Save(tempFile); &lt;br/&gt;} 
<span class="hljs-keyword">public</span> <span class="hljs-keyword">struct</span> Replacement 
{ 
  <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Id; 
  <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span>? InsertValue; 
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> RemoveElement; 
} 

Seitenlayout mit Paged.js

Mit der freien und quelloffenen Java­Script-Bibliothek Paged.js [4] kann die Vorlage zum echten Druckerzeugnis werden. Paged.js dient nach eigenen Aussagen dazu, ein Seitenlayout im Browser zu erzeugen, um aus HTML-Inhalten und CSS ein PDF-Dokument zu erzeugen. Dazu genügt es, das HTML-Template um die folgenden Zeilen zu ergänzen. Das Ergebnis ist in Bild 2 zu sehen:
Aus HTML und CSSerzeugt Paged.js ein PDF-Dokument(Bild 2) © Autor
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"paged.polyfill.js"</span>&gt;</span><span class="undefined"></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"interface.css"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span></span>
<span class="hljs-tag">  <span class="hljs-attr">type</span>=<span class="hljs-string">"text/css"</span>&gt;</span> 
Paged.js erzeugt automatisch eine Seitenansicht und fängt – wie bei Büchern gewohnt – mit der ersten (also der ungeraden) Seite auf der rechten Seite an. Damit das Tool auch lokal funktioniert, sind für die entsprechende Browser-Instanz einige Sicherheitsfunktionen zu deaktivieren, siehe dazu der Kasten Lokale JavaScript-Dateien im Browser verwenden.

Lokale JavaScript-Dateien im Browser verwenden

Aus Sicherheitsgründen ist der Zugriff auf lokale Dateien in Chromium deaktiviert. Gleiches gilt für das Ausführen von Java­Script-Dateien, die mittels <em>file://</em>-Protokoll eingebunden sind. Das heißt, man muss entweder die HTML-Dateien erst auf einen Webserver schieben, um sie von dort per <em>http(s)://</em> abzurufen, oder man nutzt das Startup-Flag --disable-web-security.
Mit der Seitenansicht liefert Paged.js gleich auch eine Druckvorschau (Bild 2).Das Tool besteht aus drei Modulen, die folgende Aufgaben haben:
  • Der Chunker zerlegt die (eine, endlose) HTML-Seite in ein-
  • � zelne Papier-Seiten.
  • Der Polisher übersetzt die Paged.js-eigenen CSS-Definitionen in für den Browser verständliches CSS.
  • Der Previewer ruft Chunker und Po­lisher auf und erzeugt die Ansicht im Browser. Zusätzlich ergänzt er die Inhalte noch um weitere Referenzen, um beispielsweise die linke und rechte Seiten zu unterscheiden.
Dieser Ablauf erklärt auch, warum ein Ersetzen von Dummy-Texten via Puppeteer nur mit deutlich größerem Aufwand funktioniert und das Html Agility Pack sinnvoll ist: Der Chunker ist bereits gelaufen, wenn Puppeteer das DOM manipulieren kann. Werden jetzt der Text und insbesondere dessen Länge verändert, würden Seitenumbrüche nicht mehr an den richtigen Stellen sitzen. Schlimmer noch: Aufgrund der Art, wie Paged.js die Texte formatiert, kann es dann dazu kommen, dass Text einfach nicht angezeigt wird, weil er sich außerhalb des sichtbaren (gedruckten) Bereichs befindet.

Paged.js das Seitenformat steuern lassen

Scharfe Augen haben es in Bild 2 vielleicht schon gesehen: Das Seitenformat ist nicht DIN A4, sondern das US-Letter-Format. Das ist leicht in CSS mit der folgenden Zeile anzupassen:
@page {<span class="hljs-keyword">size</span> = A4;} 
Die Steuerung des Layouts, der Seitenränder sowie von Kopf- und Fußzeilen sollte Puppeteer Sharp besser Paged.js überlassen, damit die Voransicht und der Chunker auch mit dem gewünschten Format arbeiten. Damit dieser Parameter auch beim Erzeugen des PDF berücksichtigt wird, ist PreferCSS­Page­Size = true zu setzen:
PdfOptions <span class="hljs-attr">pdfOptions</span> = new() { 
  <span class="hljs-attr">PreferCSSPageSize</span> = <span class="hljs-literal">true</span>, 
  <span class="hljs-attr">PrintBackground</span> = <span class="hljs-literal">true</span>, 
  <span class="hljs-attr">DisplayHeaderFooter</span> = <span class="hljs-literal">false</span>}; 

Seitenränder flexibel gestalten

Seitenränder lassen sich ebenfalls über die @page-Regel in Verbund mit dem CSS-Attribut margin steuern. Sofern nicht alle Seiten gleiche Seitenränder haben sollen, kann der folgende Code den Abstand für ein Buch mit symmetrischen Abständen für gegenüberliegende Seiten festlegen:
@<span class="hljs-keyword">page</span><span class="hljs-selector-pseudo">:left</span> { 
  <span class="hljs-attribute">margin-left</span>: <span class="hljs-number">25mm</span>; 
  <span class="hljs-attribute">margin-right</span>: <span class="hljs-number">10mm</span>; 
} 
@<span class="hljs-keyword">page</span><span class="hljs-selector-pseudo">:right</span> { 
  <span class="hljs-attribute">margin-left</span>: <span class="hljs-number">10mm</span>; 
  <span class="hljs-attribute">margin-right</span>: <span class="hljs-number">25mm</span>; 
} 
Analog können @page:first die erste Seite und @page:nth(n+2) alle folgenden Seiten mit unterschiedlichen Seitenrändern definieren.

Umbrüche steuern

Umbrüche können durch die CSS-Eigenschaften break-before und break-after erzwungen werden. Als Werte sind jeweils page, left, right und avoid interessant. Während page immer zu einem einfachen Seitenumbruch führt, sorgen left und right dafür, dass der Text auf einer linken oder rechten Seite startet. Etwaig notwendige leere Seiten erzeugt Paged.js automatisch.avoid versucht, den Absatz mit dem vorherigen zusammenzuhalten, funktioniert jedoch nur eingeschränkt [5] und sollte entsprechend intensiv vor dem Einsatz getestet werden. Um einen Absatz oder Bereich beim Seitenumbruch zusammenhalten, hilft die Angabe von page-break-inside: avoid.

Dynamische Inhalte

Seine wahre Stärke spielt Paged.js bei dynamischen Inhalten aus, und es sei hier auch auf die gute Dokumentation auf der Homepage des Projekts verwiesen. Die folgenden Beispiele können nur einen ersten Eindruck der Möglichkeiten vermitteln. Ein typischer Einsatzzweck für dynamische Inhalte sind Seitenzahlen oder zum Beispiel sich wiederholende Titel in der Kopfzeile. In Paged.js ist der Seitenrand dafür in 16 Boxen unterteilt (Bild 3), die per Selektor angesprochen werden können.
Paged.js teilteine Druckseite in 16 Bereiche ein, die sich über Selektoren ansprechen lassen(Bild 3) © Autor
Folgendes Listing zeigt ein Beispiel für eine Seitennummerierung. Da es sich hierbei um Inhalte und nicht um deren Darstellung handelt, empfiehlt der Autor, diese nicht in der CSS-Datei, sondern direkt in der HTML-Datei abzulegen:
<span class="hljs-params">&lt;style&gt;</span> 
  @<span class="hljs-class">page </span>{ 
    @bottom-<span class="hljs-class">right </span>{ 
      content: <span class="hljs-string">"Seite "</span> counter(page) <span class="hljs-string">" von "</span>
        counter(pages); 
    } 
  } 
<span class="hljs-params">&lt;/style&gt;</span> 
16 Boxen an den Seitenrändern lassen sich per Selektor durch Paged.js ansprechen [6] (Bild 3). Um dynamische Inhalte in einer der 16 Boxen abzulegen, sollten diese Inhalte durch Ersetzen des inneren HTML eines <span>-Elements in die Vorlage eingebracht werden (siehe Listing 3). Anschließend lassen sie sich per string-set in der gewünschten Box anzeigen:
&lt;head&gt;<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="undefined"> </span></span>
<span class="xml"><span class="undefined">  @page { </span></span>
<span class="xml"><span class="undefined">    @top-center { </span></span>
<span class="xml"><span class="undefined">      content: string(sonderaktion) </span></span>
<span class="xml"><span class="undefined">    } </span></span>
<span class="xml"><span class="undefined">  } </span></span>

<span class="xml"><span class="undefined">  #sonderaktion { </span></span>
<span class="xml"><span class="undefined">    string-set:</span></span>
<span class="xml"><span class="undefined">      sonderaktion content(text) </span></span>
<span class="xml"><span class="undefined">  } </span></span>

<span class="xml"><span class="undefined">  .collapsed { </span></span>
<span class="xml"><span class="undefined">    visibility: collapse; </span></span>
<span class="xml"><span class="undefined">  } </span></span>
<span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span></span> 
&lt;body&gt; 
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"sonderaktion"</span></span></span>
<span class="xml"><span class="hljs-tag">  <span class="hljs-attr">class</span>=<span class="hljs-string">"collapsed"</span>&gt;</span>Nur im April: 10%</span>
<span class="xml">    Rabatt!</span>
<span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> 
Auch das Anzeigen der Überschrift des aktuellen Kapitels der jeweiligen Seite in der Kopfzeile ist möglich. Aus Platzgründen sei hier auf die Anleitung verwiesen. Zusätzlich ist über Hooks der Eingriff in den Rendering-Prozess von Paged.js an verschiedenen Stellen möglich, um gewünschte Funktionen zu ergänzen.

Fazit

Puppeteer Sharp, Html Agility Pack und Paged.js bilden ein starkes Gespann, um professionelle PDF-Dokumente zu erzeugen. Das Einbinden von Bildern hat der Artikel zwar nicht gezeigt, es ist aber durch Einfügen von Image-Tags und deren Layout mit CSS ebenfalls möglich. Die Bildbearbeitung kann dann beispielsweise mit ImageSharp erfolgen [7].Was noch fehlt, sind eine Silbentrennung für die deutsche Sprache sowie die Möglichkeit, durch punktuelles Verringern von Zeichen- oder Zeilenabständen Texte in den dafür vorgesehenen Platz einzupassen. Bei der Silbentrennung besteht die Chance, dass diese Funktion in Chromium noch dieses Jahr ergänzt wird; für Englisch ist dies bereits möglich. Alternativ wäre es möglich die Texte in .NET zu parsen und um weiche Trennzeichen (­in HTML) zu ergänzen. Der Autor konnte jedoch keine Bibliothek finden, die dies bietet.Die hier vorgestellte Lösung ist in sehr ähnlicher Form trotz Einschränkungen im produktivem Einsatz und hat einen bestehenden Prozess abgelöst, der zuerst ein Word-Dokument erstellt hat, das anschließend manuell als PDF gedruckt wurde. Die Nutzer ließen sich davon überzeugen, dass die verlorene Flexibilität und die noch bestehenden Einschränkungen durch die Zeitersparnis mehr als aufgewogen werden.Der dritte und abschließende Teil dieser Artikelreihe zu Puppeteer Sharp wird das Web-Crawling mit der Bibliothek demonstrieren.
Projektdateien herunterladen

Fussnoten

  1. Puppeteer Sharp, http://www.puppeteersharp.com
  2. Jan Hendrik Schreier, Mit Googles Werkzeugkiste, Web-UI- und End-to-End-Tests, dotnetpro 7/2021, Seite 71 ff., http://www.dotnetpro.de/A2107PuppeteerSharp
  3. Html Agility Pack (HAP), https://html-agility-pack.net
  4. Paged.js, https://pagedjs.org
  5. Paged Media, [MIX] Fix `break-avoid: after`, http://www.dotnetpro.de/SL2108PuppeteerSharp1
  6. Paged.js, Generated Content in Margin Boxes, http://www.dotnetpro.de/SL2108PuppeteerSharp2
  7. ImageSharp, http://www.dotnetpro.de/SL2108PuppeteerSharp3

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Evolutionäres Prototyping von Business-Apps - Low Code/No Code und KI mit Power Apps
Microsoft baut Power Apps zunehmend mit Features aus, um die Low-Code-/No-Code-Welt mit der KI und der professionellen Programmierung zu verbinden.
19 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige