How to Handle Password-Protected PDFs with pdf-inspector: A Complete Guide

Pass the password via the PdfOptions builder when calling process_pdf_with_options or process_pdf_mem_with_options, and pdf-inspector will automatically decrypt the document using lopdf, falling back to an empty password for owner-only encryption.

Handling encrypted PDFs in Rust requires careful integration with low-level document parsers. The pdf-inspector library from the firecrawl organization simplifies this by providing a high-level API that accepts passwords through a builder pattern and handles decryption transparently. This guide demonstrates how to handle password-protected PDFs with pdf-inspector using both file-based and in-memory workflows.

Setting the Password via PdfOptions

The decryption workflow begins with the PdfOptions struct defined in src/lib.rs. This builder exposes an optional password field that accepts the encryption key as a String.

pub struct PdfOptions {
    ...
    /// Password for decrypting an encrypted PDF. `None` falls back to the
    /// empty password (owner‑only encryption).
    pub password: Option<String>,
}

(see src/lib.rs L185-L188)

When you instantiate PdfOptions using PdfOptions::new(), you can chain the .password() method to supply the decryption key. The library passes this value downstream to the document loader, ensuring the password is available before any parsing begins.

Decryption Architecture and Implementation

Once you invoke a processing function, pdf-inspector routes the password through internal loader functions located in src/lib.rs. Every high-level entry point—including process_pdf_with_options and process_pdf_mem_with_options—delegates to either load_document_from_path_with_password or load_document_from_mem_with_password.

pub(crate) fn load_document_from_path_with_password<P: AsRef<Path>>(
    path: P,
    password: Option<&str>,
) -> Result<(Document, u32), PdfError> { ... }

(see src/lib.rs L75-L78)

These helpers forward the optional password to load_document_bytes, where the actual decryption logic resides.

The Decryption Routine

Inside load_document_bytes, the code detects encryption and calls decrypt_document_bytes (lines 40-48). This function invokes lopdf’s Document::load_mem_with_options with the supplied password. If authentication fails, the routine automatically retries with an empty string, handling the common "owner-only" protection scenario without requiring user intervention.

fn decrypt_document_bytes(buf: &[u8], password: Option<&str>) -> Result<Document, lopdf::Error> {
    let pw = password.unwrap_or("");
    match Document::load_mem_with_options(buf, lopdf::LoadOptions::with_password(pw)) {
        Ok(doc) => Ok(doc),
        Err(inner) if !pw.is_empty() => {
            Document::load_mem_with_options(buf, lopdf::LoadOptions::with_password(""))
                .map_err(|_| inner)
        }
        Err(inner) => Err(inner),
    }
}

(see src/lib.rs L40-L48)

Processing Password-Protected Files and Buffers

After supplying the password via the builder, the remainder of the pipeline—detection, extraction, and markdown generation—operates identically to unencrypted documents. The password is never exposed in log output because PdfOptions implements a custom Debug trait that redacts sensitive fields.

From Disk

Use process_pdf_with_options to read a password-protected file from the filesystem:

use pdf_inspector::{process_pdf_with_options, PdfOptions};

fn main() -> Result<(), pdf_inspector::PdfError> {
    let opts = PdfOptions::new()
        .password("my-secret-pw");

    let result = process_pdf_with_options("protected.pdf", opts)?;
    println!("PDF type: {:?}, pages: {}", result.pdf_type, result.page_count);
    
    if let Some(md) = result.markdown {
        println!("{}", md);
    }
    Ok(())
}

From Memory

For in-memory workflows—such as processing downloaded bytes—use process_pdf_mem_with_options:

let data = std::fs::read("protected.pdf")?;
let opts = PdfOptions::new().password("my-secret-pw");
let result = pdf_inspector::process_pdf_mem_with_options(&data, opts)?;

Both variants return the same result structure containing the decrypted document metadata and extracted content.

Summary

  • Supply passwords using PdfOptions::new().password("your-key") before calling any processing function.
  • Automatic fallback to empty password handles owner-only encryption without manual retry logic.
  • Secure by default—the Debug implementation for PdfOptions redacts passwords to prevent accidental logging.
  • Unified API—once decrypted, detection and extraction pipelines work identically for both encrypted and unencrypted PDFs.
  • Core implementation resides in src/lib.rs, specifically lines 40-48 for decryption and lines 185-188 for the options builder.

Frequently Asked Questions

What happens if I provide the wrong password?

If the supplied password fails to decrypt the document, decrypt_document_bytes attempts to open the PDF with an empty password as a fallback. If both attempts fail, the function returns the original lopdf::Error, and pdf-inspector propagates this error to the caller, allowing you to handle invalid credentials appropriately.

Does pdf-inspector support owner-only encrypted PDFs?

Yes. Owner-only encryption—where the document opens without a user password but restricts permissions—is handled automatically. When you provide None or the wrong password, the fallback logic in src/lib.rs lines 40-48 retries with an empty string, which successfully decrypts owner-protected files that do not require an open password.

Can I process password-protected PDFs from memory instead of disk?

Absolutely. Use the process_pdf_mem_with_options function with a byte slice and the same PdfOptions builder pattern. The internal load_document_from_mem_with_password function handles the decryption identically to the file-based variant, making it ideal for web servers or streaming applications.

Is the password visible in logs or debug output?

No. The PdfOptions struct implements a custom Debug trait that redacts the password field. When you debug-print the options or encounter errors, the password is replaced with a placeholder, ensuring sensitive credentials never appear in application logs or panic traces.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →