All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning (against our API, not pdfium-render's).
[Unreleased]
0.3.3 - 2026-06-25
Added
ExPdfium.Bitmap.to_vix/1: convert aBitmapstraight into a correctly-interpretedVix.Vips.Image— it readsformat, sets the band count, reorders pdfium's native BGR order into RGB (:bgr/:bgrx/:bgra), drops the:bgrxpadding byte, and strips row-stride padding, so callers stop branching on.formatand getting the R↔B swap wrong.Vixis an optional dependency (not pulled in transitively); without it the function returns{:error, :vix_not_loaded}.ExPdfium.bounds_to_pixels/3(read): convert anyExPdfium.bounds/0(PDF points, origin bottom-left,y-up) into raster pixel coordinates (origin top-left,y-down) at a given DPI — the points→pixels scale plus the Y-flip that every overlay oftext_segments/2/search_text/3/links/2/annotations/2/images/2boxes needs and that is easy to get silently wrong. Returns%{left, top, right, bottom}in pixels.ExPdfium.open_file/2andExPdfium.open_blob/2(read): explicit path-only / bytes-only document openers, removing the source-kind guessingopen/2's"%PDF"heuristic does (which is ambiguous for PDFs with junk bytes before the header, or paths that begin with"%PDF").open/2stays as the convenience.ExPdfium.parse_pdf_date/1(read): parse a PDF date string (asmetadata/1returns, e.g."D:20210812004758+01'00'") into a UTCDateTime. HandlesZ/±HH'mm'offsets and truncated forms;{:error, :invalid_date}otherwise.ExPdfium.object_display_rotation/3(read): the clockwise rotation in degrees to apply to an extracted image in a top-left-origin raster library (Vix/libvips, Pillow, ImageMagick) so it appears upright as displayed —object_display_matrix/3's rotation already converted out of PDF's y-up frame. For a plain scanned page it equalspage_info/2's:rotation.
Changed
- Documented the y-up vs y-down handedness trap on
object_display_matrix/3andExPdfium.matrix/0: the matrix is PDF space (origin bottom-left,yup), so a rotation angle read from it and applied directly in a y-down raster library comes out 180° wrong on 90°/270° pages (it cancels at 0°/180°, so it looks fine on unrotated docs). Negate the angle,y-flip the image, or useobject_display_rotation/3.
0.3.2 - 2026-06-25
Added
ExPdfium.object_display_matrix/3(read): the composed content→display transform for a page object — its content-space:matrixmultiplied by the page-level/Rotate— as a singleExPdfium.matrix/0. This is the transform to orient an extracted image as it appears on the page (e.g. turn a native-resimage_raw_data/3JPEG the right way up for OCR) without re-rendering. The library hands you the transform as data — it deliberately does not rotate pixels (that belongs in your image pipeline). Pure Elixir; rotation direction is verified against rendered pages for all of 0/90/180/270.
Changed
- Clarified the
:matrixorientation story onpage_objects/2,images/2, andExPdfium.matrix/0: the matrix is in the page's unrotated content space and does not carry the page-level/Rotate(the usual rotation for scanned pages). For the as-displayed orientation, compose it withpage_info/2's:rotation— whose:width/:heightare already display-oriented, a different coordinate frame. Docs only; no API change.
0.3.1 - 2026-06-25
Added
- Object transformation matrix (read):
page_objects/2andimages/2now include a:matrixkey — the object's[a b c d e f]transform asExPdfium.matrix/0(%{a:, b:, c:, d:, e:, f:}), ornilif pdfium can't report it. It maps an image's unit square onto the page in content space, so a caller can recover the transform baked into the object (scale, plus any object-level rotation/flip) without re-rendering. (Note: this is content space, not display space — compose withpage_info/2's:rotationfor the page-level/Rotate; see the 0.3.2 doc clarification.) Additive and backwards-compatible.
0.3.0 - 2026-06-25
Added
- Annotation authoring (write): create annotations on a page.
ExPdfium.add_text_annotation/5(sticky note: icon + popup contents),add_free_text_annotation/5(a visible text box),add_square_annotation/4(rectangle box with fill/stroke), andadd_link_annotation/5(clickable URI).ExPdfium.delete_annotation/3removes an annotation by its 0-based page index.- Rectangles take a
ExPdfium.bounds/0map (%{left:, bottom:, right:, top:}), matching whatannotations/2reads back. The text-markup family (highlight/underline/strikeout/squiggly) is intentionally deferred: pdfium's own renderer does not display them without an explicit appearance stream it will not auto-generate.
- Flatten (write):
ExPdfium.flatten_page/2andflatten/1bake a page's (or the whole document's) annotations and form fields into static page content, so they render identically everywhere and can no longer be edited as annotations. - Signatures (read):
ExPdfium.signatures/1returns the document's digital signatures as%{reason, signing_date, bytes}(the raw PKCS#7/Contents; the signer identity lives insidebytes). Unsigned documents return{:ok, []}. - Render refinements & thumbnails:
ExPdfium.render_page/3gains:grayscale,:annotations, and:form_fieldstoggles (annotations/form fields render by default).ExPdfium.thumbnails/2renders one smallExPdfium.Bitmapper page (defaults towidth: 200), accepting the same options asrender_page/3.
- Document creation (write): build PDFs from scratch.
ExPdfium.new/0(empty document) andExPdfium.add_page/3(named sizes:letter/:a4/… or{w, h}points;at:to insert).ExPdfium.draw_text/5(Standard-14 fonts, size, color),draw_rectangle/4,draw_line/5,draw_circle/5(fill/stroke/width), anddraw_image/4(place anExPdfium.Bitmap; pdfium is BGRA-native, so:rgba/:rgbxare R↔B swapped automatically — any Bitmap fromrender_page/3,image_data/3, or Vix works).- Coordinates are PDF points (origin bottom-left); colors are
{r,g,b}/{r,g,b,a}.
- Comprehensive document metadata:
ExPdfium.metadata/1now also returns document-level properties alongside the/Infodictionary —:version(the PDF version, e.g."1.7", ornil),:page_count, and:page_mode(the catalog/PageMode::none/:outline/:thumbnails/:fullscreen/:optional_content/:attachments/:unset). (Custom/Infokeys and XMP metadata remain out of reach — pdfium exposes no API for either.) - Image & object extraction (read):
ExPdfium.page_objects/2→ every object on a page, typed (:text/:path/:image/:shading/:form/:unsupported) with bounds and an index.ExPdfium.images/2→ image objects with intrinsic size, bits-per-pixel, and PDF streamfilters(e.g.["DCTDecode"]).ExPdfium.image_data/3→ decoded pixels as an%ExPdfium.Bitmap{}(native channel order::gray/:bgr/:bgrx/:bgra).ExPdfium.image_raw_data/3→ the original encoded stream (a"DCTDecode"image's bytes are a ready JPEG).ExPdfium.Bitmap'sformattype now also covers:bgrx,:bgr, and:gray(image extraction);render_page/3still yields:rgba/:bgra.
- Writing — page assembly & save (v0.3, reopening the write scope that was
out of scope through v0.2):
ExPdfium.save_to_bytes/1andsave_to_file/2— full save (FPDF_SaveAsCopy) that leaves the document open for further edits.ExPdfium.append/2— merge: copy all of another document's pages onto the end.ExPdfium.extract_pages/2— build a new document from selected pages, in any order (the split/subset primitive).ExPdfium.delete_pages/2— delete a page index or an inclusive range.ExPdfium.rotate_page/3— set a page's absolute rotation (0/90/180/270).- In-place mutators return
{:ok, doc}(the same handle) so they thread throughwith/pipelines;extract_pages/2returns{:ok, new_doc}. All writes are serialized through the same global pdfium lock as reads. New error atoms::same_document,:empty_selection,:cannot_delete_all_pages,:bad_range,:bad_rotation,:save_failed, and the operation-failure atoms:create_failed/:copy_failed/:append_failed/:delete_failed.
0.2.0 - 2026-06-25
Added
- Phase 6 — forms & annotations (read), completing the read-only scope:
ExPdfium.form_type/1→:none|:acrobat|:xfa_full|:xfa_foreground.ExPdfium.form_fields/1→ AcroForm fields, one entry per widget across all pages (%{name, type, value, checked, read_only, required, page, bounds}). For checkbox/radio groups,valueis the group's selected on-state andcheckedflags the selected widget (pdfium does not expose per-option export names). XFA form data is unavailable without a V8-enabled pdfium build.ExPdfium.annotations/2→ a page's annotations, markup and widget alike (%{type, bounds, contents, name, hidden, printed};typeis the PDF/Subtype).
- Phase 5 — structure & navigation:
ExPdfium.outline/1→ the bookmark tree (%{title, page, children}nodes).ExPdfium.links/2→ a page's links (%{bounds, uri, page};urifor web links,pagefor internal destinations).ExPdfium.attachments/1→ embedded files (%{index, name, size}) andExPdfium.attachment_data/2→ an attachment's bytes.
- Phase 4 — metadata, page geometry & permissions:
ExPdfium.metadata/1→ document info map (title/author/subject/keywords/ creator/producer/creation_date;modification_dateis usuallynil, a pdfium-render limitation — see the docs).ExPdfium.page_info/2→%{width, height, rotation, label, boxes}(size in points, rotation in degrees, boundary boxes media/crop/bleed/trim/art).ExPdfium.permissions/1→ map of 8 boolean permission flags.
- Phase 3 — text extraction & search:
ExPdfium.extract_text/2(one page) andextract_text/1(whole document, pages joined by a form feed).ExPdfium.text_segments/2returns text runs with per-segment bounding boxes (PDF points, origin bottom-left).ExPdfium.search_text/3,4with:match_caseand:whole_wordoptions; each match carries its text and bounding rects. Empty query →{:error, :empty_query}.
- Phase 2 — render a page to a bitmap:
ExPdfium.render_page/3returns{:ok, %ExPdfium.Bitmap{data, width, height, stride, format}}, an uncompressed 4-channel buffer ready forVix.Vips.Image.new_from_binary/5.- Sizing by
:dpi(default 72),:scale, or:width/:height. :format:rgba(default) or:bgra;:background:white(default) or:transparent.- Errors:
:page_out_of_bounds,:document_closed,:render_failed,:unsupported_format,:unsupported_background,:bad_option. - GC-driven document close is deferred to a dedicated cleanup thread so it never blocks a BEAM scheduler while a long render holds the pdfium lock.
- Sizing by
- Phase 1 — open documents & page count:
ExPdfium.open/1,2opens a PDF from a file path or in-memory binary, with an optional:passwordfor encrypted documents. Returns{:ok, %ExPdfium.Document{}}.ExPdfium.page_count/1returns{:ok, n}.ExPdfium.close/1releases the document early (idempotent); documents are also closed automatically on garbage collection (no manual-close leak).- Errors are mapped from pdfium:
:enoent,:invalid_pdf,:password_error,:unsupported_security,:file_error,:io_error,:document_closed. - pdfium is not thread-safe, so all pdfium operations are serialized through a single global lock; calls are safe from any number of BEAM processes but run one at a time.
[0.1.0] - 2026-06-24
First release — Phase 0: proves the toolchain and the precompiled-release path
end to end. PDF document/page/text APIs land in later phases (see PORTING.md).
Added
- Project scaffold:
rustler_precompiledconfig, tag-driven release pipeline, and the porting plan (PORTING.md). - Phase 0 (toolchain):
ExPdfium.pdfium_version/0, a load-proof NIF that binds and initializes pdfium. Pinnedpdfium-render = "=0.8.37". The dev/test build binds pdfium dynamically; the libpdfium directory is passed to the NIF via aset_dynamic_lib_dir/1function argument (env vars set withSystem.put_envdon't reach a NIF).
Changed
- pdfium-render must be built with the
syncfeature (not justthread_safe): onlysyncadds theSend + Syncimpls that let the single globalPdfiumlive in astatic.release.ymlupdated accordingly. - Pinned pdfium binary tag bumped
chromium/7506→chromium/7543to match the pdfium API version pdfium-render 0.8.37 binds (pdfium_latest). - Shipping strategy: the precompiled NIF binds pdfium dynamically and bundles
the dynamic
libpdfiuminside each per-target tarball (rustler_precompiled extracts it next to the NIF; the NIF self-locates it viadladdr). bblanchon ships no staticlibpdfium.a, so static linking isn't used. The optionalstatic/libcpp/libstdcppfeatures remain for a user-supplied.a.