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-beta.18"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(())
}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(())
}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;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("input.pdf")?;
let matches = doc.search("invoice number")?;
for m in &matches {
println!(
"Page {}: {:?} -> "{}"",
m.page + 1,
m.rect,
m.text
);
}
println!("{} match(es) found", matches.len());
Ok(())
}A read-only Document is sufficient for text search.
let doc = PdfDocument::open("input.pdf")?;doc.search() performs a case-insensitive Unicode-normalized search across all pages and returns a Vec of TextMatch.
let matches = doc.search("invoice number")?;Each TextMatch carries the zero-based page index, the bounding Rect in page coordinates, and the matched text fragment.
for m in &matches {
println!(
"page={} x1={:.1} y1={:.1} x2={:.1} y2={:.1}",
m.page + 1,
m.rect.x_min, m.rect.y_min,
m.rect.x_max, m.rect.y_max,
);
}Use SearchOptions to enable case-sensitive matching or regex search.
use pdfluent::text::SearchOptions;
let opts = SearchOptions::new()
.case_sensitive(true)
.whole_word(true);
let matches = doc.search_with("Total", opts)?;For large documents, searching page by page avoids loading the full text index at once.
let page = doc.page(1)?;
let matches = page.search("signature")?;
for m in &matches {
println!("Found at {:?}", m.rect);
}Replace placeholder text, update document dates, or redact strings across all pages of a PDF.
use pdfluent::{PdfDocument, TextReplacement};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("template.pdf")?;
doc.replace_text_all(&[
TextReplacement::exact("{{CUSTOMER_NAME}}", "Acme Corp"),
TextReplacement::exact("{{INVOICE_DATE}}", "2024-04-01"),
TextReplacement::exact("{{TOTAL}}", "EUR 4,200.00"),
])?;
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")?;TextReplacement::exact() matches the literal string on any page. Matching is case-sensitive by default.
use pdfluent::TextReplacement;
doc.replace_text_all(&[
TextReplacement::exact("{{CUSTOMER_NAME}}", "Acme Corp"),
TextReplacement::exact("{{INVOICE_DATE}}", "2024-04-01"),
TextReplacement::exact("{{TOTAL}}", "EUR 4,200.00"),
])?;TextReplacement::regex() accepts any regex pattern. Capture groups are supported in the replacement string.
use pdfluent::TextReplacement;
// Replace phone numbers with a redacted placeholder
doc.replace_text_all(&[
TextReplacement::regex(
r"\+?\d[\d\s\-]{8,14}\d",
"[PHONE REDACTED]",
)?,
// Update year in date strings
TextReplacement::regex(
r"2023-(\d{2}-\d{2})",
"2024-$1",
)?,
])?;Use page_mut(n).replace_text() to limit replacement to one page.
let mut page = doc.page_mut(0)?;
let count = page.replace_text(
TextReplacement::exact("DRAFT", "FINAL"),
)?;
println!("Replaced {} occurrence(s) on page 1", count);
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);Add text at a specific position on any PDF page. Useful for stamps, approval marks, page numbers, and annotations.
use pdfluent::{PdfDocument, TextOptions, Color};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("document.pdf")?;
let mut page = doc.page_mut(0)?;
page.add_text(
"APPROVED",
TextOptions {
x: 50.0,
y: 750.0,
font_size: 36.0,
color: Color::rgb(0, 128, 0),
..TextOptions::default()
},
)?;
doc.save("document-stamped.pdf")?;
Ok(())
}page_mut(n) gives you a mutable reference to page n. Pages are zero-indexed.
use pdfluent::PdfDocument;
let mut doc = PdfDocument::open("document.pdf")?;
let mut page = doc.page_mut(0)?;
println!("Page size: {}x{} pt", page.width(), page.height());Use TextOptions to set position, size, and color. The default font is Helvetica (one of the 14 PDF base fonts, no embedding required).
use pdfluent::{TextOptions, Color};
page.add_text(
"APPROVED",
TextOptions {
x: 50.0,
y: 750.0,
font_size: 36.0,
color: Color::rgb(0, 128, 0),
..TextOptions::default()
},
)?;Load a TTF or OTF font from disk and embed it in the document. Embedded fonts are required for non-Latin scripts and for precise rendering across all PDF viewers.
use pdfluent::Font;
let font = Font::from_file("fonts/Inter-Regular.ttf")?;
let font_ref = doc.embed_font(font)?;
let mut page = doc.page_mut(0)?;
page.add_text(
"Invoice Total: EUR 1,234.56",
TextOptions {
x: 50.0,
y: 200.0,
font_size: 12.0,
font: Some(font_ref),
color: Color::black(),
..TextOptions::default()
},
)?;Set rotation_degrees on TextOptions to rotate the text. 45 degrees is a common watermark angle.
page.add_text(
"DRAFT",
TextOptions {
x: 200.0,
y: 300.0,
font_size: 72.0,
color: Color::rgba(200, 0, 0, 80), // semi-transparent red
rotation_degrees: 45.0,
..TextOptions::default()
},
)?;
doc.save("document-watermarked.pdf")?;