Technisch artikel

PDF/UA-2-validatie in Delphi: niet zomaar UA-1 deel 2

PDFlibPas valideert PDF/UA-2-bestanden (ISO 14289-2) in Delphi via TPDFlib.CheckFilePDFUACompliance met een target part van 2, waarmee een apart PDF/UA-2-profiel draait: de rol van elk structuurelement wordt herleid via PDF 2.0-namespaces en /RoleMapNS, afgebroken of onleesbare invoer wordt als fout gemeld, en het coverage-rapport dat terugkomt beweert nooit volledige certificering

Dat laatste deel is belangrijker dan het klinkt. Toegankelijkheidsvalidatie is een terrein waar een groen vinkje naar een inkoopspreadsheet wordt gekopieerd en daarna nooit meer wordt bevraagd. Een validator die stilletjes de helft van de standaard overslaat en toch "geslaagd" print, is erger dan geen validator, want hij verandert een onbekende in een vals bekende. PDFlibPas kiest het tegenovergestelde standpunt: controleer wat mechanisch controleerbaar is, zeg luid wat niet gecontroleerd is, en behandel elke "kon niet afronden" als bevinding in plaats van als succes

Waarom kunt u PDF/UA-2 niet valideren met PDF/UA-1-regels?

PDF/UA-2 is niet PDF/UA-1 met een opgehoogd partnummer. PDF/UA-1 rust op PDF 1.7 en één platte role map; PDF/UA-2 rust op PDF 2.0 (ISO 32000-2), waar elk structuurelement tot een namespace kan behoren en elke namespace zijn eigen role map heeft. Een UA-1-checker op een UA-2-bestand mist zowel echte problemen als verzint ze

De verzonnen zijn het gênante deel. PDF/UA-1-validators controleren doorgaans regels over Note-elementen met een ID en over hoe de ongenummerde H-kop gebruikt mag worden. Geen van beide regels is overdraagbaar. In de structurenamespace van PDF 2.0 worden voetnoten uitgedrukt met FENote, en koppen lopen van H1 tot Hn zonder plafond van zes niveaus. PDF/UA-2 gaat verder en verbiedt de ongenummerde H-rol ronduit. Past u UA-1-koplogica toe op die boom, dan klaagt u of over dingen die de standaard toestaat, of u laat juist het enige door waar hij verbiedt

Daarom is het PDF/UA-2-profiel van PDFlibPas een apart codepad, geen vlag op het oude. Het weigert ook UA-1-clausenummers te hergebruiken. Diagnostiek die met de UA-1-checker gedeeld wordt, komt in UA-2-rapporten onder ISO 14289-2 te staan, en waar een clausummer niet tegen de tekst van de standaard is geverifieerd, laat het rapport het clausumveld leeg in plaats van te gokken. Een leeg clausum voldoet minder dan een overtuigend clausummer, maar het is de enige verdedigbare uitkomst zolang niemand de koppeling heeft bevestigd

Hoe PDFlibPas rollen via RoleMapNS herleidt

PDFlibPas herleidt elke structuurrol naar een standaardrol in een bekende namespace, volgt /RoleMapNS-ketens over namespaces heen en meldt elke keten die op een custom rol eindigt, in een lus belandt of de toewijzingsregels breekt. Dit is de kern van PDF/UA-2-structuurvalidatie, want assistieve technologie begrijpt alleen standaardrollen; een custom /InvoiceLine betekent niets totdat hij ergens op uitkomt als P of TR

De resolver vertrekt vanuit de /S-naam van het element en diens /NS-namespace. Zonder namespace zit het element impliciet in de standaardnamespace van PDF 1.7 en geldt de legacy-/RoleMap in de wortel van de structuurboom. Met een namespace geldt de eigen /RoleMapNS van het namespace-woordenboek, en een toewijzing kan via een array van twee elementen naar een rol in een andere namespace wijzen. De controles die hieruit komen zijn concreet:

  • Een namespace waarnaar een structuurelement verwijst, moet een indirect woordenboek zijn met een geldige /NS-URI en moet voorkomen in de /Namespaces-array van de wortel van de structuurboom (diagnostic 10054)
  • Een custom rol moet uiteindelijk uitkomen op een standaardrol in de PDF 1.7-namespace, de PDF 2.0-namespace (http://iso.org/pdf2/ssn) of de MathML-namespace; circulaire ketens en doodlopende wegen worden als 10055 gemeld
  • Een standaardrol mag niet naar een custom rol worden omgeleid, en een rol mag niet binnen zijn eigen expliciete namespace worden omgeleid
  • De structuurwortel moet precies één Document-element in de PDF 2.0-namespace bevatten (10057), en de /P van elk element moet naar de werkelijke ouder wijzen (10058)
  • Een ongenummerde H, of een legacy-Note uit PDF 1.7 die niet naar een PDF 2.0-rol is afgebeeld, wordt als 10056 gemeld

Een detail om over te nemen in uw eigen tooling: PDFlibPas valideert elke gedeclareerde toewijzing, niet alleen die vanuit de boom bereikbaar zijn. Een kapotte entry in een role map die momenteel door geen enkel element wordt gebruikt, blijft een latent defect, want de volgende inhoudsupdate begint die te gebruiken. Ook MathML wordt eerlijk behandeld. De MathML-namespace wordt als geldig herleidingsdoel herkend, maar de MathML-vocabulaire zelf wordt niet gevalideerd, en het coverage-rapport zegt dat er expliciet bij

Een PDF/UA-2-controle draaien vanuit Delphi

TPDFlib.CheckFilePDFUACompliance is het entrypoint van één aanroep: geeft u het bestand, wachtwoord, target part en opties door, dan krijgt u een string-list-handle terug met gecodeerde bevindingen. Een handle van nul met LastErrorCode gelijk aan nul betekent dat de geïmplementeerde controles niets vonden; een LastErrorCode ongelijk aan nul betekent dat de aanroep zelf mislukte

var
  Lib: TPDFlib;
  Issues, I: Integer;
begin
  Lib := TPDFlib.Create;
  try
    // TargetPart: 0 = bepalen uit XMP pdfuaid:part, 1 = PDF/UA-1, 2 = PDF/UA-2
    // Options: 1 = stoppen bij de eerste fout, 0 = elke bevinding verzamelen
    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 zijn 1-based
        Writeln(Lib.GetStringListItem(Issues, I));
    finally
      Lib.ReleaseStringList(Issues);
    end;
  finally
    Lib.Free;
  end;
end;

Kies de target part bewust. Een target van 0 leest pdfuaid:part uit de XMP-metadata en kiest daar het profiel, wat klopt voor een generieke inbox-validator. Voor uw eigen uitvoerpijplijn geeft u expliciet 2 door. Vergeet uw generator de identificatie te schrijven, dan valt auto-detectie terug op de UA-1-regels en valideert u de verkeerde standaard; een expliciete 2 levert in plaats daarvan diagnostic 10050 op voor de ontbrekende part, plus 10051 als pdfuaid:rev niet 2024 is en 10052 als de effectieve PDF-versie niet 2.0 is. Aan de schrijfkant schrijft TPDFlib.SetPDFUA2Mode die identificatie, zet de versie omhoog naar 2.0 en stelt de structurenamespace van PDF 2.0 als standaard in; het namespace-model zelf wordt uitgelegd in de walkthrough over PDF 2.0-, PDF/A-4- en PDF/UA-2-namespaces

Wat gebeurt er als de validator door zijn budget heen raakt?

PDFlibPas werkt volgens het fail-closed-principe: raakt de PDF/UA-2-checker een traversallimiet, een lus, een parserlimiet of een document dat hij niet kan openen, dan emitteert hij een diagnostic, en een resultaat zonder diagnostics wordt nooit tot geslaagd verklaard. Diagnostic 10059 is gereserveerd voor precies dit geval van "de lezer kwam niet klaar"

De budgetten staan expliciet in de coverage-JSON: structuurdiepte 256, 100.000 structuurobjecten en 256 sprongen langs elke role-map-keten. Die nummers bestaan omdat structuurbomen uit onvertrouwde invoer komen. Een vijandig of simpelweg kapot bestand kan elementen duizenden niveaus diep nesten, een kind terug naar een voorouder laten wijzen of een role map bouwen die via honderden namespaces kettent. Een validator zonder budgetten crasht of hangt, en een validator met budgetten maar zonder diagnostics doet iets subtielers en ergers: hij stopt vroegtijdig, vindt in het gelezen deel niets mis en meldt succes

Op één niveau hoger geldt hetzelfde principe. Geeft de onderliggende lezer een score onder de 100 terug maar geen bevindingsregels — wat kan gebeuren als het parsen afbreekt voordat er ook maar één regel draait — dan voegt PDFlibPas zelf een 10059-regel in, zodat de lege lijst niet voor een schone uitslag kan doorgaan. Ook een exception tijdens de controle wordt een 10059-regel. Dat is weinig code, en het dicht de meest voorkomende manier waarop validatiepijplijnen hun eigenaar voorliegen

Eén parse delen met PDF/A en een rapport bouwen

Voor batchwerk parseert de validation-pipeline-API een bestand één keer en draait meerdere controles tegen de gedeelde parse. RunPDFUAValidationPipelineTask is het PDF/UA-lid van die familie en neemt dezelfde target-part-waarden als de één-aanroep-functie, zodat een preflight van PDF/A plus PDF/UA-2 niet twee volledige lezingen van een groot document hoeft te betalen

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 inspecteren en vrijgeven zoals in het vorige voorbeeld ...
    Writeln('Parses: ', Lib.GetValidationPipelineParseCount(Pipe));
    // ComplianceTests: 1 = PDF/A, 2 = PDF/UA, 0 = beide; ReportFormat 1 = JSON
    Json := Lib.CreateValidationPipelineReport(Pipe, 0, 0, 1);
  finally
    Lib.ReleaseValidationPipeline(Pipe);
  end;
end;

Het gestructureerde rapport labelt de toegankelijkheidssectie als PDF/UA-2 en ISO 14289-2 zodra het bestand zich als part 2 identificeert, en elke PDF/UA-sectie voert "fullCertification": false samen met het ingesloten coverage-object. Het tekstuele preflight-rapport voegt een regel toe dat alleen geïmplementeerde controles hebben gedraaid en dat volledige toegankelijkheidscertificering aanvullende validatie en menselijke beoordeling vergt. De bredere rapportformaten, regel-ID's en CI-gatingpatronen worden beschreven in de PDF/A- en PDF/UA-preflight-gids

Wat garandeert het coverage-rapport eigenlijk?

TPDFlib.GetPDFUAReadValidationCoverageJSON garandeert één ding: een exacte lijst van wat de PDF/UA-2-checker bekeken heeft en wat niet. Voor part 2 geeft hij fullCertification vast op false terug, een checked-array, een unverified-array en de traversallimieten. Hij garandeert niet dat een bestand toegankelijk is, en hij is zo ontworpen dat geen enkel codepad hem iets anders kan laten zeggen

var
  Coverage: WideString;
begin
  Coverage := Lib.GetPDFUAReadValidationCoverageJSON(2);
  // Bewaar hem naast het validatieresultaat, niet in plaats ervan
  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;

De checked-lijst dekt identificatie (part, revisie, prefix), de effectieve PDF-versie, titelmetadata, /MarkInfo, DisplayDocTitle, namespace-verwijzingen, role-map-herleiding, de documentwortel, structuurouders, boomlussen en de H- en Note-regels. De unverified-lijst is net zo specifiek: content-tagging en leesvolgorde, structurele insluiting, fonts en Unicode-toewijzing, annotaties en formulieren, natuurlijke taal, alternatieve beschrijvingen, de MathML-vocabulaire en menselijke toegankelijkheidsbeoordeling. Een flink aantal daarvan is zelfs niet mechanisch beslisbaar. Geen enkele parser kan u vertellen of een alt-tekst de grafiek echt beschrijft of dat de leesvolgorde voor iemand die ernaar luistert logisch is

Beschouw de coverage-JSON als deel van het resultaat. Archiveer hem bij elke validatierun, en als een auditor vraagt wat "geslaagd" op een gegeven datum betekende, kunt u de exacte scope overhandigen in plaats van die uit geheugen en een versienummer te reconstrueren

Kruiscontrole tegen veraPDF

De PDF/UA-2-kern is gekruist getest tegen de commandline-validator van veraPDF 1.30.3 met diens ua2-profiel, en de resultaten lopen gelijk waar de dekking overlapt. Een onafhankelijk gegenereerd geldig kernvoorbeeld haalde beide tools; een voorbeeld dat pdfuaid:rev 2023 declareert en een voorbeeld met de ongenummerde H-rol faalden bij beide tools

Het vierde voorbeeld is het gesprek waard. Het bevatte tekeninhoud die helemaal niet getagd was, en veraPDF vlagde het terwijl de PDFlibPas-kerncontroles dat niet deden, want content-tagging valt buiten de huidige dekking. Dat gat is precies de reden dat content-tagging-and-reading-order bovenaan de unverified-lijst staat. Een tweede validator draaien is goedkope verzekering, en een meningsverschil tussen twee tools is vaak de snelste manier om de rand te vinden van wat elk controleert. Voor de schrijfkant van hetzelfde probleem — bomen bouwen die in eerste instantie al slagen — zie de gids over de tagged-PDF-structuurboom

FAQ

Certificeert PDFlibPas een bestand als PDF/UA-2-conform?

Nee. PDFlibPas draait de geïmplementeerde machinecontroles voor PDF/UA-2 en geeft een coverage-rapport terug waarin fullCertification altijd false is, met daarin elk gebied dat nog andere tools of menselijke beoordeling nodig heeft

Welke TargetPart-waarde geeft u door aan CheckFilePDFUACompliance?

Geef 2 door als u weet dat het bestand PDF/UA-2 moet zijn, zodat ontbrekende identificatie wordt gemeld. Geef 0 door om de part automatisch uit XMP pdfuaid:part te detecteren, en 1 om de PDF/UA-1-regels af te dwingen

Wat betekent diagnostic 10059 in een PDF/UA-2-rapport?

Diagnostic 10059 betekent dat de validator niet kon afronden: het bestand kon niet geparseerd of geopend worden, een traversallimiet werd overschreden, er werd een lus gevonden of er deed zich een exception voor. Hij telt altijd als mislukking, nooit als geslaagd

PDF/UA-2-validatie, de gedeelde validation-pipeline en de PDF 2.0-authoring-API's zitten in de losLab PDF Library for Delphi, naast PDF/A-preflight en gestructureerde validatierapporten