A practical guide for Rust developers working with PDF/A. Two calls do the work -- convert_to_pdfa and validate_pdfa -- and this page shows what each of them can and cannot promise.
Convert a standard PDF into an archival PDF/A document. `convert_to_pdfa` embeds fonts, normalises colour with an output intent, drops the constructs PDF/A forbids, and writes the PDF/A identification metadata. It returns a new document; the one you opened is left as it was.
use pdfluent::{PdfAProfile, PdfDocument};
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("report.pdf")?;
// A new document. The source is not modified.
let archived = doc.convert_to_pdfa(PdfAProfile::A2b)?;
let report = archived.validate_pdfa(PdfAProfile::A2b)?;
println!("compliant: {}", report.is_compliant());
archived.save("report_pdfa2b.pdf")?;
Ok(())
}Open the document you want to archive. Conversion reads it and builds a second document, so a shared reference is enough.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("report.pdf")?;PdfAProfile has three variants: A1b, A2b, and A3b. A2b is the usual choice; pick A1b only when a policy demands the older, stricter profile, and A3b when the archive has to carry attachments.
use pdfluent::PdfAProfile;
let profile = PdfAProfile::A2b;convert_to_pdfa runs the whole pipeline in one call: font embedding and repair, colour normalisation with an output intent, removal of JavaScript and other forbidden constructs, and the XMP identification block. Damaged input is repaired first where that is possible.
let archived = doc.convert_to_pdfa(profile)?;Conversion is best effort, so check the result rather than assume it. Some documents cannot be made conformant without changing how they look, and an archival converter must not do that quietly.
let report = archived.validate_pdfa(profile)?;
for violation in &report.violations {
eprintln!("[{}] {}", violation.rule, violation.message);
}Write the archival copy to its own path. The original file on disk is untouched.
archived.save("report_pdfa2b.pdf")?;Check a document against PDF/A-1b, PDF/A-2b, or PDF/A-3b. `validate_pdfa` returns a report holding every finding, each with the rule it comes from, a message, and a severity.
use pdfluent::{PdfAProfile, PdfDocument};
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("archive_candidate.pdf")?;
let report = doc.validate_pdfa(PdfAProfile::A2b)?;
println!("compliant: {}", report.is_compliant());
for violation in &report.violations {
println!("[{}] {}", violation.rule, violation.message);
}
Ok(())
}Validation reads the whole structure, so a large file costs more than a small one. No mutable access is needed.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("archive_candidate.pdf")?;The profile you pass is the profile the report is about. Validating an A1b document against A2b tells you nothing about the claim it makes.
use pdfluent::PdfAProfile;
let profile = PdfAProfile::A2b;validate_pdfa returns a PdfAValidationReport. The Err case means the document could not be read at all, not that it failed validation -- a non-conformant document is a successful run with findings in it.
let report = doc.validate_pdfa(profile)?;is_compliant is true when nothing of Severity::Error was found. Warnings are reported as well and do not block conformance, so count the two apart if your pipeline treats them differently.
use pdfluent::compliance::Severity;
let errors = report
.violations
.iter()
.filter(|violation| violation.severity == Severity::Error)
.count();
println!("{} error(s) out of {} finding(s)", errors, report.violations.len());
println!("compliant: {}", report.is_compliant());In a pipeline, turn a non-conformant document into a non-zero exit code so the run stops instead of archiving it.
if !report.is_compliant() {
eprintln!("{} finding(s)", report.violations.len());
std::process::exit(1);
}A PDF can claim a conformance level in its metadata and still fail the rules for it. Validate against each profile in turn and let the answer come from the validator rather than from the claim.
use pdfluent::{PdfAProfile, PdfDocument};
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("archive.pdf")?;
for profile in [PdfAProfile::A1b, PdfAProfile::A2b, PdfAProfile::A3b] {
let report = doc.validate_pdfa(profile)?;
println!("{:?}: {}", profile, report.is_compliant());
}
Ok(())
}Read-only access is enough; nothing here writes.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("archive.pdf")?;PDF/A-1b is the narrowest of the three and PDF/A-3b the widest, so this order lets the first pass be the answer.
use pdfluent::PdfAProfile;
let levels = [PdfAProfile::A1b, PdfAProfile::A2b, PdfAProfile::A3b];Stop at the first profile the document conforms to. None means it meets none of the three, which is the useful answer when a document claims otherwise.
let mut strictest = None;
for profile in levels {
if doc.validate_pdfa(profile)?.is_compliant() {
strictest = Some(profile);
break;
}
}
println!("{:?}", strictest);When a level fails, the report says why. The rule identifier is the clause of ISO 19005 that was broken, so it can be looked up.
let report = doc.validate_pdfa(PdfAProfile::A1b)?;
for violation in &report.violations {
println!(
"[{}] {} ({:?})",
violation.rule, violation.message, violation.severity
);
}A stored list of findings is unreadable without the profile it was measured against, and the report carries it.
println!("checked against {:?}", report.profile);There is no separate repair call in PDFluent 1.0. Conversion is the repair: `convert_to_pdfa` embeds fonts, writes the output intent and strips what PDF/A forbids, and validating before and after tells you how much of the gap it closed.
use pdfluent::{PdfAProfile, PdfDocument};
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("noncompliant.pdf")?;
let before = doc.validate_pdfa(PdfAProfile::A2b)?;
println!("before: {} finding(s)", before.violations.len());
// Conversion is the repair; there is no fix-in-place call.
let repaired = doc.convert_to_pdfa(PdfAProfile::A2b)?;
let after = repaired.validate_pdfa(PdfAProfile::A2b)?;
println!("after: {} finding(s)", after.violations.len());
if after.is_compliant() {
repaired.save("repaired_a2b.pdf")?;
}
Ok(())
}Validate first, so that afterwards you can say what changed rather than hope something did.
use pdfluent::{PdfAProfile, PdfDocument};
let doc = PdfDocument::open("noncompliant.pdf")?;
let before = doc.validate_pdfa(PdfAProfile::A2b)?;The conversion pipeline is what repairs the document: fonts are embedded, colour is normalised with an output intent, forbidden constructs are removed, and the identification metadata is written.
let repaired = doc.convert_to_pdfa(PdfAProfile::A2b)?;The difference between the two counts is the part of the problem that conversion could solve.
let after = repaired.validate_pdfa(PdfAProfile::A2b)?;
println!(
"{} -> {} finding(s)",
before.violations.len(),
after.violations.len()
);Whatever survives conversion needs a decision from a person: a font that may not be embedded has to be replaced, and encrypted content has to be unlocked at the source.
for violation in &after.violations {
println!("[{}] {}", violation.rule, violation.message);
}Writing a file that still fails validation puts the problem in the archive instead of in the pipeline.
if after.is_compliant() {
repaired.save("repaired_a2b.pdf")?;
} else {
eprintln!("still not conformant -- not writing an archive copy");
}