A practical guide for Rust developers showing how to perform common PDF text operations with the PDFluent SDK.
Text replacement is in the base crate.
# Cargo.toml
[dependencies]
pdfluent = "1.0.0"Read all text content from a PDF document. PDFluent preserves reading order and handles multi-column layouts, right-to-left scripts, and CID fonts.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("file.pdf")?;
println!("{}", doc.extract_text()?);
Ok(())
}Complete program, compiled against the published crate: Code on GitHub →
The same code, typed and run on screen. Watch on YouTube
Load the PDF. Text extraction works page by page, so memory usage stays low even for large documents.
use pdfluent::prelude::*;
let doc = PdfDocument::open("contract.pdf")?;Access a page by its 1-based index and call text(). The method returns a plain String with words separated by spaces and paragraphs separated by newlines.
let page = doc.page(1)?;
let text = page.text()?;
println!("{}", text);Iterate over doc.pages() to process every page. Each call to text() is independent.
let full_text: String = doc
.pages()
.map(|p| p.text().unwrap_or_default())
.collect::<Vec<_>>()
.join("\n\n");Use doc.text_with_layout() to get a Vec<TextBlock> at the document level. Each block carries the text, the page number, and the bounding box in PDF points (bottom-left origin).
for block in doc.text_with_layout()? {
println!(
"[page {}] [{:.1},{:.1}] {:?}",
block.page, block.x, block.y, block.text,
);
}Read the text content of each page as a plain string or as structured spans with font and position data.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("file.pdf")?;
for page in doc.pages() {
println!("{}", page.text()?);
}
Ok(())
}Complete program, compiled against the published crate: Code on GitHub →
The same code, typed and run on screen. Watch on YouTube
Open the PDF. Text extraction is per-page and streams cleanly.
use pdfluent::prelude::*;
let doc = PdfDocument::open("document.pdf")?;doc.pages() returns an iterator of Page<'_>. Each Page has a text() method that returns Result<String>.
for page in doc.pages() {
let text = page.text()?;
println!("page {}: {} chars", page.number(), text.len());
}For downstream processing, join the per-page strings with page separators.
let combined: String = doc
.pages()
.map(|p| p.text().unwrap_or_default())
.collect::<Vec<_>>()
.join("\n\n");Get each word or character with its x, y, width, and height on the page. Useful for building search, redaction, or document analysis tools.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("file.pdf")?;
for block in doc.text_with_layout()? {
println!("p{} {:?} {}", block.page, block.bbox, block.text);
}
Ok(())
}Load the PDF.
use pdfluent::prelude::*;
let doc = PdfDocument::open("document.pdf")?;Returns Vec<TextBlock> document-wide. Each TextBlock carries the text, its 1-based page number, and bounding-box coordinates in PDF points (bottom-left origin).
let blocks = doc.text_with_layout()?;
println!("{} text blocks", blocks.len());Read block.page, block.x, block.y, block.width, block.height, block.text.
for block in doc.text_with_layout()? {
if block.page == 1 {
println!("[{:.1},{:.1}] {:?}", block.x, block.y, block.text);
}
}Detect and extract structured table data from PDF pages. Get rows and cells as Rust values without writing custom parsing logic.
// Planned 1.1 API — not available in pdfluent 1.0.
// For 1.0, use `page.text()` and parse the result manually.
use pdfluent::PdfDocument;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let doc = PdfDocument::open("report.pdf")?;
let page = doc.page(1)?;
for table in page.extract_tables()? {
for row in &table.rows {
let cells: Vec<&str> = row.iter()
.map(|c| c.text.as_str())
.collect();
println!("{}", cells.join(" | "));
}
}
Ok(())
}Table extraction works on a per-page basis. Open the document and select the page that contains the table.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("financial_report.pdf")?;
let page = doc.page(1)?; // 0-indexed, so this is page 2extract_tables() returns a Vec<Table>. Each Table has a rows field: a Vec<Vec<TableCell>>. Cells span columns if they have a colspan greater than 1.
let tables = page.extract_tables()?;
println!("Found {} table(s) on this page", tables.len());Each TableCell contains the text content and the column span. Iterate rows and cells to process the data.
for (ti, table) in tables.iter().enumerate() {
println!("Table {}: {} rows", ti + 1, table.rows.len());
for row in &table.rows {
for cell in row {
print!("[{}] ", cell.text.trim());
}
println!();
}
}Write a simple CSV from the extracted rows. Use the csv crate for proper quoting.
use std::io::Write;
let mut out = std::fs::File::create("table.csv")?;
for row in &tables[0].rows {
let line = row.iter()
.map(|c| format!(""{}"", c.text.replace('"', """")))
.collect::<Vec<_>>()
.join(",");
writeln!(out, "{}", line)?;
}Use TableExtractionOptions to adjust the line-merge tolerance and minimum cell size, which helps with tables that have thin or invisible borders.
use pdfluent::TableExtractionOptions;
let opts = TableExtractionOptions::default()
.line_tolerance(2.0)
.min_cell_width(20.0);
let tables = page.extract_tables_with_options(&opts)?;Find all occurrences of a string in a PDF and retrieve the bounding box of each match on each page.
use pdfluent::PdfDocument;
use pdfluent::text_edit::TextQuery;
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("input.pdf")?;
let matches = doc.find_text(TextQuery::exact("invoice number"))?;
for m in &matches {
let [x0, y0, x1, y1] = m.bbox;
println!("page {}: [{x0:.1} {y0:.1} {x1:.1} {y1:.1}] {:?}", m.page, m.text);
}
println!("{} match(es) found", matches.len());
Ok(())
}find_text takes &mut self because it builds and caches the text layer on first use, so bind the document as mutable.
let mut doc = PdfDocument::open("input.pdf")?;TextQuery::exact matches the string as written. Matching is case-sensitive by default.
use pdfluent::text_edit::TextQuery;
let matches = doc.find_text(TextQuery::exact("invoice number"))?;Each TextMatch carries a 1-based page number and a bounding box as [x_min, y_min, x_max, y_max] in PDF points.
for m in &matches {
let [x0, y0, x1, y1] = m.bbox;
println!(
"page={} x0={x0:.1} y0={y0:.1} x1={x1:.1} y1={y1:.1} text={:?}",
m.page, m.text,
);
}The builder controls case folding, which pages to walk, and how many matches to return. Page ranges are 1-based.
let matches = doc.find_text(
TextQuery::exact("total")
.case_insensitive(true)
.pages(1..=3)
.limit(50),
)?;TextQuery::regex compiles the pattern up front and returns an error if it is invalid. There is no whole-word flag; use a word boundary in the pattern.
let matches = doc.find_text(TextQuery::regex(r"\bTotal\b")?)?;Replace placeholder text, update document dates, or redact strings across all pages of a PDF.
use pdfluent::{PdfDocument, ReplaceOptions, TextQuery};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("template.pdf")?;
for (placeholder, value) in [
("{{CUSTOMER_NAME}}", "Acme Corp"),
("{{INVOICE_DATE}}", "2024-04-01"),
("{{TOTAL}}", "EUR 4,200.00"),
] {
doc.replace_text(
TextQuery::exact(placeholder),
value,
ReplaceOptions::default(),
)?;
}
doc.save("invoice-filled.pdf")?;
Ok(())
}The source is typically a template PDF with placeholder strings. Load it as normal.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("template.pdf")?;TextQuery::exact() matches the literal string on every page. Matching is case-sensitive unless you add .case_insensitive(true). Each call returns a TextReplacementReport that accounts for every occurrence it found.
use pdfluent::{ReplaceOptions, TextQuery};
let report = doc.replace_text(
TextQuery::exact("{{CUSTOMER_NAME}}"),
"Acme Corp",
ReplaceOptions::default(),
)?;
println!(
"{} of {} occurrence(s) replaced",
report.replacements_applied, report.matches_found,
);TextQuery::regex() takes any pattern the regex crate accepts. The replacement is a literal string: capture groups are not expanded into it. A pattern that can match the empty string is rejected, because it would match at every position in the document.
use pdfluent::{ReplaceOptions, TextQuery};
// Replace phone numbers with a redacted placeholder
doc.replace_text(
TextQuery::regex(r"\+?\d[\d\s\-]{8,14}\d")?,
"[PHONE REDACTED]",
ReplaceOptions::default(),
)?;Scope the query instead of the document: TextQuery::pages() takes a 1-based page range, so pages(1..=1) is the first page only.
use pdfluent::{ReplaceOptions, TextQuery};
let report = doc.replace_text(
TextQuery::exact("DRAFT").pages(1..=1),
"FINAL",
ReplaceOptions::default(),
)?;
println!("Replaced {} occurrence(s) on page 1", report.replacements_applied);
doc.save("invoice-final.pdf")?;Extract and diff the text of two PDF documents page by page to find additions, deletions, and changes.
use pdfluent::PdfDocument;
use std::collections::HashSet;
fn main() -> pdfluent::Result<()> {
let text_a = PdfDocument::open("version_a.pdf")?.text()?;
let text_b = PdfDocument::open("version_b.pdf")?.text()?;
if text_a == text_b {
println!("Documents are text-identical.");
} else {
let lines_a: HashSet<&str> = text_a.lines().collect();
let lines_b: HashSet<&str> = text_b.lines().collect();
for line in text_b.lines().filter(|l| !lines_a.contains(l)) {
println!("+ {}", line.trim());
}
for line in text_a.lines().filter(|l| !lines_b.contains(l)) {
println!("- {}", line.trim());
}
}
Ok(())
}Open the two PDF files you want to compare as read-only Documents.
let doc_a = PdfDocument::open("original.pdf")?;
let doc_b = PdfDocument::open("revised.pdf")?;TextDiff::compare extracts the plain text from each page and computes a line-level diff using the longest common subsequence algorithm.
let text_a = doc_a.text()?;
let text_b = doc_b.text()?;is_identical() is a quick check before iterating individual changes.
if text_a == text_b {
println!("No text differences found.");
return Ok(());
}Each DiffChange carries the page index, change kind (Added, Removed, or Changed), and the text content.
use std::collections::HashSet;
let lines_a: HashSet<&str> = text_a.lines().collect();
let lines_b: HashSet<&str> = text_b.lines().collect();
for line in text_b.lines().filter(|l| !lines_a.contains(l)) {
println!("+ {}", line.trim());
}
for line in text_a.lines().filter(|l| !lines_b.contains(l)) {
println!("- {}", line.trim());
}If the documents have different page counts, pages that exist only in one document are reported as whole-page additions or deletions.
use std::collections::HashSet;
let lines_a: HashSet<&str> = text_a.lines().collect();
let lines_b: HashSet<&str> = text_b.lines().collect();
let added = text_b.lines().filter(|l| !lines_a.contains(l)).count();
let removed = text_a.lines().filter(|l| !lines_b.contains(l)).count();
println!("Pages in A: {}", doc_a.page_count());
println!("Pages in B: {}", doc_b.page_count());
println!("Total line changes: {}", added + removed);