Skip to content

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 + PdfUaValidator builder), and Go (doc.ValidatePdfUa()). WASM exposes a raw pass/fail check via validatePdfUa when 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 /Lang entry on the document catalog
  • MissingStructureTree – document is not tagged
  • MissingAltText – Figure element lacks alt text
  • HeadingSkipped – heading levels jump (e.g., H1 to H3)
  • MissingTableHeaders – table lacks TH elements
  • FormFieldNoLabel – form field has no associated label
  • InvalidRoleMapping – non-standard type not mapped to a standard element
  • ArtifactNotMarked – decorative content not marked as artifact
  • MissingUnicode – 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