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"}
]
endThen 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}
endPossible 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}
endError 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.