Extract text, tables, search, replace, and compare PDFs

A practical guide for Rust developers showing how to perform common PDF text operations with the PDFluent SDK.

Add PDFluent to Cargo.toml

Text replacement is in the base crate.

toml
# Cargo.toml
[dependencies]
pdfluent = "1.0.0"

Extract text from a PDF in Rust

Read all text content from a PDF document. PDFluent preserves reading order and handles multi-column layouts, right-to-left scripts, and CID fonts.

rust
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

  1. Open the document

    Load the PDF. Text extraction works page by page, so memory usage stays low even for large documents.

    rust
    use pdfluent::prelude::*;
    
    let doc = PdfDocument::open("contract.pdf")?;
  2. Extract text from a single page

    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.

    rust
    let page = doc.page(1)?;
    let text = page.text()?;
    println!("{}", text);
  3. Extract text from all pages

    Iterate over doc.pages() to process every page. Each call to text() is independent.

    rust
    let full_text: String = doc
        .pages()
        .map(|p| p.text().unwrap_or_default())
        .collect::<Vec<_>>()
        .join("\n\n");
  4. Extract text with layout positions

    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).

    rust
    for block in doc.text_with_layout()? {
        println!(
            "[page {}] [{:.1},{:.1}] {:?}",
            block.page, block.x, block.y, block.text,
        );
    }
  • PDFluent decodes ToUnicode CMaps and Type1/TrueType encodings automatically.
  • Scanned PDFs with no embedded text return empty strings. Use an OCR step before extraction if needed.
  • Right-to-left text (Arabic, Hebrew) is returned in logical order, not visual order.
  • Ligatures and composed characters are decomposed to their Unicode equivalents where a mapping exists.
  • Page indexing is 1-based throughout the SDK (RFC 0001 §1).

Extract text page by page from a PDF in Rust

Read the text content of each page as a plain string or as structured spans with font and position data.

rust
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

  1. Open the document

    Open the PDF. Text extraction is per-page and streams cleanly.

    rust
    use pdfluent::prelude::*;
    
    let doc = PdfDocument::open("document.pdf")?;
  2. Iterate pages and extract text

    doc.pages() returns an iterator of Page<'_>. Each Page has a text() method that returns Result<String>.

    rust
    for page in doc.pages() {
        let text = page.text()?;
        println!("page {}: {} chars", page.number(), text.len());
    }
  3. Collect into a single String

    For downstream processing, join the per-page strings with page separators.

    rust
    let combined: String = doc
        .pages()
        .map(|p| p.text().unwrap_or_default())
        .collect::<Vec<_>>()
        .join("\n\n");
  • Text extraction follows the PDF content stream order, which may differ from visual reading order in multi-column layouts. Use extract_spans() and sort by rect position for precise column order.
  • Characters with custom encoding or Type3 fonts may not map cleanly to Unicode. PDFluent uses ToUnicode maps where available.
  • Encrypted PDFs must be opened with PdfDocument::open_with before text extraction.
  • For scanned PDFs without text layer, text() returns an empty string. You need OCR for image-based documents.

Extract text with bounding box positions in Rust

Get each word or character with its x, y, width, and height on the page. Useful for building search, redaction, or document analysis tools.

rust
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(())
}
  1. Open the document

    Load the PDF.

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

    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).

    rust
    let blocks = doc.text_with_layout()?;
    println!("{} text blocks", blocks.len());
  3. Access per-block fields

    Read block.page, block.x, block.y, block.width, block.height, block.text.

    rust
    for block in doc.text_with_layout()? {
        if block.page == 1 {
            println!("[{:.1},{:.1}] {:?}", block.x, block.y, block.text);
        }
    }
  • Coordinates use the PDF coordinate system: origin at the bottom-left, y increases upward.
  • For screen rendering where y starts at the top, compute screen_y = page_height_pts - (word.y + word.height).
  • Word grouping is heuristic. Very close characters that share a text run are merged into one word entry.

Extract table data from PDFs in Rust

Detect and extract structured table data from PDF pages. Get rows and cells as Rust values without writing custom parsing logic.

rust
// 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(())
}
  1. Open the PDF and access the page

    Table extraction works on a per-page basis. Open the document and select the page that contains the table.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("financial_report.pdf")?;
    let page = doc.page(1)?; // 0-indexed, so this is page 2
  2. Extract all tables from a page

    extract_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.

    rust
    let tables = page.extract_tables()?;
    println!("Found {} table(s) on this page", tables.len());
  3. Iterate rows and cells

    Each TableCell contains the text content and the column span. Iterate rows and cells to process the data.

    rust
    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!();
        }
    }
  4. Export a table to CSV

    Write a simple CSV from the extracted rows. Use the csv crate for proper quoting.

    rust
    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)?;
    }
  5. Tune table detection

    Use TableExtractionOptions to adjust the line-merge tolerance and minimum cell size, which helps with tables that have thin or invisible borders.

    rust
    use pdfluent::TableExtractionOptions;
    
    let opts = TableExtractionOptions::default()
        .line_tolerance(2.0)
        .min_cell_width(20.0);
    
    let tables = page.extract_tables_with_options(&opts)?;
  • Table detection uses both ruling lines and whitespace-gap analysis. Documents with well-defined borders produce the most accurate results.
  • Merged cells (rowspan/colspan) are detected and reported in the TableCell.colspan and TableCell.rowspan fields.
  • For pages with multiple tables, each Table value includes its bounding box so you can identify which table on the page it corresponds to.

Search for text in a PDF and get positions in Rust

Find all occurrences of a string in a PDF and retrieve the bounding box of each match on each page.

rust
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(())
}
  1. Open the PDF

    find_text takes &mut self because it builds and caches the text layer on first use, so bind the document as mutable.

    rust
    let mut doc = PdfDocument::open("input.pdf")?;
  2. Run a literal search

    TextQuery::exact matches the string as written. Matching is case-sensitive by default.

    rust
    use pdfluent::text_edit::TextQuery;
    
    let matches = doc.find_text(TextQuery::exact("invoice number"))?;
  3. Read the match metadata

    Each TextMatch carries a 1-based page number and a bounding box as [x_min, y_min, x_max, y_max] in PDF points.

    rust
    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,
        );
    }
  4. Narrow the query

    The builder controls case folding, which pages to walk, and how many matches to return. Page ranges are 1-based.

    rust
    let matches = doc.find_text(
        TextQuery::exact("total")
            .case_insensitive(true)
            .pages(1..=3)
            .limit(50),
    )?;
  5. Search by pattern

    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.

    rust
    let matches = doc.find_text(TextQuery::regex(r"\bTotal\b")?)?;
  • Coordinates use the PDF convention: the origin is the bottom-left of the page and y grows upward. To draw on a top-left screen canvas, subtract y from the page height.
  • Page numbers on a TextMatch are 1-based, so they can be printed as-is.
  • A regex query is rejected at construction if the pattern can match the empty string, because an empty match has no position to report.
  • Each match carries an opaque MatchId that is only valid against the document revision that produced it. Feed those ids straight into replace_text_matches; re-run find_text after any edit is applied.
  • For an encrypted PDF, supply the password when opening: PdfDocument::open_with("input.pdf", OpenOptions::new().with_password("secret"))?.

Find and replace text in a PDF in Rust

Replace placeholder text, update document dates, or redact strings across all pages of a PDF.

rust
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(())
}
  1. Open the source document

    The source is typically a template PDF with placeholder strings. Load it as normal.

    rust
    use pdfluent::PdfDocument;
    
    let mut doc = PdfDocument::open("template.pdf")?;
  2. Replace exact placeholder strings

    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.

    rust
    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,
    );
  3. Replace text using a regex pattern

    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.

    rust
    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(),
    )?;
  4. Replace text on a single page

    Scope the query instead of the document: TextQuery::pages() takes a 1-based page range, so pages(1..=1) is the first page only.

    rust
    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")?;
  • Text in PDFs is stored as glyph sequences in content streams, not as plain strings. PDFluent reconstructs words by analyzing glyph positions before matching. Hyphenated text at line breaks may not match a single-word pattern.
  • The replacement is re-encoded in the font of the text it replaces. Where that font cannot carry a character, PDFluent falls back to a standard font and reports it per edit as font_substituted rather than dropping the character.
  • Replacement is not redaction. The text is rewritten in the content stream, but a replaced document is not a safe way to remove confidential text -- use the redaction API for that.
  • Documents that carry a digital signature are refused, because rewriting the content stream would invalidate the signature. Overriding that is an explicit choice in ReplaceOptions.

Compare the text content of two PDFs in Rust

Extract and diff the text of two PDF documents page by page to find additions, deletions, and changes.

rust
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(())
}
  1. Open both documents

    Open the two PDF files you want to compare as read-only Documents.

    rust
    let doc_a = PdfDocument::open("original.pdf")?;
    let doc_b = PdfDocument::open("revised.pdf")?;
  2. Run a text diff

    TextDiff::compare extracts the plain text from each page and computes a line-level diff using the longest common subsequence algorithm.

    rust
    let text_a = doc_a.text()?;
    let text_b = doc_b.text()?;
  3. Check if the documents are identical

    is_identical() is a quick check before iterating individual changes.

    rust
    if text_a == text_b {
        println!("No text differences found.");
        return Ok(());
    }
  4. Iterate changes

    Each DiffChange carries the page index, change kind (Added, Removed, or Changed), and the text content.

    rust
    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());
    }
  5. Compare page counts and report structural differences

    If the documents have different page counts, pages that exist only in one document are reported as whole-page additions or deletions.

    rust
    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);
  • Text comparison ignores visual formatting (fonts, sizes, colors). Two pages that look different but have the same words will show no text differences.
  • PDFluent normalizes whitespace and Unicode before diffing. Extra spaces and different line break encodings do not produce spurious differences.
  • For visual comparison (pixel-level diff), use the render API to produce images and compare them separately.
  • Large documents with many changes may produce a large DiffChange list. Use diff.summary() to get a compact per-page summary instead.