Technischer Artikel

PDF/UA-2-Validierung in Delphi: nicht nur UA-1, Teil 2

PDFlibPas validiert PDF/UA-2-Dateien (ISO 14289-2) in Delphi über TPDFlib.CheckFilePDFUACompliance mit einer Ziel-Part von 2; dahinter läuft ein eigenes PDF/UA-2-Profil: Es löst Strukturrollen über PDF-2.0-Namespaces und /RoleMapNS auf, meldet abgeschnittene oder unlesbare Eingaben als Fehler und liefert einen Coverage-Bericht, der nie volle Zertifizierung behauptet

Dieser letzte Teil ist wichtiger, als er klingt. Bei der Barrierefreiheits-Validierung wird ein grüner Haken gern in eine Beschaffungstabelle kopiert und nie wieder hinterfragt. Ein Validator, der stillschweigend die Hälfte des Standards überspringt und trotzdem „pass“ ausgibt, ist schlechter als gar keiner, denn er verwandelt ein Unbekanntes in ein falsches Bekanntes. PDFlibPas macht das Gegenteil: prüfen, was sich mechanisch prüfen lässt; laut sagen, was nicht geprüft wurde; und jedes „konnte nicht abgeschlossen werden“ als Fund werten, nicht als Erfolg

Warum Sie PDF/UA-2 nicht mit PDF/UA-1-Regeln prüfen können

PDF/UA-2 ist nicht PDF/UA-1 mit hochgezählter Part-Nummer. PDF/UA-1 baut auf PDF 1.7 und einer einzigen flachen Role Map auf; PDF/UA-2 baut auf PDF 2.0 (ISO 32000-2) auf, wo jedes Strukturelement zu einem Namespace gehören kann und jeder Namespace seine eigene Role Map mitbringt. Ein UA-1-Checker, den Sie auf eine UA-2-Datei loslassen, übersieht echte Probleme und erfindet nebenbei falsche

Die falschen sind der peinliche Teil. PDF/UA-1-Validatoren erzwingen typischerweise Regeln dazu, dass Note-Elemente eine ID tragen, und dazu, wie die unnummerierte H-Überschrift verwendet werden darf. Keine dieser Regeln lässt sich übertragen. Im PDF-2.0-Struktur-Namespace werden Fußnoten als FENote ausgedrückt, und Überschriften heißen H1 bis Hn ohne Sechs-Ebenen-Deckel. PDF/UA-2 geht weiter und verbietet die unnummerierte H-Rolle kurzerhand ganz. Wenden Sie UA-1-Überschriftenlogik auf diesen Baum an, bemängeln Sie entweder Dinge, die der Standard erlaubt, oder winken genau das eine durch, das er verbietet

Deshalb ist das PDFlibPas-PDF/UA-2-Profil ein separater Code-Pfad, kein Flag auf dem alten. Er recycelt auch keine UA-1-Klauselnummern. Diagnosen, die der UA-1-Checker ebenfalls ausgibt, werden in UA-2-Berichten auf ISO 14289-2 umgebunden, und wo eine Klauselnummer nicht gegen den Standardtext verifiziert wurde, lässt der Bericht das Klauselfeld leer, statt zu raten. Eine leere Klausel befriedigt weniger als eine selbstbewusste Nummer, aber sie ist die einzige vertretbare Ausgabe, solange niemand das Mapping bestätigt hat

Wie PDFlibPas Rollen über RoleMapNS auflöst

PDFlibPas löst jede Strukturrolle zu einer Standardrolle in einem bekannten Namespace auf, folgt dabei /RoleMapNS-Ketten über Namespace-Grenzen hinweg und meldet jede Kette, die auf einer Custom-Rolle endet, eine Schleife bildet oder die Mapping-Regeln bricht. Das ist der Kern der PDF/UA-2-Strukturvalidierung, denn Assistive Technology versteht nur Standardrollen; ein Custom-/InvoiceLine bedeutet nichts, solange es nicht bei etwas wie P oder TR landet

Der Resolver startet beim /S-Namen des Elements und seinem /NS-Namespace. Ohne Namespace liegt das Element implizit im PDF-1.7-Standard-Namespace, und die altbekannte /RoleMap an der Wurzel des Strukturbaums greift. Mit Namespace greift die eigene /RoleMapNS des Namespace-Dictionary, und ein Mapping kann über ein zweielementiges Array auf eine Rolle in einem anderen Namespace zeigen. Die Prüfungen, die daraus entstehen, sind konkret:

  • Ein von einem Strukturelement referenzierter Namespace muss ein indirektes Dictionary mit gültiger /NS-URI sein und im /Namespaces-Array der Strukturbaumwurzel gelistet sein (Diagnose 10054)
  • Eine Custom-Rolle muss irgendwann zu einer Standardrolle im PDF-1.7-Namespace, im PDF-2.0-Namespace (http://iso.org/pdf2/ssn) oder im MathML-Namespace aufgelöst werden; kreisförmige Ketten und Sackgassen werden als 10055 gemeldet
  • Eine Standardrolle darf nicht auf eine Custom-Rolle umgemappt werden, und eine Rolle darf nicht innerhalb ihres eigenen expliziten Namespace umgemappt werden
  • Die Strukturwurzel muss genau ein Document-Element im PDF-2.0-Namespace enthalten (10057), und das /P jedes Elements muss auf sein tatsächliches Eltern-Element zeigen (10058)
  • Ein unnummeriertes H oder ein Legacy-Note aus PDF 1.7, das nicht auf eine PDF-2.0-Rolle gemappt ist, wird als 10056 gemeldet

Ein Detail, das sich für das eigene Tooling abschreiben lohnt: PDFlibPas validiert jedes deklarierte Mapping, nicht nur die vom Baum aus erreichbaren. Ein kaputter Eintrag in einer Role Map, die derzeit kein Element nutzt, bleibt trotzdem ein latenter Defekt, denn das nächste Content-Update wird ihn verwenden. MathML wird ebenso ehrlich behandelt: Der MathML-Namespace gilt als gültiges Auflösungsziel, aber das MathML-Vokabular selbst wird nicht validiert, und der Coverage-Bericht sagt das auch so

Eine PDF/UA-2-Prüfung aus Delphi heraus ausführen

TPDFlib.CheckFilePDFUACompliance ist der Ein-Aufruf-Einstiegspunkt: Datei, Passwort, Ziel-Part und Optionen übergeben, und Sie bekommen ein String-List-Handle mit codierten Findings zurück. Ein Handle von null bei gleichzeitig null in LastErrorCode heißt: Die implementierten Prüfungen haben nichts gefunden; ein LastErrorCode ungleich null bedeutet, dass der Aufruf selbst fehlgeschlagen ist

var
  Lib: TPDFlib;
  Issues, I: Integer;
begin
  Lib := TPDFlib.Create;
  try
    // TargetPart: 0 = aus XMP pdfuaid:part erkennen, 1 = PDF/UA-1, 2 = PDF/UA-2
    // Options: 1 = beim ersten Fehler abbrechen, 0 = jedes Finding sammeln
    Issues := Lib.CheckFilePDFUACompliance('report.pdf', '', 2, 0);
    if Issues = 0 then
    begin
      if Lib.LastErrorCode <> 0 then
        Writeln('Validation did not run, error ', Lib.LastErrorCode)
      else
        Writeln('No issues from the implemented PDF/UA-2 checks');
    end
    else
    try
      for I := 1 to Lib.GetStringListCount(Issues) do  // Items sind 1-basiert
        Writeln(Lib.GetStringListItem(Issues, I));
    finally
      Lib.ReleaseStringList(Issues);
    end;
  finally
    Lib.Free;
  end;
end;

Wählen Sie die Ziel-Part bewusst. Eine 0 liest pdfuaid:part aus den XMP-Metadaten und wählt das Profil danach, was für einen generischen Inbox-Validator richtig ist. Für die eigene Output-Pipeline übergeben Sie explizit 2. Hat der Generator vergessen, die Identifikation zu schreiben, fällt die Auto-Erkennung auf die UA-1-Regeln zurück, und Sie validieren den falschen Standard; eine explizite 2 erzeugt stattdessen die Diagnose 10050 für die fehlende Part, dazu 10051, wenn pdfuaid:rev nicht 2024 ist, und 10052, wenn die effektive PDF-Version nicht 2.0 ist. Auf der Erzeuger-Seite schreibt TPDFlib.SetPDFUA2Mode genau diese Identifikation, hebt die Version auf 2.0 und wählt den PDF-2.0-Struktur-Namespace als Default; das Namespace-Modell selbst erklärt der Walkthrough zu PDF-2.0-, PDF/A-4- und PDF/UA-2-Namespaces

Was passiert, wenn dem Validator das Budget ausgeht?

PDFlibPas arbeitet fail-closed: Trifft der PDF/UA-2-Checker auf ein Traversallimit, einen Zyklus, ein Parser-Limit oder ein Dokument, das er nicht öffnen kann, gibt er eine Diagnose aus, und ein Ergebnis ohne Diagnosen wird nie zu einem Pass hochgestuft. Diagnose 10059 ist genau für diesen Fall „der Reader konnte nicht zu Ende lesen“ reserviert

Die Budgets sind explizit und im Coverage-JSON publiziert: Strukturtiefe 256, 100.000 Strukturobjekte und 256 Hops entlang jeder Role-Map-Kette. Diese Zahlen existieren, weil Strukturbäume aus nicht vertrauenswürdiger Eingabe stammen. Eine feindliche oder schlicht kaputte Datei kann Elemente tausende Ebenen tief verschachteln, ein Kind zurück auf einen Vorfahren zeigen lassen oder eine Role Map bauen, die sich durch hunderte Namespaces zieht. Ein Validator ohne Budgets stürzt ab oder hängt sich auf, und ein Validator mit Budgets, aber ohne Diagnosen, tut etwas Subtileres und Schlimmeres: Er hört früh auf, findet in dem gelesenen Teil nichts Falsches und meldet Erfolg

Dasselbe Prinzip greift eine Ebene höher. Liefert der zugrundeliegende Reader einen Score unter 100, aber keine Finding-Zeilen zurück – was passieren kann, wenn das Parsen abbricht, bevor irgendeine Regel läuft –, fügt PDFlibPas eine 10059-Zeile ein, damit die leere Liste nicht mit einem sauberen Ergebnis verwechselt werden kann. Eine Exception während der Prüfung wird ebenfalls zu einer 10059-Zeile. Das ist wenig Code, und es schließt den häufigsten Weg, auf dem Validierungspipelines ihre Betreiber belügen

Einen Parse mit PDF/A teilen und einen Report bauen

Für Batch-Arbeit parst die Validation-Pipeline-API eine Datei einmal und fährt mehrere Prüfungen gegen den geteilten Parse. RunPDFUAValidationPipelineTask ist das PDF/UA-Mitglied dieser Familie und nimmt dieselben Ziel-Part-Werte wie die Ein-Aufruf-Funktion, sodass ein PDF/A-plus-PDF/UA-2-Preflight nicht zwei komplette Reads eines großen Dokuments bezahlt

var
  Pipe, PDFAIssues, UAIssues: Integer;
  Json: WideString;
begin
  Pipe := Lib.BeginValidationPipeline('archive.pdf', '');
  if Pipe = 0 then
    Exit;
  try
    PDFAIssues := Lib.RunValidationPipelineTask(Pipe, 1, 0);       // 1 = PDF/A
    UAIssues := Lib.RunPDFUAValidationPipelineTask(Pipe, 2, 0);    // PDF/UA-2
    // ... beide Handles wie im vorigen Beispiel auswerten und freigeben ...
    Writeln('Parses: ', Lib.GetValidationPipelineParseCount(Pipe));
    // ComplianceTests: 1 = PDF/A, 2 = PDF/UA, 0 = beides; ReportFormat 1 = JSON
    Json := Lib.CreateValidationPipelineReport(Pipe, 0, 0, 1);
  finally
    Lib.ReleaseValidationPipeline(Pipe);
  end;
end;

Der strukturierte Report labelt den Barrierefreiheits-Abschnitt als PDF/UA-2 und ISO 14289-2, wenn sich die Datei als Part 2 identifiziert, und jeder PDF/UA-Abschnitt trägt "fullCertification": false samt dem eingebetteten Coverage-Objekt. Der Text-Preflight-Report ergänzt eine Zeile, dass nur implementierte Prüfungen gelaufen sind und dass eine vollständige Barrierefreiheits-Zertifizierung zusätzliche Validierung und manuelles Review braucht. Die weiteren Report-Formate, Rule-IDs und CI-Gating-Muster beschreibt der PDF/A- und PDF/UA-Preflight-Guide

Was garantiert der Coverage-Bericht tatsächlich?

TPDFlib.GetPDFUAReadValidationCoverageJSON garantiert genau eines: eine exakte Liste dessen, was der PDF/UA-2-Checker untersucht hat und was nicht. Für Part 2 liefert er fullCertification fix auf false, ein checked-Array, ein unverified-Array und die Traversallimits. Er garantiert nicht, dass eine Datei barrierefrei ist, und er ist so gebaut, dass kein Code-Pfad ihn zum Gegenteil bringen kann

var
  Coverage: WideString;
begin
  Coverage := Lib.GetPDFUAReadValidationCoverageJSON(2);
  // Legen Sie es neben das Validierungsergebnis, nicht an dessen Stelle
  TFile.WriteAllText('report.coverage.json', Coverage, TEncoding.UTF8);
  if Pos('"fullCertification":false', Coverage) > 0 then
    Writeln('Machine checks only; schedule a human accessibility review');
end;

Die Checked-Liste deckt Identifikation (Part, Revision, Prefix), effektive PDF-Version, Titel-Metadaten, /MarkInfo, DisplayDocTitle, Namespace-Referenzen, Role-Map-Auflösung, die Dokumentwurzel, Struktur-Eltern, Baum-Zyklen sowie die H- und Note-Regeln ab. Die Unverified-Liste ist genauso konkret: Content-Tagging und Lesereihenfolge, Struktur-Kapselung, Fonts und Unicode-Mapping, Annotationen und Formulare, natürliche Sprache, alternative Beschreibungen, das MathML-Vokabular und menschliches Barrierefreiheits-Review. Einiges davon ist nicht einmal mechanisch entscheidbar. Kein Parser kann Ihnen sagen, ob ein Alt-Text das Diagramm tatsächlich beschreibt oder ob die Lesereihenfolge für jemanden Sinn ergibt, der sie vorgelesen bekommt

Behandeln Sie das Coverage-JSON als Teil des Ergebnisses. Archivieren Sie es mit jedem Validierungslauf, und wenn ein Auditor fragt, was an einem bestimmten Datum „bestanden“ bedeutete, können Sie den exakten Scope aushändigen, statt ihn aus Erinnerung und einer Versionsnummer zu rekonstruieren

Gegencheck gegen veraPDF

Der PDF/UA-2-Kern wurde gegen den veraPDF-1.30.3-Kommandozeilen-Validator mit dessen ua2-Profil gegengecheckt, und die Ergebnisse passen dort zusammen, wo sich die Coverage überschneidet. Eine unabhängig erzeugte gültige Core-Probe bestand beide Tools; eine Probe mit pdfuaid:rev 2023 und eine Probe mit der unnummerierten H-Rolle fielen bei beiden durch

Über die vierte Probe lohnt sich das Reden. Sie enthielt Zeichnen-Content, der gar nicht getaggt war, und veraPDF markierte sie, während die PDFlibPas-Core-Prüfungen es nicht taten, denn Content-Tagging liegt außerhalb der aktuellen Coverage. Genau diese Lücke ist der Grund, warum content-tagging-and-reading-order die Unverified-Liste anführt. Ein zweiter Validator ist billige Versicherung, und eine Meinungsverschiedenheit zwischen zwei Tools ist oft der schnellste Weg, die Grenze des jeweils Geprüften zu finden. Für die Erzeuger-Seite desselben Problems – Bäume bauen, die von Anfang an durchgehen – siehe den Guide zum Tagged-PDF-Strukturbaum

FAQ

Zertifiziert PDFlibPas eine Datei als PDF/UA-2-konform?

Nein. PDFlibPas fährt die implementierten Maschinen-Prüfungen für PDF/UA-2 aus und liefert einen Coverage-Bericht, in dem fullCertification stets false ist und jeder Bereich aufgelistet steht, der noch andere Tools oder menschliches Review braucht

Welchen TargetPart-Wert sollte ich an CheckFilePDFUACompliance übergeben?

Übergeben Sie 2, wenn die Datei PDF/UA-2 sein muss, damit eine fehlende Identifikation gemeldet wird. Eine 0 erkennt die Part automatisch aus XMP pdfuaid:part, eine 1 erzwingt die PDF/UA-1-Regeln

Was bedeutet Diagnose 10059 in einem PDF/UA-2-Bericht?

Diagnose 10059 heißt, der Validator konnte nicht fertig werden: Die Datei ließ sich nicht parsen oder öffnen, ein Traversallimit wurde überschritten, ein Zyklus gefunden oder eine Exception geworfen. Sie zählt immer als Fehler, nie als Bestehen

Die PDF/UA-2-Validierung, die geteilte Validation-Pipeline und die PDF-2.0-Authoring-APIs kommen in der losLab PDF Library for Delphi, zusammen mit PDF/A-Preflight und strukturierten Validierungs-Reports