This guide covers specific PDF operations for developers using the PDFluent SDK. Learn to manage document properties and optimise for the web.
Add the pdfluent crate to Cargo.toml.
[dependencies]
pdfluent = "1.0.0"Read title, author, subject, keywords, producer, creator and timestamps from any PDF's document-information dictionary.
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
Open the document. Metadata is cached on the PdfDocument and read lazily from the Info dictionary on first access.
use pdfluent::prelude::*;
let doc = PdfDocument::open("report.pdf")?;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.
let meta = doc.metadata();
println!("title = {:?}", meta.title);
println!("author = {:?}", meta.author);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.
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); }Loop over files. Dropping the document at the end of each iteration keeps memory bounded across large batches.
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),
}
}
}Set title, author, subject and keywords on any PDF via the MetadataMut builder. Changes are buffered until commit(), then flushed to the document.
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
Open with a mutable binding. MetadataMut borrows &mut on the document for the duration of the builder chain.
use pdfluent::prelude::*;
let mut doc = PdfDocument::open("report.pdf")?;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().
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"]);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.
doc.metadata_mut()
.set_title("Q3 Financial Report")
.set_author("Finance Team")
.commit()?;save() writes the PDF to disk. The metadata changes are part of that write; no separate flush step required.
doc.save("report_tagged.pdf")?;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.
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(())
}Load the document to which you want to add XMP metadata.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("report.pdf")?;Metadata provides setters for Dublin Core and XMP Basic properties. All fields are optional.
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");Register a custom namespace to store application-specific metadata alongside the standard Dublin Core fields.
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");set_xmp_metadata() serialises the XMP packet and embeds it in the PDF. Existing XMP metadata is replaced.
doc.set_xmp_metadata(xmp)?;
doc.save("report_with_xmp.pdf")?;
println!("XMP metadata written.");Useful for pre-flight checks, compatibility filtering, and document auditing pipelines.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("file.pdf")?;
let v = doc.version();
println!("PDF {}.{}", v.major, v.minor);
Ok(())
}pdf_version() reads the %PDF-x.y header from the first 8 bytes of the file. It does not require full document parsing.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("document.pdf")?;
let version = doc.version();
println!("{}.{}", version.major, version.minor);Use predefined constants to write readable version checks. PdfVersion implements PartialOrd.
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),
}Use PdfDocument::peek_version() to read only the header bytes. This is faster when you need to filter files before loading them.
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 /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.
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(())
}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.
use std::fs::File;
use std::io::Read;
let mut head = [0u8; 1024];
let read = File::open("file.pdf")?.read(&mut head)?;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.
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.");
}The check above is about the byte order the file claims for itself. Everything about its content comes from opening it as usual.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("file.pdf")?;
println!("{} pages, PDF {}", doc.page_count(), doc.version());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.
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(())
}Linearizing rewrites the order of the objects in the file, so the document is opened for modification.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("catalogue.pdf")?;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.
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),
}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.
qpdf --linearize catalogue_plain.pdf catalogue_linear.pdf