PDFlibPas kann ein Dokument taggen, während es gezeichnet wird. Schaltet man SetAutoTagMode ein, werden aus gewöhnlichen DrawText-Aufrufen Absätze, Text, der unmittelbar nach RegisterHeading gezeichnet wird, wird zu einer Überschrift dieser Ebene, wiederkehrende Kopf- und Fußzeilen werden zu Artefakten, die ein Reader überspringt, Bilder werden zu Abbildungen, und DrawTableRows trägt die Tabelle, ihre Zeilen und ihre Zellen in den Strukturbaum ein
Die Alternative — und bis vor Kurzem die einzige Möglichkeit — bestand darin, jeden Zeichenaufruf per Hand in BeginTag und EndTag einzuhüllen. Das funktioniert und ist für Dokumente mit ungewöhnlicher Struktur nach wie vor das richtige Werkzeug. Für den gewöhnlichen Bericht, die Rechnung oder den Kontoauszug bedeutet es jedoch, dass die Barrierefreiheit der Ausgabe davon abhängt, dass niemand über alle Codepfade, die etwas zeichnen, hinweg jemals ein Paar vergisst
Was die Modus-Bits abdecken
SetAutoTagMode nimmt eine Bitmaske entgegen und liefert den zuvor geltenden Modus zurück. AUTOTAG_TEXT (1) taggt Text als Absatz oder als Überschrift, sobald eine fällig ist. AUTOTAG_FURNITURE (2) markiert wiederkehrende Kopfzeilen, Fußzeilen und Seitennummern als Artefakte. AUTOTAG_FIGURE (4) macht aus einem gezeichneten Bild eine Abbildung oder einen Artefakt, wenn es als dekorativ deklariert wurde. AUTOTAG_TABLE (8) trägt gezeichnete Tabellen in den Strukturbaum ein. AUTOTAG_DEFAULT ist 15, also alle vier
Einschalten des Modus markiert das Dokument zusätzlich als getaggt, und dieser Schritt ist weniger kosmetisch, als er klingt. Ein Reader behandelt ein Dokument als ungetaggt, sofern der Catalog nichts anderes sagt (ISO 32000-1 §14.7.1), weshalb eine Datei, die einen vollständigen Strukturbaum ohne /MarkInfo-Deklaration enthält, von unterstützenden Technologien als völlig strukturlos angekündigt wird. Der Baum ist vorhanden; nichts liest ihn
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetAutoTagMode(AUTOTAG_DEFAULT); // text + furniture + figures + tables
Lib.AddStandardFont(4);
Lib.SetTextSize(18);
Lib.RegisterHeading(1, 'Annual service report');
Lib.DrawText(72, 96, 'Annual service report'); // becomes H1
Lib.SetTextSize(11);
Lib.DrawText(72, 130, 'Every unit installed before 2024 was inspected.');
Lib.SaveToFile('report.pdf');
finally
Lib.Free;
end;
end;
Woher weiß eine Überschrift, zu welchem Text sie gehört?
RegisterHeading benennt die Ebene für den als Nächstes gezeichneten Text und wartet auf Text. Wird ein Bild dazwischen gezeichnet, wird dieses Bild zu einer Abbildung, und die Überschrift bleibt für den darauf folgenden Text offen. Dieses Verhalten ist bewusst gewählt: Die Alternative, bei der das Bild die Überschriftsebene übernimmt, führte zu Dokumenten, in denen eine dekorative Trennlinie unter einem Titel als der Titel angekündigt wurde
Dieselbe Regel des „auf ein Element verbraucht" steuert die Abbildungen. RegisterFigure liefert die Beschreibung, die das nächste Bild erhält, und RegisterDecoration deklariert das nächste Bild als Trennlinie, Rahmen oder Hintergrund ohne Bedeutung. Beide werden durch genau ein Bild verbraucht, sodass ein späteres Bild niemals eine Beschreibung erbt, die für ein früheres bestimmt war — und genau so wird in handgetaggtem Code Alt-Text am falschen Bild befestigt
Die Beschreibung ist wichtiger als jede andere einzelne Zeichenfolge in einem barrierefreien Dokument. Ein sehbehinderter Leser erhält die Beschreibung anstelle des Bildes, und das ist alles, was er erhält. „Diagramm" ist keine Beschreibung; „Quartalsumsatz nach Region, mit der östlichen Region im Q3 am höchsten" ist eine
Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0); // becomes a tagged Figure
Lib.RegisterDecoration; // meaningless rule
Lib.AddImageFromFile('divider.png', 0); // drawn inside a layout artifact
Tabellen, Kopfzeilen und wo die Wiederholungsentscheidung liegt
Mit dem Tabellen-Bit eingeschaltet trägt DrawTableRows die Tabelle, ihre Zeilen und ihre Zellen in den Strukturbaum ein, sodass ein Reader sagen kann, in welcher Spalte ein Wert steht, statt die gesamte Tabelle als Folge zusammenhangslosen Textes vorzulesen. SetTableHeaderRowCount benennt, wie viele führende Zeilen Kopfzeilen sind; diese Zeilen werden als Kopfzellen mit Spalten-Geltungsbereich geschrieben, was es einem Reader ermöglicht, die Überschrift des Wertes anzusagen, auf dem sich der Nutzer gerade befindet
Auf diese Weise benannte Kopfzeilen bleiben, wo sie sind. Sie oben auf jeder Seite zu wiederholen ist eine Layout-Entscheidung, und das bleibt sie auch: DrawTaggedTableRows nimmt genau dafür ein RepeatHeaderRows-Argument entgegen. Die beiden getrennt zu halten, vermeidet, dass der Strukturbaum für jeden Seitenumbruch eine zweite Kopie der Kopfzeile erhält — was eine automatische Wiederholung erzeugen würde
var
TableID: Integer;
begin
TableID := Lib.CreateTable(40, 3);
Lib.SetTableHeaderRowCount(TableID, 1); // row 1 is the header band
Lib.SetTableCellContent(TableID, 1, 1, 'Part');
Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
// ... fill the data rows ...
// Draw rows 1..40 into a 600pt band, repeating one header row per page
Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;
Mischen von automatischem und manuellem Tagging
Automatisches Tagging tritt innerhalb eines per Hand geöffneten Tags in den Hintergrund. Ein Teil eines Dokuments kann vom eigenen Code beschrieben werden und der Rest der Bibliothek überlassen, ohne dass die beiden ineinanderschachteln — und das ist die Anordnung, die die meisten realen Dokumente wollen. Die Titelseite und der Signaturblock haben eine Struktur, die nur man selbst versteht; die zweihundert Seiten Fließtext dazwischen nicht
Zwei Sicherheitsregeln halten die Ausgabe sauber. Innerhalb eines Artefakts wird nichts getaggt, weil Inhalt, der als Artefakt markiert ist, kein Strukturelement tragen darf. Und leerer Text öffnet kein Element, sodass ein vereinzelnter DrawText mit leerer Zeichenfolge kein Strukturelement erzeugen kann, das ein Reader als leer ankündigen würde. Beides sind die Art von Fehlern, die handgetaggte Dokumente leise anhäufen und die ein Validator Monate später in einem Block meldet
Was automatisches Tagging weiterhin nicht für einen entscheidet
Lesereihenfolge jenseits der Zeichenreihenfolge, semantische Rollen, die nicht Absatz, Überschrift, Abbildung oder Tabelle sind, und Sprachdeklarationen. Automatisches Tagging weist Struktur in der Reihenfolge zu, in der Inhalt gezeichnet wird — wenn der Layout-Code die Seitenleiste vor dem Haupttext zeichnet, ist das die Reihenfolge, die der Baum aufzeichnet. Für Dokumente, bei denen visuelle Reihenfolge und Lesereihenfolge tatsächlich voneinander abweichen, bleibt die manuelle Tagging-API das richtige Werkzeug, und die Erläuterung der getaggten PDF- und Barrierefreiheits-Struktur behandelt Rollen, Geltungsbereiche und Kopfzeilen-Bindungen im Detail
Wenn das Dokument fertig ist, sollte man validieren statt annehmen: Die Hinweise zum PDF/A- und PDF/UA-Preflight zeigen, wie man ein Urteil über die erzeugte Struktur erhält, und die Erläuterung des datensatzgesteuerten Berichtsexports behandelt, wo diese Aufrufe in eine Berichts-Engine passen, die ihr Layout aus Daten generiert
PDFlibPas ist eine native Pascal-PDF-Bibliothek für Delphi, C++Builder und Lazarus ohne externe PDF-Laufzeit, sodass barrierefreie Ausgabe von demselben Code erzeugt wird, der das Dokument zeichnet — siehe die PDFlibPas-Produktseite für die vollständige API- und Plattformliste