Convert, validate, fix, and add ICC profiles

A practical guide for Rust developers working with PDF/A standards. Learn how to convert, validate, and correct documents for long-term archiving.

Convert a PDF to PDF/A-1b, PDF/A-2b, or PDF/A-3b in Rust

Convert a standard PDF into a PDF/A archiving format. PDFluent embeds fonts, adds XMP metadata, and fixes common compliance issues automatically.

rust
use pdfluent::{PdfDocument, PdfALevel, ConvertOptions};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut doc = PdfDocument::open("report.pdf")?;

    let opts = ConvertOptions::default()
        .level(PdfALevel::PdfA2b)
        .embed_missing_fonts(true);

    let report = doc.convert_to_pdf_a(&opts)?;
    doc.save("report_pdfa2b.pdf")?;

    println!("Converted. Remaining issues: {}", report.unresolved.len());
    Ok(())
}
  1. Open the source PDF

    Open the document you want to convert. PDFluent will modify it in place before you save it.

    rust
    use pdfluent::PdfDocument;
    
    let mut doc = PdfDocument::open("report.pdf")?;
  2. Set conversion options

    Choose the target PDF/A level and configure automatic fixes. embed_missing_fonts will substitute missing fonts with embedded versions where possible.

    rust
    use pdfluent::{PdfALevel, ConvertOptions};
    
    let opts = ConvertOptions::default()
        .level(PdfALevel::PdfA2b)
        .embed_missing_fonts(true)
        .add_xmp_metadata(true)
        .remove_javascript(true)
        .remove_transparency(false); // only needed for PDF/A-1
  3. Run the conversion

    convert_to_pdf_a() applies all automatic fixes and returns a ConversionReport listing what was fixed and what could not be fixed automatically.

    rust
    let report = doc.convert_to_pdf_a(&opts)?;
    
    println!("Fixed: {}", report.fixes.len());
    for fix in &report.fixes {
        println!("  + {}", fix.description);
    }
  4. Check for unresolved issues

    Some problems cannot be fixed automatically. Check report.unresolved before saving to decide whether to proceed.

    rust
    if !report.unresolved.is_empty() {
        eprintln!("Unresolved issues:");
        for issue in &report.unresolved {
            eprintln!("  ! {}", issue.message);
        }
    }
  5. Save the converted document

    Save to a new path to keep the original intact.

    rust
    doc.save("report_pdfa2b.pdf")?;
  • PDF/A-1b does not allow transparency. If your PDF uses transparency effects, either use PDF/A-2b or flatten the transparency first.
  • Fonts that cannot be embedded due to licensing restrictions will appear in report.unresolved. You must replace them manually.
  • JavaScript actions, launch actions, and embedded movies are removed during conversion because they are not allowed in PDF/A.
  • After conversion, run validate_pdf_a() to confirm the output is fully conformant before archiving.

Validate PDF/A compliance in Rust

Check whether a PDF meets PDF/A-1b, PDF/A-2b, or PDF/A-3b archiving requirements. Get a structured list of violations with page and object references.

rust
use pdfluent::{PdfDocument, PdfAProfile};

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("invoice.pdf")?;
    let report = doc.validate_pdfa(PdfAProfile::A2b)?;
    if report.is_compliant() {
        println!("PDF/A-2B compliant");
    }
    Ok(())
}
  1. Open the PDF

    Open the document you want to validate. The validator reads the entire structure, so larger files take slightly longer.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("archive_candidate.pdf")?;
  2. Choose the conformance level

    PDFluent supports PDF/A-1b, PDF/A-2b, and PDF/A-3b. Choose the level your archiving policy requires.

    rust
    use pdfluent::PdfAProfile;
    
    // Options: PdfAProfile::A1b, A2b, A3b
    let level = PdfAProfile::A2b;
  3. Run the validator

    Call validate_pdf_a() with the level. It returns a ValidationReport.

    rust
    let report = doc.validate_pdfa(level)?;
  4. Check the result

    report.is_conformant is true when no violations were found. report.violations is a Vec<Violation> with details for each issue.

    rust
    println!("Compliant: {}", report.is_compliant());
    println!("Violations: {}", report.violations.len());
    
    for v in &report.violations {
        println!(
            "Rule {} on page {:?}: {}",
            v.rule, v.page, v.message,
        );
    }
  5. Exit with a non-zero code on failure

    In a CI pipeline, return exit code 1 when violations are found so the build fails automatically.

    rust
    if !report.is_compliant() {
        eprintln!("{} violation(s) found", report.violations.len());
        std::process::exit(1);
    }
  • PDF/A-1b requires all fonts to be embedded. A missing font embedding is the most common violation.
  • PDF/A-2b adds support for JPEG 2000 compression and Optional Content (layers).
  • PDF/A-3b allows embedding of arbitrary file attachments, which PDF/A-1b and PDF/A-2b prohibit.
  • The validator checks structure and metadata but does not re-render pages. It cannot catch rendering errors.

Detect the PDF/A conformance level of a document in Rust

Read the XMP metadata to determine whether a PDF claims PDF/A-1, 2, or 3 conformance, and run a structural validation check.

rust
use pdfluent::{PdfDocument, PdfAProfile};

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("invoice.pdf")?;
    let report = doc.validate_pdfa(PdfAProfile::A2b)?;
    println!("compliant: {}", report.is_compliant());
    Ok(())
}
  1. Open the document

    Read-only access is sufficient for conformance checking.

    rust
    let doc = PdfDocument::open("archive.pdf")?;
  2. Read the claimed PDF/A level from XMP

    pdf_a_level() inspects the pdfaid:part and pdfaid:conformance XMP fields. It returns the declared level, not a validated one.

    rust
    use pdfluent::PdfAProfile;
    
    let report = doc.validate_pdfa(PdfAProfile::A2b)?;
    println!("PDF/A-2b compliant: {}", report.is_compliant());
  3. Run a structural validation

    validate_pdf_a() checks the rules for the claimed level: embedded fonts, no encryption, no transparency (A-1), color spaces, output intent, and XMP metadata.

    rust
    let report = doc.validate_pdfa(PdfAProfile::A2b)?;
    println!("Compliant: {}", report.is_compliant());
    println!("Violations: {}", report.violations.len());
  4. Print detailed validation errors

    Each ValidationError carries a rule ID (e.g. "6.3.3-1"), a human-readable message, and optionally the object number of the offending PDF object.

    rust
    for v in &report.violations {
        println!("[{}] {} (page {:?})", v.rule, v.message, v.page);
    }
  5. Validate against a specific level

    To validate against a target level regardless of the XMP claim, pass it explicitly.

    rust
    use pdfluent::PdfAProfile;
    
    // Validate against a specific profile (A1b, A2b, or A3b)
    let report = doc.validate_pdfa(PdfAProfile::A1b)?;
    println!("A1b compliant: {}", report.is_compliant());
  • A PDF can claim PDF/A compliance in its XMP metadata but still fail structural validation. Always run validate_pdf_a() rather than relying on the XMP claim alone.
  • PDF/A-1b requires all fonts embedded; PDF/A-1a additionally requires tagged structure (logical reading order).
  • Transparency is forbidden in PDF/A-1 but allowed in PDF/A-2 and later. Flattening transparency is required for A-1 compliance.
  • The output intent (ICC color profile) is mandatory for all PDF/A levels. Use add_pdf_a_output_intent() to add one.

Fix common PDF/A validation errors in Rust

Auto-repair the most frequent PDF/A conformance problems: missing output intent, unembedded fonts, transparency, and missing XMP metadata.

rust
use pdfluent::{PdfDocument, pdfa::{PdfALevel, PdfAFixOptions}};

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("noncompliant.pdf")?;

    let opts = PdfAFixOptions::for_level(PdfALevel::A2b)
        .embed_missing_fonts(true)
        .flatten_transparency(true)
        .add_xmp_metadata(true);

    let report = doc.fix_pdf_a_errors(opts)?;
    println!("Fixed {} issue(s)", report.fixed_count());

    doc.save("fixed.pdf")?;
    Ok(())
}
  1. Validate first to understand the errors

    Run validate_pdf_a() before fixing to get a baseline list of issues.

    rust
    let doc = PdfDocument::open("noncompliant.pdf")?;
    let before = doc.validate_pdf_a()?;
    println!("Errors before: {}", before.error_count());
    for e in before.errors() {
        println!("  [{}] {}", e.rule_id(), e.message());
    }
  2. Build fix options

    PdfAFixOptions controls which repairs are attempted. Each fix is opt-in to avoid unintended side effects.

    rust
    use pdfluent::pdfa::{PdfALevel, PdfAFixOptions};
    
    let opts = PdfAFixOptions::for_level(PdfALevel::A2b)
        .embed_missing_fonts(true)
        .flatten_transparency(true)
        .add_xmp_metadata(true)
        .add_output_intent_if_missing(true)
        .remove_javascript(true)
        .remove_embedded_files(false); // keep attachments for PDF/A-3
  3. Apply the repairs

    fix_pdf_a_errors returns a FixReport listing what was changed. Inspect it to confirm each fix was applied.

    rust
    let mut doc = PdfDocument::open("noncompliant.pdf")?;
    let report = doc.fix_pdf_a_errors(opts)?;
    
    for fix in report.applied_fixes() {
        println!("Fixed: {}", fix.description());
    }
    for skipped in report.skipped_fixes() {
        println!("Skipped (manual): {}", skipped.description());
    }
  4. Re-validate after fixing

    Confirm no errors remain. Some issues (e.g. non-embeddable fonts) require manual intervention.

    rust
    let after = doc.validate_pdf_a()?;
    if after.is_conformant() {
        println!("Document is now PDF/A-2b conformant.");
    } else {
        println!("Remaining errors: {}", after.error_count());
    }
  5. Save the repaired document

    Write the fixed file. The output is a full save, not an incremental update.

    rust
    doc.save("fixed_a2b.pdf")?;
  • Flattening transparency is a destructive operation. It may change the visual appearance of pages with transparency blending modes.
  • Fonts with embedding restrictions (fsType = 2) cannot be embedded. These cases are reported as skipped fixes and require replacing the font.
  • Removing JavaScript is required for PDF/A-1. JavaScript actions are also prohibited in PDF/A-2 and PDF/A-3.
  • If the document has an existing output intent with a non-ICC color space, the fixer replaces it with sRGB by default.

Add an ICC color profile output intent to a PDF/A in Rust

Embed an ICC color profile as the /OutputIntent to satisfy the PDF/A requirement for a declared color space.

rust
use pdfluent::{PdfDocument, pdfa::OutputIntent};

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("input.pdf")?;

    // Use the built-in sRGB IEC61966-2.1 profile
    let intent = OutputIntent::srgb();
    doc.set_output_intent(intent)?;

    doc.save("with_output_intent.pdf")?;
    Ok(())
}
  1. Open the document

    The output intent is a document-level property, not page-level. Open with mutable access.

    rust
    let mut doc = PdfDocument::open("input.pdf")?;
  2. Use a built-in profile

    PDFluent ships sRGB IEC61966-2.1 and ISO Coated v2 (FOGRA39) profiles. These cover the vast majority of PDF/A archiving requirements.

    rust
    use pdfluent::pdfa::OutputIntent;
    
    // For screen/web documents:
    let srgb = OutputIntent::srgb();
    
    // For print/CMYK documents:
    let fogra39 = OutputIntent::iso_coated_v2();
  3. Load a custom ICC profile

    If you have a specific ICC profile file, load the bytes and construct an OutputIntent from them.

    rust
    let icc_bytes = std::fs::read("custom_profile.icc")?;
    let intent = OutputIntent::from_icc_bytes(
        &icc_bytes,
        "Custom CMYK Profile",    // OutputConditionIdentifier
        "GTS_PDFA1",              // S (subtype)
    )?;
  4. Attach the intent to the document

    set_output_intent replaces any existing output intent. A PDF/A document must have exactly one.

    rust
    doc.set_output_intent(intent)?;
  5. Verify with a validation run

    Re-validate to confirm the output intent requirement is now satisfied.

    rust
    let report = doc.validate_pdf_a()?;
    let output_intent_errors: Vec<_> = report
        .errors()
        .filter(|e| e.rule_id().starts_with("6.2"))
        .collect();
    println!("Output intent errors: {}", output_intent_errors.len());
    doc.save("with_output_intent.pdf")?;
  • All PDF/A conformance levels (1b, 1a, 2b, 2a, 2u, 3b) require an OutputIntent that declares the color space.
  • sRGB is suitable for RGB-based documents. If pages contain CMYK colors, use a CMYK ICC profile (FOGRA39 for European print, SWOP for North America).
  • A document may have multiple output intents for different color spaces, but only one per output condition identifier.
  • The ICC profile data is compressed and embedded as a stream object. Typical sRGB profiles add about 3KB to the file size.