Technischer Artikel

Tagged-PDF-Strukturbäume in Delphi mit PDFlibPas bauen

Lassen Sie einen Screenreader über eine typische generierte Rechnung laufen und hören Sie, was er findet: Die Gesamtsumme wird vor den Positionen angesagt, der Seitenfuß unterbricht einen Absatz, die Positionstabelle wird zu einem undifferenzierten Wortstrom abgeflacht. Die Seite sieht perfekt aus und ist semantisch leer, weil der Content Stream Zeichenreihenfolge aufzeichnet, nicht Bedeutung. Die Antwort von PDF ist der in ISO 32000-1 §14.7 definierte Strukturbaum — eine logische Hierarchie aus Überschriften, Absätzen, Tabellen und Abbildungen, die über den gemalten Inhalt gelegt wird —, und die Ökonomie ist eindeutig: Struktur beim Erzeugen auszugeben kostet Minuten Code, sie nachträglich in fertige Dokumente einzubauen ist ein Remediation-Projekt. losLab PDF Library (PDFlibPas) legt diese Mechanik für Delphi- und C++Builder-Anwendungen über eine kleine Menge Aufrufe offen, die jede Zeichenoperation in ihre logische Rolle einrahmen.

Wie Marked Content an den Strukturbaum bindet

Zwei Schichten arbeiten zusammen. Im Content Stream werden Zeichenoperationen in Marked-Content-Sequenzen geklammert, jede mit einem ganzzahligen MCID. Im Dokumentkatalog bildet der Strukturbaum diese MCIDs auf eine Hierarchie typisierter Elemente ab — H1, P, Table, Figure — mit Attributen wie Alternativtext und Sprache. Eigene Elementtypen sind erlaubt, müssen aber über die Role Map auf Standardrollen auflösen (ISO 32000-1 §14.8.4), und Inhalt ohne Bedeutung — Linien, Hintergründe, wiederholtes Seitenmobiliar — wird als Artefakt markiert, damit Assistive Technology ihn überspringt, statt ihn mitten im Satz vorzulesen.

PDFlibPas pflegt beide Schichten hinter einem Klammerpaar: BeginTag öffnet ein Strukturelement und startet die Marked-Content-Sequenz, Zeichenaufrufe landen darin, und EndTag schließt beides. Die Buchführung — MCIDs, Parent Tree, Seitenreferenzen — geschieht intern und entfernt den fehleranfälligsten Teil von handgebautem Tagging.

Zwei dokumentweite Schalter rahmen die Arbeit, bevor ein Tag geöffnet wird. SetMarkInfo schreibt das Katalogflag, das das Dokument als getaggt deklariert, und IsTaggedPDF liest es zurück — der billigste Erstcheck, wenn entschieden werden muss, ob eine eingehende Datei überhaupt Struktur besitzt, die es zu erhalten lohnt. Für Sprache setzt SetDocumentLanguage allein den Dokumentstandard, während SetPDFUAMode ihn als Teil vollständiger PDF/UA-Ausgabe setzt; eine Datei kann sinnvoll getaggt sein, ohne PDF/UA-Konformität zu beanspruchen, und ein gestufter Rollout beginnt oft genau dort.

Beim Zeichnen taggen, nicht hinterher

Das Erzeugungsmuster, das funktioniert, behandelt die Tag-Klammer als Teil der Signatur jedes Zeichenaufrufs, nie als späteren Durchgang:

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // top-left origin
    Lib.SetPDFUAMode('en-US');                 // bumps the save version to PDF 1.7
    Lib.SetInformation(1, 'Service Manual');   // /Title is mandatory for PDF/UA
    Lib.AddRoleMap('ManualTitle', 'H1');       // custom type -> standard role
    Lib.AddStandardFont(4);
    Lib.SetTextSize(18);
    Lib.BeginTagEx2('ManualTitle', '', '', 'en-US', '', 'h1-cover', '');
    Lib.DrawText(72, 96, 'Service Manual');
    Lib.EndTag;
    Lib.BeginTag('Figure', 'Exploded view of the gearbox assembly', '');
    Lib.AddImageFromFile('gearbox.png', 0);
    Lib.EndTag;
    Lib.BeginArtifact('Layout');               // page decoration: excluded from reading
    // ... draw rules and background tint ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Drei Aufrufe in dieser Sequenz tragen Compliance-Gewicht. SetPDFUAMode aktiviert PDF/UA-Ausgabe und hebt die Speicher-Version still auf PDF 1.7 — das kollidiert mit Version Pinning: Ein Dokument, das mit LockSaveVersion auf PDF 1.4 fixiert ist, verweigert bei aktivem UA-Modus das Speichern mit Fehlercode 602. Diese Kombination taucht auf, wenn Archivprofile und Barrierefreiheitsanforderungen von verschiedenen Teams konfiguriert werden. SetInformation(1, ...) schreibt den Dokumenttitel, den ISO 14289 von Viewern statt des Dateinamens angezeigt erwartet; sein Fehlen ist einer der häufigsten PDF/UA-Befunde in der Praxis. Und AddRoleMap registriert den eigenen Typ ManualTitle als H1 — lassen Sie das aus, melden die unten beschriebenen Diagnosen die nicht gemappte Rolle.

Überschriftendisziplin braucht eine bewusste Policy statt Ad-hoc-Ebenen. Screenreader-Nutzer navigieren per Überschriftenkürzel, also bricht ein Template, das von H1 zu H3 springt, weil die Zwischenebene im visuellen Design zu groß aussah, die Navigation auf eine Weise, die keine visuelle Prüfung erkennt — und genau dieser Defekt wird durch die Diagnose HEADING-LEVEL-SKIP benannt. Die visuellen Styles jedes Templates einmal zentral auf eine feste Überschriftenleiter zu mappen verhindert den Drift.

Tabellen, die ein Screenreader tatsächlich navigieren kann

Gezeichnete Gitterlinien bedeuten außerhalb des Bildschirms nichts. Was Screenreader navigieren, sind Strukturbeziehungen: welche Zellen Kopfzellen sind, wofür jeder Kopf gilt und wie Datenzellen in unregelmäßigen Layouts an Köpfe gebunden werden. Die Strukturelement-Attributaufrufe behandeln alle drei:

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // valid only while this TH is open
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // header spans the value and unit columns
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // explicit binding for irregular tables
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

Die Reihenfolgeregel ist strikt und still durchgesetzt: Jeder SetStructElem*-Aufruf wirkt auf den aktuell offenen Tag — zwischen dessen BeginTag und EndTag — und gibt 0 zurück, ohne etwas zu werfen, wenn kein Tag offen ist oder das Attribut nicht passt. Ein falsch platzierter Aufruf verschwindet einfach. Rückgabewerte während der Entwicklung in Assertions zu packen fängt den Drift früh ab; im Feld zeigt sich eine fehlende Scope-Angabe erst, wenn ein Accessibility-Audit mit echtem Screenreader über die Tabelle läuft. Die über BeginTagEx2 übergebenen Element-IDs speisen den ID Tree (ISO 32000-1 §14.7.4), wodurch die SetStructElemHeaders-Bindung auflösbar wird.

Dieselbe Attributfamilie deckt die restlichen Strukturen ab, auf die Assistive Technology angewiesen ist. SetStructElemListNumbering deklariert, wie Listeneinträge beschriftet sind, damit ein Screenreader die Position in der Liste ansagen kann, statt Aufzählungsglyphen vorzulesen; SetStructElemBBox hält die Bounding Box von Abbildungen und Tabellen fest, die Reflow-Ansichten zur Platzierung verwenden; SetStructElemActualText liefert Ersatztext für Läufe, deren Glyphen nicht lesbaren Zeichen entsprechen, etwa aus Vektorgrafik gebaute Initialen. Alle folgen derselben Regel: Der Aufruf bindet an den offenen Tag, oder er verschwindet.

Artefakte, Sprache und das Diagnose-Gate vor dem Speichern

Wiederholtes Seitenmobiliar — laufende Köpfe, Falzmarken, Wasserzeichen, Hintergrundtönungen — gehört in BeginArtifact- und EndArtifact-Klammern, damit es nie in den Lesestrom gelangt. Sprache ist vererbbar: Der Dokumentstandard kommt aus dem Argument von SetPDFUAMode, und Läufe in einer anderen Sprache überschreiben ihn pro Element über BeginTagEx oder SetStructElemLang. Das hält ein französisches Zitat in einem englischen Handbuch aussprechbar.

Vor dem Speichern führt GetPDFUADiagnostics die strukturellen Prüfungen der Bibliothek über das In-Memory-Dokument aus und gibt Befunde als Text zurück — ein leerer String bedeutet, dass nichts gefunden wurde. Die Diagnosen nennen die klassischen Authoringfehler direkt: FIGURE-NO-ALT für Bilder ohne Alternativtext, HEADING-LEVEL-SKIP für ein H3 nach einem H1, ROLEMAP-UNMAPPED für eigene Typen, die nie registriert wurden. Das in den Build zu verdrahten — Dokumentensatz erzeugen, bei nicht leerer Diagnose scheitern — macht Barrierefreiheitsregressionen zu compile-time-artigen Fehlern statt zu Auditbefunden. Das vollständige Konformitätsurteil gehört weiterhin dem Preflight auf der gespeicherten Datei, behandelt in PDF/A- und PDF/UA-Preflight in Delphi, weil manche Normalisierungen erst beim Serialisieren angewendet werden.

Annotation-Navigation hat einen eigenen Knopf. PDF/UA erwartet, dass die Tastaturnavigation durch Formularfelder und Links der Strukturreihenfolge folgt, und SetTabOrderMode schreibt den seitenbezogenen Tab-Order-Eintrag, den Viewer beachten, während GetTabOrderMode für das Audit eingehender Dateien verfügbar ist. Es ist genau die Art Anforderung, die niemand bemerkt, bis ein reiner Tastaturnutzer den Fehler meldet, und sie kostet einen Aufruf pro Dokument.

Strukturbäume überleben nicht jeden Merge

Getaggte Dokumente bleiben nur getaggt, wenn jeder spätere Verarbeitungsschritt den Baum erhält. Innerhalb von PDFlibPas liegt die scharfe Kante in der Merge-Listen-Familie: MergeFileListFast tauscht Strukturbaum-Erhaltung gegen Geschwindigkeit, was für gescannte Bildstapel der richtige Tausch ist und für getaggte Berichte genau der falsche — die Ausgabe öffnet sauber, rendert identisch und hat ihre Accessibility-Schicht vollständig verloren. Verwenden Sie MergeFileList in der Standardform oder die Strict-Variante, sobald eine Eingabe getaggt ist, und machen Sie IsTaggedPDF zu einem Teil der Post-Assembly-Assertions, damit ein still abgeflachter Stapel nicht ausgeliefert wird. Montage-Pipelines für große Dokumentensätze tragen weitere Kompromisse dieser Art; sie werden in Large PDF Merge, Split und Direct Access vertieft.

Die Verifikationsschleife schließt außerhalb der Bibliothek: Öffnen Sie die Ausgabe in Acrobat, inspizieren Sie das Tags Panel und lesen Sie mindestens ein Dokument pro Template-Familie mit einem echten Screenreader. Diagnosen fangen Strukturfehler; nur ein menschliches Ohr fängt eine Leseordnung, die technisch gültig und praktisch verwirrend ist. Evaluierungsbuilds und die vollständige Tagging-API-Referenz stehen auf der Produktseite losLab PDF Library for Delphi.