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-beta.18"

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(())
}
  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(())
}
  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 Document::open_with_password 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;

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

    A read-only Document is sufficient for text search.

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

    doc.search() performs a case-insensitive Unicode-normalized search across all pages and returns a Vec of TextMatch.

    rust
    let matches = doc.search("invoice number")?;
  3. Access match metadata

    Each TextMatch carries the zero-based page index, the bounding Rect in page coordinates, and the matched text fragment.

    rust
    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,
        );
    }
  4. Search with options

    Use SearchOptions to enable case-sensitive matching or regex search.

    rust
    use pdfluent::text::SearchOptions;
    
    let opts = SearchOptions::new()
        .case_sensitive(true)
        .whole_word(true);
    
    let matches = doc.search_with("Total", opts)?;
  5. Search on a single page

    For large documents, searching page by page avoids loading the full text index at once.

    rust
    let page = doc.page(1)?;
    let matches = page.search("signature")?;
    for m in &matches {
        println!("Found at {:?}", m.rect);
    }
  • Search results reference page coordinates (origin bottom-left). If you need screen coordinates, invert the y-axis relative to the page height.
  • PDFluent normalizes Unicode NFKC before comparison. Ligatures like fi are decomposed, so searching "fi" will match the ligature glyph.
  • Encrypted PDFs must be decrypted before text extraction. Open with Document::open_with_password first.
  • For regex search, the pattern is matched against the Unicode text stream, not the raw PDF content bytes.

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, 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(())
}
  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

    TextReplacement::exact() matches the literal string on any page. Matching is case-sensitive by default.

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

    TextReplacement::regex() accepts any regex pattern. Capture groups are supported in the replacement string.

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

    Use page_mut(n).replace_text() to limit replacement to one page.

    rust
    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")?;
  • 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.
  • Replacement text must use the same font and encoding as the original text. PDFluent matches the font of the first character in the matched range and re-encodes the replacement accordingly.
  • Replacing text that spans multiple words with a shorter string may leave visual gaps. Use TextReplacementOptions::adjust_spacing(true) to redistribute the space.
  • For full content redaction (permanent removal), combine replace_text with page.flatten() to prevent recovery from the content stream.

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.

Overlay text on a PDF page in Rust

Add text at a specific position on any PDF page. Useful for stamps, approval marks, page numbers, and annotations.

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

    page_mut(n) gives you a mutable reference to page n. Pages are zero-indexed.

    rust
    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());
  2. Add text with default font

    Use TextOptions to set position, size, and color. The default font is Helvetica (one of the 14 PDF base fonts, no embedding required).

    rust
    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()
        },
    )?;
  3. Use a custom embedded font

    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.

    rust
    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()
        },
    )?;
  4. Add a rotated watermark text

    Set rotation_degrees on TextOptions to rotate the text. 45 degrees is a common watermark angle.

    rust
    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")?;
  • PDF coordinates start at bottom-left. y: 750.0 on an A4 page (842 pt tall) places the text baseline 750 pt from the bottom, which is near the top.
  • The 14 standard PDF fonts (Helvetica, Times-Roman, Courier, and variants) do not need to be embedded. All other fonts must be embedded for reliable rendering.
  • Text added with add_text() is added on top of existing content. It is selectable and searchable. Use page.add_text_as_image() to burn text as a raster if you want to prevent selection.
  • For multi-line text blocks, use page.add_text_block() which handles line wrapping within a bounding box.