Öffnen Sie einen generierten Bericht in einem Screenreader, und die üblichen Ausfälle zeigen sich innerhalb einer Minute: „Page 3 of 40“ wird auf jeder Seite vorgelesen, das Firmenlogo wird als „Grafik“ angekündigt und dann kommt nichts, und eine Tabelle kommt als ein einziger langer Strom von Zahlen ohne Spaltennamen heraus. Die PDF Library for Delphi vermeidet alle drei mit automatischem Struktur-Tagging: Einmal SetAutoTagMode aufrufen, und gewöhnliche Zeichenaufrufe erzeugen Absätze, Überschriften, Abbildungen, Tabellen und überspringbare Seiten-Dekoration, während das Dokument gezeichnet wird
Der manuelle Weg existiert weiter. Sie können jeden Zeichenaufruf in BeginTag und EndTag verpacken, und für Dokumente mit ungewöhnlicher Struktur ist das das richtige Werkzeug. Für einen gewöhnlichen Bericht, eine Rechnung oder einen Kontoauszug bedeutet Hand-Tagging jedoch, dass die Barrierefreiheit davon abhängt, dass niemand jemals ein Paar vergisst, in jedem Codepfad, der etwas zeichnet, über die gesamte Lebensdauer des Produkts. Eine Compliance-Anforderung würde wir darauf nicht wetten
Was SetAutoTagMode einschaltet
SetAutoTagMode nimmt eine Bitmaske entgegen und liefert den vor dem Aufruf geltenden Modus zurück. Jedes Bit deckt eine Art von Inhalt ab:
AUTOTAG_TEXT(1): Text wird ein Absatz, oder eine Überschrift, wenn eine registriert wurdeAUTOTAG_FURNITURE(2): wiederkehrende Kopfzeilen, Fußzeilen und Seitennummern werden Artefakte, die ein Reader überspringtAUTOTAG_FIGURE(4): ein gezeichnetes Bild wird eine Abbildung, oder ein Artefakt, wenn Sie es als dekorativ deklariert habenAUTOTAG_TABLE(8): gezeichnete Tabellen tragen ihre Zeilen und Zellen in den StrukturbaumAUTOTAG_DEFAULT(15): alle vier, und der Wert, den wir für jeden neuen Berichts-Code empfehlen
Das Einschalten des Modus markiert das Dokument zusätzlich als getaggt, und dieser Schritt zählt mehr, als er aussieht. Nach ISO 32000-1 §14.7.1 behandelt ein Reader eine Datei als ungetaggt, sofern das /MarkInfo-Dictionary des Catalog nicht /Marked true sagt. Eine Datei kann einen vollständigen, korrekten Strukturbaum tragen und trotzdem als völlig strukturlos angekündigt werden, weil das eine Flag fehlt, das assistiven Technologien sagt, dass sie danach suchen sollen. Der Baum ist da. Nichts liest ihn
Das Text-Tagging arbeitet pro Aufruf. Jeder DrawText-, DrawWrappedText- oder DrawTextBox-Aufruf wird ein Element, ein Absatz, der Zeile für Zeile mit fünf DrawText-Aufrufen gezeichnet wird, erreicht den Reader also als fünf Einzeilen-Absätze. Wenn Ihr Layout-Code die Zeilen schon selbst bricht, lohnt sich eine Änderung: Geben Sie den ganzen Absatz an DrawWrappedText und lassen Sie einen Aufruf einen Absatz erzeugen
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1); // Ursprung oben links, y wächst nach unten
Lib.SetDocumentLanguage('en-US');
Lib.SetAutoTagMode(AUTOTAG_DEFAULT); // vor jedem RegisterHeading-Aufruf
Lib.AddStandardFont(4);
Lib.SetTextSize(18);
Lib.RegisterHeading(1, 'Annual service report');
Lib.DrawText(72, 96, 'Annual service report'); // als H1 getaggt
Lib.SetTextSize(11);
Lib.DrawWrappedText(72, 130, 450,
'Every unit installed before 2024 was inspected. ' +
'Units that failed the torque check were serviced on site.'); // ein P
// ... weitere Seiten ...
// läuft über alle vorhandenen Seiten, also aufrufen, sobald die Seiten gezeichnet sind
Lib.AddPageFooter('', 'Page &P of &N', 36, 1.0, 1); // Pagination-Artefakt
Lib.SaveToFile('report.pdf');
finally
Lib.Free;
end;
end;
Der Kommentar in der dritten Zeile steht da nicht ohne Grund. RegisterHeading queued eine Überschriftsebene nur, wenn das Text-Tagging bereits eingeschaltet ist, und SetAutoTagMode räumt jede gequeute Ebene weg, wenn es läuft. Registrieren Sie die Überschrift zuerst und schalten Sie den Modus danach ein, und der Titel kommt als schlichter Absatz heraus, ohne dass irgendein Fehler Ihnen etwas sagt
Warum wartet eine Überschrift das davor gezeichnete Bild ab?
RegisterHeading benennt die Ebene für den nächsten Text, und nur Text kann sie verbrauchen. Wird dazwischen ein Bild gezeichnet, wird das Bild eine Abbildung, und die Überschrift bleibt für den folgenden Text gequeut. Die naheliegende Implementierung lässt jedes Tag die gequeute Ebene verbrauchen, und dann produziert die gewöhnliche Sequenz – Überschrift registrieren, Banner-Bild zeichnen, Titel zeichnen – ein Banner, das als H2 getaggt ist, und einen echten Titel, der zum Absatz degradiert wurde. Ein Reader, der per Überschrift navigiert, landet auf einem Bild
Zwei weitere Dinge folgen daraus, wie die Queue arbeitet. Die Überschriftsebene hängt an dem Text, der als Nächstes kommt, egal welcher String an RegisterHeading übergeben wurde: Zeichnet Ihr Layout einen kleinen „Section 3“-Aufhänger über den Titel, wird der Aufhänger die Überschrift. Und Ebenen unter sechs werden als H6 geschrieben, denn ein Reader kennt nichts darüber hinaus. Das Title-Argument macht weiterhin seinen eigenen Job und füttert die Gliederung, die DrawTOC später in ein Inhaltsverzeichnis verwandelt
Bilder bekommen dieselbe Behandlung des „auf ein Element verbraucht“. RegisterFigure liefert die Beschreibung, die das nächste Bild erhält, und RegisterDecoration deklariert das nächste Bild als Linie, Rahmen oder Hintergrund ohne Bedeutung, was dann als Layout-Artefakt geschrieben wird (ISO 32000-1 §14.8.2.2). Jede Deklaration wird von genau einem Bild verbraucht, sodass ein späteres Bild niemals eine Beschreibung erbt, die für ein früheres geschrieben war. Alt-Text am falschen Bild ist schlimmer als keiner, denn der Reader behauptet dann etwas Falsches mit voller Überzeugung
Absichtlich haben wir nicht versucht zu erraten, welche Bilder dekorativ sind. Ein Bild ist entweder Inhalt, den der Reader beschreiben soll, oder Inhalt, den er überspringen soll, und nur der aufrufende Code weiß, welches. Der Fall, den es zu vermeiden gilt, ist der dritte: Ein Bild, das ohne beide Deklarationen gezeichnet wird, wird eine Abbildung ohne Beschreibung, die bei PDF/UA durchfällt und den Screenreader anhält, um „Grafik“ zu sagen und dann zu schweigen. Wenn Sie Ihren Berichts-Code nach DrawImage greppen, sollte jeder Treffer ein RegisterFigure oder RegisterDecoration direkt darüber stehen haben
Lib.RegisterHeading(2, 'Gearbox inspection'); // für den nächsten Text gequeut
Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0); // lädt und selektiert das Bild
Lib.DrawImage(72, 160, 360, 270); // wird eine getaggte Figure
Lib.RegisterDecoration; // bedeutungslose Linie
Lib.AddImageFromFile('divider.png', 0);
Lib.DrawImage(72, 450, 360, 4); // gezeichnet innerhalb eines Layout-Artefakts
Lib.DrawText(72, 480, 'Gearbox inspection'); // erst jetzt erscheint die H2
Die Beschreibung verdient mehr Sorgfalt als jeder andere String im Dokument. Ein nicht sehender Leser erhält sie anstelle des Bildes, und das ist alles, was er erhält. „Diagramm“ sagt ihm nichts; „Quartalsumsatz nach Region, mit der östlichen Region im Q3 am höchsten“ sagt ihm, was ein sehender Leser in zwei Sekunden aus dem Bild zieht
Tabellen: Kopfzeilen werden deklariert, nie erraten
Mit dem Tabellen-Bit eingeschaltet schreibt DrawTableRows die Tabelle, ihre Zeilen und ihre Zellen in den Strukturbaum, sodass ein Reader sagen kann, in welcher Spalte ein Wert steht, statt das Gitter als Folge zusammenhangslosen Textes vorzulesen. SetTableHeaderRowCount sagt, wie viele führende Zeilen Kopfzeilen sind. Diese Zeilen werden als TH-Zellen mit Spalten-Geltungsbereich geschrieben, was den Reader „Torque“ sagen lässt, bevor der Wert kommt, zu dem sich der Nutzer bewegt hat
Die Kopfzeilenanzahl defaultet auf null, und das war eine Entscheidung. Die erste Zeile einer Tabelle ist meist die Kopfzeile, raten läge also meistens richtig. Die Kosten des falschen Rats liegen jedoch schief: Eine Datenzeile, die für eine Kopfzeile gehalten wird, wird vor jedem Wert ihrer Spalte vorgelesen, was weit verwirrender ist als eine Tabelle ohne Kopfzeilen überhaupt. Eine Zeile Code von Ihnen ist billiger als dieser Ausfallmodus
Kopfzeilen zu deklarieren und sie auf jeder Seite zu wiederholen sind getrennte Entscheidungen. SetTableHeaderRowCount ist semantisch und ändert nichts auf der Seite. Die Wiederholung ist Layout und gehört zum RepeatHeaderRows-Argument von DrawTaggedTableRows. Wäre beides eine Einstellung, würde die Bitte um korrekte Kopfzeilen-Tags auch Kopfzeilenbänder zeichnen, die nie jemand bestellt hat, und ein Layout, das Seite für Seite abgenommen war, würde sich verschieben. Zeilen, die als RepeatHeaderRows übergeben werden, werden ebenfalls als Kopfzeilen getaggt, Sie müssen also nicht zweimal dasselbe sagen
var
TableID: Integer;
begin
TableID := Lib.CreateTable(40, 3);
Lib.SetTableHeaderRowCount(TableID, 1); // Zeile 1 ist das Kopfband
Lib.SetTableCellContent(TableID, 1, 1, 'Part');
Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
// ... Datenzeilen füllen ...
// Zeilen 1..40 in ein 600pt-Band zeichnen, pro Seite eine Kopfzeile wiederholen
Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;
Per Hand getaggte Abschnitte in einem automatischen Dokument
Automatisches Tagging tritt zurück, solange ein von Hand geöffnetes Tag noch offen ist. Das erlaubt Ihrem Code, einen Teil eines Dokuments selbst zu beschreiben und den Rest der Bibliothek zu ü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 Sie selbst verstehen. Die zweihundert Seiten Fließtext dazwischen nicht
Drei Regeln halten die Ausgabe gültig, welche Mischung Sie auch verwenden. Innerhalb eines Artefakts wird nichts getaggt, denn die Marked-Content-Sequenz eines Artefakts kann kein Strukturelement tragen, und eine Datei, die es versucht, fällt durch die PDF/UA-Validierung. Leerer Text öffnet kein Element, ein vereinzeltes DrawText mit leerem String kann also kein Element hinterlassen, das ein Reader als leer ankündigt. Und ein Bibliotheksaufruf, der intern über andere Zeichenaufrufe zeichnet, öffnet auf der äußeren Ebene genau ein Tag, sodass seine Teile nie in einem zweiten eigenen Element enden
Lesen Sie nach dem Speichern den Baum zurück, bevor Sie ihm vertrauen. Die Lese-API ist 1-basiert, was uns erwischt hat: Unsere erste Prüfschleife lief von 0 bis StructTreeItemCount - 1, bekam am Anfang einen leeren Datensatz, erreichte nie das letzte Element, und eine Zeit lang waren wir überzeugt, ein Absatz sei verschwunden. Nichts war verschwunden. Die Schleife war nur an beiden Enden um eins daneben
var
I: Integer;
begin
// Ein von Hand getaggtes Pull-Quote; das automatische Tagging hält sich heraus
Lib.BeginTag('BlockQuote', '', '');
Lib.DrawWrappedText(72, 520, 450,
'All readings were taken with the unit at operating temperature.');
Lib.EndTag;
Lib.SaveToFile('report.pdf');
// Den Baum zurücklesen: Indizes laufen von 1 bis StructTreeItemCount
if Lib.LoadFromFile('report.pdf', '') = 1 then
for I := 1 to Lib.StructTreeItemCount do
Writeln(Lib.GetStructTreeItemText(I, 2), ' ', // aufgelöster Typ
Lib.GetStructTreeItemText(I, 4)); // /Alt, falls vorhanden
end;
Eine solche Ausgabe ist ein schneller Sanity-Check, kein Urteil. Was Sie sehen wollen, ist ein H1 vor dem ersten Absatz, jede Abbildung mit ihrer Beschreibung, und keine Abbildung mit leerem /Alt
Wenn Zeichenreihenfolge und Lesereihenfolge auseinandergehen
Automatisches Tagging zeichnet Elemente in der Reihenfolge auf, in der der Inhalt gezeichnet wird. Zeichnet Ihr Layout-Code die Sidebar vor dem Body, ist das die Reihenfolge, die der Baum hält, und die Reihenfolge, in der ein Screenreader liest. Für Dokumente, bei denen visuelle und Lesereihenfolge wirklich auseinanderfallen, oder die andere Strukturtypen oder Sprache pro Element brauchen, ist die manuelle Tagging-API die bessere Wahl, und der Walkthrough in Tagged PDF und Accessibility-Struktur behandelt Rollen, Geltungsbereiche und Kopfzeilen-Bindungen im Detail
Für Seiten, die Sie nicht selbst gezeichnet haben, etwa importierte Seiten ohne Marked Content, arbeitet AutoTagPages nachträglich: Es nimmt die mediane Textgröße über die übergebenen Seiten als Fließtext an, behandelt deutlich größere Läufe als Überschriften und markiert andere Malerei als Artefakte. Es ist das richtige Werkzeug für Inhalt, den Sie nicht neu erzeugen können. Für alles, was Ihr eigener Code zeichnet, ziehen wir das Taggen zum Zeichenzeitpunkt vor, denn dort wird der Bibliothek gesagt, was eine Überschrift ist, statt aus der Schriftgröße zu raten
Wenn das Dokument fertig ist, schicken Sie es durch einen Checker statt anzunehmen: Die Hinweise zum PDF/A- und PDF/UA-Preflight zeigen, wie man ein Urteil über die produzierte Struktur bekommt, und der Artikel zum dataset-getriebenen Report-Export zeigt, wo diese Aufrufe in einer Report-Engine sitzen, die ihr Layout aus Daten baut
Weil die PDF Library for Delphi in nativem Pascal für Delphi, C++Builder und Lazarus geschrieben ist, ohne externe PDF-Laufzeit, kommt die getaggte Ausgabe aus demselben Code, der die Seite zeichnet. Die vollständige API-Referenz und Plattformliste finden Sie auf der PDF Library for Delphi-Produktseite