Technischer Artikel

Automatisches Struktur-Tagging für barrierefreies PDF in Delphi

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