HotPDF kann ein paginiertes Dokument aus einem deklarativen Baum statt aus Koordinaten aufbauen. Man setzt ein THPDFDOMDocument aus Abschnitten, Stapeln, Text, Listen und Tabellen zusammen, übergibt es an THPDFDOMRenderer, und der Renderer vermisst, paginiert, zeichnet Seitenrahmenelemente und gibt auf Wunsch den PDF/UA-Strukturbaum aus, der das Ergebnis barrierefrei macht. Der Layoutcode berechnet dabei nie eine y-Koordinate
Wer schon einmal einen koordinatengetriebenen Berichtsgenerator gewartet hat, weiß, warum das wichtig ist. Die erste Version funktioniert. Dann wächst eine Kundenadresse auf drei Zeilen, eine Tabelle bekommt weitere Zeilen, eine lokalisierte Überschrift bricht um, und jede nachgelagerte y-Position ist falsch. Die Korrekturen häufen sich als manuelle Seitenumbruch-Prüfungen, verstreut über die Geschäftslogik, und die zwei Jahre später eintreffende Anforderung nach getaggtem PDF lässt sich nicht in Code nachrüsten, der keine Ahnung hat, was ein Absatz ist
Was der Baum besitzt, und warum der Besitz strikt geregelt ist
Das DOM erzwingt auf jeder Ebene Alleinbesitz: Das Dokument besitzt seine Abschnitte, ein Abschnitt besitzt seinen Body, Header und Footer, und Stapel, Container und Tabellen besitzen ihre Kinder. Wiederverwendung geschieht über Clone oder über eine registrierte Factory, niemals durch Anhängen desselben Objekts an zwei Eltern. Diese Regel ist keine Förmlichkeit. Eine Komponente, die zweimal im Baum erscheint, würde mit unterschiedlichen Einschränkungen zweimal vermessen und beim Abbau zweimal freigegeben
Die praktische Konsequenz für aufrufenden Code ist, dass Hilfsfunktionen neue Instanzen zurückgeben. Eine Factory mit RegisterComponent zu registrieren und CreateComponent aufzurufen liefert ein benanntes Rezept, das jedes Mal eine frische Komponente erzeugt, was der richtige Weg für wiederkehrende Rahmenelemente wie einen Unterschriftenblock oder eine rechtliche Fußzeile im Baum ist
uses
HPDFDoc, HPDFLayoutDOM;
var
Doc: THPDFDOMDocument;
Section: THPDFDOMSection;
Table: THPDFDOMTable;
Row: THPDFDOMTableRow;
I: Integer;
begin
Doc := THPDFDOMDocument.Create;
Doc.GenerateStructure := True; // den PDF/UA-Strukturbaum ausgeben
Doc.Language := 'en-US';
Section := Doc.AddSection;
Section.PageWidth := 595; // A4 in Punkt
Section.PageHeight := 842;
Section.MarginLeft := 56;
Section.MarginTop := 56;
Section.MarginRight := 56;
Section.MarginBottom := 56;
Section.Style.FontName := 'Helvetica';
Section.Style.FontSize := 10;
Section.Body.AddHeading('Annual maintenance report', 1);
Section.Body.AddText('Every asset inspected during the reporting ' +
'period is listed below, grouped by site.');
Section.Body.AddSpacer(12);
Table := THPDFDOMTable.Create('assets');
Table.AddColumn(3); // Gewichtungen, keine absoluten Breiten
Table.AddColumn(1);
Table.AddColumn(1);
Table.RepeatHeaders := True;
Row := Table.AddRow(18, True); // Kopfzeile
Row[0].Text := 'Asset';
Row[1].Text := 'Last service';
Row[2].Text := 'Status';
for I := 0 to High(Assets) do
begin
Row := Table.AddRow(16);
Row[0].Text := Assets[I].Name;
Row[1].Text := Assets[I].ServiceDate;
Row[2].Text := Assets[I].Status;
end;
Section.Body.Add(Table);
end;
Wie vermeidet die Paginierung quadratische Kosten?
Die naive Art, einen Baum zu paginieren, besteht darin, alles zu klonen, was nicht passte, und auf die nächste Seite zu übertragen. Bei einer Tabelle mit zehntausend Zeilen klont das die verbleibenden Zeilen einmal pro Seite und macht aus einem linearen Dokument ein quadratisches
HotPDF splittet stattdessen eng begrenzt. Der oberste Renderer durchläuft die Body-Kinder nach Index und klont niemals einen ganzen Abschnitt oder Body. Nur verschachtelte Stapel und Container, die tatsächlich über eine Seitengrenze hinausreichen, bekommen ihren betroffenen Teilbaum geklont, und die beiden schwergewichtigen Blatttypen tragen einen Cursor statt einer Kopie: Eine Textfortsetzung speichert den noch offenen Quell-Zeichenbereich, und eine Tabellenfortsetzung speichert den noch zu platzierenden Zeilenabschnitt. Lange Dokumente bleiben linear, und lange Absätze kosten gleich viel, ob sie einmal oder fünfmal umbrechen
Die Vermessung bleibt bei Seiteneffekten ehrlich. THPDFLayoutElement.Measure muss frei von Zeichnungs-Seiteneffekten sein, und die tatsächliche Platzierung läuft immer über THotPDF.PlaceLayoutElement, dieselbe zentrale Routine, die das platzierte Fragment erneut vermisst, die Overflow-Eigentümerschaft einrichtet und Diagnosedaten festhält. Der DOM-Renderer entscheidet nur über die Regel für neue Seiten, Seitenrahmenelemente, Abstände und die Lebensdauer der Fortsetzungen
Die Tabellenkopf-Regeln, die ein endloses Dokument verhindern
Tabellenköpfe über Seiten hinweg zu wiederholen klingt einfach und verbirgt zwei Fehlerfälle. HotPDF verlangt, dass Kopfzeilen nur im ersten Lauf aufeinanderfolgender Zeilen auftreten und dass der erste Umbruch alle Kopfzeilen plus mindestens eine Body-Zeile unterbringt. Ohne die zweite Regel erzeugt ein Kopf, der höher ist als der verbleibende Platz, eine Seite, die nur aus dem Kopf besteht, gefolgt von einer weiteren identischen Seite, unendlich
Fortsetzungsseiten zeichnen den Kopf neu, und diese neu gezeichnete Kopie wird als Artefakt statt als Inhalt markiert, was sowohl für Barrierefreiheit als auch für Textextraktion die richtige Antwort ist. Die ursprüngliche Kopfzeile bleibt genau einmal in der logischen Tabellenstruktur erhalten. Überspringt man dies, verkündet ein Screenreader die Spaltentitel mitten in den Daten erneut, und ein Textextraktor fügt eine doppelte Kopfzeile zwischen Body-Zeilen ein
Es gibt zudem eine defensive Obergrenze für die Fortsetzungstiefe, denn eine benutzerdefinierte Komponente kann Split so implementieren, dass es immer einen gleichwertigen Rest zurückgibt. Der Renderer prüft die Grenze, nachdem der Rest abgetrennt wurde und bevor die nächste Seite beginnt, und der aktuelle Durchlauf gibt den Rest in seinem eigenen finally-Block frei, sodass eine fehlerhafte Drittanbieterkomponente mit einem diagnostizierbaren Fehler scheitert, statt eine Festplatte zu füllen
Ein logisches Element, viele Seitenfragmente
Automatisches Tagging ist der Punkt, an dem sich das Paginierungsmodell und das Strukturmodell einig sein müssen. Ein über zwei Seiten geteilter Absatz ist ein logischer Absatz, muss also ein Strukturelement bleiben. Aber Marked-Content-Kennungen gelten pro Seite, daher braucht jedes sichtbare Fragment seine eigene MCID auf der Seite, auf der es erscheint
HotPDF löst dies, indem es ein einziges Strukturelement beibehält und für jedes Fragment eine Marked-Content-Referenz an dessen /K-Array anhängt, wobei das Paar /Pg und /MCID Seite und Kennung identifiziert. Der ParentTree-Eintrag für diese MCID verweist zurück auf dasselbe Element. Genau das erwartet ISO 14289, und deshalb unterscheiden sich Fortsetzungsklone von gewöhnlichen Klonen: Ein gewöhnlicher Clone bedeutet neuen logischen Inhalt und bekommt eine neue semantische Identität, während der interne Fortsetzungsklon die Identität der Komponente erbt, die er fortsetzt
Die Wiederverwendung von Elementen wird über einen nach Komponentenzeiger sortierten Index semantischer Identitäten nachgeschlagen und per binärem Vergleich gesucht, was das Nachschlagen bei großen Bäumen logarithmisch hält. Der Index hält nur nicht besitzende Referenzen; die Lebensdauer der Strukturobjekte selbst verbleibt beim PDF-Objektgraphen
Strukturregeln, die der Renderer im Voraus durchsetzt
Mit aktiviertem GenerateStructure werden mehrere PDF/UA-Regeln bereits während der Baum gerendert wird geprüft, statt erst nachdem die Datei existiert. Überschriften beginnen bei Ebene 1 und dürfen keine Ebenen überspringen. LI darf nur innerhalb von L vorkommen, und Lbl sowie LBody nur innerhalb von LI. TR gehört zu einer Tabelle, und TH sowie TD zu einer Zeile. Eine Abbildung ohne Alternativtext wird im PDF/UA-Modus zurückgewiesen
Frühzeitiges Zurückweisen ist hier die bewusste Wahl. Ein Validator, der einen fehlenden Alternativtext meldet, nachdem das Dokument geschrieben wurde, sagt einem, dass eine Charge von zehntausend Kontoauszügen neu erzeugt werden muss; ein Renderer, der die Komponente ablehnt, sagt einem, welche Komponente betroffen ist, während die Daten, die sie erzeugt haben, noch im Zugriff sind. Die Konformitätsprüfung gehört weiterhin als eigener Schritt in die Pipeline, und die Mechanik dazu wird in PDF/A-, PDF/X- und PDF/UA-Validierung behandelt
var
Pdf: THotPDF;
Renderer: THPDFDOMRenderer;
Stats: THPDFDOMRenderStatistics;
begin
Pdf := THotPDF.Create(nil);
Renderer := THPDFDOMRenderer.Create;
try
Pdf.FileName := 'maintenance-report.pdf';
Pdf.BeginDoc;
Stats := Renderer.Render(Doc, Pdf);
Pdf.EndDoc;
Writeln(Format('%d page(s), %d placement(s), %d split(s)',
[Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
[Stats.StructureElementCount, Stats.MarkedContentCount,
Stats.ArtifactCount]));
Writeln(Format('deepest continuation chain: %d',
[Stats.MaximumContinuationDepth]));
finally
Renderer.Free;
Doc.Free;
Pdf.Free;
end;
end;
Der Statistikdatensatz ist nützlicher, als er zunächst wirkt. Ein stark steigender SplitCount nach einer Vorlagenänderung bedeutet meist, dass eine Komponente begonnen hat, höher zu messen als ihr Container. Ein schleichend zunehmender MaximumContinuationDepth ist die Frühwarnung für eine Komponente, deren Split pro Seite zu wenig Fortschritt macht. Und der Vergleich von ArtifactCount mit der Anzahl der Fortsetzungsseiten bestätigt, dass wiederholte Kopfzeilen tatsächlich als Artefakte getaggt wurden
Wo das DOM neben der direkten API einzuordnen ist
Das DOM ersetzt das direkte Zeichnen nicht; es sitzt auf denselben Seitenobjekten auf. Alles, was der Renderer platziert, kann mit direkten Aufrufen auf THotPDF verschachtelt werden, was wichtig ist, wenn ein Bericht ein handplatziertes Element wie ein Unterschriftsbild an einer exakten Position benötigt. Der Seitenabschluss bleibt unter der Kontrolle von AddPage und EndDoc, sodass der sofortige Flush-Modus keine abgeschlossenen Seiten im Speicher hält und der residente Speicher weiterhin von den laufenden Fortsetzungen, Schriftressourcen und dem gewöhnlichen Dokumentobjektgraphen bestimmt wird
Das DOM sollte gewählt werden, wenn der Inhalt datengetrieben und das Layout regelgetrieben ist, und direktes Zeichnen für feste Grafiken beibehalten werden. Wer aktuell speziell mit Tabellenpaginierung zu kämpfen hat, sollte zunächst den engeren Ansatz in Tabellen in PDF erzeugen lesen, und Textverhalten wie Blocksatz wird in Textausrichtung beschrieben
Deklaratives Layout, automatisches Tagging und die direkte Zeichen-API sind in derselben Komponente für Delphi und C++Builder enthalten; die vollständige Funktionsliste findet sich auf der HotPDF-Delphi-PDF-Komponentenseite