Elixir CI Hex.pm Hex Docs Hex.pm pdf_oxide License: MIT

Elixir bindings for pdf_oxide, a high-performance PDF library written in Rust. Built with Rustler.

⚠️ Status: This project is under active development. The public API may change between minor versions until the 1.0 release. The issue tracker is currently disabled.

Features

Reading

  • Open PDFs from file paths or in-memory binaries
  • Read page count, PDF version, metadata, permissions, page labels, outlines, optional-content layers, and spot inks
  • Read annotations and AcroForm fields, with check boxes, radio groups, combo boxes and the rest classified from their field flags

Extraction and conversion

  • Extract text, words, lines, characters, and spans with page geometry and typographic metadata, read a page as typed regions grouped by column, or find a document's running headers and footers
  • Convert individual pages or whole documents to Markdown, HTML, or plain text
  • Convert whole documents to Word, PowerPoint, or Excel files, and convert those files to PDF
  • Search literal text or regular expressions and locate matches on the page
  • Detect tables and render them as Markdown, HTML, or plain text
  • Extract vector paths, rectangles, straight lines, raster images, and embedded fonts
  • Restrict extraction by region and configure artifacts, layers, inks, reading order, table detection, and span merging

Rendering

  • Render pages to PNG, JPEG or raw RGBA at a chosen resolution or into a thumbnail box, render prepress ink separations, and rasterize a whole document

Editing and forms

  • Fill AcroForm fields, flatten forms and annotations, export field data as FDF or XFDF, and save edited PDFs to a file or binary
  • Delete, reorder, rotate and crop pages, and paint over regions, in a mutable editor
  • Create a new PDF from Markdown, HTML, plain text, or JPEG and PNG images, ready to edit and save
  • Merge documents, and split one into pages, ranges, chunks or bookmarked sections in memory
  • Redact pages destructively, removing the covered text from the page rather than covering it, and strip document metadata, JavaScript and attachments
  • Set a document's title, author, subject, keywords, creator, producer and dates, and keep the ones it has through a rewrite
  • Attach files to a document, and list the attachments a document carries

Security and signatures

  • Write a password-protected PDF, with the permission flags a reader reads back, and edit, re-key or decrypt one that already carries a password
  • Read what a document's digital signatures claim — signer, time, reason, the byte range each covers and the field each sits in — and list the fields still waiting for a signature
  • Verify a signature against the bytes it covers, check whether those bytes are the whole file, and read the signer's certificate
  • Read the RFC 3161 timestamp a signature carries, checking both that the authority issued it and that it covers that signature
  • Report a signature's PAdES baseline level, reach a document's archival timestamp, and read the security store kept for validating them later

Diagnostics and lifecycle

  • Check a document against PDF/A, PDF/UA and PDF/X, with each finding's code, clause and location, and convert an edited document to PDF/A
  • Capture diagnostics for content a damaged page drops without failing, and forward them to Logger, or read the conditions the reader tolerated as structured warnings
  • Share one open document across processes for concurrent native reads
  • Release document, editor, image, font, and table resources explicitly when desired

Requirements

  • Elixir ~> 1.15
  • A compatible Erlang/OTP release

The NIF ships as a precompiled binary through rustler_precompiled, so normal installation does not require Rust. A stable Rust toolchain is needed only when building the NIF from source.

Installation

Add pdf_elixide to mix.exs:

def deps do
  [
    {:pdf_elixide, "~> 0.23.0"}
  ]
end

Then fetch and compile the dependency:

mix deps.get
mix compile

The precompiled NIF is downloaded automatically on the first build.

Quick start

Open and inspect a document

Document inspection lives on PdfElixide.Document:

alias PdfElixide.Document

doc = Document.open!("path/to/file.pdf")

Document.version(doc)
#=> {1, 7}
{:ok, page_count} = Document.page_count(doc)
{:ok, first_page} = Document.text(doc, 0)
{:ok, all_text} = Document.text(doc)

Page indices are zero-based.

Most fallible functions have a bang variant that returns the value directly and raises PdfElixide.Error on failure:

page_count = Document.page_count!(doc)
text = Document.text!(doc, 0)

For a page, text! preserves inferred visual rows while to_plain_text! generally reflows prose. Both also convert a whole document. The Text extraction guide compares them and says which to reach for.

Documents loaded from memory use the same API:

bytes = File.read!("path/to/file.pdf")
memory_doc = Document.from_binary!(bytes)
:ok = Document.close(memory_doc)

Extract structured content

Extractors return structs carrying their page and geometry:

{:ok, words} = Document.words(doc, 0)

Every extractor is also available from a page value, and a document is enumerable over its pages:

alias PdfElixide.Document.Page

doc
|> Enum.at(0)
|> Page.words!()

See the PdfElixide.Document documentation for the full set of extractors, their return types and their extraction options.

Convert to Markdown, HTML, or plain text

Convert one page or the whole document:

{:ok, markdown} = Document.to_markdown(doc)
{:ok, first_page_markdown} = Document.to_markdown(doc, 0)

{:ok, html} = Document.to_html(doc)
{:ok, positioned_html} = Document.to_html(doc, preserve_layout: true)

{:ok, plain} = Document.to_plain_text(doc)

Options control heading and table detection, images, form fields, reading order, and related conversion behavior. The result of to_html/1,2,3 is an HTML fragment rather than a complete document; consult its API documentation before rendering untrusted paths through :image_output_dir.

Searches return matches with page numbers and bounding boxes:

Document.search!(doc, "Figure 3")
Document.search!(doc, "figure 3", 4, case_insensitive: true)
Document.search!(doc, ~S"Figure \d+", literal: false)

Patterns are literal by default. Regular expressions use Rust regex syntax. The Text search guide covers pattern options, match geometry, and the per-page search index.

Fill a form

Open a mutable editor, change existing fields, and save the result:

alias PdfElixide.Editor
alias PdfElixide.Form

"path/to/form.pdf"
|> Editor.open!()
|> Form.put_value!("full_name", "Jane Doe")
|> Form.put_value!("subscribe", true)
|> Editor.save!("path/to/filled.pdf")
|> Editor.close()

Most editing functions return the same mutable editor handle, so rebinding does not fork its state. Editor.to_binary/2 returns a PDF binary instead of writing a file. See the Forms guide for field kinds and flags, bulk updates, save behavior, and button-field limitations.

Read a signature

Signatures are read, never produced — open a document signed elsewhere:

alias PdfElixide.Signature

path = "path/to/signed.pdf"
signed_doc = Document.open!(path)

signature =
  try do
    [signature] = Signature.list!(signed_doc)
    signature
  after
    Document.close(signed_doc)
  end

signature.signer_name
#=> "Alice Example"

Signature.verify!(signature, File.read!(path))
#=> :valid

What list/1 reports are the signer's claims; verify/2 is what checks one against the bytes it covers. The Signatures guide covers verification, coverage, certificates, timestamps, PAdES levels, and the security store.

Release native resources

Native memory is released automatically when the BEAM garbage-collects a handle. Long-lived processes can release it at a chosen point:

:ok = Document.close(doc)
true = Document.closed?(doc)

{:error, %PdfElixide.Error{reason: :closed}} = Document.text(doc, 0)

close/1 is idempotent and waits for calls already using the same handle. Editors, extracted images, fonts, and tables provide the same close/1 and closed?/1 pair. Closing an editor discards unsaved edits; closing a document does not invalidate images, fonts, or tables already extracted from it. See the Concurrency guide before sharing handles with workers that may also close them.

Documentation

Full API documentation is published on HexDocs.

License

Released under the MIT License.