PDFlibPas validates PDF/UA-2 (ISO 14289-2) files in Delphi through TPDFlib.CheckFilePDFUACompliance with a target part of 2, which runs a separate PDF/UA-2 profile: it resolves structure roles through PDF 2.0 namespaces and /RoleMapNS, reports truncated or unreadable input as a failure, and returns a coverage report that never claims full certification
That last part matters more than it sounds. Accessibility validation is a field where a green tick gets copied into a procurement spreadsheet and never questioned again. A validator that quietly skips half the standard and still prints "pass" is worse than no validator, because it converts an unknown into a false known. PDFlibPas takes the opposite stance: check what can be checked mechanically, say loudly what was not checked, and treat every "could not finish" as a finding rather than a success
Why can't you validate PDF/UA-2 with PDF/UA-1 rules?
PDF/UA-2 is not PDF/UA-1 with the part number bumped. PDF/UA-1 sits on PDF 1.7 and a single flat role map; PDF/UA-2 sits on PDF 2.0 (ISO 32000-2), where every structure element can belong to a namespace and each namespace carries its own role map. A UA-1 checker pointed at a UA-2 file will both miss real problems and invent fake ones
The fake ones are the embarrassing part. PDF/UA-1 validators typically enforce rules about Note elements carrying an ID and about how the unnumbered H heading may be used. Neither rule transfers. In the PDF 2.0 structure namespace, footnotes are expressed with FENote, and headings are H1 through Hn with no six-level ceiling. PDF/UA-2 goes further and prohibits the unnumbered H role outright. Apply UA-1 heading logic to that tree and you either complain about things the standard allows or wave through the one thing it forbids
So the PDFlibPas PDF/UA-2 profile is a separate code path, not a flag on the old one. It also refuses to reuse UA-1 clause numbers. Diagnostics shared with the UA-1 checker are rebound to ISO 14289-2 in UA-2 reports, and where a clause number has not been verified against the standard text, the report leaves the clause field empty instead of guessing. An empty clause is less satisfying than a confident clause number, but it is the only defensible output when nobody has confirmed the mapping
How PDFlibPas resolves roles through RoleMapNS
PDFlibPas resolves every structure role to a standard role in a known namespace, following /RoleMapNS chains across namespaces, and reports any chain that ends on a custom role, loops, or breaks the mapping rules. This is the core of PDF/UA-2 structure validation, because assistive technology only understands standard roles; a custom /InvoiceLine means nothing until it lands on something like P or TR
The resolver starts from the element's /S name and its /NS namespace. With no namespace, the element is implicitly in the PDF 1.7 standard namespace and the legacy /RoleMap in the structure tree root applies. With a namespace, the namespace dictionary's own /RoleMapNS applies, and a mapping can point to a role in a different namespace via a two-element array. The checks that come out of this are concrete:
- A namespace referenced from a structure element must be an indirect dictionary with a valid
/NSURI and must be listed in the structure tree root's/Namespacesarray (diagnostic 10054) - A custom role must eventually resolve to a standard role in the PDF 1.7 namespace, the PDF 2.0 namespace (
http://iso.org/pdf2/ssn), or the MathML namespace; circular chains and dead ends are reported as 10055 - A standard role may not be remapped to a custom one, and a role may not be remapped within its own explicit namespace
- The structure root must contain exactly one
Documentelement in the PDF 2.0 namespace (10057), and every element's/Pmust point at its actual parent (10058) - An unnumbered
H, or a legacy PDF 1.7Notethat is not mapped to a PDF 2.0 role, is reported as 10056
One detail worth copying into your own tooling: PDFlibPas validates every declared mapping, not only the ones reachable from the tree. A broken entry in a role map that no element currently uses is still a latent defect, because the next content update will start using it. MathML is handled honestly too. The MathML namespace is recognised as a valid resolution target, but the MathML vocabulary itself is not validated, and the coverage report says so
Running a PDF/UA-2 check from Delphi
TPDFlib.CheckFilePDFUACompliance is the one-call entry point: pass the file, password, target part, and options, and you get back a string list handle containing coded issues. A handle of zero with LastErrorCode equal to zero means the implemented checks found nothing; a nonzero LastErrorCode means the call itself failed
var
Lib: TPDFlib;
Issues, I: Integer;
begin
Lib := TPDFlib.Create;
try
// TargetPart: 0 = detect from XMP pdfuaid:part, 1 = PDF/UA-1, 2 = PDF/UA-2
// Options: 1 = stop at the first failure, 0 = collect every issue
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 are 1-based
Writeln(Lib.GetStringListItem(Issues, I));
finally
Lib.ReleaseStringList(Issues);
end;
finally
Lib.Free;
end;
end;
Choose the target part deliberately. A target of 0 reads pdfuaid:part from the XMP metadata and picks the profile from that, which is right for a generic inbox validator. For your own output pipeline, pass 2 explicitly. If your generator forgot to write the identification, auto-detection falls back to the UA-1 rules and you validate the wrong standard; an explicit 2 instead produces diagnostic 10050 for the missing part, plus 10051 if pdfuaid:rev is not 2024 and 10052 if the effective PDF version is not 2.0. On the authoring side, TPDFlib.SetPDFUA2Mode writes that identification, raises the version to 2.0, and selects the PDF 2.0 structure namespace as the default; the namespace model itself is explained in the PDF 2.0, PDF/A-4, and PDF/UA-2 namespaces walkthrough
What happens when the validator runs out of budget?
PDFlibPas fails closed: when the PDF/UA-2 checker hits a traversal limit, a cycle, a parser limit, or a document it cannot open, it emits a diagnostic, and a result without diagnostics is never promoted to a pass. Diagnostic 10059 is reserved for exactly this "the reader could not finish" case
The budgets are explicit and published in the coverage JSON: structure depth 256, 100,000 structure objects, and 256 hops along any role-map chain. Those numbers exist because structure trees come from untrusted input. A hostile or simply broken file can nest elements thousands of levels deep, point a child back at an ancestor, or build a role map that chains through hundreds of namespaces. A validator without budgets either crashes or hangs, and a validator with budgets but no diagnostics does something subtler and worse: it stops early, finds nothing wrong in the part it read, and reports success
The same principle applies one level up. If the underlying reader returns a score below 100 but no issue lines, which can happen when parsing aborts before any rule runs, PDFlibPas inserts a 10059 line so the empty list cannot be mistaken for a clean result. An exception during the check becomes a 10059 line as well. That is a small amount of code, and it closes the most common way validation pipelines lie to their owners
Sharing one parse with PDF/A and building a report
For batch work, the validation pipeline API parses a file once and runs several checks against the shared parse. RunPDFUAValidationPipelineTask is the PDF/UA member of that family and takes the same target part values as the one-call function, which keeps a PDF/A plus PDF/UA-2 preflight from paying for two full reads of a large document
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
// ... inspect and release both handles as in the previous example ...
Writeln('Parses: ', Lib.GetValidationPipelineParseCount(Pipe));
// ComplianceTests: 1 = PDF/A, 2 = PDF/UA, 0 = both; ReportFormat 1 = JSON
Json := Lib.CreateValidationPipelineReport(Pipe, 0, 0, 1);
finally
Lib.ReleaseValidationPipeline(Pipe);
end;
end;
The structured report labels the accessibility section PDF/UA-2 and ISO 14289-2 when the file identifies as part 2, and every PDF/UA section carries "fullCertification": false together with the embedded coverage object. The text preflight report adds a line stating that only implemented checks ran and that full accessibility certification needs additional validation and human review. The broader report formats, rule IDs, and CI gating patterns are described in the PDF/A and PDF/UA preflight guide
What does the coverage report actually guarantee?
TPDFlib.GetPDFUAReadValidationCoverageJSON guarantees one thing: an exact list of what the PDF/UA-2 checker examined and what it did not. For part 2 it returns fullCertification fixed at false, a checked array, an unverified array, and the traversal limits. It does not guarantee that a file is accessible, and it is designed so that no code path can make it say otherwise
var
Coverage: WideString;
begin
Coverage := Lib.GetPDFUAReadValidationCoverageJSON(2);
// Store it next to the validation result, not instead of it
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;
The checked list covers identification (part, revision, prefix), effective PDF version, title metadata, /MarkInfo, DisplayDocTitle, namespace references, role-map resolution, the document root, structure parents, tree cycles, and the H and Note rules. The unverified list is just as specific: content tagging and reading order, structure containment, fonts and Unicode mapping, annotations and forms, natural language, alternative descriptions, the MathML vocabulary, and human accessibility review. Several of those are not even mechanically decidable. No parser can tell you whether an alt text actually describes the chart, or whether the reading order makes sense to someone listening to it
Treat the coverage JSON as part of the result. Archive it with every validation run, and when an auditor asks what "passed" meant on a given date, you can hand over the exact scope instead of reconstructing it from memory and a version number
Cross-checking against veraPDF
The PDF/UA-2 core was cross-checked against the veraPDF 1.30.3 command-line validator using its ua2 profile, and the results line up where coverage overlaps. An independently generated valid core sample passed both tools; a sample declaring pdfuaid:rev 2023 and a sample using the unnumbered H role failed both tools
A fourth sample is the one worth talking about. It contained drawing content that was not tagged at all, and veraPDF flagged it while the PDFlibPas core checks did not, because content tagging is outside the current coverage. That gap is exactly why content-tagging-and-reading-order heads the unverified list. Running a second validator is cheap insurance, and a disagreement between two tools is often the fastest way to find the edge of what each one checks. For the authoring side of the same problem, building trees that pass in the first place, see the tagged PDF structure tree guide
FAQ
Does PDFlibPas certify a file as PDF/UA-2 compliant?
No. PDFlibPas runs the implemented machine checks for PDF/UA-2 and returns a coverage report with fullCertification always false, listing every area that still needs other tools or human review
What TargetPart value should I pass to CheckFilePDFUACompliance?
Pass 2 when you know the file must be PDF/UA-2, so missing identification is reported. Pass 0 to auto-detect the part from XMP pdfuaid:part, and 1 to force the PDF/UA-1 rules
What does diagnostic 10059 mean in a PDF/UA-2 report?
Diagnostic 10059 means the validator could not complete: the file could not be parsed or opened, a traversal limit was exceeded, a cycle was found, or an exception occurred. It always counts as a failure, never as a pass
PDF/UA-2 validation, the shared validation pipeline, and the PDF 2.0 authoring APIs ship in the losLab PDF Library for Delphi, alongside PDF/A preflight and structured validation reports