Read and write PDF metadata

This guide covers specific PDF operations for developers using the PDFluent SDK. Learn to manage document properties and optimise for the web.

Add PDFluent to your project

Add the pdfluent crate to Cargo.toml.

toml
[dependencies]
pdfluent = "1.0.0"

Read PDF metadata in Rust

Read title, author, subject, keywords, producer, creator and timestamps from any PDF's document-information dictionary.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("report.pdf")?;
    let meta = doc.metadata();
    println!("title: {:?}", meta.title);
    println!("author: {:?}", meta.author);
    Ok(())
}

Complete program, compiled against the published crate: Code on GitHub →

The same code, typed and run on screen. Watch on YouTube

  1. Open the PDF

    Open the document. Metadata is cached on the PdfDocument and read lazily from the Info dictionary on first access.

    rust
    use pdfluent::prelude::*;
    
    let doc = PdfDocument::open("report.pdf")?;
  2. Call doc.metadata()

    metadata() returns a Metadata struct — a plain snapshot with public fields. There's no Result to unwrap for reads; missing entries surface as None / empty Vec.

    rust
    let meta = doc.metadata();
    println!("title = {:?}", meta.title);
    println!("author = {:?}", meta.author);
  3. Inspect every standard field

    Metadata exposes title, author, subject, keywords (Vec<String>), producer, creator, creation_date and modification_date. Dates are PDF D-format strings (e.g. "D:20260421103000+02'00'") — parse them through your preferred date library if you need a DateTime.

    rust
    let meta = doc.metadata();
    if let Some(ref t) = meta.title { println!("T: {}", t); }
    if let Some(ref a) = meta.author { println!("A: {}", a); }
    for k in &meta.keywords { println!("K: {}", k); }
  4. Bulk-read for a directory of PDFs

    Loop over files. Dropping the document at the end of each iteration keeps memory bounded across large batches.

    rust
    use pdfluent::prelude::*;
    use std::fs;
    
    for entry in fs::read_dir("./inbox")? {
        let path = entry?.path();
        if path.extension().map(|e| e == "pdf").unwrap_or(false) {
            match PdfDocument::open(&path) {
                Ok(doc) => {
                    let m = doc.metadata();
                    println!(
                        "{}: {} — {}",
                        path.display(),
                        m.title.as_deref().unwrap_or("(no title)"),
                        m.author.as_deref().unwrap_or("(no author)"),
                    );
                }
                Err(e) => eprintln!("{}: {}", path.display(), e),
            }
        }
    }
  • Metadata.title and Metadata.author are Option<String>; a document may have no title or no author set.
  • Metadata.keywords is Vec<String>, parsed from the PDF's /Keywords entry — an empty vector means no keywords.
  • creation_date and modification_date are PDF D-format strings; conversion to chrono::DateTime is an application-side concern.
  • The 1.0 SDK exposes the Info-dictionary surface on Metadata. Full XMP metadata read (structured RDF) is tracked for a later release.

Write PDF metadata in Rust

Set title, author, subject and keywords on any PDF via the MetadataMut builder. Changes are buffered until commit(), then flushed to the document.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("report.pdf")?;
    doc.metadata_mut()
        .set_title("Q4 Report")
        .set_author("Finance Team")
        .commit()?;
    doc.save("report-tagged.pdf")?;
    Ok(())
}

Complete program, compiled against the published crate: Code on GitHub →

The same code, typed and run on screen. Watch on YouTube

  1. Open the PDF for mutation

    Open with a mutable binding. MetadataMut borrows &mut on the document for the duration of the builder chain.

    rust
    use pdfluent::prelude::*;
    
    let mut doc = PdfDocument::open("report.pdf")?;
  2. Chain setters via metadata_mut()

    metadata_mut() returns a MetadataMut builder. Each setter returns &mut Self so you can chain them. Changes are buffered locally — nothing is written until commit().

    rust
    let mut meta = doc.metadata_mut();
    meta.set_title("Q3 Financial Report")
        .set_author("Finance Team")
        .set_subject("Quarterly earnings")
        .set_keywords(&["finance", "q3", "2026"]);
  3. Commit to flush changes into the document

    commit() writes the buffered changes to the Info dictionary. It returns Result<()>; call it explicitly so you can handle write errors. MetadataMut also flushes on drop, but in that path errors are silenced.

    rust
    doc.metadata_mut()
        .set_title("Q3 Financial Report")
        .set_author("Finance Team")
        .commit()?;
  4. Save the tagged document

    save() writes the PDF to disk. The metadata changes are part of that write; no separate flush step required.

    rust
    doc.save("report_tagged.pdf")?;
  • metadata_mut() is infallible — a PDF always has an Info dictionary slot, created lazily by commit() if absent.
  • The 1.0 setter surface is set_title, set_author, set_subject, set_keywords. Producer, creator, creation_date and modification_date are read-only in 1.0 (they're written by PDFluent at save time).
  • keywords takes a &[&str] and is serialised joined with commas in the PDF's /Keywords entry, which is the common convention.
  • Non-ASCII values (titles with accented characters, CJK text) round-trip correctly — PDFluent picks UTF-16BE with BOM when needed.
  • Writing metadata does not require any specific capability; it's part of the core SDK surface and available at every tier.

Write XMP metadata to a PDF in Rust

Write Dublin Core, XMP Basic, and custom XMP metadata packets to a PDF. XMP metadata is readable by search engines, DAM systems, and archival tools.

rust
use pdfluent::{PdfDocument, Metadata};

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

    let xmp = Metadata::new()
        .title("PDFluent Technical Whitepaper")
        .creator("Engineering Team")
        .description("Architecture overview of the PDFluent Rust SDK")
        .subject(vec!["PDF", "Rust", "SDK"])
        .rights("Copyright 2025 PDFluent")
        .language("en-US");

    doc.set_xmp_metadata(xmp)?;
    doc.save("whitepaper_with_xmp.pdf")?;
    Ok(())
}
  1. Open the PDF

    Load the document to which you want to add XMP metadata.

    rust
    use pdfluent::PdfDocument;
    
    let mut doc = PdfDocument::open("report.pdf")?;
  2. Build the XMP metadata object

    Metadata provides setters for Dublin Core and XMP Basic properties. All fields are optional.

    rust
    use pdfluent::Metadata;
    
    let xmp = Metadata::new()
        .title("Annual Report 2025")
        .creator("Finance Department")
        .description("Consolidated financial statements for fiscal year 2025")
        .subject(vec!["Finance", "Annual Report", "2025"])
        .publisher("Acme Corp")
        .rights("All rights reserved")
        .language("en-GB")
        .creation_date("2025-03-01T09:00:00Z")
        .modify_date("2025-04-14T15:30:00Z");
  3. Add a custom XMP namespace and property

    Register a custom namespace to store application-specific metadata alongside the standard Dublin Core fields.

    rust
    let xmp = xmp
        .custom_namespace("http://ns.acme.com/pdf/1.0/", "acme")
        .custom_property("acme:documentId", "DOC-2025-0042")
        .custom_property("acme:department", "Legal")
        .custom_property("acme:confidentiality", "Internal");
  4. Write the metadata and save

    set_xmp_metadata() serialises the XMP packet and embeds it in the PDF. Existing XMP metadata is replaced.

    rust
    doc.set_xmp_metadata(xmp)?;
    doc.save("report_with_xmp.pdf")?;
    println!("XMP metadata written.");
  • XMP metadata in PDFs is stored as an XML packet in the /Metadata stream of the document catalog.
  • Setting XMP metadata does not change the DocInfo dictionary (/Author, /Title, etc.). Use doc.set_info() to set both.
  • XMP supports multi-language values via xml:lang attributes. Use .title_lang("fr-FR", "Rapport Annuel") for localised titles.
  • After calling set_xmp_metadata(), any existing digital signature becomes invalid. Set metadata before signing.

Read the PDF spec version from a document in Rust

Useful for pre-flight checks, compatibility filtering, and document auditing pipelines.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("file.pdf")?;
    let v = doc.version();
    println!("PDF {}.{}", v.major, v.minor);
    Ok(())
}
  1. Open the document and call pdf_version()

    pdf_version() reads the %PDF-x.y header from the first 8 bytes of the file. It does not require full document parsing.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("document.pdf")?;
    let version = doc.version();
    
    println!("{}.{}", version.major, version.minor);
  2. Compare versions with the PdfVersion enum

    Use predefined constants to write readable version checks. PdfVersion implements PartialOrd.

    rust
    let v = doc.version();
    
    match (v.major, v.minor) {
        (1, 0) => println!("Very old document"),
        (1, 4) => println!("PDF 1.4 - supports transparency"),
        (1, 5) => println!("PDF 1.5 - supports object streams"),
        (1, 6) => println!("PDF 1.6 - supports AES-128"),
        (1, 7) => println!("PDF 1.7 - supports AES-256"),
        (2, 0) => println!("PDF 2.0 - latest standard"),
        _ => println!("Other version: {}.{}", v.major, v.minor),
    }
  3. Read version without fully opening the document

    Use PdfDocument::peek_version() to read only the header bytes. This is faster when you need to filter files before loading them.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("document.pdf")?;
    let version = doc.version();
    println!("PDF {}.{}", version.major, version.minor);
    
    // Only proceed for documents that are at least PDF 1.6
    if (version.major, version.minor) >= (1, 6) {
        // ...
    }
  • The version in the %PDF header is the declared version. Some tools update the DocumentCatalog Version entry without changing the header. PDFluent returns the higher of the two values.
  • PDF 2.0 (ISO 32000-2) is the current standard. PDF 1.7 (ISO 32000-1) is still the most common version in real-world documents.
  • Versions below 1.4 do not support transparency groups. Versions below 1.5 do not support cross-reference streams.

Detect if a PDF is linearized (web-optimized) in Rust

The /Linearized dictionary is the first object of the file, so the first kilobyte carries the claim -- PDFluent 1.0 has no reader that can verify it.

rust
use std::fs::File;
use std::io::Read;

fn main() -> pdfluent::Result<()> {
    // PDFluent 1.0 has no linearization reader. What can be read without one is
    // the claim: the /Linearized dictionary is by definition the first object of
    // a linearized file, so it is in the first kilobyte if it is anywhere.
    let mut head = [0u8; 1024];
    let read = File::open("file.pdf")?.read(&mut head)?;
    let claims_linearized = String::from_utf8_lossy(&head[..read]).contains("/Linearized");

    // A claim, not a verdict: nothing here proves the token belongs to that
    // dictionary rather than to a string or a comment, and nothing checks that
    // the offsets it carries still describe the file.
    println!("claims fast web view: {claims_linearized}");
    Ok(())
}
  1. Read the first kilobyte

    A linearized file puts its linearization dictionary before anything else, so no PDF parser is needed to look for it -- and nothing else in the file has to be read.

    rust
    use std::fs::File;
    use std::io::Read;
    
    let mut head = [0u8; 1024];
    let read = File::open("file.pdf")?.read(&mut head)?;
  2. Look for the key, and treat it as a claim

    The dictionary is recognisable by its /Linearized entry. A hit says the file presents itself as linearized; it does not prove the token belongs to the first indirect object, and it says nothing about whether the offsets in that dictionary are still true. For a verdict you need a parser that walks the dictionary -- 1.0 does not have one.

    rust
    let claims = String::from_utf8_lossy(&head[..read]).contains("/Linearized");
    if claims {
        println!("The file claims fast web view. The claim is not verified here.");
    } else {
        println!("No linearization dictionary in the first kilobyte.");
    }
  3. Open it with PDFluent for everything else

    The check above is about the byte order the file claims for itself. Everything about its content comes from opening it as usual.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("file.pdf")?;
    println!("{} pages, PDF {}", doc.page_count(), doc.version());
  • There is no doc.is_linearized() in PDFluent 1.0, and no linearization_info() or validate_linearization() either. This page shows the check that does work, rather than a call that would have to be invented for it.
  • It is a claim and not a verdict. /Linearized could sit inside a string, a comment or an unrelated object, and a real reader would have to parse the first indirect object and check the offsets and the hint stream against the file.
  • The key only says the file was linearized once. Any edit after that -- an added page, an incremental signature -- leaves the key in place while the offsets it carries stop being true.
  • Linearization only pays off over HTTP with byte-range requests enabled. For a download or a local file it changes nothing.
  • PDFluent 1.0 does not produce linearized files either; see the section on linearizing a PDF for what the call does today.

Linearize a PDF for faster web loading in Rust

linearize() is on PdfDocument and refuses in 1.0: hint streams are a 1.x MINOR. Handle the refusal, and linearize with qpdf until it lands.

rust
use pdfluent::{Error, PdfDocument};

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

    match doc.linearize() {
        // Only this branch produces a fast-web-view file.
        Ok(()) => {
            doc.save("brochure_linear.pdf")?;
            println!("brochure_linear.pdf is linearized");
        }
        // 1.0 does not build hint streams, and the call refuses out loud rather
        // than writing an ordinary file and calling it fast web view. So say so,
        // and write the file under a name that does not claim otherwise.
        Err(Error::Unsupported(what)) => {
            doc.save("brochure.pdf.out")?;
            println!("not in this release: {what}");
            println!("brochure.pdf.out is written and NOT linearized; run qpdf --linearize over it");
        }
        Err(e) => return Err(e),
    }

    Ok(())
}
  1. Open the PDF

    Linearizing rewrites the order of the objects in the file, so the document is opened for modification.

    rust
    use pdfluent::PdfDocument;
    
    let mut doc = PdfDocument::open("catalogue.pdf")?;
  2. Ask for it, and let the two outcomes write different files

    linearize() is on PdfDocument and returns Error::Unsupported in 1.0: hint-stream construction and the object reordering behind it are a 1.x MINOR. Saving still works -- but a file saved after the refusal is an ordinary PDF, so do not give it a name that claims fast web view, and do not let a pipeline treat the two outcomes as one. SaveOptions::with_linearize(true) is refused for the same reason rather than quietly ignored.

    rust
    use pdfluent::Error;
    
    match doc.linearize() {
        Ok(()) => {
            doc.save("catalogue_linear.pdf")?;
            println!("linearized");
        }
        Err(Error::Unsupported(what)) => {
            doc.save("catalogue_plain.pdf")?;
            println!("skipped: {what}; catalogue_plain.pdf is not linearized");
        }
        Err(e) => return Err(e),
    }
  3. Linearize with an external tool for now

    qpdf does it as a post-step over the file PDFluent wrote, and it is the tool the API documentation itself points at until the engine grows its own. Serve the result with Accept-Ranges: bytes.

    rust
    qpdf --linearize catalogue_plain.pdf catalogue_linear.pdf
  • PDFluent 1.0 does not linearize. doc.linearize() exists and returns Error::Unsupported, and SaveOptions::with_linearize(true) is refused rather than ignored. Both are deliberate: the alternative is a call that returns Ok on a file it did not touch.
  • There is no LinearizeOptions type. Nothing configures a linearizer that is not there.
  • Linearization only helps when the file is served over HTTP with byte-range requests enabled (Accept-Ranges: bytes).
  • Any modification after linearizing -- extra pages, an incremental signature -- undoes it. It belongs at the end of a pipeline, not in the middle.