Technischer Artikel

PDF/UA-Strukturbaum-Validierung in Delphi mit PDFium

Ihr Preflight meldet die Datei als PDF/UA-konform. veraPDF öffnet dieselbe Datei und bemängelt eine Abbildung (Figure) ohne Alternativtext gemäß Klausel 7.3. Beide Werkzeuge haben recht, und die Lücke dazwischen ist das eigentliche Problem bei der Überprüfung der Barrierefreiheit durch reines Scannen von Bytes. Ein Durchlauf auf Byte-Ebene bestätigt, dass die Datei *angibt*, getaggt zu sein: Er findet den /StructTreeRoot, das /MarkInfo /Marked true, das pdfuaid:part im XMP-Paket, den Dokumententitel und die Sprache. Dies sind Format-Marker, und sie sind notwendig. Sie sagen Ihnen jedoch nichts darüber aus, ob die tatsächliche Abbildung auf Seite vier eine Beschreibung trägt, die ein Screenreader vorlesen kann. Diese Antwort lebt im Tag-Baum, und um sie zu erhalten, müssen Sie den Baum durchlaufen

Die PDFium-Komponente ist eine native VCL-PDF-Bibliothek für Delphi und C++Builder, und ihr ValidatePdfUa führt beide Durchläufe durch. Der Durchlauf auf Byte-Ebene kümmert sich um die Format-Marker. Darüber hinaus gibt es einen Strukturbaum-Durchlauf, der den Live-Tag-Baum lädt, jedes Element durchläuft und eine kleine Gruppe von zuverlässigen Inhaltsregeln überprüft, bei denen ein fehlendes Attribut einen echten Mangel an Barrierefreiheit anstelle einer bloßen stilistischen Vorliebe bedeutet. In diesem Artikel geht es um diesen zweiten Durchlauf: was er prüft, warum die Regel-Logik eine reine Funktion ohne darunterliegende DLL ist und wo sie bewusst Halt macht

Warum ein Byte-Scan ein fehlendes Alt-Attribut nicht sehen kann

ISO 14289-1 (PDF/UA-1) ist eine Ebene von Anforderungen oberhalb von ISO 32000. Einige dieser Anforderungen sind strukturell und in der Rohdatei sichtbar: Der Katalog muss einen Strukturbaum deklarieren, die Viewer-Einstellungen müssen DisplayDocTitle festlegen und Schriften müssen eingebettet sein. Ein Token-Scanner, der Stream-Inhalte bereinigt und Namenstoken innerhalb von Begrenzungsgrenzen abgleicht, kann all dies überprüfen. Genau das tut ValidatePdfUaCompliance von PDFium für Klauseln wie 7.1, 7.18 und 7.21

But „jede Abbildung hat einen Alternativtext“ ist keine Eigenschaft der Dateisyntax. Es ist eine Eigenschaft der *logischen Struktur* – dem Baum aus getaggten Elementen, der Inhalte einer Bedeutung zuordnet. Der Alt-Eintrag einer Abbildung kann sich im Dictionary des Strukturelements befinden, über eine /ActualText-Spanne bereitgestellt werden oder von einem benutzerdefinierten Typ stammen, der per Rollen-Mapping zugewiesen wurde. Sie können ihn nicht zuverlässig finden, indem Sie im Byte-Stream nach /Alt suchen, da diese Zeichenfolge in nicht zusammenhängenden Kontexten vorkommen kann, möglicherweise innerhalb eines Objekt-Streams komprimiert ist und Ihnen nichts darüber aussagt, zu *welchem* Strukturelement sie gehört. Der ehrlichste Weg, diese Frage zu beantworten, besteht darin, den Strukturbaum des Dokuments Element für Element abzufragen – genau die Ebene, die veraPDF und PAC auswerten. Das ist die Linie, um die herum die Tier-1-Prüfungen von PDFium aufgebaut sind: Byte-Scan für das Format, Baum-Durchlauf für den Inhalt

Auslesen des Live-Tag-Baums

Das Ausgangsmaterial ist TPdf.GetStructureElements (auch als Eigenschaft StructureElements verfügbar), was ein TPdfStructureElements zurückgibt – ein flaches Array von TPdfStructureElement-Records in der Reihenfolge des Dokuments. Jeder Record ist die projection eines Strukturelements durch die Zugriffsfunktionen von PDFium, mit den Feldern, die die Barrierefreiheitsregeln tatsächlich benötigen:

type
  TPdfStructureElement = record
    Level: Integer;            // depth in the tag tree
    ParentIndex: Integer;      // index of parent element, or -1
    TypeName: WString;         // standard /S name: Figure, Formula, Note...
    Title: WString;            // /T
    AlternateText: WString;    // /Alt   (FPDF_StructElement_GetAltText)
    ActualText: WString;       // /ActualText
    Expansion: WString;        // /E
    ID: WString;               // /ID    (FPDF_StructElement_GetID)
    Language: WString;         // /Lang
    MarkedContentIDs: TPdfIntegerArray;
    // ... child bookkeeping fields
  end;

Das Feld TypeName ist dasjenige, auf das sich der Validator stützt. Es stammt von FPDF_StructElement_GetType, welches den Standard-Strukturtyp des Elements – seinen /S-Namen – zurückgibt, nachdem PDFium das Rollen-Mapping aufgelöst hat. AlternateText stammt von FPDF_StructElement_GetAltText, ActualText von FPDF_StructElement_GetActualText und ID von FPDF_StructElement_GetID. Da das Array flach und sortiert ist, kann der Validator das gesamte Dokument auf einmal analysieren, anstatt zu rekursieren – was für die eine Regel wichtig ist, die global statt pro Element gilt

Der Prüfer ist eine reine Funktion – und das ist Absicht

Die Regel-Logik lebt nicht innerhalb der Methode, die mit der DLL kommuniziert. Sie ist eine eigenständige, öffentliche und reine Funktion (pure function):

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Sie nimmt ein flaches Element-Array entgegen und gibt eine Menge von Problemen zurück. Sie ruft keine PDFium-Funktion auf, öffnet kein Dokument und greift auf keinen globalen Zustand zu. Diese Trennung ist bewusst gewählt und zahlt sich zweifach aus. Erstens die Testbarkeit: Sie können ein synthetisches TPdfStructureElements-Array in einem Unit-Test aufbauen – eine Abbildung ohne Alternativtext, eine Formel, deren einziger barrierefreier Text in ActualText steht, zwei Notes, die sich eine ID teilen – und das Ergebnisset prüfen, ohne dass die pdfium.dll überhaupt vorhanden sein muss. Die Regel-Logik wird offline verifiziert; der DLL-Durchlauf wird separat durch einen Live-Dokumenten-Smoketest überprüft, der übersprungen wird, wenn die Bibliothek fehlt

Zweitens die Klarheit der Verantwortung. TPdf.ValidatePdfUa besitzt den komplexen Teil – das Laden jeder Seite, das Auslesen ihrer Elemente, das Akkumulieren dieser – und übergibt dann ein sauberes Array an den reinen Prüfer. „Daten beschaffen“ (DLL, Seiteneffekte, Lebenszeit) und „Regeln beurteilen“ (rein, deterministisch) vermischen sich niemals. Wenn eine Regel geändert werden muss, ändern Sie eine Funktion, die keine I/O-Operationen enthält

Was die drei Regeln tatsächlich prüfen

Der Strukturbaum-Durchlauf meldet drei Problemwerte, die am Ende von TPdfUaValidationIssues angehängt sind, damit das Enum für bestehende Aufrufer ABI-stabil bleibt: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt und pvuaiNoteMissingId. Der Rumpf ist klein genug, um ihn vollständig zu verstehen:

for I := 0 to High(Elements) do
begin
  T := string(Elements[I].TypeName);
  if T = 'Figure' then
  begin
    // §7.3 — a Figure needs an alternate representation:
    // an Alt entry OR ActualText. Flag only when BOTH are empty.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFigureMissingAlt);
  end
  else if T = 'Formula' then
  begin
    // §7.7 — same rule as Figure: Alt OR ActualText.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFormulaMissingAlt);
  end
  else if T = 'Note' then
  begin
    // §7.9 — every Note must have a unique ID.
    NoteId := string(Elements[I].ID);
    if NoteId = '' then
      Include(Result, pvuaiNoteMissingId)
    else
      for J := 0 to I - 1 do
        if (string(Elements[J].TypeName) = 'Note') and
           (string(Elements[J].ID) = NoteId) then
        begin
          Include(Result, pvuaiNoteMissingId);
          Break;
        end;
  end;
end;

Klausel 7.3 regelt Abbildungen: Ein Figure-Element muss eine Textalternative bereitstellen. Die frühe Version dieser Prüfung untersuchte nur den Alt-Eintrag, was sie strenger machte als die Referenz-Validatoren. PDF/UA akzeptiert eine Abbildung, deren barrierefreier Text stattdessen über ActualText bereitgestellt wird – Ersatztext ist eine gültige alternative Darstellung –, sodass die Regel eine Abbildung nur dann bemängelt, wenn *sowohl* Alt als auch ActualText leer sind. Klausel 7.7 deckt Formeln ab, und nach derselben Korrektur verwendet sie denselben Alt-oder-ActualText-Test; ein Beispiel aus dem Konformitäts-Korpus, das einer Formel (Formula) ihren barrierefreien Text allein über ActualText gab, wurde fälschlicherweise abgewiesen, bis der Formula-Zweig an den Figure-Zweig angepasst wurde

Klausel 7.9 ist anderer Natur. Eine Fußnote (Note) muss eine /ID besitzen, und diese ID muss dokumentenweit eindeutig sein. Eine fehlende ID ist ein Fehler auf Elementebene. Eine *doppelte* ID ist eine Beziehung zwischen zwei Elementen, weshalb das flache Array wichtig ist: Für jede Note scannt der Prüfer rückwärts über die bereits gesehenen Elemente und meldet eine Kollision mit jeder früheren Note, die dieselbe ID trägt. Die Kosten dafür sind die offensichtlichen O(n²) über der Anzahl der Notes, was für jedes reale Dokument unerheblich ist und die Funktion als eine einzige lesbare Schleife ohne zusätzlichen, synchron zu haltenden Index belässt

Akkumulieren über Seiten hinweg, damit Eindeutigkeit global ist

PDFium legt Strukturelemente pro Seite offen, nicht pro Dokument. Daher muss die Orchestrierung in ValidatePdfUa diese sammeln, bevor die Regeln ausgeführt werden. Sie durchläuft jede Seite mit FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, unabhängig davon, welche Seite die Komponente derzeit geöffnet hat, und hängt die Elemente jeder Seite an ein einziges Array an. Erst dann ruft sie den reinen Prüfer auf:

// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
   (not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
  AllElems := nil;
  PageTotal := FPDF_GetPageCount(FDocument);
  for I := 0 to PageTotal - 1 do
  begin
    Page := FPDF_LoadPage(FDocument, I);
    if Page = nil then Continue;
    try
      PageElems := GetStructureElementsForPage(Page);
    finally
      FPDF_ClosePage(Page);
    end;
    // append PageElems into AllElems ...
  end;
  Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;

Die Akkumulation macht die Eindeutigkeitsprüfung nach 7.9 korrekt. Zwei Notes auf unterschiedlichen Seiten können sich eine ID teilen; wenn Sie Seite für Seite validieren würden, würden Sie die Kollision niemals bemerken, da der Elementsatz jeder Seite in sich konsistent aussieht. Das Erstellen eines dokumentenweiten Arrays ist der einzige Weg, wie das Duplikat sichtbar wird. Der Schutz auf der Vorderseite ist ebenfalls erwähnenswert: Der Baum-Durchlauf wird nur ausgeführt, wenn der Byte-Level-Durchlauf *nicht* pvuaiMissingStructTreeRoot gemeldet hat. Ein ungetaggtes Dokument hat keinen Baum zu durchlaufen und wurde bereits wegen der fehlenden Strukturwurzel markiert, sodass die seitenweisen Ladevorgänge vollständig übersprungen werden. Der tiefe Durchlauf kostet nothing bei Dokumenten, die nicht davon profitieren können

Designbedingt konservativ: Lieber leise übersehen als Fehlalarm auslösen

Die wichtigste Eigenschaft dieses Validators ist das, was er *nicht* tut. Er gleicht nur die Standard-/S-Typnamen ab, die FPDF_StructElement_GetType direkt zurückgibt – Figure, Formula, Note. Ein Dokument, das einen benutzerdefinierten Typ definiert und diesen per Rollen-Mapping auf Figure abbildet, meldet je nachdem, wie PDFium den Typ auflöst, seinen eigenen Namen. Wenn das passiert, erkennt der Prüfer ihn nicht und bleibt stumm. Das ist ein Falsch-Negativ-Ergebnis und entspricht dem beabsichtigten Verhalten. Die Designregel lautet, **eher zu wenig zu melden als jemals ein Falsch-Positiv-Ergebnis zu erzeugen**, da ein Preflight-Werkzeug, das bei konformen Dateien Fehlalarm auslöst, seine Benutzer dazu bringt, es zu ignorieren – und ein ignorierter Validator ist schlimmer als gar keiner. Dekorative Bilder leben im Artifact-Stream, nicht im Strukturbaum, sodass sie ohnehin niemals als Figures auftauchen; Sie erhalten keine Beschwerde über einen fehlenden Alternativtext bei einer Hintergrundlinie, die korrekt als Artefakt markiert ist

Dies ist auch der Grund, warum der Umfang auf drei Regeln beschränkt ist. Die Verschachtelung von Überschriftenebenen (Klausel 7.4), der Tabellenkopf-Bereich (7.5) und die Erkennung von Zyklen im Rollen-Mapping (7.1) sind allesamt berechtigte PDF/UA-Anforderungen. Eine fehlerfreie Prüfung erfordert jedoch eine echte Graphen- und Attributanalyse, und eine naive Prüfung erzeugt genau die Falsch-Positiv-Ergebnisse, die das Design verbietet – PDF/UA erlaubt Überschriftenmuster wie H1, H2, H3, H3, die eine einfache Regel wie „muss streng ansteigen“ fälschlicherweise ablehnen würde. Diese Prüfungen werden dedizierten Konformitätswerkzeugen überlassen. Die Tier-1-Gruppe ist die Teilmenge, bei der ein fehlendes Attribut eindeutig ist

Die Grenze, klar formuliert

Zwei Einschränkungen sollte man kennen, bevor man dies in ein Release-Gate einbaut. Erstens ist der Prüfer nur so gut wie das, was PDFium aus dem Strukturelement lesen kann. Eine Handvoll Konformitäts-Korpus-Dateien, die die Referenz-Validatoren bestehen, verwenden einen Mechanismus für Alternativtext, den PDFium nicht offenlegt, sodass FPDF_StructElement_GetAltText leer zurückgibt, obwohl die Datei tatsächlich konform ist. Der reine Prüfer bemängelt dann „korrekt“ einen fehlenden Alternativtext auf unvollständigen Daten – ein Falsch-Positiv-Ergebnis, das in der Zugriffsabdeckung der DLL begründet liegt, nicht in der Regel-Logik. Eine Lockerung der Regel zur Absorption dieser Fälle würde sie auch für die tatsächlichen Fehler blind machen, die sie abfangen soll. Daher werden diese als bekannte PDFium-Einschränkung dokumentiert, anstatt sie zu vertuschen

Zweitens handelt es sich hierbei um einen Preflight, nicht um eine Zertifizierung. Tier-1 fängt die offensichtlichen Inhaltsfehler ab, die ein Byte-Scan strukturell nicht erfassen kann, und tut dies ohne Fehlalarme. Die vollständige PDF/UA-Konformität – einschließlich Überschriftensemantik, Tabellenstruktur und korrekter Lesereihenfolge – gehört jedoch immer noch in einen vollständigen Validator und letztendlich in die Hände eines menschlichen Prüfers. Verwenden Sie ValidatePdfUa, um offensichtliche Mängel schnell und kostengünstig in Ihrer eigenen Pipeline auszusortieren, und überlassen Sie veraPDF oder PAC das letzte Wort. Derselbe Strukturbaum-Durchlauf bildet die Grundlage für den Bau eines barrierefreien PDF-Readers in Delphi, bei dem der Tag-Baum die Lesereihenfolge und den gesprochenen Text steuert, und er ergänzt die Arbeit auf Metadatenebene bei der Überprüfung von PDF-Anmerkungen aus Delphi

Die hier gezeigten Strukturbaum-APIs und der ValidatePdfUa-Validator werden mit der PDFium-Komponente für Delphi und C++Builder (VCL) und Lazarus/FPC (LCL) ausgeliefert. Die Produktseite verlinkt die vollständige API-Referenz, einschließlich des vollständigen TPdfStructureElement-Record-Layouts und der Problemaufzählung hinter diesen Prüfungen