Technischer Artikel

HTML zu PDF in Delphi: der HTML5-Paged-Media-Pfad

HotPDF wandelt HTML über THPDFHTMLImporter.RenderHTML5 in PDF-Seiten um, ein versioniertes Paged-Media-Profil mit einem echten DOM, einer echten CSS-Kaskade, Tabellen, Seitenumbrüchen und optionaler AcroForm-Erzeugung aus HTML-Formularsteuerelementen. Es läuft vollständig im eigenen Prozess: keine Browser-Engine, kein Chromium, kein externer Konverter und standardmäßig überhaupt kein Datei- oder Netzwerkzugriff

Der Grund für einen separaten Einstiegspunkt statt eines verbesserten RenderHTML ist Kompatibilität. Die ältere Methode rendert seit Jahren Rechnungen und Briefe in Produktivanwendungen, und ein nachsichtiges DOM mit vollständiger Kaskade würde die Ausgabe von Dokumenten verändern, die auf das alte Verhalten abgestimmt wurden. Deshalb behält die alte Methode ihre exakte Semantik, und alles Neue steckt hinter RenderHTML5, dessen Profilversion über HTML5ProfileVersion gemeldet wird und dessen unterstützte Meilensteine über HTML5ProfileMilestones aufgelistet werden

Was fügt das HTML5-Profil tatsächlich hinzu?

In der Praxis zählen vier Fähigkeiten. Ein strukturelles DOM, in dem html und body Container statt Textknoten sind. Eine CSS-Kaskade mit korrekter Rangfolge. Tabellenlayout. Und eine explizite Steuerung der Seitenumbrüche, was „Paged Media“ bedeutet: HTML wurde für ein scrollendes Viewport entworfen, und die Umwandlung in Seiten erfordert die Entscheidung, wo Inhalte umbrechen dürfen

Der Punkt mit den Containern klingt nach einem Implementierungsdetail, ist aber tatsächlich der Unterschied zwischen Funktionieren und Nichtfunktionieren. Werden die Wurzelelemente als Textcontainer behandelt, kollabiert das gesamte DOM beim Parsen zu einem einzigen Textblock, und jede Stylesheet-Regel, jeder Seitenumbruch, jede Tabelle und jeder Formular-Teilbaum wird danach auf nichts angewendet. Jeder HTML-Renderer, der aus einem strukturierten Dokument eine einzige undifferenzierte Textwand erzeugt, hat genau diesen Fehler gemacht

Die Kaskadenreihenfolge im Detail

Die CSS-Rangfolge ist keine einfache Sortierung. HotPDF wendet zunächst normale Deklarationen aus passenden Regeln in der Reihenfolge von Spezifität und dann Quellreihenfolge an, darüber inline-normale Deklarationen, dann die !important-Deklarationen aus passenden Regeln und schließlich inline-!important. Vier Durchgänge, in dieser Reihenfolge

HotPDF löst in Delphi jede CSS-Eigenschaft durch vier geordnete Kaskadendurchgänge auf, vom normalen Regelabgleich bis zu inline-!important-Deklarationen
Spätere Durchgänge gewinnen bei Gleichständen bewusst: Inline-Deklarationen übertrumpfen Stylesheets, und !important übertrumpft alles normal Deklarierte

Implementierungen, die dies auf „der spätere gewinnt“ reduzieren, treffen die häufigen Fälle richtig und die interessanten falsch, was schlimmer ist, als sie einheitlich falsch zu treffen, denn der Fehler zeigt sich nur in Vorlagen, die !important verwenden, um ein Basis-Stylesheet zu überschreiben — genau die Vorlagen, die von Leuten geschrieben wurden, die erwarten, dass die Kaskade funktioniert:

uses
  HPDFDoc, HPDFHTMLImport;

var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Html, Css: WideString;
begin
  Pdf := THotPDF.Create(nil);
  Importer := THPDFHTMLImporter.Create(Pdf);
  try
    Pdf.FileName := 'statement.pdf';
    Pdf.BeginDoc;

    Importer.Margin := 56;              // Punkt
    Importer.BaseFontName := 'Arial';
    Importer.BaseFontSize := 10;
    Importer.CreateAcroForms := False;  // Steuerelemente rendern, keine Felder erzeugen

    Css := '@page { margin: 56pt } ' +
           'table { border-collapse: collapse } ' +
           'tr.total { page-break-inside: avoid; font-weight: bold }';
    Html := LoadTemplate('statement.html');

    if not Importer.RenderHTML5(Html, Css) then
      raise Exception.Create('nothing was emitted - check the markup');

    Pdf.EndDoc;

    Writeln(Format('profile=%d nodes=%d rules=%d layout ops=%d',
      [Importer.Statistics.ProfileVersion,
       Importer.Statistics.ParsedNodeCount,
       Importer.Statistics.CSSRuleCount,
       Importer.Statistics.LayoutOperationCount]));
  finally
    Importer.Free;
    Pdf.Free;
  end;
end;

Das zweite Argument ist ein Autoren-Stylesheet, das zusätzlich zu dem angewendet wird, was das Dokument mitbringt — der saubere Weg, um Hausregeln wie Seitenränder und Hurenkinder-Kontrolle durchzusetzen, ohne jede Vorlage einzeln zu bearbeiten

Warum ruft der Importer nie etwas ab?

Ein HTML-zu-PDF-Konverter, der externe Referenzen auflöst, ist eine Server-Side-Request-Forgery-Maschine im Kostüm eines Dokumentgenerators. Richten Sie ihn auf benutzerdefiniertes Markup, und er wird bereitwillig http://169.254.169.254/ oder eine Datei-URL von Ihrem Anwendungsserver abrufen und das Ergebnis in ein PDF einbetten, das jemand herunterlädt

HotPDF liest daher standardmäßig weder Dateien noch das Netzwerk. Bilder und andere Ressourcen gelangen auf genau zwei Wegen hinein: als data:-URIs innerhalb des Markups oder über den OnResource-Callback, bei dem Ihr Code entscheidet, was eine URI bedeutet, und die Bytes liefert:

Der HTML-Importer von HotPDF in Delphi löst jede Ressourcen-URI lokal auf, akzeptiert data-URIs und OnResource-Callbacks und verweigert Datei- und Netzwerkzugriffe hinter Zähl- und Byte-Obergrenzen
Nur lokal eintreffende Bytes erreichen die Seite, abgelehnte Referenzen erhöhen den Zähler, und kein Abruf verlässt jemals den Prozess
procedure TReportForm.SupplyResource(Sender: TObject;
  const URI: WideString; ResourceKind: THPDFHTMLResourceKind;
  var Data: TBytes; var ContentType: string; var Handled: boolean);
begin
  Handled := False;
  // Nur aus einem freigegebenen lokalen Asset-Verzeichnis bedienen. Die URI
  // niemals an einen HTTP-Client übergeben und niemals ungeprüft
  // an einen Pfad anhängen
  if StartsText('asset:', URI) then
  begin
    Data := LoadAssetBytes(Copy(URI, 7, MaxInt));
    ContentType := 'image/png';
    Handled := Length(Data) > 0;
  end;
end;

Jede tatsächlich eintreffende Ressource wird trotzdem gegen drei getrennte Obergrenzen gezählt: wie viele Ressourcen, wie viele Bytes pro Ressource und wie viele Bytes insgesamt. Ein Callback, der getreu zurückgibt, worum gebeten wird, kann nicht dazu gebracht werden, den Speicher zu erschöpfen, nur weil ein Dokument dasselbe 50-MB-Bild zweihundertmal referenziert

Vier Budgets, ein Fehlermodus

Parserknoten, CSS-Regeln, Layout-Operationen und Ressourcen werden unabhängig voneinander gemessen, denn sie versagen unabhängig voneinander. Ein Dokument kann klein und dennoch pathologisch teuer im Layout sein; ein anderes kann flach sein mit einem Stylesheet, das darauf ausgelegt ist, den Selektor-Abgleich explodieren zu lassen. Das Erschöpfen eines dieser Budgets löst EHPDFHTMLImportBudget aus, eine eigene Exception-Klasse, damit eine Budgeterschöpfung getrennt von fehlerhafter Eingabe abgefangen werden kann

Die Statistik wird zu Beginn jedes RenderHTML5-Aufrufs zurückgesetzt, sodass die Zahlen einen einzelnen Import beschreiben und nicht die Lebensdauer des Importer-Objekts. In einem Dienst, der nicht vertrauenswürdige Vorlagen rendert, protokollieren Sie ParsedNodeCount, LayoutOperationCount und ResourceRejectCount pro Dokument: Eine steigende Ablehnungszahl bedeutet meist, dass ein Vorlagenautor Assets referenziert, die Ihr Callback nicht kennt, und eine Layout-Operationszahl nahe der Obergrenze bedeutet, dass das nächste etwas größere Dokument fehlschlagen wird

Die Wahl zwischen HTML-Import und der Layout-API

Der HTML-Import gewinnt, wenn der Inhalt von Personen, die keine Delphi-Entwickler sind, als HTML verfasst wird, wenn sich Vorlagen häufiger ändern als Code oder wenn derselbe Inhalt auch als E-Mail versendet werden muss. Der deklarative Layoutbaum, beschrieben unter deklaratives PDF-Layout mit getaggter Ausgabe, gewinnt, wenn das Dokument von Ihrem eigenen Code aus Daten erzeugt wird, wenn Sie automatisch erzeugte PDF/UA-Struktur benötigen oder wenn präzise Kontrolle über die Paginierung wichtiger ist als Komfort beim Verfassen

Beide schließen sich nicht aus. Eine gängige Form ist der HTML-Import für den Berichtskörper, während Kopf-, Fuß- und Unterschriftsblöcke direkt gezeichnet werden, da diese eine feste Geometrie haben, die keine Vorlage besitzen sollte. Tabellen sind der eine Bereich, in dem es sich lohnt, beide zu messen: Komplexe Finanztabellen paginieren manchmal besser über den dedizierten Tabellenweg, beschrieben unter Tabellen in PDF erzeugen

Setzen Sie CreateAcroForms bewusst. Ausgeschaltet werden HTML-Formularsteuerelemente als statische Grafik gerendert, was für einen gedruckten Kontoauszug gewünscht ist. Eingeschaltet werden daraus echte interaktive AcroForm-Felder, was für ein ausfüllbares Formular gewünscht ist, und das daraus folgende Feldverhalten ist unter Hinzufügen von Formularfeldern zu einem geladenen PDF beschrieben

HTML-Import, deklaratives Layout und die direkte Zeichen-API richten sich in Delphi und C++Builder alle an dasselbe Dokumentobjekt; die vollständige Funktionsliste finden Sie auf der HotPDF-Delphi-PDF-Komponentenseite