ExPdfInspector exposes Firecrawl's Rust pdf-inspector library to Elixir through a Rustler NIF. It is designed for local PDF classification, OCR routing, native text extraction, and Markdown generation.

It does not run OCR. Instead, it reports which pages need OCR so your application can route only those pages to an OCR engine or service.

Requirements

The package compiles its Rust NIF during mix compile. Your development, CI, and release build environments need:

  • a Rust toolchain and Cargo;
  • a supported Elixir and Erlang/OTP installation;
  • the system build tools required by the Rust target platform.

Runtime hosts do not need Cargo when you deploy a release that already contains the compiled NIF. Build the release for the same operating system and CPU architecture as the runtime host.

Installation

Add the package to mix.exs:

def deps do
  [
    {:ex_pdf_inspector, "~> 0.1.0"}
  ]
end

Then fetch and compile it:

mix deps.get
mix compile

Process a PDF

process_pdf/1 performs classification, layout analysis, and Markdown extraction in one call:

{:ok, result} = ExPdfInspector.process_pdf("report.pdf")

result.pdf_type
#=> :text_based

result.page_count
#=> 12

result.markdown
#=> "# Quarterly report\n..."

Use process_pdf_bytes/1 when the document is already in memory, for example after an upload:

{:ok, result} =
  upload_path
  |> File.read!()
  |> ExPdfInspector.process_pdf_bytes()

Both variants return the same result shape.

Route pages to OCR

The result combines a document-level classification with page-level routing information:

{:ok, result} = ExPdfInspector.detect_pdf("archive.pdf")

case result.pages_needing_ocr do
  [] -> {:native_text, result.pdf_type}
  pages -> {:ocr_required, pages, result.ocr_reasons_by_page}
end

Possible document types are :text_based, :scanned, :image_based, and :mixed. detect_pdf/1 and detect_pdf_bytes/1 skip Markdown extraction and return nil in the :markdown field.

Page filters accepted as input are zero-based. The pages_needing_ocr values in results come from the upstream library and are human-readable page numbers.

Extract content directly

For plain text only:

{:ok, text} = ExPdfInspector.extract_text("report.pdf")

For position, font, style, links, and form-field metadata:

{:ok, items} = ExPdfInspector.extract_text_with_positions("report.pdf")

Enum.map(items, fn item ->
  {item.page, item.text, item.x, item.y, item.font_size, item.item_type}
end)

To retain page boundaries and OCR metadata:

{:ok, result} = ExPdfInspector.extract_pages_markdown("report.pdf", nil)

Enum.each(result.pages, fn page ->
  IO.puts("Page #{page.page}: #{page.markdown}")
end)

Pass a list such as [0, 2] instead of nil to select pages.

Handle errors

PDF-reading functions return tagged tuples:

case ExPdfInspector.process_pdf(path) do
  {:ok, result} ->
    {:ok, result}

  {:error, %{code: :encrypted}} ->
    {:error, :password_required}

  {:error, %{code: code, message: message}} ->
    Logger.warning("PDF processing failed: #{code}: #{message}")
    {:error, :invalid_pdf}
end

Error codes are stable atoms defined by ExPdfInspector and are intended for pattern matching: :io_error, :not_a_pdf, :invalid_structure, :parse_error, and :encrypted. They are never created from document content or other external strings. Error messages remain strings for logging and diagnostics.

For an encrypted document, pass a password with ExPdfInspector.PdfOptions:

options = %ExPdfInspector.PdfOptions{password: System.fetch_env!("PDF_PASSWORD")}
ExPdfInspector.process_pdf("protected.pdf", options)

Do not log passwords or untrusted document contents.

Next steps

Read Processing and Options for processing modes, detection strategies, and Markdown controls, or open the ExPdfInspector module documentation for the complete API.