PDF/UA Accessibility
PDF/UA (ISO 14289) defines requirements for universally accessible PDF documents. PDF Oxide validates structure trees, heading sequences, alt text, table headers, language declarations, and more.
Binding coverage. PDF/UA validation is exposed in Python (
doc.validate_pdf_ua()), Rust (validate_pdf_ua+PdfUaValidatorbuilder), and Go (doc.ValidatePdfUa()). WASM exposes a raw pass/fail check viavalidatePdfUawhen available. A public C# wrapper is not yet surfaced — use the Rust CLI (pdf-oxide validate --pdfua doc.pdf) or call through one of the supported bindings.
Supported Levels
| Level | Standard | Description |
|---|---|---|
| PDF/UA-1 | ISO 14289-1:2014 | Base accessibility requirements |
| PDF/UA-2 | ISO 14289-2:2024 | Enhanced requirements, aligned with WCAG 2.1 |
Quick Validation
from pdf_oxide import PdfDocument
doc = PdfDocument("document.pdf")
result = doc.validate_pdf_ua()
print(f"Valid: {result.valid}")
for error in result.errors:
print(f" {error}")
use pdf_oxide::PdfDocument;
use pdf_oxide::compliance::{validate_pdf_ua, PdfUaLevel};
let mut doc = PdfDocument::open("accessible.pdf")?;
let result = validate_pdf_ua(&mut doc, PdfUaLevel::UA1)?;
if result.has_errors() {
println!("Not PDF/UA-1 compliant:");
for error in &result.errors {
println!(" [{}] {} (clause {})",
error.code, error.message,
error.clause.as_deref().unwrap_or("n/a"));
}
} else {
println!("Document is PDF/UA-1 compliant");
}
doc, err := pdfoxide.Open("accessible.pdf")
if err != nil { log.Fatal(err) }
defer doc.Close()
valid, errs, err := doc.ValidatePdfUa()
if err != nil { log.Fatal(err) }
if valid {
fmt.Println("Document is PDF/UA-1 compliant")
} else {
fmt.Println("Not PDF/UA-1 compliant:")
for _, e := range errs {
fmt.Printf(" %s\n", e)
}
}
Validator API
The PdfUaValidator builder allows configuring specific checks:
use pdf_oxide::PdfDocument;
use pdf_oxide::compliance::{PdfUaValidator, PdfUaLevel};
let mut doc = PdfDocument::open("report.pdf")?;
let result = PdfUaValidator::new()
.check_heading_sequence(true)
.check_color_contrast(true)
.allow_custom_types(vec!["Caption".into(), "Aside".into()])
.validate(&mut doc, PdfUaLevel::UA1)?;
println!("Errors: {}", result.errors.len());
println!("Warnings: {}", result.warnings.len());
println!("Structure elements checked: {}",
result.stats.structure_elements_checked);
Configuration Options
| Method | Default | Description |
|---|---|---|
check_heading_sequence(bool) |
true |
Validate H1-H6 do not skip levels |
check_color_contrast(bool) |
true |
Flag potential contrast issues |
allow_custom_types(Vec<String>) |
[] |
Permit non-standard structure types without warning |
Structure Tree Inspection
Before running validation, you can inspect the document’s structure tree and mark info:
use pdf_oxide::PdfDocument;
let mut doc = PdfDocument::open("tagged.pdf")?;
// Check if the document claims to be tagged
let mark_info = doc.mark_info()?;
println!("Marked: {}", mark_info.marked);
println!("Suspects: {}", mark_info.suspects);
// Access the structure tree
if let Some(tree) = doc.structure_tree()? {
println!("Root tag: {}", tree.root_type);
println!("Children: {}", tree.children.len());
}
The mark_info() method returns:
| Field | Type | Description |
|---|---|---|
marked |
bool |
Whether the document declares itself as tagged |
suspects |
bool |
Whether tag assignments may be incorrect |
user_properties |
bool |
Whether user properties are present |
When suspects is true, PDF Oxide automatically falls back to geometric ordering for text extraction instead of relying on the potentially unreliable structure tree.
What Gets Checked
The validator covers the following PDF/UA requirements:
Document-Level
| Check | Clause | Description |
|---|---|---|
| Language | 7.2 | /Lang entry present in the catalog |
| Title | 7.1 | Document title set and displayed in title bar |
| Tagged | 7.1 | MarkInfo dictionary declares Marked = true |
| XMP metadata | 7.1 | pdfuaid:part declared in XMP stream |
Structure
| Check | Clause | Description |
|---|---|---|
| Structure tree | 7.1 | Complete structure tree rooted at StructTreeRoot |
| Role mapping | 7.5 | Non-standard types mapped to standard structure elements |
| Heading hierarchy | 7.4.2 | Headings (H1-H6) do not skip levels |
| Artifact marking | 7.3 | Decorative content marked as artifact |
| Reading order | 7.2 | Structure tree defines a logical reading order |
Content
| Check | Clause | Description |
|---|---|---|
| Alt text for images | 7.3 | /Alt or /ActualText on Figure elements |
| Table headers | 7.5 | TH elements present in table structures |
| Form labels | 7.6.2 | Form fields have associated labels or tooltips |
| Link text | 7.18 | Link annotations have descriptive content |
| List structure | 7.4.3 | Lists use L, LI, Lbl, LBody structure |
Font and Text
| Check | Clause | Description |
|---|---|---|
| Unicode mapping | 7.21.3 | All text has a Unicode representation |
| Font embedding | 7.21.4 | Fonts embedded or standard Base14 fonts |
| ActualText | 7.21.5 | Ligatures and special glyphs have /ActualText |
UaValidationResult
pub struct UaValidationResult {
pub level: PdfUaLevel,
pub errors: Vec<UaComplianceError>,
pub warnings: Vec<ComplianceWarning>,
pub stats: UaValidationStats,
}
UaComplianceError
Each error includes optional WCAG alignment:
pub struct UaComplianceError {
pub code: UaErrorCode,
pub message: String,
pub location: Option<String>,
pub wcag_ref: Option<String>,
pub clause: Option<String>,
}
The wcag_ref field maps the PDF/UA violation to the corresponding WCAG success criterion (e.g., "1.1.1" for non-text content, "1.3.1" for info and relationships).
UaErrorCode Categories
The UaErrorCode enum includes error categories such as:
MissingLanguage– no/Langentry on the document catalogMissingStructureTree– document is not taggedMissingAltText– Figure element lacks alt textHeadingSkipped– heading levels jump (e.g., H1 to H3)MissingTableHeaders– table lacksTHelementsFormFieldNoLabel– form field has no associated labelInvalidRoleMapping– non-standard type not mapped to a standard elementArtifactNotMarked– decorative content not marked as artifactMissingUnicode– text without Unicode mapping
Practical Example: Accessibility Report
Generate a human-readable accessibility report from validation results:
use pdf_oxide::PdfDocument;
use pdf_oxide::compliance::{validate_pdf_ua, PdfUaLevel};
let mut doc = PdfDocument::open("document.pdf")?;
let result = validate_pdf_ua(&mut doc, PdfUaLevel::UA1)?;
println!("=== PDF/UA Accessibility Report ===");
println!("Level: PDF/UA-{}", result.level.xmp_part());
println!("Status: {}", if result.has_errors() { "FAIL" } else { "PASS" });
println!();
if result.has_errors() {
println!("Errors ({}):", result.errors.len());
for (i, error) in result.errors.iter().enumerate() {
print!(" {}. [{}] {}", i + 1, error.code, error.message);
if let Some(ref wcag) = error.wcag_ref {
print!(" (WCAG {})", wcag);
}
println!();
}
}
if result.has_warnings() {
println!("\nWarnings ({}):", result.warnings.len());
for warning in &result.warnings {
println!(" - [{}] {}", warning.code, warning.message);
}
}
println!("\nStats:");
println!(" Structure elements checked: {}",
result.stats.structure_elements_checked);
Next Steps
- PDF/A Validation – archival compliance
- PDF/X Print Production – print production compliance
- API Reference – complete Rust API