Technischer Artikel

Flexbox, CSS Grid und Fußnoten in PDF aus Delphi

PDF Library for Delphi rendert HTML mit echtem zweidimensionalem Layout in eine PDF-Seite: display: flex und display: grid werden vermessen und platziert, statt zu gestapelten Blöcken degradiert zu werden, und Fußnoten werden am unteren Rand der Box reserviert, die ihre Referenz trägt, mit einer Nummerierung, die über Spalten und Seiten hinweg fortlaufend bleibt. Die Einstiegspunkte sind die bekannten, DrawHTMLTextBox für eine einzelne Box und DrawHTMLStory für mehrspaltigen Fluss

Das ist wichtig, weil HTML heute der Weg ist, wie die meisten Berichtsinhalte ankommen. Vorlagen werden von Menschen erstellt, die CSS schreiben, Dashboards werden als Karten entworfen, und ein Renderer, der eine Flex-Zeile still zu vier gestapelten Blöcken zusammenfallen lässt, erzeugt ein Dokument, das dem Entwurf überhaupt nicht ähnelt. Bevor diese Fähigkeit existierte, war der einzige zweidimensionale Container, den die Engine vermaß, die Tabelle, sodass jedes Karten-Layout von Hand als Tabelle neu erstellt werden musste

Was hat sich am Layout-Modell geändert?

Die frühere Hauptschleife pflegte eine einzelne Zeilenbox und schritt die Seite hinunter voran. Dieses Modell behandelt Inline-Inhalt und gestapelte Blöcke einwandfrei und kann keinen Container ausdrücken, dessen Kinder im Verhältnis zueinander bemessen werden. Tabellen waren die einzige Ausnahme, mit ihrer eigenen zweistufigen Vermessung

Flex und Grid fügen jeweils einen begrenzten Vermessungsdurchlauf über die Kinder eines Containers hinzu, und das wichtige Wort ist begrenzt. Ein Flex-Container vermisst bis zu 256 direkte Kinder in ein festes Array. Ein Grid nutzt eine Belegungsmatrix von höchstens 64 mal 64 Zellen für deterministische automatische Platzierung. Diese Obergrenzen existieren, damit ein feindseliges oder generiertes Stylesheet keine unbegrenzte Rekursion oder quadratischen Platzierungsspeicher auslösen kann, was eine reale Sorge ist, wenn das HTML aus einer Vorlage stammt, die ein Kunde bearbeitet

Wie Flex-Elemente ihre Größen erhalten

In Zeilenrichtung summiert der Container die Basis jedes Elements zusammen mit seinen Grow- und Shrink-Gewichten und verteilt dann den übrigen Raum, positiv oder negativ, gemäß diesen Gewichten. Mit flex-wrap wird jede Zeile unabhängig gelöst, sodass eine Zeile, die in zwei Zeilen umbricht, freien Raum pro Zeile statt über den gesamten Container zuweist. In Spaltenrichtung läuft dieselbe Hauptachsenverteilung gegen entweder eine explizite Höhe oder die Inhaltshöhe

justify-content, align-items, gap und die umgekehrten Richtungen arbeiten mit Geometrie, die bereits vermessen wurde. Sie bewegen Boxen; sie lösen nie eine erneute Vermessung des Elementinhalts aus. Diese Trennung ist es, die verhindert, dass ein komplexes Dashboard seine Kinder mehrfach vermisst

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  Html, Remainder: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    Html :=
      '<div style="display:flex; gap:12px;">' +
      '  <div style="flex:2 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Revenue</b><br/>EUR 4,182,300</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Margin</b><br/>18.4%</div>' +
      '  <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
      '    <b>Backlog</b><br/>92 days</div>' +
      '</div>';

    Remainder := Lib.DrawHTMLTextBox(40, 40, 515, 120, Html);
    if Remainder <> '' then
      Log('content did not fit - carry the remainder to the next box');

    Lib.SaveToFile('dashboard.pdf');
  finally
    Lib.Free;
  end;
end;

Der Rückgabewert ist die Fortsetzungszeichenkette, mit der jeder HTML-Zeicheneinstiegspunkt meldet, was nicht hineinpasste. Sie an die nächste Box oder die nächste Seite übergeben, und der Fluss setzt dort fort, wo er aufhörte

Grid-Platzierung, und was eine Spur (Track) sein kann

Grid-Spuren akzeptieren feste Längen, Prozentwerte, die fr-Einheit, einfache repeat()-Ausdrücke und minmax(). Automatische Platzierung füllt die Belegungsmatrix deterministisch, sodass dasselbe HTML immer dieselbe Anordnung erzeugt. Explizite Koordinaten dürfen sich überlappen, was beabsichtigt ist: Ein Entwurf, der ein Badge über eine Karte legt, drückt Absicht aus, keinen Fehler. Ist nur eine Achse explizit angegeben, sucht die Platzierung nur entlang der anderen Achse

Elemente, die mehrere Zeilen überspannen, tragen ihre gemessene Höhe zurück in die von ihnen abgedeckten Zeilen ein, gemittelt über diese, was verhindert, dass ein hohes überspannendes Element eine einzelne Zeile zusammendrückt, während seine Nachbarn niedrig bleiben:

Html :=
  '<div style="display:grid; grid-template-columns:repeat(3, 1fr); ' +
  '            gap:10px;">' +
  '  <div style="grid-row:span 2; background:#eef;">Site plan</div>' +
  '  <div>Inspector</div>' +
  '  <div>Date</div>' +
  '  <div style="grid-column:2 / span 2;">Findings summary</div>' +
  '</div>';

Remainder := Lib.DrawHTMLTextBox(40, 180, 515, 260, Html);

Flex- und Grid-Kinder werden über denselben HTML-Renderer wie alles andere gerendert, was die Eigenschaft ist, die das Feature nutzbar macht statt zu einer separaten Welt. Schriften, die CSS-Kaskade, Links, Bilder, Tabellen und weitere verschachtelte Flex- oder Grid-Container verhalten sich innerhalb eines Flex-Elements exakt so wie auf der obersten Ebene, und der äußere Layoutplan hält die endgültigen Text- und Rechteck-Befehle fest, sodass wiederholtes Zeichnen den vorhandenen Vermessungs-Cache wiederverwendet

Warum sind Fußnoten ein Paginierungsproblem?

Eine Fußnote ist kein Inhalt, der dem Absatz folgt, der ihre Referenz enthält; sie ist Inhalt, der am unteren Rand derselben Box wie ihre Referenz erscheinen muss. Das kehrt die übliche Vermessungsreihenfolge um, denn der für den Fließtext verfügbare Raum hängt jetzt von Inhalt ab, der noch nicht layoutet wurde

Der Renderer vermisst die Notiz daher, sobald er auf die Referenz trifft, und zieht die Notizfläche vom Höhenbudget des Fließtexts der aktuellen begrenzten Box ab. Passen Referenz, bisheriger Fließtext und Notiz nicht alle zusammen hinein, wandern die Fußnotenmarkierung und alles danach gemeinsam in die Fortsetzungszeichenkette. Diese Regel verhindert die beiden klassischen Fehler: eine Notiz, die den Fließtext überdruckt, und eine Notiz, die auf einer Seite gestrandet ist, deren Referenz auf der vorherigen liegt

In einer begrenzten Box wird die Notizfläche am unteren Rand fixiert, mit einer Trennlinie darüber. Bei unbegrenzter Vermessung, wo es keine Boxhöhe gibt, an der fixiert werden könnte, folgt die Notizfläche unmittelbar auf den Fließtext. Die Nummerierung wird in einem Erweiterungsfeld auf dem Fortsetzungsstapel mitgeführt, sodass DrawHTMLTextBox und DrawHTMLStory die Sequenz über Spalten und Seiten hinweg fortlaufend halten, und eine Fortsetzungszeichenkette, die vor Einführung dieses Felds erzeugt wurde, setzt weiterhin korrekt fort

// Fußnoten innerhalb einer mehrspaltigen Story halten eine durchlaufende Sequenz
Html := LoadTemplate('chapter.html');    // verwendet float:footnote-Marker
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
  2,        // Spalten
  16,       // Spaltenabstand in Punkt
  20,       // maximale Seiten für diese Story
  Html);
if Remainder <> '' then
  Log('story exceeded its page budget');

Praktische Hinweise für Vorlagenautoren

Innerhalb der dokumentierten Obergrenzen entwerfen. Ein Flex-Container mit mehr als 256 direkten Kindern ist fast immer eine Datentabelle im Flex-Kostüm, und der Tabellenpfad vermisst sie ohnehin besser. Ein Grid größer als 64 mal 64 ist eine Tabellenkalkulation, und derselbe Rat gilt. Für mehrspaltigen Fließtext regelt das in Silbentrennung und ausgeglichene Textspalten beschriebene Spalten- und Silbentrennungsverhalten, wie der Fluss innerhalb jeder Spalte aussieht

Vermessen, bevor gezeichnet wird, wenn ein Layout passen muss. GetHTMLTextHeight meldet die Höhe, die eine gegebene Breite benötigen würde, was der günstige Weg ist, sich vor dem endgültigen Zeichnen zwischen zwei Layouts zu entscheiden. Und eine nicht leere Fortsetzungszeichenkette als normal behandeln, nicht als Ausnahme: Sie ist der Mechanismus, mit dem langer Inhalt paginiert, kein Fehlersignal

Stammt das HTML aus einer Berichts-Engine statt aus handgeschriebenen Vorlagen, harmoniert der datengetriebene Weg in der Dataset-Berichts-Engine gut damit und erzeugt das Markup, das Flex und Grid dann anordnen. Und muss derselbe Inhalt das PDF auch wieder verlassen, schließt der semantische Exportpfad in PDF nach Markdown und DOCX exportieren den Kreislauf

HTML-Layout, Berichtserzeugung und semantischer Export sind Teil einer Bibliothek für Delphi, C++Builder und Free Pascal; die vollständige Funktionsliste findet sich auf der PDF-Library-für-Delphi-Seite