Elixir bindings for pdfium, Google's
Chromium PDF engine, via the Rust pdfium-render
crate. The native library ships precompiled (rustler_precompiled), so
there is no Rust toolchain or separately-installed pdfium to set up.
A read & write toolkit
Read: open, render, extract/search text, metadata, page geometry,
permissions, structure (bookmarks/links/attachments), forms/annotations, and
images & page objects.
Write: page assembly — merge (append/2), split/subset
(extract_pages/2), delete_pages/2, rotate_page/3; document creation —
new/0, add_page/3, and draw_text/draw_rectangle/draw_line/
draw_circle/draw_image; annotation authoring — add_text_annotation/5,
add_free_text_annotation/5, add_square_annotation/4, add_link_annotation/5,
delete_annotation/3; and save_to_bytes/1 / save_to_file/2.
Form-filling and the text-markup annotation family (highlight/underline/…) are
arriving in later 0.3.x releases.
Untrusted input
pdfium runs in-process as a native library, so a genuine crash in it would take down the BEAM VM (a Rust panic is contained; a native fault is not). ExPdfium validates and bounds caller arguments and capped page-render/image decode sizes, but extraction returns data proportional to the document's content — a large (or maliciously compressed) embedded file, image, or signature blob allocates memory proportional to its decoded size. When processing untrusted PDFs at scale, run that work behind OS memory limits and/or in an isolated, supervised OS process (e.g. a dedicated, restartable node) rather than relying on per-call caps.
Example
{:ok, doc} = ExPdfium.open("file.pdf")
{:ok, 3} = ExPdfium.page_count(doc)
{:ok, %ExPdfium.Bitmap{data: data, width: w, height: h}} =
ExPdfium.render_page(doc, 0, dpi: 300)
{:ok, image} = Vix.Vips.Image.new_from_binary(data, w, h, 4, :VIPS_FORMAT_UCHAR)
:ok = ExPdfium.close(doc)
# Encrypted documents:
{:ok, doc} = ExPdfium.open("secret.pdf", password: "hunter2")
Summary
Documents
Explicitly close a document, releasing pdfium memory early. Optional and idempotent.
Open a PDF from a file path or an in-memory binary.
Number of pages in the document.
Rendering
Render a 0-indexed page to an ExPdfium.Bitmap (an uncompressed 4-channel
pixel buffer).
Render every page to a small bitmap, returned in page order.
Text & search
Extract the plain text of the whole document. Pages are joined by a form-feed
("\f") character. Returns {:error, :document_closed} if the document has
been closed.
Extract the plain text of a 0-indexed page.
Search a page for query, returning the matches.
Return the page's text as runs (segments), each with its bounding box.
Forms & annotations
Return the annotations on a 0-indexed page, in page order.
Read the document's AcroForm fields, one entry per widget, across all pages.
Return which interactive-form technology the document uses.
Images & objects
Decode the image object at object_index (see page_objects/2 / images/2) to
a pixel bitmap.
Return the original, still-encoded stream of the image object at object_index.
List the image objects on a 0-indexed page, with how each is stored.
The composed content→display transformation matrix for a 0-indexed object on a 0-indexed page.
List every object on a 0-indexed page, in page order.
Creating documents
Add a blank page to doc.
Draw a circle of radius centered at {cx, cy} on a 0-indexed page.
Place a decoded image (an ExPdfium.Bitmap) into the :at rectangle on a
0-indexed page, scaling it to fill those bounds.
Draw a straight line from {x1, y1} to {x2, y2} on a 0-indexed page.
Draw a rectangle covering the bounds/0 rectangle on a 0-indexed page.
Draw text with its baseline starting at {x, y} (PDF points, bottom-left
origin) on a 0-indexed page.
Create a new, empty in-memory PDF document. Add pages with add_page/3 and
content with the draw_* functions, then save_to_bytes/1 / save_to_file/2.
Annotating
Add a free-text annotation: a visible text box inside bounds.
Add a link annotation covering bounds that opens uri when clicked.
Add a square (rectangle) annotation filling bounds.
Add a text (sticky-note) annotation at a point, in PDF points (bottom-left origin).
Delete the annotation at 0-based index on a 0-indexed page.
Writing (page assembly)
Append a copy of every page of source onto the end of doc (merge).
Delete a page, or an inclusive range of pages, from doc.
Build a new document from the given 0-indexed pages of source.
Flatten every page (see flatten_page/2). Returns {:ok, doc}.
Flatten a 0-indexed page's annotations and form fields into its static content.
Set a page's absolute rotation, in degrees.
Serialize the document to PDF bytes.
Save the document to a file at path.
Diagnostics
Return a marker string confirming the native pdfium library loaded and initialized. Useful as a smoke test that the precompiled NIF is healthy.
Types
A bounding rectangle in PDF user-space points (1/72 inch). The origin is the
page's bottom-left corner and y increases upward, so top >= bottom.
A page object's transformation matrix, the six values a, b, c, d, e, f of the
PDF cm transform [a b c d e f]. A point (x, y) in the object's own space
maps to (a·x + c·y + e, b·x + d·y + f) on the page.
Documents
@spec close(ExPdfium.Document.t()) :: :ok
Explicitly close a document, releasing pdfium memory early. Optional and idempotent.
Documents are also closed when garbage-collected, but that close is processed asynchronously (on a background thread, so it can't stall a scheduler while a long render holds the pdfium lock). Call this for deterministic, immediate release.
@spec open( Path.t() | binary(), keyword() ) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Open a PDF from a file path or an in-memory binary.
A binary beginning with "%PDF" is treated as document bytes; any other binary
is treated as a file path. (A few PDFs carry junk bytes before the header; pass
those as an explicit path, or strip the leading bytes.)
Options
:password— password for an encrypted PDF (defaultnil)
Errors
Returns {:error, reason} where reason is one of:
:enoent— the path does not exist:invalid_pdf— the bytes are not a parseable PDF:password_error— the document is encrypted and the password was missing or incorrect:unsupported_security— unsupported encryption/security handler:file_error/:io_error/:open_failed— other read/open failures:bad_source— internal: malformed source argument (e.g. a non-UTF-8 path)
@spec page_count(ExPdfium.Document.t()) :: {:ok, non_neg_integer()} | {:error, :document_closed | :lock_poisoned}
Number of pages in the document.
Returns {:error, :document_closed} if the document has been closed with
close/1.
65,535-page limit
pdfium-render represents page indices as a 16-bit integer, so documents with
more than 65,535 pages are not supported and this count wraps (reports
count mod 65536). Such documents are pathological; if you handle untrusted
input that might contain one, treat the page count as unreliable above that
bound.
Rendering
@spec render_page(ExPdfium.Document.t(), non_neg_integer(), keyword()) :: {:ok, ExPdfium.Bitmap.t()} | {:error, atom()}
Render a 0-indexed page to an ExPdfium.Bitmap (an uncompressed 4-channel
pixel buffer).
Options
Sizing (highest precedence first; the default is dpi: 72):
:widthand/or:height— output size in pixels (aspect-preserving if only one is given):scale— multiple of the natural size (1.0== 72 DPI):dpi— dots per inch (e.g.150,300)
Other:
:format—:rgba(default) or:bgra(pdfium's native order, no conversion):background—:white(default) or:transparent:grayscale— render in grayscale (defaultfalse). The bitmap is still 4-channel; the color channels just carry equal gray values.:annotations— draw annotations (defaulttrue); setfalseto render the page without its markup/widget overlay:form_fields— draw interactive form-field content (defaulttrue)
Bitmap layout
data is width * height * 4 bytes, row-major, stride (== width * 4) bytes
per row, 8 bits per channel. Hand it straight to Vix/Image:
{:ok, %ExPdfium.Bitmap{data: data, width: w, height: h}} =
ExPdfium.render_page(doc, 0, dpi: 300)
{:ok, image} = Vix.Vips.Image.new_from_binary(data, w, h, 4, :VIPS_FORMAT_UCHAR)Errors
:page_out_of_bounds— no such page index:document_closed— the document was closed:unsupported_format/:unsupported_background— bad option value:render_failed— pdfium failed to render the page
@spec thumbnails( ExPdfium.Document.t(), keyword() ) :: {:ok, [ExPdfium.Bitmap.t()]} | {:error, atom()}
Render every page to a small bitmap, returned in page order.
Takes the same options as render_page/3 (sizing, :format, :grayscale, …).
When no sizing option is given, sizing defaults to width: 200. A document with
no pages returns {:ok, []}; if any page fails to render, the first error is
returned.
{:ok, thumbs} = ExPdfium.thumbnails(doc, width: 160)
# => [%ExPdfium.Bitmap{...}, ...] # one per page
Text & search
@spec extract_text(ExPdfium.Document.t()) :: {:ok, String.t()} | {:error, atom()}
Extract the plain text of the whole document. Pages are joined by a form-feed
("\f") character. Returns {:error, :document_closed} if the document has
been closed.
@spec extract_text(ExPdfium.Document.t(), non_neg_integer()) :: {:ok, String.t()} | {:error, atom()}
Extract the plain text of a 0-indexed page.
Returns {:error, :document_closed} or {:error, :page_out_of_bounds} as
appropriate. A page with no text returns {:ok, ""}.
@spec search_text(ExPdfium.Document.t(), non_neg_integer(), String.t(), keyword()) :: {:ok, [%{text: String.t(), rects: [bounds()]}]} | {:error, atom()}
Search a page for query, returning the matches.
Each match is %{text: String.t(), rects: [t:bounds/0]} — a match can span more
than one rect when it wraps across lines.
Options
:match_case— case-sensitive (defaultfalse):whole_word— match whole words only (defaultfalse)
An empty query returns {:error, :empty_query}.
@spec text_segments(ExPdfium.Document.t(), non_neg_integer()) :: {:ok, [%{text: String.t(), bounds: bounds()}]} | {:error, atom()}
Return the page's text as runs (segments), each with its bounding box.
Each element is %{text: String.t(), bounds: t:bounds/0}. Bounds are in PDF
points (see bounds/0).
Metadata & geometry
Forms & annotations
@spec annotations(ExPdfium.Document.t(), non_neg_integer()) :: {:ok, [map()]} | {:error, atom()}
Return the annotations on a 0-indexed page, in page order.
Each annotation is:
%{
type: atom(), # the PDF /Subtype, e.g. :text, :highlight,
# :link, :widget, :ink, :stamp, :free_text…
bounds: t:bounds/0 | nil, # the annotation rectangle, in PDF points
contents: String.t() | nil, # the /Contents text
name: String.t() | nil, # the annotation's /NM name (not a field name)
hidden: boolean(),
printed: boolean()
}Widget annotations (form-field controls) are listed alongside markup
annotations; use form_fields/1 to read their field values. A page with no
annotations returns {:ok, []}.
@spec form_fields(ExPdfium.Document.t()) :: {:ok, [map()]} | {:error, atom()}
Read the document's AcroForm fields, one entry per widget, across all pages.
Each field is:
%{
name: String.t() | nil, # the field's /T name
type: :text | :checkbox | :radio_button | :combo_box | :list_box |
:push_button | :signature | :unknown,
value: String.t() | nil, # text/combo/list value, or the selected on-state of a button group
checked: boolean() | nil, # checkbox/radio only; nil for other types
read_only: boolean(),
required: boolean(),
page: non_neg_integer(), # 0-indexed page the widget sits on
bounds: t:bounds/0 | nil
}A checkbox or radio group shares one name across its option widgets, so it
surfaces as one entry per option widget. For these, value is the group's
currently-selected on-state (the same string on every widget in the group),
and checked flags which widget is the selected one — so to find a radio
group's answer, take the value of the entry whose checked is true. A
document with no form returns {:ok, []}.
value and checked are read straight from pdfium without coercion: a
checked checkbox is %{value: "Yes", checked: true}, never flattened to a
string.
Limitations
- This reads a group's selected value, not its available options — pdfium
does not expose per-option export names for checkbox/radio groups. A naive
Map.new(fields, &{&1.name, &1.value})collapses a group to one entry; to find a group's answer, take thevalueof the entry whosecheckedistrue. - A multi-select list box reports only pdfium's single
valuestring, so additional selections beyond the first are not surfaced.
@spec form_type(ExPdfium.Document.t()) :: {:ok, :none | :acrobat | :xfa_full | :xfa_foreground} | {:error, atom()}
Return which interactive-form technology the document uses.
One of :none, :acrobat (a classic AcroForm), :xfa_full, or
:xfa_foreground (XFA forms). A document with no form returns {:ok, :none}.
XFA caveat
Reading XFA form data requires a pdfium build with the V8 JavaScript engine,
which ExPdfium does not ship. form_fields/1 reads AcroForm fields; for an
:xfa_full document the AcroForm view may be empty or partial.
Images & objects
@spec image_data(ExPdfium.Document.t(), non_neg_integer(), non_neg_integer()) :: {:ok, ExPdfium.Bitmap.t()} | {:error, atom()}
Decode the image object at object_index (see page_objects/2 / images/2) to
a pixel bitmap.
Returns {:ok, %ExPdfium.Bitmap{}}. Unlike render_page/3 (always 4-channel),
an extracted image keeps its native channel order — format is :gray (1
channel), :bgr (3), or :bgrx / :bgra (4) — so check it before handing
data to an image library.
These are the image's raw stored samples: image masks (soft/stencil) and
object transforms are not applied, so a masked image comes back without its
transparency, and the bitmap size may differ from images/2's reported
dimensions. For the composited, as-displayed result, render the page with
render_page/3 instead.
Errors: :object_not_found (no object at that index), :not_an_image (the
object isn't an image), :image_too_large (the image's declared pixel count
exceeds a safety cap, so it isn't decoded), :page_out_of_bounds,
:image_failed.
@spec image_raw_data(ExPdfium.Document.t(), non_neg_integer(), non_neg_integer()) :: {:ok, binary()} | {:error, atom()}
Return the original, still-encoded stream of the image object at object_index.
This is the image exactly as stored, with its PDF filters not applied: for a
"DCTDecode" image the bytes are a ready-to-write JPEG; for "FlateDecode"
they are zlib-compressed samples, not a standalone image file. Check filters
from images/2 to know which. For always-decodable pixels, use image_data/3.
Errors: :object_not_found, :not_an_image, :page_out_of_bounds,
:image_failed.
@spec images(ExPdfium.Document.t(), non_neg_integer()) :: {:ok, [map()]} | {:error, atom()}
List the image objects on a 0-indexed page, with how each is stored.
Each is:
%{
index: non_neg_integer(), # object index, for image_data/3
width: non_neg_integer(), # intrinsic image width, in pixels
height: non_neg_integer(), # intrinsic image height, in pixels
bits_per_pixel: non_neg_integer(),
filters: [String.t()], # PDF stream filters, e.g. ["DCTDecode"]
bounds: t:bounds/0 | nil, # where it sits on the page, in points
matrix: t:matrix/0 | nil # placement transform (scale/rotation/flip)
}width/height are pdfium's reported pixel dimensions (a 0 means pdfium
couldn't read it), and index is valid only until the document is mutated.
Use image_data/3 to get decoded pixels, or image_raw_data/3 for the original
encoded bytes — filters tells you the encoding (a "DCTDecode" raw stream is a
ready JPEG; "FlateDecode" is zlib-compressed samples, not a standalone file).
matrix is the image's placement transform (see matrix/0). Because it maps
the unit square onto the page, a caller can recover the transform baked into the
image object — scale, plus any object-level rotation or flip — without
re-rendering.
Orientation needs the page rotation too
The matrix is in content space and does not carry the page-level /Rotate,
which is the usual rotation for scanned pages. For the as-displayed
orientation, compose this matrix with page_info/2's :rotation. (Note also
that page_info/2's :width/:height are already display-oriented, a
different frame from this matrix — easy to conflate.) Using the object matrix
alone will leave a /Rotate-rotated scan turned the wrong way.
object_display_matrix/3 does this composition for you, returning the
content→display transform directly.
@spec object_display_matrix( ExPdfium.Document.t(), non_neg_integer(), non_neg_integer() ) :: {:ok, matrix()} | {:error, atom()}
The composed content→display transformation matrix for a 0-indexed object on a 0-indexed page.
page_objects/2 and images/2 give an object's :matrix in the page's
unrotated content space. This composes that matrix with the page-level
/Rotate (from page_info/2), returning the single matrix/0 that maps the
object's own space straight to display coordinates (origin bottom-left of the
page as shown, y up). object_index is the same index page_objects/2 /
images/2 report.
That is exactly the transform needed to orient an extracted image as it appears
on the page — e.g. to turn a native-resolution image_raw_data/3 JPEG the right
way up for OCR — composing the object's own scale/rotation/flip with the page
rotation, without re-rendering.
This library deliberately does not rotate pixels for you (that is image
processing best left to your image pipeline); it hands you the transform as data.
Apply it in Vix/Image, or read the rotation off it (e.g. via :b/:c) to
pass an orientation hint to your OCR engine.
Returns {:error, :object_not_found} if there is no such object, or
{:error, :no_matrix} if pdfium could not report the object's matrix.
Discrete page rotation only
The page-rotation part covers PDF /Rotate (0/90/180/270). Any sub-90°
rotation or skew lives in the object matrix itself and is preserved exactly in
the composition.
@spec page_objects(ExPdfium.Document.t(), non_neg_integer()) :: {:ok, [map()]} | {:error, atom()}
List every object on a 0-indexed page, in page order.
Each object is %{index: non_neg_integer(), type: atom(), bounds: t:bounds/0 | nil, matrix: t:matrix/0 | nil}, where type is one of :text, :path,
:image, :shading, :form (an XObject form), or :unsupported. index is
the object's position in the page's object list — pass it to image_data/3 /
image_raw_data/3. It is valid only until the document is mutated (a write op
can shift object indices).
matrix is the object's transformation matrix (see matrix/0); it is nil
only if pdfium cannot report it.
Creating documents
@spec add_page(ExPdfium.Document.t(), atom() | {number(), number()}, keyword()) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Add a blank page to doc.
size is a named paper size (:letter, :legal, :tabloid, :a3, :a4,
:a5) or {width, height} in PDF points. By default the page is appended; pass
at: index to insert it at a 0-based position (an index past the end appends).
Returns {:ok, doc}, or {:error, :bad_page_size} for an unrecognized size.
@spec draw_circle( ExPdfium.Document.t(), non_neg_integer(), {number(), number()}, number(), keyword() ) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Draw a circle of radius centered at {cx, cy} on a 0-indexed page.
Options
:fill— fill color, ornil(defaultnil):stroke— outline color, ornil(defaultnil):stroke_width— outline width in points (default1)
@spec draw_image( ExPdfium.Document.t(), non_neg_integer(), ExPdfium.Bitmap.t(), keyword() ) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Place a decoded image (an ExPdfium.Bitmap) into the :at rectangle on a
0-indexed page, scaling it to fill those bounds.
The bitmap is the same struct render_page/3 and image_data/3 produce, so you
can place a rendered page or an extracted image; to place a file, decode it to
pixels first (e.g. with Vix — see the README). pdfium stores images in BGR order
and ExPdfium handles the byte-order conversion, so :rgba, :bgra, :bgrx,
:bgr, and :gray bitmaps all work.
Options
:at— thebounds/0rectangle to fill (required; left/bottom should be the lower-left corner — inverted bounds mirror the image)
A bitmap whose data length doesn't match width * height * channels returns
{:error, :bad_image_data}.
@spec draw_line( ExPdfium.Document.t(), non_neg_integer(), {number(), number()}, {number(), number()}, keyword() ) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Draw a straight line from {x1, y1} to {x2, y2} on a 0-indexed page.
Options
:stroke— line color (default black):stroke_width— line width in points (default1)
@spec draw_rectangle(ExPdfium.Document.t(), non_neg_integer(), bounds(), keyword()) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Draw a rectangle covering the bounds/0 rectangle on a 0-indexed page.
Options
:fill— fill color{r,g,b}/{r,g,b,a}, ornilfor no fill (defaultnil):stroke— outline color, ornilfor no outline (defaultnil):stroke_width— outline width in points (default1)
@spec draw_text( ExPdfium.Document.t(), non_neg_integer(), {number(), number()}, String.t(), keyword() ) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Draw text with its baseline starting at {x, y} (PDF points, bottom-left
origin) on a 0-indexed page.
Options
:font— a Standard-14 font atom::helvetica,:helvetica_bold,:helvetica_oblique,:helvetica_bold_oblique,:times_roman,:times_bold,:times_italic,:times_bold_italic,:courier,:courier_bold,:courier_oblique,:courier_bold_oblique,:symbol,:zapf_dingbats(default:helvetica). An unknown font →:unknown_font.:size— font size in points (default12):color—{r, g, b}or{r, g, b, a}, 0–255 (default black)
@spec new() :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Create a new, empty in-memory PDF document. Add pages with add_page/3 and
content with the draw_* functions, then save_to_bytes/1 / save_to_file/2.
Annotating
@spec add_free_text_annotation( ExPdfium.Document.t(), non_neg_integer(), bounds(), String.t(), keyword() ) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Add a free-text annotation: a visible text box inside bounds.
bounds is a bounds/0 map (%{left:, bottom:, right:, top:}, PDF points).
:fill sets the box's interior background and :stroke its border (each a
color or nil, default nil). Returns {:ok, doc}.
Text color
The text itself renders in pdfium's default appearance (black). Setting the
FreeText font color needs an FFI entry point (FPDFAnnot_SetFontColor) that
the bundled pdfium build does not expose, so no text-color option is offered.
For colored text, draw it with draw_text/5 instead.
@spec add_link_annotation( ExPdfium.Document.t(), non_neg_integer(), bounds(), String.t(), keyword() ) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Add a link annotation covering bounds that opens uri when clicked.
bounds is a bounds/0 map (PDF points). The link reads back via
links/2. Returns {:ok, doc}.
@spec add_square_annotation( ExPdfium.Document.t(), non_neg_integer(), bounds(), keyword() ) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Add a square (rectangle) annotation filling bounds.
bounds is a bounds/0 map (PDF points). :fill is the interior color
(default nil, transparent) and :stroke the border color (default
{0, 0, 0}). Returns {:ok, doc}.
@spec add_text_annotation( ExPdfium.Document.t(), non_neg_integer(), {number(), number()}, String.t(), keyword() ) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Add a text (sticky-note) annotation at a point, in PDF points (bottom-left origin).
The note shows as an icon at {x, y}; text is its popup contents, returned by
annotations/2 as :contents. :color (default {255, 230, 0}) sets the icon
color. Returns {:ok, doc}.
@spec delete_annotation(ExPdfium.Document.t(), non_neg_integer(), non_neg_integer()) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Delete the annotation at 0-based index on a 0-indexed page.
The index matches the order returned by annotations/2. Returns {:ok, doc},
or {:error, :annotation_not_found} if the index is out of range.
Writing (page assembly)
@spec append(ExPdfium.Document.t(), ExPdfium.Document.t()) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Append a copy of every page of source onto the end of doc (merge).
Mutates doc in place and returns {:ok, doc} (the same handle). source is
not modified. Appending a document to itself returns {:error, :same_document}.
Returns {:error, :document_closed} if either document is closed.
@spec delete_pages(ExPdfium.Document.t(), non_neg_integer() | Range.t()) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Delete a page, or an inclusive range of pages, from doc.
Pass a single 0-indexed page (delete_pages(doc, 3)) or an inclusive,
ascending, unit-step range (delete_pages(doc, 2..4) deletes pages 2, 3, and
4). Mutates doc in place and returns {:ok, doc}.
Errors:
:page_out_of_bounds— the index/range falls outside the document:cannot_delete_all_pages— the range would remove every page (a zero-page document is degenerate); extract what you want withextract_pages/2instead:bad_range— a descending or non-unit-step range (e.g.4..2or0..6//2); these are rejected rather than silently reinterpreted
@spec extract_pages(ExPdfium.Document.t(), [non_neg_integer()]) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Build a new document from the given 0-indexed pages of source.
indices is a list of page indices in the desired output order; duplicates are
allowed (e.g. [2, 0, 0, 1]). This is the split/subset primitive — splitting a
document is a few extract_pages/2 calls. source is left untouched, and the
returned document is independent (close/GC it separately).
Returns {:error, :empty_selection} for an empty list, or
{:error, :page_out_of_bounds} if any index is out of range (validated before
any page is copied, so no partial document is produced).
@spec flatten(ExPdfium.Document.t()) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Flatten every page (see flatten_page/2). Returns {:ok, doc}.
@spec flatten_page(ExPdfium.Document.t(), non_neg_integer()) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Flatten a 0-indexed page's annotations and form fields into its static content.
After flattening, the annotation/form overlay is baked into the page and renders
identically everywhere (and can no longer be edited as annotations). pdfium uses
the print appearance. A page with nothing to flatten is a no-op. Mutates doc
in place; returns {:ok, doc}.
@spec rotate_page(ExPdfium.Document.t(), non_neg_integer(), 0 | 90 | 180 | 270) :: {:ok, ExPdfium.Document.t()} | {:error, atom()}
Set a page's absolute rotation, in degrees.
degrees must be 0, 90, 180, or 270; anything else returns
{:error, :bad_rotation}. Mutates doc in place and returns {:ok, doc}. The
rotation persists through save_to_bytes/1 / save_to_file/2.
@spec save_to_bytes(ExPdfium.Document.t()) :: {:ok, binary()} | {:error, atom()}
Serialize the document to PDF bytes.
A full save (pdfium's FPDF_SaveAsCopy) reflecting any edits made via the
writing functions. It does not close or alter doc, so you can keep
editing and save again. Returns {:error, :document_closed} if the document
has been closed, or {:error, :save_failed} if pdfium cannot serialize it.
The whole document is buffered in memory (there is no streaming save), so peak usage is roughly the document size; this is fine for typical PDFs.
@spec save_to_file(ExPdfium.Document.t(), Path.t()) :: :ok | {:error, atom()}
Save the document to a file at path.
Equivalent to save_to_bytes/1 followed by File.write/2. Returns :ok, or
{:error, reason} — either a document error (e.g. :document_closed) or a
File.write/2 posix reason (e.g. :enoent, :eacces).
Diagnostics
@spec pdfium_version() :: String.t()
Return a marker string confirming the native pdfium library loaded and initialized. Useful as a smoke test that the precompiled NIF is healthy.
pdfium exposes no build-version string through its public C API, so this is a fixed confirmation marker rather than a version number.
Types
A bounding rectangle in PDF user-space points (1/72 inch). The origin is the
page's bottom-left corner and y increases upward, so top >= bottom.
A page object's transformation matrix, the six values a, b, c, d, e, f of the
PDF cm transform [a b c d e f]. A point (x, y) in the object's own space
maps to (a·x + c·y + e, b·x + d·y + f) on the page.
For an image object the matrix maps the unit square [0,1]×[0,1] onto the
placement, so a/d carry scale, b/c shear/rotation, and e/f the
translation.
Content space, not display space
This matrix lives in the page's unrotated content coordinate space. It does
not include the page-level /Rotate (the dominant rotation for scanned
documents). page_info/2 reports that separately as :rotation (0/90/180/270),
and its :width/:height are already display-oriented — a different frame from
this matrix. To get an object's as-displayed orientation, compose this matrix
with the page rotation; the matrix alone recovers only the transform baked into
the object itself.