defmodule PdfOxide do @moduledoc """ Idiomatic Elixir bindings for pdf_oxide — fast PDF text, Markdown and HTML extraction, plus building PDFs from Markdown/HTML/text. Backed by a NIF over the pdf_oxide C ABI; CPU-bound extraction runs on dirty CPU schedulers so it never blocks the BEAM. Handles are NIF resources freed by the GC. Functions return `{:ok, value}` / `{:error, code}`; the `!` variants raise `PdfOxide.Error`. Page indices are 0-based. """ alias PdfOxide.Native defmodule Document do @moduledoc "An opened PDF document handle (NIF resource)." defstruct [:ref] end defmodule Pdf do @moduledoc "A built PDF handle (NIF resource)." defstruct [:ref] end defmodule DocumentEditor do @moduledoc """ A mutable PDF editing handle (NIF resource). Open one with `PdfOxide.open_editor/1` or `PdfOxide.open_editor_from_bytes/1`, mutate it in place (rotate/crop/redact/flatten/merge/…) and serialise with `PdfOxide.editor_save/2` or `PdfOxide.editor_save_to_bytes/1`. The native handle is freed by the GC or eagerly via `PdfOxide.editor_close/1`. Page indices are 0-based. """ defstruct [:ref] end defmodule Page do @moduledoc """ A lightweight view of a single (0-based) page. Holds its `Document` so the underlying native handle stays alive as long as the page is referenced. """ defstruct [:doc, :index] end defmodule Error do defexception [:code, :op] @impl true def message(%{code: code, op: op}), do: "pdf_oxide: #{op} failed (error code #{code})" end defmodule Bbox do @moduledoc "An axis-aligned bounding box (PDF user-space units)." defstruct [:x, :y, :width, :height] end defmodule Char do @moduledoc "A single extracted character. `character` is a Unicode codepoint (integer)." defstruct [:character, :bbox, :font_name, :font_size] end defmodule Word do @moduledoc "An extracted word with its layout/style metadata." defstruct [:text, :bbox, :font_name, :font_size, :bold, :sequence] end defmodule TextLine do @moduledoc "An extracted line of text." defstruct [:text, :bbox, :word_count] end defmodule Table do @moduledoc """ An extracted table. Read a cell's text with `cell/3` (0-based `row`/`col`). `cells` holds the cell text as a row-major list of lists. """ defstruct [:row_count, :col_count, :has_header, :cells] end defmodule Font do @moduledoc "An embedded/referenced font on a page." defstruct [:name, :type, :encoding, :embedded, :subset] end defmodule Image do @moduledoc "An embedded image. `data` holds its raw bytes." defstruct [:width, :height, :bits_per_component, :format, :colorspace, :data] end defmodule Annotation do @moduledoc "A page annotation with its placement and style metadata." defstruct [:type, :subtype, :content, :author, :rect, :border_width] end defmodule Path do @moduledoc "An extracted vector path (its bbox and stroke/fill style)." defstruct [:bbox, :stroke_width, :has_stroke, :has_fill, :operation_count] end defmodule FormField do @moduledoc "An AcroForm field: its `name`, current `value`, `type`, and flags." defstruct [:name, :value, :type, :read_only, :required] end defmodule SearchResult do @moduledoc "A single search hit: its `text`, 0-based `page` and `bbox`." defstruct [:text, :page, :bbox] end defmodule RenderedImage do @moduledoc """ A rendered page raster. `width`/`height` are in pixels and `data` holds the encoded image bytes (PNG by default). `ref` is the live native handle kept so `PdfOxide.save/2` can write the image with the renderer's own encoder; it is freed by the GC. """ defstruct [:ref, :width, :height, :data] end defmodule DocumentBuilder do @moduledoc """ A PDF *creation* builder handle (NIF resource). Create one with `PdfOxide.builder/0`, set metadata, start pages with `builder_page/3`, `builder_letter_page/1` or `builder_a4_page/1`, then `builder_build/1` / `builder_save/2`. The native handle is freed by the GC or eagerly via `PdfOxide.builder_close/1`. """ defstruct [:ref] end defmodule PageBuilder do @moduledoc """ A page-creation builder handle (NIF resource) started off a `DocumentBuilder`. Emit content with the fluent `page_*` ops, then commit it to its parent with `PdfOxide.page_done/1` (which consumes the handle) — or drop it with `PdfOxide.page_close/1`. """ defstruct [:ref] end defmodule EmbeddedFont do @moduledoc """ A loaded TTF/OTF font handle (NIF resource) for embedding via `PdfOxide.builder_register_embedded_font/3`. A successful register *consumes* the font; the wrapper is left holding a freed handle and must not be used again. Otherwise free it with `PdfOxide.font_close/1` (also GC-freed). """ defstruct [:ref] end defmodule Certificate do @moduledoc """ Signing credentials (X.509 certificate + private key) as a native handle. Load with `PdfOxide.certificate_from_bytes/2` (PKCS#12) or `PdfOxide.certificate_from_pem/2`, read its accessors, and free with `PdfOxide.certificate_close/1` (also GC-freed). """ defstruct [:ref] end defmodule SignatureInfo do @moduledoc """ A parsed PDF signature (native handle). Read its signer/time/reason metadata and verify it with `PdfOxide.signature_verify/1` / `PdfOxide.signature_verify_detached/2`. Free with `PdfOxide.signature_close/1` (also GC-freed). """ defstruct [:ref] end defmodule Timestamp do @moduledoc """ A parsed RFC 3161 timestamp token (native handle). Free with `PdfOxide.timestamp_close/1` (also GC-freed). """ defstruct [:ref] end defmodule TsaClient do @moduledoc """ An RFC 3161 Time-Stamping Authority client (native handle). Request tokens with `PdfOxide.tsa_request_timestamp/2` / `PdfOxide.tsa_request_timestamp_hash/3`. Free with `PdfOxide.tsa_close/1` (also GC-freed). """ defstruct [:ref] end defmodule Dss do @moduledoc """ A Document Security Store (native handle): the document-level certs/CRLs/OCSPs that back long-term-validation signatures. Free with `PdfOxide.dss_close/1` (also GC-freed). """ defstruct [:ref] end defmodule PdfAResult do @moduledoc "Result of a PDF/A validation pass (native handle)." defstruct [:ref] end defmodule PdfUaResult do @moduledoc "Result of a PDF/UA accessibility validation pass (native handle)." defstruct [:ref] end defmodule PdfXResult do @moduledoc "Result of a PDF/X validation pass (native handle)." defstruct [:ref] end defmodule UaStats do @moduledoc "Accessibility element counts from a PDF/UA validation pass." defstruct [:struct, :images, :tables, :forms, :annotations, :pages] end defmodule Barcode do @moduledoc """ A generated/decoded barcode or QR code (native handle). Read its payload, format and confidence, render it to PNG/SVG, or stamp it onto an editor page with `PdfOxide.add_barcode_to_page/7`. Free with `PdfOxide.barcode_close/1` (also GC-freed). """ defstruct [:ref] end defmodule OcrEngine do @moduledoc """ An OCR engine (native handle) built from detection/recognition model and dictionary file paths via `PdfOxide.ocr_engine/3`. Free with `PdfOxide.ocr_engine_close/1` (also GC-freed). """ defstruct [:ref] end defmodule Renderer do @moduledoc """ A reusable page renderer (native handle) with fixed dpi/format/quality/ anti-aliasing, created with `PdfOxide.renderer/4`. Free with `PdfOxide.renderer_close/1` (also GC-freed). """ defstruct [:ref] end defmodule ElementList do @moduledoc """ An opaque list of page elements (native handle) from `PdfOxide.page_elements/2`. Read its length with `PdfOxide.element_count/1` (the per-element accessors land in a later phase). Free with `PdfOxide.element_list_close/1` (also GC-freed). """ defstruct [:ref] end # ── Pdf builder ──────────────────────────────────────────────────────────── @doc "Build a PDF from Markdown." def from_markdown(md), do: wrap_pdf(Native.from_markdown(md)) @doc "Build a PDF from HTML." def from_html(html), do: wrap_pdf(Native.from_html(html)) @doc "Build a PDF from plain text." def from_text(text), do: wrap_pdf(Native.from_text(text)) @doc """ Write to `path` — a built `Pdf`, or a `RenderedImage` page raster. """ def save(%Pdf{ref: ref}, path), do: Native.pdf_save(ref, path) def save(%RenderedImage{ref: ref}, path), do: Native.img_save(ref, path) @doc "Serialize a built PDF to a binary." def to_bytes(%Pdf{ref: ref}), do: Native.pdf_save_to_bytes(ref) @doc "Free a document, built PDF or editor's native handle now (idempotent)." def close(%Document{ref: ref}), do: Native.doc_close(ref) def close(%Pdf{ref: ref}), do: Native.pdf_close(ref) def close(%DocumentEditor{ref: ref}), do: Native.editor_close(ref) # ── Document ───────────────────────────────────────────────────────────────── @doc "Open a PDF from a path." def open(path), do: wrap_doc(Native.doc_open(path)) @doc "Open a password-protected PDF." def open_with_password(path, password), do: wrap_doc(Native.doc_open_pw(path, password)) @doc "Open a PDF from a binary." def open_from_bytes(bytes), do: wrap_doc(Native.doc_open_bytes(bytes)) @doc "Number of pages." def page_count(%Document{ref: ref}), do: Native.doc_page_count(ref) @doc "PDF version as `%{major: _, minor: _}`." def version(%Document{ref: ref}) do {major, minor} = Native.doc_version(ref) %{major: major, minor: minor} end @doc "Whether the document is encrypted." def encrypted?(%Document{ref: ref}), do: Native.doc_is_encrypted(ref) @doc "Whether the document has a logical structure tree." def structure_tree?(%Document{ref: ref}), do: Native.doc_has_structure_tree(ref) @doc "Reading-order text for a (0-based) page." def extract_text(%Document{ref: ref}, page), do: Native.doc_extract_text(ref, page) @doc "Plain text for a page." def to_plain_text(%Document{ref: ref}, page), do: Native.doc_to_plain_text(ref, page) @doc "Markdown for a page." def to_markdown(%Document{ref: ref}, page), do: Native.doc_to_markdown(ref, page) @doc "HTML for a page." def to_html(%Document{ref: ref}, page), do: Native.doc_to_html(ref, page) @doc "Markdown for the whole document." def to_markdown_all(%Document{ref: ref}), do: Native.doc_to_markdown_all(ref) @doc "HTML for the whole document." def to_html_all(%Document{ref: ref}), do: Native.doc_to_html_all(ref) @doc "Plain text for the whole document." def to_plain_text_all(%Document{ref: ref}), do: Native.doc_to_plain_text_all(ref) @doc """ Authenticate an encrypted document with `password`. Returns `{:ok, true}` on success and `{:ok, false}` for a wrong password (not an error). """ def authenticate(%Document{ref: ref}, password), do: Native.doc_authenticate(ref, password) @doc "Structured content for a page as a JSON string." def extract_structured_json(%Document{ref: ref}, page), do: Native.doc_extract_structured_json(ref, page) @doc """ Extract the individual characters of a (0-based) page as a list of `Char`. """ def extract_chars(%Document{ref: ref}, page) do with {:ok, list} <- Native.doc_extract_chars(ref, page) do {:ok, Enum.map(list, fn {cp, x, y, w, h, font, size} -> %Char{ character: cp, bbox: %Bbox{x: x, y: y, width: w, height: h}, font_name: font, font_size: size } end)} end end @doc """ Extract the words of a (0-based) page as a list of `Word`. """ def extract_words(%Document{ref: ref}, page) do with {:ok, list} <- Native.doc_extract_words(ref, page) do {:ok, Enum.map(list, fn {text, x, y, w, h, font, size, bold, sequence} -> %Word{ text: text, bbox: %Bbox{x: x, y: y, width: w, height: h}, font_name: font, font_size: size, bold: bold, sequence: sequence } end)} end end @doc """ Extract the text lines of a (0-based) page as a list of `TextLine`. """ def extract_text_lines(%Document{ref: ref}, page) do with {:ok, list} <- Native.doc_extract_text_lines(ref, page) do {:ok, Enum.map(list, fn {text, x, y, w, h, word_count} -> %TextLine{ text: text, bbox: %Bbox{x: x, y: y, width: w, height: h}, word_count: word_count } end)} end end @doc """ Extract the tables of a (0-based) page as a list of `Table`. Use `cell/3` to read a table's (0-based) cell text. """ def extract_tables(%Document{ref: ref}, page) do with {:ok, list} <- Native.doc_extract_tables(ref, page) do {:ok, Enum.map(list, fn {row_count, col_count, has_header, cells} -> %Table{ row_count: row_count, col_count: col_count, has_header: has_header, cells: cells } end)} end end @doc "Text of a table's (0-based) `row`/`col` cell." def cell(%Table{cells: cells}, row, col), do: cells |> Enum.at(row, []) |> Enum.at(col) @doc """ Extract the embedded/referenced fonts of a (0-based) page as a list of `Font`. """ def embedded_fonts(%Document{ref: ref}, page) do with {:ok, list} <- Native.doc_embedded_fonts(ref, page) do {:ok, Enum.map(list, fn {name, type, encoding, embedded, subset} -> %Font{ name: name, type: type, encoding: encoding, embedded: embedded, subset: subset } end)} end end @doc """ Extract the embedded images of a (0-based) page as a list of `Image`. """ def embedded_images(%Document{ref: ref}, page) do with {:ok, list} <- Native.doc_embedded_images(ref, page) do {:ok, Enum.map(list, fn {width, height, bpc, format, colorspace, data} -> %Image{ width: width, height: height, bits_per_component: bpc, format: format, colorspace: colorspace, data: data } end)} end end @doc """ Extract the annotations of a (0-based) page as a list of `Annotation`. """ def page_annotations(%Document{ref: ref}, page) do with {:ok, list} <- Native.doc_page_annotations(ref, page) do {:ok, Enum.map(list, fn {type, subtype, content, author, x, y, w, h, border_width} -> %Annotation{ type: type, subtype: subtype, content: content, author: author, rect: %Bbox{x: x, y: y, width: w, height: h}, border_width: border_width } end)} end end @doc """ Extract the vector paths of a (0-based) page as a list of `Path`. """ def extract_paths(%Document{ref: ref}, page) do with {:ok, list} <- Native.doc_extract_paths(ref, page) do {:ok, Enum.map(list, fn {x, y, w, h, stroke_width, has_stroke, has_fill, operation_count} -> %Path{ bbox: %Bbox{x: x, y: y, width: w, height: h}, stroke_width: stroke_width, has_stroke: has_stroke, has_fill: has_fill, operation_count: operation_count } end)} end end @doc """ Search a (0-based) page for `term`, returning a list of `SearchResult`. """ def search(%Document{ref: ref}, page, term, case_sensitive) do with {:ok, list} <- Native.doc_search_page(ref, page, term, case_sensitive) do {:ok, Enum.map(list, &to_search_result/1)} end end @doc """ Search the whole document for `term`, returning a list of `SearchResult`. """ def search_all(%Document{ref: ref}, term, case_sensitive) do with {:ok, list} <- Native.doc_search_all(ref, term, case_sensitive) do {:ok, Enum.map(list, &to_search_result/1)} end end defp to_search_result({text, page, x, y, w, h}), do: %SearchResult{text: text, page: page, bbox: %Bbox{x: x, y: y, width: w, height: h}} # ── page rendering (phase 3) ───────────────────────────────────────────────── @doc """ Render a (0-based) `page_index` to a `RenderedImage`. `format` is an image format code (0 = PNG, the default). """ def render_page(%Document{ref: ref}, page_index, format \\ 0), do: wrap_image(Native.doc_render_page(ref, page_index, format)) @doc """ Render a (0-based) `page_index` at `zoom` (1.0 = 100%) to a `RenderedImage`. `format` is an image format code (0 = PNG, the default). """ def render_page_zoom(%Document{ref: ref}, page_index, zoom, format \\ 0), do: wrap_image(Native.doc_render_page_zoom(ref, page_index, zoom * 1.0, format)) @doc """ Render a (0-based) `page_index` as a thumbnail fitting `size` pixels on the longest side, to a `RenderedImage`. `format` is an image format code (0 = PNG, the default). """ def render_page_thumbnail(%Document{ref: ref}, page_index, size, format \\ 0), do: wrap_image(Native.doc_render_page_thumbnail(ref, page_index, size, format)) # ── Page ───────────────────────────────────────────────────────────────────── @doc """ A `Page` view for the (0-based) `index`. The page keeps its document alive, so it must not outlive a `close/1` on the document. """ def page(%Document{} = doc, index) when is_integer(index), do: %Page{doc: doc, index: index} @doc "Reading-order text for the page." def text(%Page{doc: doc, index: index}), do: extract_text(doc, index) @doc "Markdown for the page." def markdown(%Page{doc: doc, index: index}), do: to_markdown(doc, index) @doc "HTML for the page." def html(%Page{doc: doc, index: index}), do: to_html(doc, index) @doc "Plain text for the page." def plain_text(%Page{doc: doc, index: index}), do: to_plain_text(doc, index) # ── DocumentEditor ─────────────────────────────────────────────────────────── @doc "Open a PDF for editing from a path." def open_editor(path), do: wrap_editor(Native.editor_open(path)) @doc "Open a PDF for editing from a binary." def open_editor_from_bytes(bytes), do: wrap_editor(Native.editor_open_bytes(bytes)) @doc "Number of pages in the editor." def editor_page_count(%DocumentEditor{ref: ref}), do: Native.editor_page_count(ref) @doc "PDF version as `%{major: _, minor: _}`." def editor_version(%DocumentEditor{ref: ref}) do {major, minor} = Native.editor_version(ref) %{major: major, minor: minor} end @doc "Whether the editor has unsaved modifications." def editor_modified?(%DocumentEditor{ref: ref}), do: Native.editor_is_modified(ref) @doc "The editor's source path (empty for a bytes-opened editor)." def editor_source_path(%DocumentEditor{ref: ref}), do: Native.editor_source_path(ref) @doc "Read `/Info.Producer`." def get_producer(%DocumentEditor{ref: ref}), do: Native.editor_get_producer(ref) @doc "Set `/Info.Producer`." def set_producer(%DocumentEditor{ref: ref}, value), do: Native.editor_set_producer(ref, value) @doc "Read `/Info.CreationDate` as a raw PDF date string." def get_creation_date(%DocumentEditor{ref: ref}), do: Native.editor_get_creation_date(ref) @doc "Set `/Info.CreationDate` (raw PDF date string)." def set_creation_date(%DocumentEditor{ref: ref}, date), do: Native.editor_set_creation_date(ref, date) @doc "Delete a (0-based) page." def delete_page(%DocumentEditor{ref: ref}, page_index), do: Native.editor_delete_page(ref, page_index) @doc "Move a (0-based) page `from` → `to`." def move_page(%DocumentEditor{ref: ref}, from, to), do: Native.editor_move_page(ref, from, to) @doc "Rotate a single (0-based) page by `degrees` (additive)." def rotate_page_by(%DocumentEditor{ref: ref}, page, degrees), do: Native.editor_rotate_page_by(ref, page, degrees) @doc "Rotate all pages by `degrees` (relative)." def rotate_all_pages(%DocumentEditor{ref: ref}, degrees), do: Native.editor_rotate_all_pages(ref, degrees) @doc "Set the absolute rotation of a (0-based) page." def set_page_rotation(%DocumentEditor{ref: ref}, page, degrees), do: Native.editor_set_page_rotation(ref, page, degrees) @doc "Rotation (degrees) of a (0-based) page." def get_page_rotation(%DocumentEditor{ref: ref}, page), do: Native.editor_get_page_rotation(ref, page) @doc "Crop `left`/`right`/`top`/`bottom` margins off every page." def crop_margins(%DocumentEditor{ref: ref}, left, right, top, bottom), do: Native.editor_crop_margins(ref, left * 1.0, right * 1.0, top * 1.0, bottom * 1.0) @doc "CropBox of a (0-based) page as a `Bbox`." def get_page_crop_box(%DocumentEditor{ref: ref}, page), do: wrap_box(Native.editor_get_crop_box(ref, page)) @doc "Set the CropBox of a (0-based) page." def set_page_crop_box(%DocumentEditor{ref: ref}, page, x, y, w, h), do: Native.editor_set_crop_box(ref, page, x * 1.0, y * 1.0, w * 1.0, h * 1.0) @doc "MediaBox of a (0-based) page as a `Bbox`." def get_page_media_box(%DocumentEditor{ref: ref}, page), do: wrap_box(Native.editor_get_media_box(ref, page)) @doc "Set the MediaBox of a (0-based) page." def set_page_media_box(%DocumentEditor{ref: ref}, page, x, y, w, h), do: Native.editor_set_media_box(ref, page, x * 1.0, y * 1.0, w * 1.0, h * 1.0) @doc "Apply (burn in) redactions on a single (0-based) page." def apply_page_redactions(%DocumentEditor{ref: ref}, page), do: Native.editor_apply_page_redactions(ref, page) @doc "Apply all pending redactions across the document." def apply_all_redactions(%DocumentEditor{ref: ref}), do: Native.editor_apply_all_redactions(ref) @doc "Whether a (0-based) page is marked for redaction." def page_marked_for_redaction?(%DocumentEditor{ref: ref}, page), do: Native.editor_is_marked_for_redaction(ref, page) @doc "Remove the redaction mark from a (0-based) page." def unmark_page_for_redaction(%DocumentEditor{ref: ref}, page), do: Native.editor_unmark_for_redaction(ref, page) @doc "Erase a single rectangular region on a (0-based) page." def erase_region(%DocumentEditor{ref: ref}, page, x, y, w, h), do: Native.editor_erase_region(ref, page, x * 1.0, y * 1.0, w * 1.0, h * 1.0) @doc """ Erase multiple rectangular regions on a (0-based) page. `rects` is a list of `{x, y, w, h}` tuples. """ def erase_regions(%DocumentEditor{ref: ref}, page, rects) when is_list(rects) do quads = Enum.map(rects, fn {x, y, w, h} -> {x * 1.0, y * 1.0, w * 1.0, h * 1.0} end) Native.editor_erase_regions(ref, page, quads) end @doc "Clear all pending erase-region entries for a (0-based) page." def clear_erase_regions(%DocumentEditor{ref: ref}, page), do: Native.editor_clear_erase_regions(ref, page) @doc "Flatten annotations on a (0-based) page." def flatten_annotations(%DocumentEditor{ref: ref}, page), do: Native.editor_flatten_annotations(ref, page) @doc "Flatten annotations across the whole document." def flatten_all_annotations(%DocumentEditor{ref: ref}), do: Native.editor_flatten_all_annotations(ref) @doc "Whether a (0-based) page is marked for annotation-flatten." def page_marked_for_flatten?(%DocumentEditor{ref: ref}, page), do: Native.editor_is_marked_for_flatten(ref, page) @doc "Remove the flatten mark from a (0-based) page." def unmark_page_for_flatten(%DocumentEditor{ref: ref}, page), do: Native.editor_unmark_for_flatten(ref, page) @doc "Set a form field value (UTF-8)." def set_form_field_value(%DocumentEditor{ref: ref}, name, value), do: Native.editor_set_form_field_value(ref, name, value) @doc "Flatten all forms (bake field values into page content)." def flatten_forms(%DocumentEditor{ref: ref}), do: Native.editor_flatten_forms(ref) @doc "Flatten forms on a specific (0-based) page." def flatten_forms_on_page(%DocumentEditor{ref: ref}, page_index), do: Native.editor_flatten_forms_on_page(ref, page_index) @doc "Number of warnings from the last form-flatten." def flatten_warnings_count(%DocumentEditor{ref: ref}), do: Native.editor_flatten_warnings_count(ref) @doc "The `index`-th flatten warning string." def flatten_warning(%DocumentEditor{ref: ref}, index), do: Native.editor_flatten_warning(ref, index) @doc "Merge pages from a source PDF on disk into this document." def merge_from(%DocumentEditor{ref: ref}, source_path), do: Native.editor_merge_from(ref, source_path) @doc "Merge pages from an in-memory PDF binary into this document." def merge_from_bytes(%DocumentEditor{ref: ref}, bytes), do: Native.editor_merge_from_bytes(ref, bytes) @doc """ Convert to PDF/A in place (`level` 0..7). Works on a `DocumentEditor` or an opened `Document`. """ def convert_to_pdf_a(%DocumentEditor{ref: ref}, level), do: Native.editor_convert_to_pdf_a(ref, level) def convert_to_pdf_a(%Document{ref: ref}, level), do: Native.doc_convert_to_pdf_a(ref, level) @doc "Embed a file attachment `name` with `bytes` into the document." def embed_file(%DocumentEditor{ref: ref}, name, bytes), do: Native.editor_embed_file(ref, name, bytes) @doc "Extract a subset of (0-based) `pages` to a new in-memory PDF binary." def extract_pages_to_bytes(%DocumentEditor{ref: ref}, pages) when is_list(pages), do: Native.editor_extract_pages_to_bytes(ref, pages) @doc "Save the edited document to `path`." def editor_save(%DocumentEditor{ref: ref}, path), do: Native.editor_save(ref, path) @doc "Serialize the edited document to a binary." def editor_save_to_bytes(%DocumentEditor{ref: ref}), do: Native.editor_save_to_bytes(ref) @doc "Serialize the edited document to bytes with compress/GC/linearize options." def editor_save_to_bytes_with_options( %DocumentEditor{ref: ref}, compress, garbage_collect, linearize ), do: Native.editor_save_to_bytes_with_options(ref, compress, garbage_collect, linearize) @doc "Save the edited document AES-256 encrypted to `path`." def editor_save_encrypted(%DocumentEditor{ref: ref}, path, user_password, owner_password), do: Native.editor_save_encrypted(ref, path, user_password, owner_password) @doc "Serialize the edited document AES-256 encrypted to a binary." def editor_save_encrypted_to_bytes(%DocumentEditor{ref: ref}, user_password, owner_password), do: Native.editor_save_encrypted_to_bytes(ref, user_password, owner_password) @doc "Free the editor's native handle now (idempotent)." def editor_close(%DocumentEditor{ref: ref}), do: Native.editor_close(ref) # ── EmbeddedFont ───────────────────────────────────────────────────────────── @doc "Load a TTF/OTF font from a file path into an `EmbeddedFont`." def font_from_file(path), do: wrap_font(Native.font_from_file(path)) @doc """ Load a TTF/OTF font from a binary into an `EmbeddedFont`. `name` may be an empty string to use the font's own PostScript name. """ def font_from_bytes(bytes, name \\ ""), do: wrap_font(Native.font_from_bytes(bytes, name)) @doc """ Free an `EmbeddedFont`'s native handle now (idempotent). A no-op after a successful `builder_register_embedded_font/3` consumed the font. """ def font_close(%EmbeddedFont{ref: ref}), do: Native.font_close(ref) # ── DocumentBuilder ────────────────────────────────────────────────────────── @doc "Create a new PDF-creation `DocumentBuilder`." def builder, do: wrap_doc_builder(Native.dbld_create()) @doc "Set the document title." def builder_set_title(%DocumentBuilder{ref: ref}, title), do: Native.dbld_set_title(ref, title) @doc "Set the document author." def builder_set_author(%DocumentBuilder{ref: ref}, author), do: Native.dbld_set_author(ref, author) @doc "Set the document subject." def builder_set_subject(%DocumentBuilder{ref: ref}, subject), do: Native.dbld_set_subject(ref, subject) @doc "Set the document keywords (comma-separated)." def builder_set_keywords(%DocumentBuilder{ref: ref}, keywords), do: Native.dbld_set_keywords(ref, keywords) @doc "Set the creator application name." def builder_set_creator(%DocumentBuilder{ref: ref}, creator), do: Native.dbld_set_creator(ref, creator) @doc "Run JavaScript when the document is opened (`/OpenAction`)." def builder_on_open(%DocumentBuilder{ref: ref}, script), do: Native.dbld_on_open(ref, script) @doc "Set the document's natural language tag (e.g. \"en-US\")." def builder_language(%DocumentBuilder{ref: ref}, lang), do: Native.dbld_language(ref, lang) @doc "Enable PDF/UA-1 tagged-PDF mode." def builder_tagged_pdf_ua1(%DocumentBuilder{ref: ref}), do: Native.dbld_tagged_pdf_ua1(ref) @doc "Add a role-map entry: custom structure type → standard PDF structure type." def builder_role_map(%DocumentBuilder{ref: ref}, custom, standard), do: Native.dbld_role_map(ref, custom, standard) @doc """ Register a TTF/OTF `EmbeddedFont` under `name`. On success the builder *consumes* the font handle — do not use or `font_close/1` it afterwards. """ def builder_register_embedded_font(%DocumentBuilder{ref: ref}, name, %EmbeddedFont{ref: fref}), do: Native.dbld_register_embedded_font(ref, name, fref) @doc "Start a US Letter page, returning a `PageBuilder`." def builder_letter_page(%DocumentBuilder{ref: ref}), do: wrap_page_builder(Native.dbld_letter_page(ref)) @doc "Start an A4 page, returning a `PageBuilder`." def builder_a4_page(%DocumentBuilder{ref: ref}), do: wrap_page_builder(Native.dbld_a4_page(ref)) @doc "Start a custom `width`×`height` (PDF points) page, returning a `PageBuilder`." def builder_page(%DocumentBuilder{ref: ref}, width, height), do: wrap_page_builder(Native.dbld_page(ref, width * 1.0, height * 1.0)) @doc "Build the PDF and return its bytes." def builder_build(%DocumentBuilder{ref: ref}), do: Native.dbld_build(ref) @doc "Build and save the PDF to `path`." def builder_save(%DocumentBuilder{ref: ref}, path), do: Native.dbld_save(ref, path) @doc "Build and save the PDF AES-256 encrypted to `path`." def builder_save_encrypted(%DocumentBuilder{ref: ref}, path, user_password, owner_password), do: Native.dbld_save_encrypted(ref, path, user_password, owner_password) @doc "Build the PDF AES-256 encrypted and return its bytes." def builder_to_bytes_encrypted(%DocumentBuilder{ref: ref}, user_password, owner_password), do: Native.dbld_to_bytes_encrypted(ref, user_password, owner_password) @doc "Free the builder's native handle now (idempotent)." def builder_close(%DocumentBuilder{ref: ref}), do: Native.dbld_close(ref) # ── PageBuilder ────────────────────────────────────────────────────────────── @doc "Set the font + size for subsequent text on this page." def page_font(%PageBuilder{ref: ref}, name, size), do: Native.pbld_font(ref, name, size * 1.0) @doc "Move the cursor to absolute `(x, y)` (PDF points, from lower-left)." def page_at(%PageBuilder{ref: ref}, x, y), do: Native.pbld_at(ref, x * 1.0, y * 1.0) @doc "Emit a line of text at the cursor, then advance one line-height." def page_text(%PageBuilder{ref: ref}, text), do: Native.pbld_text(ref, text) @doc "Emit a heading with `level` (1–6) and `text`." def page_heading(%PageBuilder{ref: ref}, level, text), do: Native.pbld_heading(ref, level, text) @doc "Emit a paragraph with automatic line wrapping." def page_paragraph(%PageBuilder{ref: ref}, text), do: Native.pbld_paragraph(ref, text) @doc "Advance the cursor down by `points`." def page_space(%PageBuilder{ref: ref}, points), do: Native.pbld_space(ref, points * 1.0) @doc "Draw a horizontal rule across the page." def page_horizontal_rule(%PageBuilder{ref: ref}), do: Native.pbld_horizontal_rule(ref) @doc "Attach a URL link to the previously-emitted text element." def page_link_url(%PageBuilder{ref: ref}, url), do: Native.pbld_link_url(ref, url) @doc "Link the previous text to an internal (0-based) `page_index`." def page_link_page(%PageBuilder{ref: ref}, page_index), do: Native.pbld_link_page(ref, page_index) @doc "Link the previous text to a named destination." def page_link_named(%PageBuilder{ref: ref}, destination), do: Native.pbld_link_named(ref, destination) @doc "Link the previous text to a JavaScript action." def page_link_javascript(%PageBuilder{ref: ref}, script), do: Native.pbld_link_javascript(ref, script) @doc "Run JavaScript when this page is opened (`/AA /O`)." def page_on_open(%PageBuilder{ref: ref}, script), do: Native.pbld_on_open(ref, script) @doc "Run JavaScript when this page is closed (`/AA /C`)." def page_on_close(%PageBuilder{ref: ref}, script), do: Native.pbld_on_close(ref, script) @doc "Set a keystroke JS action on the most-recently-added form field." def page_field_keystroke(%PageBuilder{ref: ref}, script), do: Native.pbld_field_keystroke(ref, script) @doc "Set a format JS action on the most-recently-added form field." def page_field_format(%PageBuilder{ref: ref}, script), do: Native.pbld_field_format(ref, script) @doc "Set a validate JS action on the most-recently-added form field." def page_field_validate(%PageBuilder{ref: ref}, script), do: Native.pbld_field_validate(ref, script) @doc "Set a calculate JS action on the most-recently-added form field." def page_field_calculate(%PageBuilder{ref: ref}, script), do: Native.pbld_field_calculate(ref, script) @doc "Highlight the previous text with an RGB colour (channels 0.0–1.0)." def page_highlight(%PageBuilder{ref: ref}, r, g, b), do: Native.pbld_highlight(ref, r * 1.0, g * 1.0, b * 1.0) @doc "Underline the previous text (RGB 0.0–1.0)." def page_underline(%PageBuilder{ref: ref}, r, g, b), do: Native.pbld_underline(ref, r * 1.0, g * 1.0, b * 1.0) @doc "Strikeout the previous text (RGB 0.0–1.0)." def page_strikeout(%PageBuilder{ref: ref}, r, g, b), do: Native.pbld_strikeout(ref, r * 1.0, g * 1.0, b * 1.0) @doc "Squiggly-underline the previous text (RGB 0.0–1.0)." def page_squiggly(%PageBuilder{ref: ref}, r, g, b), do: Native.pbld_squiggly(ref, r * 1.0, g * 1.0, b * 1.0) @doc "Attach a sticky-note annotation to the previous text." def page_sticky_note(%PageBuilder{ref: ref}, text), do: Native.pbld_sticky_note(ref, text) @doc "Place a free-standing sticky note at absolute `(x, y)`." def page_sticky_note_at(%PageBuilder{ref: ref}, x, y, text), do: Native.pbld_sticky_note_at(ref, x * 1.0, y * 1.0, text) @doc "Apply a text watermark to the entire page." def page_watermark(%PageBuilder{ref: ref}, text), do: Native.pbld_watermark(ref, text) @doc "Apply the standard \"CONFIDENTIAL\" diagonal watermark." def page_watermark_confidential(%PageBuilder{ref: ref}), do: Native.pbld_watermark_confidential(ref) @doc "Apply the standard \"DRAFT\" diagonal watermark." def page_watermark_draft(%PageBuilder{ref: ref}), do: Native.pbld_watermark_draft(ref) @doc "Attach a standard stamp annotation by `type_name`." def page_stamp(%PageBuilder{ref: ref}, type_name), do: Native.pbld_stamp(ref, type_name) @doc "Place a free-flowing text annotation inside the given rectangle." def page_freetext(%PageBuilder{ref: ref}, x, y, w, h, text), do: Native.pbld_freetext(ref, x * 1.0, y * 1.0, w * 1.0, h * 1.0, text) @doc """ Add a single-line text form field. `default_value` may be an empty string for a blank field. """ def page_text_field(%PageBuilder{ref: ref}, name, x, y, w, h, default_value \\ ""), do: Native.pbld_text_field(ref, name, x * 1.0, y * 1.0, w * 1.0, h * 1.0, default_value) @doc "Add a checkbox form field. `checked` is non-zero for initially-ticked." def page_checkbox(%PageBuilder{ref: ref}, name, x, y, w, h, checked), do: Native.pbld_checkbox(ref, name, x * 1.0, y * 1.0, w * 1.0, h * 1.0, checked) @doc """ Add a dropdown combo-box. `options` is a list of strings; `selected` may be an empty string for no initial selection. """ def page_combo_box(%PageBuilder{ref: ref}, name, x, y, w, h, options, selected \\ "") when is_list(options), do: Native.pbld_combo_box(ref, name, x * 1.0, y * 1.0, w * 1.0, h * 1.0, options, selected) @doc """ Add a radio-button group. `values`/`xs`/`ys`/`ws`/`hs` are parallel lists describing each button; `selected` may be an empty string. """ def page_radio_group(%PageBuilder{ref: ref}, name, values, xs, ys, ws, hs, selected \\ "") when is_list(values) do f = fn list -> Enum.map(list, &(&1 * 1.0)) end Native.pbld_radio_group(ref, name, values, f.(xs), f.(ys), f.(ws), f.(hs), selected) end @doc "Add a clickable push button with a visible caption." def page_push_button(%PageBuilder{ref: ref}, name, x, y, w, h, caption), do: Native.pbld_push_button(ref, name, x * 1.0, y * 1.0, w * 1.0, h * 1.0, caption) @doc "Add an unsigned signature placeholder field." def page_signature_field(%PageBuilder{ref: ref}, name, x, y, w, h), do: Native.pbld_signature_field(ref, name, x * 1.0, y * 1.0, w * 1.0, h * 1.0) @doc "Add a footnote: inline `ref_mark` + page-end `note_text`." def page_footnote(%PageBuilder{ref: ref}, ref_mark, note_text), do: Native.pbld_footnote(ref, ref_mark, note_text) @doc "Lay out `text` across `column_count` balanced columns with `gap_pt` between." def page_columns(%PageBuilder{ref: ref}, column_count, gap_pt, text), do: Native.pbld_columns(ref, column_count, gap_pt * 1.0, text) @doc "Emit `text` inline at the cursor (advances x, not y)." def page_inline(%PageBuilder{ref: ref}, text), do: Native.pbld_inline(ref, text) @doc "Emit an inline bold run." def page_inline_bold(%PageBuilder{ref: ref}, text), do: Native.pbld_inline_bold(ref, text) @doc "Emit an inline italic run." def page_inline_italic(%PageBuilder{ref: ref}, text), do: Native.pbld_inline_italic(ref, text) @doc "Emit an inline coloured run (RGB 0.0–1.0)." def page_inline_color(%PageBuilder{ref: ref}, r, g, b, text), do: Native.pbld_inline_color(ref, r * 1.0, g * 1.0, b * 1.0, text) @doc "Advance the cursor one line-height and reset x." def page_newline(%PageBuilder{ref: ref}), do: Native.pbld_newline(ref) @doc """ Place a 1-D barcode. `barcode_type`: 0=Code128 1=Code39 2=EAN13 3=EAN8 4=UPCA 5=ITF 6=Code93 7=Codabar. """ def page_barcode_1d(%PageBuilder{ref: ref}, barcode_type, data, x, y, w, h), do: Native.pbld_barcode_1d(ref, barcode_type, data, x * 1.0, y * 1.0, w * 1.0, h * 1.0) @doc "Place a QR-code image (square `size`×`size` points)." def page_barcode_qr(%PageBuilder{ref: ref}, data, x, y, size), do: Native.pbld_barcode_qr(ref, data, x * 1.0, y * 1.0, size * 1.0) @doc "Embed an image (raw JPEG/PNG bytes) at `(x, y, w, h)`." def page_image(%PageBuilder{ref: ref}, bytes, x, y, w, h), do: Native.pbld_image(ref, bytes, x * 1.0, y * 1.0, w * 1.0, h * 1.0) @doc "Embed an image with accessibility `alt_text` at `(x, y, w, h)`." def page_image_with_alt(%PageBuilder{ref: ref}, bytes, x, y, w, h, alt_text), do: Native.pbld_image_with_alt(ref, bytes, x * 1.0, y * 1.0, w * 1.0, h * 1.0, alt_text) @doc "Embed a decorative image as an `/Artifact` (no alt text)." def page_image_artifact(%PageBuilder{ref: ref}, bytes, x, y, w, h), do: Native.pbld_image_artifact(ref, bytes, x * 1.0, y * 1.0, w * 1.0, h * 1.0) @doc "Draw a stroked rectangle outline (1pt black)." def page_rect(%PageBuilder{ref: ref}, x, y, w, h), do: Native.pbld_rect(ref, x * 1.0, y * 1.0, w * 1.0, h * 1.0) @doc "Draw a filled rectangle in RGB colour (channels 0–1)." def page_filled_rect(%PageBuilder{ref: ref}, x, y, w, h, r, g, b), do: Native.pbld_filled_rect(ref, x * 1.0, y * 1.0, w * 1.0, h * 1.0, r * 1.0, g * 1.0, b * 1.0) @doc "Draw a line from `(x1, y1)` to `(x2, y2)` (1pt black)." def page_line(%PageBuilder{ref: ref}, x1, y1, x2, y2), do: Native.pbld_line(ref, x1 * 1.0, y1 * 1.0, x2 * 1.0, y2 * 1.0) @doc "Buffer a stroked rectangle with `width` + RGB colour." def page_stroke_rect(%PageBuilder{ref: ref}, x, y, w, h, width, r, g, b), do: Native.pbld_stroke_rect( ref, x * 1.0, y * 1.0, w * 1.0, h * 1.0, width * 1.0, r * 1.0, g * 1.0, b * 1.0 ) @doc "Buffer a stroked line with `width` + RGB colour." def page_stroke_line(%PageBuilder{ref: ref}, x1, y1, x2, y2, width, r, g, b), do: Native.pbld_stroke_line( ref, x1 * 1.0, y1 * 1.0, x2 * 1.0, y2 * 1.0, width * 1.0, r * 1.0, g * 1.0, b * 1.0 ) @doc """ Buffer a dashed stroked rectangle. `dash` is a list of alternating on/off lengths (empty = solid); `phase` is the starting offset. """ def page_stroke_rect_dashed(%PageBuilder{ref: ref}, x, y, w, h, width, r, g, b, dash, phase) when is_list(dash), do: Native.pbld_stroke_rect_dashed( ref, x * 1.0, y * 1.0, w * 1.0, h * 1.0, width * 1.0, r * 1.0, g * 1.0, b * 1.0, Enum.map(dash, &(&1 * 1.0)), phase * 1.0 ) @doc """ Buffer a dashed stroked line. `dash` is a list of alternating on/off lengths (empty = solid); `phase` is the starting offset. """ def page_stroke_line_dashed(%PageBuilder{ref: ref}, x1, y1, x2, y2, width, r, g, b, dash, phase) when is_list(dash), do: Native.pbld_stroke_line_dashed( ref, x1 * 1.0, y1 * 1.0, x2 * 1.0, y2 * 1.0, width * 1.0, r * 1.0, g * 1.0, b * 1.0, Enum.map(dash, &(&1 * 1.0)), phase * 1.0 ) @doc "Buffer text inside a rect. `align`: 0=Left, 1=Center, 2=Right." def page_text_in_rect(%PageBuilder{ref: ref}, x, y, w, h, text, align), do: Native.pbld_text_in_rect(ref, x * 1.0, y * 1.0, w * 1.0, h * 1.0, text, align) @doc "Buffer a same-size page transition; later ops land on the new page." def page_new_page_same_size(%PageBuilder{ref: ref}), do: Native.pbld_new_page_same_size(ref) @doc """ Buffer a buffered table. `widths`/`aligns` are length-`n_columns` lists (`aligns`: 0/1/2); `cells` is a row-major list of `n_rows * n_columns` strings. `has_header` promotes the first row. """ def page_table(%PageBuilder{ref: ref}, n_columns, widths, aligns, n_rows, cells, has_header) when is_list(widths) and is_list(aligns) and is_list(cells), do: Native.pbld_table( ref, n_columns, Enum.map(widths, &(&1 * 1.0)), aligns, n_rows, cells, has_header ) @doc """ Open a streaming table. `headers` is a list of column header strings; `widths`/`aligns` are parallel lists. `repeat_header` is non-zero to repeat the header on each page. """ def page_streaming_table_begin( %PageBuilder{ref: ref}, n_columns, headers, widths, aligns, repeat_header ) when is_list(headers) and is_list(widths) and is_list(aligns), do: Native.pbld_streaming_table_begin( ref, n_columns, headers, Enum.map(widths, &(&1 * 1.0)), aligns, repeat_header ) @doc """ Open a streaming table with a column-width `mode` (0=Fixed, 1=Sample, 2=AutoAll) plus sampling/rowspan parameters. """ def page_streaming_table_begin_v2( %PageBuilder{ref: ref}, n_columns, headers, widths, aligns, repeat_header, mode, sample_rows, min_w, max_w, max_rowspan ) when is_list(headers) and is_list(widths) and is_list(aligns), do: Native.pbld_streaming_table_begin_v2( ref, n_columns, headers, Enum.map(widths, &(&1 * 1.0)), aligns, repeat_header, mode, sample_rows, min_w * 1.0, max_w * 1.0, max_rowspan ) @doc "Set the auto-flush batch size for the open streaming table (0 = default 256)." def page_streaming_table_set_batch_size(%PageBuilder{ref: ref}, batch_size), do: Native.pbld_streaming_table_set_batch_size(ref, batch_size) @doc "Rows pushed since the last batch boundary." def page_streaming_table_pending_row_count(%PageBuilder{ref: ref}), do: Native.pbld_streaming_table_pending_row_count(ref) @doc "Complete batches recorded so far." def page_streaming_table_batch_count(%PageBuilder{ref: ref}), do: Native.pbld_streaming_table_batch_count(ref) @doc "Push one row (list of cell strings) into the open streaming table." def page_streaming_table_push_row(%PageBuilder{ref: ref}, cells) when is_list(cells), do: Native.pbld_streaming_table_push_row(ref, cells) @doc """ Push one row with per-cell `rowspans` (list of ints, empty = all rowspan=1). """ def page_streaming_table_push_row_v2(%PageBuilder{ref: ref}, cells, rowspans) when is_list(cells) and is_list(rowspans), do: Native.pbld_streaming_table_push_row_v2(ref, cells, rowspans) @doc "Mark a batch boundary in the open streaming table." def page_streaming_table_flush(%PageBuilder{ref: ref}), do: Native.pbld_streaming_table_flush(ref) @doc "Close the open streaming table." def page_streaming_table_finish(%PageBuilder{ref: ref}), do: Native.pbld_streaming_table_finish(ref) @doc """ Commit this page's buffered ops to its parent builder. *Consumes* the handle — the wrapper must not be used (or `page_close/1`-d) afterwards. """ def page_done(%PageBuilder{ref: ref}), do: Native.pbld_done(ref) @doc "Drop an uncommitted page builder's native handle now (idempotent)." def page_close(%PageBuilder{ref: ref}), do: Native.pbld_close(ref) # ── Certificate (phase 6) ──────────────────────────────────────────────────── @doc """ Load signing credentials from a PKCS#12 (.p12/.pfx) binary, decrypted with `password` (empty string for none). """ def certificate_from_bytes(bytes, password \\ "") when is_binary(bytes), do: wrap_certificate(Native.cert_load_from_bytes(bytes, password)) @doc "Load signing credentials from PEM-encoded certificate + private-key strings." def certificate_from_pem(cert_pem, key_pem), do: wrap_certificate(Native.cert_load_from_pem(cert_pem, key_pem)) @doc "The certificate's subject distinguished name." def certificate_subject(%Certificate{ref: ref}), do: Native.cert_get_subject(ref) @doc "The certificate's issuer distinguished name." def certificate_issuer(%Certificate{ref: ref}), do: Native.cert_get_issuer(ref) @doc "The certificate's serial number (string)." def certificate_serial(%Certificate{ref: ref}), do: Native.cert_get_serial(ref) @doc """ The certificate's validity window as `{:ok, {not_before, not_after}}` (Unix epoch seconds). """ def certificate_validity(%Certificate{ref: ref}), do: Native.cert_get_validity(ref) @doc "Whether the certificate is currently valid (1 = valid; see C ABI codes)." def certificate_valid?(%Certificate{ref: ref}), do: Native.cert_is_valid(ref) @doc "Free a certificate's native handle now (idempotent)." def certificate_close(%Certificate{ref: ref}), do: Native.cert_close(ref) # ── Signing (phase 6) ───────────────────────────────────────────────────────── @doc "Sign raw PDF `bytes` with `certificate`, returning the signed PDF binary." def sign_bytes(bytes, %Certificate{ref: cref}, reason \\ "", location \\ "") when is_binary(bytes), do: Native.sign_bytes(bytes, cref, reason, location) @doc """ PAdES-sign raw PDF `bytes`. `level`: 0=B-B 1=B-T 2=B-LT. `tsa_url` may be an empty string for B-B. `certs`/`crls`/`ocsps` are lists of DER binaries carrying B-LT revocation material (empty lists for B-B/B-T). """ def sign_bytes_pades(bytes, %Certificate{ref: cref}, level, tsa_url \\ "", opts \\ []) when is_binary(bytes) and is_integer(level) and is_list(opts) do Native.sign_bytes_pades( bytes, cref, level, tsa_url, Keyword.get(opts, :reason, ""), Keyword.get(opts, :location, ""), Keyword.get(opts, :certs, []), Keyword.get(opts, :crls, []), Keyword.get(opts, :ocsps, []) ) end @doc """ Struct-options variant of `sign_bytes_pades/5` — marshals the same parameters into the C `PadesSignOptionsC` struct. Returns the signed PDF binary. """ def sign_bytes_pades_opts(bytes, %Certificate{ref: cref}, level, tsa_url \\ "", opts \\ []) when is_binary(bytes) and is_integer(level) and is_list(opts) do Native.sign_bytes_pades_opts( bytes, cref, level, tsa_url, Keyword.get(opts, :reason, ""), Keyword.get(opts, :location, ""), Keyword.get(opts, :certs, []), Keyword.get(opts, :crls, []), Keyword.get(opts, :ocsps, []) ) end # ── SignatureInfo (phase 6) ──────────────────────────────────────────────────── @doc "The signer's name." def signature_signer_name(%SignatureInfo{ref: ref}), do: Native.sig_get_signer_name(ref) @doc "The stated signing reason." def signature_reason(%SignatureInfo{ref: ref}), do: Native.sig_get_signing_reason(ref) @doc "The stated signing location." def signature_location(%SignatureInfo{ref: ref}), do: Native.sig_get_signing_location(ref) @doc "The signing time as `{:ok, epoch_seconds}`." def signature_time(%SignatureInfo{ref: ref}), do: Native.sig_get_signing_time(ref) @doc "The signer's `Certificate` handle." def signature_certificate(%SignatureInfo{ref: ref}), do: wrap_certificate(Native.sig_get_certificate(ref)) @doc "The signature's PAdES level as `{:ok, level}` (-1 if unknown)." def signature_pades_level(%SignatureInfo{ref: ref}), do: Native.sig_get_pades_level(ref) @doc "Whether the signature carries an embedded timestamp." def signature_has_timestamp?(%SignatureInfo{ref: ref}), do: Native.sig_has_timestamp(ref) @doc "The signature's embedded `Timestamp` handle." def signature_timestamp(%SignatureInfo{ref: ref}), do: wrap_timestamp(Native.sig_get_timestamp(ref)) @doc "Attach `timestamp` to the signature; returns `{:ok, bool}`." def signature_add_timestamp(%SignatureInfo{ref: ref}, %Timestamp{ref: tref}), do: Native.sig_add_timestamp(ref, tref) @doc "Run the signer-attributes crypto check; returns `{:ok, code}` (1/0/-1)." def signature_verify(%SignatureInfo{ref: ref}), do: Native.sig_verify(ref) @doc """ Verify the signature end-to-end against the full PDF `bytes`; returns `{:ok, code}` (1/0/-1). """ def signature_verify_detached(%SignatureInfo{ref: ref}, bytes) when is_binary(bytes), do: Native.sig_verify_detached(ref, bytes) @doc "Free a signature's native handle now (idempotent)." def signature_close(%SignatureInfo{ref: ref}), do: Native.sig_close(ref) # ── Timestamp (phase 6) ──────────────────────────────────────────────────────── @doc "Parse a DER-encoded RFC 3161 timestamp token into a `Timestamp`." def timestamp_parse(bytes) when is_binary(bytes), do: wrap_timestamp(Native.ts_parse(bytes)) @doc "The raw DER token bytes." def timestamp_token(%Timestamp{ref: ref}), do: Native.ts_get_token(ref) @doc "The message-imprint hash bytes." def timestamp_message_imprint(%Timestamp{ref: ref}), do: Native.ts_get_message_imprint(ref) @doc "The timestamp time as `{:ok, epoch_seconds}`." def timestamp_time(%Timestamp{ref: ref}), do: Native.ts_get_time(ref) @doc "The timestamp serial number (string)." def timestamp_serial(%Timestamp{ref: ref}), do: Native.ts_get_serial(ref) @doc "The issuing TSA name." def timestamp_tsa_name(%Timestamp{ref: ref}), do: Native.ts_get_tsa_name(ref) @doc "The timestamp policy OID (string)." def timestamp_policy_oid(%Timestamp{ref: ref}), do: Native.ts_get_policy_oid(ref) @doc "The hash-algorithm code as `{:ok, code}`." def timestamp_hash_algorithm(%Timestamp{ref: ref}), do: Native.ts_get_hash_algorithm(ref) @doc "Verify the timestamp token; returns `{:ok, bool}`." def timestamp_verify(%Timestamp{ref: ref}), do: Native.ts_verify(ref) @doc "Free a timestamp's native handle now (idempotent)." def timestamp_close(%Timestamp{ref: ref}), do: Native.ts_close(ref) # ── TsaClient (phase 6) ──────────────────────────────────────────────────────── @doc """ Create an RFC 3161 TSA client for `url`. `opts`: `:username`, `:password` (empty for none), `:timeout` (seconds), `:hash_algo`, `:use_nonce`, `:cert_req`. """ def tsa_client(url, opts \\ []) when is_list(opts) do wrap_tsa( Native.tsa_create( url, Keyword.get(opts, :username, ""), Keyword.get(opts, :password, ""), Keyword.get(opts, :timeout, 30), Keyword.get(opts, :hash_algo, 0), Keyword.get(opts, :use_nonce, true), Keyword.get(opts, :cert_req, true) ) ) end @doc "Request a timestamp over `data`, returning a `Timestamp`." def tsa_request_timestamp(%TsaClient{ref: ref}, data) when is_binary(data), do: wrap_timestamp(Native.tsa_request_timestamp(ref, data)) @doc "Request a timestamp over a precomputed `hash` (with `hash_algo`)." def tsa_request_timestamp_hash(%TsaClient{ref: ref}, hash, hash_algo) when is_binary(hash) and is_integer(hash_algo), do: wrap_timestamp(Native.tsa_request_timestamp_hash(ref, hash, hash_algo)) @doc "Free a TSA client's native handle now (idempotent)." def tsa_close(%TsaClient{ref: ref}), do: Native.tsa_close(ref) # ── Dss (phase 6) ────────────────────────────────────────────────────────────── @doc "Number of certs in the DSS." def dss_cert_count(%Dss{ref: ref}), do: Native.dss_cert_count(ref) @doc "Number of CRLs in the DSS." def dss_crl_count(%Dss{ref: ref}), do: Native.dss_crl_count(ref) @doc "Number of OCSP responses in the DSS." def dss_ocsp_count(%Dss{ref: ref}), do: Native.dss_ocsp_count(ref) @doc "Number of VRI (validation-related-info) entries in the DSS." def dss_vri_count(%Dss{ref: ref}), do: Native.dss_vri_count(ref) @doc "The `index`-th DSS cert as `{:ok, der_bytes}`." def dss_cert(%Dss{ref: ref}, index), do: Native.dss_get_cert(ref, index) @doc "The `index`-th DSS CRL as `{:ok, der_bytes}`." def dss_crl(%Dss{ref: ref}, index), do: Native.dss_get_crl(ref, index) @doc "The `index`-th DSS OCSP response as `{:ok, der_bytes}`." def dss_ocsp(%Dss{ref: ref}, index), do: Native.dss_get_ocsp(ref, index) @doc "Free a DSS native handle now (idempotent)." def dss_close(%Dss{ref: ref}), do: Native.dss_close(ref) # ── Validation (phase 6) ─────────────────────────────────────────────────────── @doc "Validate a `Document` against PDF/A `level`, returning a `PdfAResult`." def validate_pdf_a(%Document{ref: ref}, level), do: wrap_pdf_a(Native.validate_pdf_a(ref, level)) @doc "Validate a `Document` against PDF/UA `level`, returning a `PdfUaResult`." def validate_pdf_ua(%Document{ref: ref}, level), do: wrap_pdf_ua(Native.validate_pdf_ua(ref, level)) @doc "Validate a `Document` against PDF/X `level`, returning a `PdfXResult`." def validate_pdf_x(%Document{ref: ref}, level), do: wrap_pdf_x(Native.validate_pdf_x(ref, level)) @doc "Whether the PDF/A result is compliant; returns `{:ok, bool}`." def pdf_a_compliant?(%PdfAResult{ref: ref}), do: Native.pdf_a_is_compliant(ref) @doc "PDF/A validation errors as a list of strings." def pdf_a_errors(%PdfAResult{ref: ref}), do: result_strings(ref, &Native.pdf_a_error_count/1, &Native.pdf_a_get_error/2) @doc "PDF/A validation warnings count (no warning accessor in the C ABI)." def pdf_a_warning_count(%PdfAResult{ref: ref}), do: Native.pdf_a_warning_count(ref) @doc "Free a PDF/A result's native handle now (idempotent)." def pdf_a_close(%PdfAResult{ref: ref}), do: Native.pdf_a_close(ref) @doc "Whether the PDF/UA result is accessible; returns `{:ok, bool}`." def pdf_ua_accessible?(%PdfUaResult{ref: ref}), do: Native.pdf_ua_is_accessible(ref) @doc "PDF/UA validation errors as a list of strings." def pdf_ua_errors(%PdfUaResult{ref: ref}), do: result_strings(ref, &Native.pdf_ua_error_count/1, &Native.pdf_ua_get_error/2) @doc "PDF/UA validation warnings as a list of strings." def pdf_ua_warnings(%PdfUaResult{ref: ref}), do: result_strings(ref, &Native.pdf_ua_warning_count/1, &Native.pdf_ua_get_warning/2) @doc "Accessibility element counts as a `{:ok, %UaStats{}}`." def pdf_ua_stats(%PdfUaResult{ref: ref}) do with {:ok, {s, im, t, f, an, pg}} <- Native.pdf_ua_get_stats(ref) do {:ok, %UaStats{struct: s, images: im, tables: t, forms: f, annotations: an, pages: pg}} end end @doc "Free a PDF/UA result's native handle now (idempotent)." def pdf_ua_close(%PdfUaResult{ref: ref}), do: Native.pdf_ua_close(ref) @doc "Whether the PDF/X result is compliant; returns `{:ok, bool}`." def pdf_x_compliant?(%PdfXResult{ref: ref}), do: Native.pdf_x_is_compliant(ref) @doc "PDF/X validation errors as a list of strings." def pdf_x_errors(%PdfXResult{ref: ref}), do: result_strings(ref, &Native.pdf_x_error_count/1, &Native.pdf_x_get_error/2) @doc "Free a PDF/X result's native handle now (idempotent)." def pdf_x_close(%PdfXResult{ref: ref}), do: Native.pdf_x_close(ref) # ── log level (phase 6) ──────────────────────────────────────────────────────── @doc "Set the global log level (0=Off 1=Error 2=Warn 3=Info 4=Debug 5=Trace)." def set_log_level(level) when is_integer(level), do: Native.oxide_set_log_level(level) @doc "Get the current global log level (0-5)." def get_log_level, do: Native.oxide_get_log_level() # ── helpers ────────────────────────────────────────────────────────────────── # ── phase 7: barcodes / QR ─────────────────────────────────────────────────── @doc """ Generate a QR code from `data`. `error_correction` (0=L 1=M 2=Q 3=H) and `size_px` tune the symbol. Returns `{:ok, %Barcode{}}`. """ def generate_qr_code(data, error_correction \\ 1, size_px \\ 256), do: wrap_barcode(Native.barcode_generate_qr(data, error_correction, size_px)) @doc """ Generate a 1-D/2-D barcode from `data`. `format` is a barcode-format code; `size_px` is the rendered size. Returns `{:ok, %Barcode{}}`. """ def generate_barcode(data, format \\ 0, size_px \\ 256), do: wrap_barcode(Native.barcode_generate(data, format, size_px)) @doc "The barcode's encoded data string." def barcode_data(%Barcode{ref: ref}), do: Native.barcode_get_data(ref) @doc "The barcode's format code." def barcode_format(%Barcode{ref: ref}), do: Native.barcode_get_format(ref) @doc "The barcode's decode confidence (0.0–1.0)." def barcode_confidence(%Barcode{ref: ref}), do: Native.barcode_get_confidence(ref) @doc "Render the barcode to PNG bytes at `size_px`." def barcode_png(%Barcode{ref: ref}, size_px \\ 256), do: Native.barcode_get_image_png(ref, size_px) @doc "Render the barcode to an SVG string at `size_px`." def barcode_svg(%Barcode{ref: ref}, size_px \\ 256), do: Native.barcode_get_svg(ref, size_px) @doc "Place a `Barcode` on a (0-based) editor `page` at `(x, y, width, height)`." def add_barcode_to_page( %DocumentEditor{ref: ref}, page, %Barcode{ref: bref}, x, y, width, height ), do: Native.editor_add_barcode_to_page( ref, page, bref, x * 1.0, y * 1.0, width * 1.0, height * 1.0 ) @doc "Free a `Barcode`'s native handle now (idempotent)." def barcode_close(%Barcode{ref: ref}), do: Native.barcode_close(ref) # ── phase 7: OCR ───────────────────────────────────────────────────────────── @doc """ Create an `OcrEngine` from detection/recognition model and dictionary file paths. Returns `{:ok, %OcrEngine{}}` or an error (e.g. missing models / the `ocr` feature disabled). """ def ocr_engine(det_model_path, rec_model_path, dict_path), do: wrap_ocr(Native.ocr_engine_create(det_model_path, rec_model_path, dict_path)) @doc "Free an `OcrEngine`'s native handle now (idempotent)." def ocr_engine_close(%OcrEngine{ref: ref}), do: Native.ocr_engine_close(ref) @doc "Whether a (0-based) page needs OCR (i.e. is scanned/hybrid)." def ocr_page_needs_ocr(%Document{ref: ref}, page), do: Native.ocr_page_needs_ocr(ref, page) @doc """ Extract text from a (0-based) page using OCR. `engine` may be `nil` (native extraction only) or an `OcrEngine`. """ def ocr_extract_text(doc, page, engine \\ nil) def ocr_extract_text(%Document{ref: ref}, page, nil), do: Native.ocr_extract_text(ref, page, nil) def ocr_extract_text(%Document{ref: ref}, page, %OcrEngine{ref: eref}), do: Native.ocr_extract_text(ref, page, eref) # ── phase 7: render variants ───────────────────────────────────────────────── @doc """ Render a (0-based) `page` with the full render-options surface. `bg_*` are 0.0–1.0 background channels; `transparent_background`, `render_annotations` are non-zero flags; `format` is an image-format code; `dpi`/`jpeg_quality` are integers. Returns `{:ok, %RenderedImage{}}`. """ def render_page_with_options( %Document{ref: ref}, page, opts \\ [] ) do dpi = Keyword.get(opts, :dpi, 150) format = Keyword.get(opts, :format, 0) {br, bg, bb, ba} = Keyword.get(opts, :background, {1.0, 1.0, 1.0, 1.0}) transparent = if Keyword.get(opts, :transparent_background, false), do: 1, else: 0 annots = if Keyword.get(opts, :render_annotations, true), do: 1, else: 0 jpeg_quality = Keyword.get(opts, :jpeg_quality, 85) wrap_image( Native.doc_render_page_with_options( ref, page, dpi, format, br * 1.0, bg * 1.0, bb * 1.0, ba * 1.0, transparent, annots, jpeg_quality ) ) end @doc """ Like `render_page_with_options/3` plus `excluded_layers` — a list of OCG `/Name` strings to suppress. """ def render_page_with_options_ex( %Document{ref: ref}, page, excluded_layers, opts \\ [] ) when is_list(excluded_layers) do dpi = Keyword.get(opts, :dpi, 150) format = Keyword.get(opts, :format, 0) {br, bg, bb, ba} = Keyword.get(opts, :background, {1.0, 1.0, 1.0, 1.0}) transparent = if Keyword.get(opts, :transparent_background, false), do: 1, else: 0 annots = if Keyword.get(opts, :render_annotations, true), do: 1, else: 0 jpeg_quality = Keyword.get(opts, :jpeg_quality, 85) wrap_image( Native.doc_render_page_with_options_ex( ref, page, dpi, format, br * 1.0, bg * 1.0, bb * 1.0, ba * 1.0, transparent, annots, jpeg_quality, excluded_layers ) ) end @doc """ Render a rectangular region of a (0-based) `page`. `crop_*` are PDF user-space points (origin bottom-left); `format` is an image-format code. """ def render_page_region( %Document{ref: ref}, page, crop_x, crop_y, crop_width, crop_height, format \\ 0 ), do: wrap_image( Native.doc_render_page_region( ref, page, crop_x * 1.0, crop_y * 1.0, crop_width * 1.0, crop_height * 1.0, format ) ) @doc "Render a (0-based) `page` to fit inside `w`×`h` pixels, preserving aspect ratio." def render_page_fit(%Document{ref: ref}, page, w, h, format \\ 0), do: wrap_image(Native.doc_render_page_fit(ref, page, w, h, format)) @doc """ Render a (0-based) `page` to a raw premultiplied RGBA8888 buffer at `dpi`, returned as a `RenderedImage` (its `data` holds the raw pixels). """ def render_page_raw(%Document{ref: ref}, page, dpi \\ 150), do: wrap_image(Native.doc_render_page_raw(ref, page, dpi)) @doc """ Create a reusable `Renderer` with fixed `dpi`/`format`/`quality` and `anti_alias`. Returns `{:ok, %Renderer{}}`. """ def renderer(dpi \\ 150, format \\ 0, quality \\ 85, anti_alias \\ true), do: wrap_renderer(Native.renderer_create(dpi, format, quality, anti_alias)) @doc "Free a `Renderer`'s native handle now (idempotent)." def renderer_close(%Renderer{ref: ref}), do: Native.renderer_close(ref) @doc "Estimate the render time (ms) for a (0-based) `page`." def estimate_render_time(%Document{ref: ref}, page), do: Native.doc_estimate_render_time(ref, page) # ── phase 7: redaction (on an editor) ──────────────────────────────────────── @doc """ Queue a redaction rectangle on a (0-based) editor `page`. Corners `(x1, y1)`–`(x2, y2)` and fill colour `(r, g, b)` are in PDF user-space / DeviceRGB (channels 0.0–1.0). """ def redaction_add(%DocumentEditor{ref: ref}, page, x1, y1, x2, y2, r, g, b), do: Native.redaction_add( ref, page, x1 * 1.0, y1 * 1.0, x2 * 1.0, y2 * 1.0, r * 1.0, g * 1.0, b * 1.0 ) @doc "Number of queued redaction regions for a (0-based) `page`." def redaction_count(%DocumentEditor{ref: ref}, page), do: Native.redaction_count(ref, page) @doc """ Destructively apply all queued redactions. `scrub_metadata` also runs the document-scrub pass; `(r, g, b)` is the overlay colour (channels 0.0–1.0). Returns `{:ok, glyphs_removed}`. """ def redaction_apply( %DocumentEditor{ref: ref}, scrub_metadata \\ false, r \\ 0.0, g \\ 0.0, b \\ 0.0 ), do: Native.redaction_apply(ref, scrub_metadata, r * 1.0, g * 1.0, b * 1.0) @doc "Sanitise the document (strip Info/XMP/JS/embedded files) without geometric redaction." def redaction_scrub_metadata(%DocumentEditor{ref: ref}), do: Native.redaction_scrub_metadata(ref) # ── phase 7: constructors ──────────────────────────────────────────────────── @doc "Build a `Pdf` from an image file at `path`." def from_image(path), do: wrap_pdf(Native.pdf_from_image(path)) @doc "Build a `Pdf` from raw image bytes." def from_image_bytes(bytes), do: wrap_pdf(Native.pdf_from_image_bytes(bytes)) @doc """ Build a `Pdf` from `html` + `css` with a single embedded font (`font_bytes` may be an empty binary for none). """ def from_html_css(html, css, font_bytes \\ <<>>), do: wrap_pdf(Native.pdf_from_html_css(html, css, font_bytes)) @doc """ Build a `Pdf` from `html` + `css` with a multi-font cascade. `fonts` is a list of `{family, font_bytes}` tuples. """ def from_html_css_with_fonts(html, css, fonts) when is_list(fonts) do families = Enum.map(fonts, fn {family, _} -> family end) font_bytes = Enum.map(fonts, fn {_, bytes} -> bytes end) wrap_pdf(Native.pdf_from_html_css_with_fonts(html, css, families, font_bytes)) end @doc "Merge the PDFs at `paths` (list of file paths) into one PDF binary." def merge(paths) when is_list(paths), do: Native.pdf_merge(paths) # ── phase 7: page getters (on a Document) ──────────────────────────────────── @doc "Width (PDF points) of a (0-based) `page`." def page_width(%Document{ref: ref}, page), do: Native.page_get_width(ref, page) @doc "Height (PDF points) of a (0-based) `page`." def page_height(%Document{ref: ref}, page), do: Native.page_get_height(ref, page) @doc "Rotation (degrees) of a (0-based) `page`." def page_rotation(%Document{ref: ref}, page), do: Native.page_get_rotation(ref, page) @doc """ Get the elements of a (0-based) `page` as an opaque `ElementList`. Read its length with `element_count/1`. """ def page_elements(%Document{ref: ref}, page), do: wrap_element_list(Native.page_get_elements(ref, page)) @doc "Number of elements in an `ElementList`." def element_count(%ElementList{ref: ref}), do: Native.elements_count(ref) @doc "Free an `ElementList`'s native handle now (idempotent)." def element_list_close(%ElementList{ref: ref}), do: Native.elements_close(ref) # ── phase 7: timestamp ─────────────────────────────────────────────────────── @doc """ Add an RFC 3161 timestamp to the signature at `sig_index` of `pdf_data`, fetched from `tsa_url`. Returns `{:ok, timestamped_pdf_bytes}`. """ def add_timestamp(pdf_data, sig_index, tsa_url), do: Native.add_timestamp(pdf_data, sig_index, tsa_url) # ── phase 8: office I/O ─────────────────────────────────────────────────────── @doc "Open a DOCX document from a binary as a `Document`." def open_from_docx_bytes(bytes) when is_binary(bytes), do: wrap_doc(Native.doc_open_from_docx_bytes(bytes)) @doc "Open a PPTX document from a binary as a `Document`." def open_from_pptx_bytes(bytes) when is_binary(bytes), do: wrap_doc(Native.doc_open_from_pptx_bytes(bytes)) @doc "Open an XLSX document from a binary as a `Document`." def open_from_xlsx_bytes(bytes) when is_binary(bytes), do: wrap_doc(Native.doc_open_from_xlsx_bytes(bytes)) @doc "Export the document to DOCX bytes (`{:ok, binary}` | `{:error, code}`)." def to_docx(%Document{ref: ref}), do: Native.doc_to_docx(ref) @doc "Export the document to PPTX bytes (`{:ok, binary}` | `{:error, code}`)." def to_pptx(%Document{ref: ref}), do: Native.doc_to_pptx(ref) @doc "Export the document to XLSX bytes (`{:ok, binary}` | `{:error, code}`)." def to_xlsx(%Document{ref: ref}), do: Native.doc_to_xlsx(ref) # ── phase 8: in-rect extractors ─────────────────────────────────────────────── @doc "Reading-order text inside the rect `(x, y, w, h)` on a (0-based) `page`." def extract_text_in_rect(%Document{ref: ref}, page, x, y, w, h), do: Native.doc_extract_text_in_rect(ref, page, x / 1, y / 1, w / 1, h / 1) @doc "Words inside the rect `(x, y, w, h)` on a (0-based) `page` as `Word`s." def extract_words_in_rect(%Document{ref: ref}, page, x, y, w, h) do with {:ok, list} <- Native.doc_extract_words_in_rect(ref, page, x / 1, y / 1, w / 1, h / 1) do {:ok, Enum.map(list, fn {text, bx, by, bw, bh, font, size, bold, sequence} -> %Word{ text: text, bbox: %Bbox{x: bx, y: by, width: bw, height: bh}, font_name: font, font_size: size, bold: bold, sequence: sequence } end)} end end @doc "Text lines inside the rect `(x, y, w, h)` on a (0-based) `page`." def extract_lines_in_rect(%Document{ref: ref}, page, x, y, w, h) do with {:ok, list} <- Native.doc_extract_lines_in_rect(ref, page, x / 1, y / 1, w / 1, h / 1) do {:ok, Enum.map(list, fn {text, bx, by, bw, bh, word_count} -> %TextLine{ text: text, bbox: %Bbox{x: bx, y: by, width: bw, height: bh}, word_count: word_count } end)} end end @doc "Tables inside the rect `(x, y, w, h)` on a (0-based) `page`." def extract_tables_in_rect(%Document{ref: ref}, page, x, y, w, h) do with {:ok, list} <- Native.doc_extract_tables_in_rect(ref, page, x / 1, y / 1, w / 1, h / 1) do {:ok, Enum.map(list, fn {row_count, col_count, has_header, cells} -> %Table{row_count: row_count, col_count: col_count, has_header: has_header, cells: cells} end)} end end @doc "Images inside the rect `(x, y, w, h)` on a (0-based) `page`." def extract_images_in_rect(%Document{ref: ref}, page, x, y, w, h) do with {:ok, list} <- Native.doc_extract_images_in_rect(ref, page, x / 1, y / 1, w / 1, h / 1) do {:ok, Enum.map(list, fn {width, height, bpc, format, colorspace, data} -> %Image{ width: width, height: height, bits_per_component: bpc, format: format, colorspace: colorspace, data: data } end)} end end # ── phase 8: auto extraction / classification ───────────────────────────────── @doc "Auto-mode (native + image-OCR) text for a (0-based) `page`." def extract_text_auto(%Document{ref: ref}, page), do: Native.doc_extract_text_auto(ref, page) @doc "Concatenated text of the whole document." def extract_all_text(%Document{ref: ref}), do: Native.doc_extract_all_text(ref) @doc """ Auto-mode page extraction as a JSON string. `options_json` may be an empty string for defaults. """ def extract_page_auto(%Document{ref: ref}, page, options_json \\ ""), do: Native.doc_extract_page_auto(ref, page, options_json) @doc "Classify a (0-based) `page` (returns a JSON classification string)." def classify_page(%Document{ref: ref}, page), do: Native.doc_classify_page(ref, page) @doc "Classify the whole document (returns a JSON classification string)." def classify_document(%Document{ref: ref}), do: Native.doc_classify_document(ref) # ── phase 8: header / footer / artifact ─────────────────────────────────────── @doc "Erase the detected header on a (0-based) `page`. Returns the erased count." def erase_header(%Document{ref: ref}, page), do: Native.doc_erase_header(ref, page) @doc "Erase the detected footer on a (0-based) `page`. Returns the erased count." def erase_footer(%Document{ref: ref}, page), do: Native.doc_erase_footer(ref, page) @doc "Erase detected artifacts on a (0-based) `page`. Returns the erased count." def erase_artifacts(%Document{ref: ref}, page), do: Native.doc_erase_artifacts(ref, page) @doc "Remove repeating headers across pages above `threshold`. Returns the count." def remove_headers(%Document{ref: ref}, threshold \\ 0.5), do: Native.doc_remove_headers(ref, threshold / 1) @doc "Remove repeating footers across pages above `threshold`. Returns the count." def remove_footers(%Document{ref: ref}, threshold \\ 0.5), do: Native.doc_remove_footers(ref, threshold / 1) @doc "Remove repeating artifacts across pages above `threshold`. Returns the count." def remove_artifacts(%Document{ref: ref}, threshold \\ 0.5), do: Native.doc_remove_artifacts(ref, threshold / 1) # ── phase 8: forms ──────────────────────────────────────────────────────────── @doc """ The document's AcroForm fields as a list of `FormField` (an empty list when the document has no form). """ def form_fields(%Document{ref: ref}) do with {:ok, list} <- Native.doc_get_form_fields(ref) do {:ok, Enum.map(list, fn {name, value, type, read_only, required} -> %FormField{ name: name, value: value, type: type, read_only: read_only, required: required } end)} end end @doc "Export the filled form data to bytes (`format_type` 0=FDF, 1=XFDF, …)." def export_form_data_to_bytes(%Document{ref: ref}, format_type \\ 0), do: Native.doc_export_form_data_to_bytes(ref, format_type) @doc "Import form data from a file at `data_path` into the document." def import_form_data(%Document{ref: ref}, data_path), do: Native.doc_import_form_data(ref, data_path) @doc "Import FDF form data bytes into a `DocumentEditor`." def import_fdf_bytes(%DocumentEditor{ref: ref}, bytes) when is_binary(bytes), do: Native.editor_import_fdf_bytes(ref, bytes) @doc "Import XFDF form data bytes into a `DocumentEditor`." def import_xfdf_bytes(%DocumentEditor{ref: ref}, bytes) when is_binary(bytes), do: Native.editor_import_xfdf_bytes(ref, bytes) @doc "Import form data from a file at `filename` into the document." def form_import_from_file(%Document{ref: ref}, filename), do: Native.form_import_from_file(ref, filename) # ── phase 8: document structure / metadata ──────────────────────────────────── @doc "The document outline (bookmarks) as a JSON string." def outline(%Document{ref: ref}), do: Native.doc_get_outline(ref) @doc "The document page labels as a JSON string." def page_labels(%Document{ref: ref}), do: Native.doc_get_page_labels(ref) @doc "The document XMP metadata as an XML/JSON string." def xmp_metadata(%Document{ref: ref}), do: Native.doc_get_xmp_metadata(ref) @doc "The document's original source bytes." def source_bytes(%Document{ref: ref}), do: Native.doc_get_source_bytes(ref) @doc "Whether the document carries an XFA form." def has_xfa?(%Document{ref: ref}), do: Native.doc_has_xfa(ref) @doc "Page count of a built `Pdf` handle." def pdf_page_count(%Pdf{ref: ref}), do: Native.doc_get_page_count(ref) @doc "Plan splitting the document by bookmarks; returns a JSON plan string." def plan_split_by_bookmarks(%Document{ref: ref}, options_json \\ ""), do: Native.doc_plan_split_by_bookmarks(ref, options_json) # ── phase 8: document-level signatures ──────────────────────────────────────── @doc "Sign the document in place with `certificate`; returns the signature index." def sign(%Document{ref: ref}, %Certificate{ref: cert}, reason \\ "", location \\ ""), do: Native.doc_sign(ref, cert, reason, location) @doc "Number of signatures present in the document." def signature_count(%Document{ref: ref}), do: Native.doc_get_signature_count(ref) @doc "Get the `SignatureInfo` for the signature at (0-based) `index`." def signature(%Document{ref: ref}, index), do: wrap_signature(Native.doc_get_signature(ref, index)) @doc "Verify all signatures; returns an aggregate status code." def verify_all_signatures(%Document{ref: ref}), do: Native.doc_verify_all_signatures(ref) @doc "Whether the document carries a document-level timestamp." def document_has_timestamp?(%Document{ref: ref}), do: Native.doc_has_timestamp(ref) @doc "Get the document's `Dss` (Document Security Store) handle." def document_dss(%Document{ref: ref}), do: wrap_dss(Native.doc_get_dss(ref)) # ── phase 8: annotation extras ──────────────────────────────────────────────── @doc "Packed RGBA color (uint32) of the annotation at `index` on a `page`." def annotation_color(%Document{ref: ref}, page, index), do: Native.annot_get_color(ref, page, index) @doc "Creation date (unix seconds) of the annotation at `index` on a `page`." def annotation_creation_date(%Document{ref: ref}, page, index), do: Native.annot_get_creation_date(ref, page, index) @doc "Modification date (unix seconds) of the annotation at `index` on a `page`." def annotation_modification_date(%Document{ref: ref}, page, index), do: Native.annot_get_modification_date(ref, page, index) @doc "Whether the annotation at `index` on a `page` is hidden." def annotation_hidden?(%Document{ref: ref}, page, index), do: Native.annot_is_hidden(ref, page, index) @doc "Whether the annotation at `index` on a `page` is marked deleted." def annotation_marked_deleted?(%Document{ref: ref}, page, index), do: Native.annot_is_marked_deleted(ref, page, index) @doc "Whether the annotation at `index` on a `page` is printable." def annotation_printable?(%Document{ref: ref}, page, index), do: Native.annot_is_printable(ref, page, index) @doc "Whether the annotation at `index` on a `page` is read-only." def annotation_read_only?(%Document{ref: ref}, page, index), do: Native.annot_is_read_only(ref, page, index) @doc "URI of the link annotation at `index` on a `page`." def link_annotation_uri(%Document{ref: ref}, page, index), do: Native.annot_link_get_uri(ref, page, index) @doc "Icon name of the text annotation at `index` on a `page`." def text_annotation_icon_name(%Document{ref: ref}, page, index), do: Native.annot_text_get_icon_name(ref, page, index) @doc "Quad-point count of the highlight annotation at `index` on a `page`." def highlight_quad_points_count(%Document{ref: ref}, page, index), do: Native.annot_highlight_quad_points_count(ref, page, index) @doc """ The `quad_index`-th quad of the highlight annotation at `index` on a `page` as `{:ok, {x1, y1, x2, y2, x3, y3, x4, y4}}`. """ def highlight_quad_point(%Document{ref: ref}, page, index, quad_index), do: Native.annot_highlight_quad_point(ref, page, index, quad_index) @doc "All annotations on a (0-based) `page` serialized as a JSON string." def annotations_to_json(%Document{ref: ref}, page), do: Native.annotations_to_json(ref, page) # ── phase 8: element / font / search JSON accessors ──────────────────────────── @doc "Type string of the element at `index` in an `ElementList`." def element_type(%ElementList{ref: ref}, index), do: Native.element_get_type(ref, index) @doc "Text of the element at `index` in an `ElementList`." def element_text(%ElementList{ref: ref}, index), do: Native.element_get_text(ref, index) @doc "Bounding box of the element at `index` in an `ElementList` as a `Bbox`." def element_rect(%ElementList{ref: ref}, index), do: wrap_box(Native.element_get_rect(ref, index)) @doc "An `ElementList` serialized as a JSON string." def elements_to_json(%ElementList{ref: ref}), do: Native.elements_to_json(ref) @doc "Font size of the font at `index` on a (0-based) `page`." def font_size(%Document{ref: ref}, page, index), do: Native.font_get_size(ref, page, index) @doc "The fonts on a (0-based) `page` serialized as a JSON string." def fonts_to_json(%Document{ref: ref}, page), do: Native.fonts_to_json(ref, page) @doc "Search the whole document for `term` and serialize the hits as JSON." def search_results_to_json(%Document{ref: ref}, term, case_sensitive \\ false), do: Native.search_results_to_json(ref, term, if(case_sensitive, do: 1, else: 0)) # ── phase 8: crypto / FIPS ───────────────────────────────────────────────────── @doc "The active crypto provider name." def crypto_active_provider, do: Native.crypto_active_provider() @doc "The crypto Bill of Materials (CBOM) as a JSON string." def crypto_cbom, do: Native.crypto_cbom() @doc "The crypto inventory as a JSON string." def crypto_inventory, do: Native.crypto_inventory() @doc "The active crypto policy as a string." def crypto_policy, do: Native.crypto_policy() @doc "Whether a FIPS-validated crypto provider is available (nonzero = yes)." def crypto_fips_available, do: Native.crypto_fips_available() @doc "Switch the process to the FIPS crypto provider; returns a status code." def crypto_use_fips, do: Native.crypto_use_fips() @doc "Set the crypto policy from `spec`; returns a status code." def crypto_set_policy(spec) when is_binary(spec), do: Native.crypto_set_policy(spec) # ── phase 8: models / config ─────────────────────────────────────────────────── @doc "The OCR/layout model manifest as a JSON string." def model_manifest, do: Native.model_manifest() @doc "Whether model prefetch is available (nonzero = yes)." def prefetch_available, do: Native.prefetch_available() @doc "Prefetch models for `languages_csv` (empty = defaults); returns JSON." def prefetch_models(languages_csv \\ ""), do: Native.prefetch_models(languages_csv) @doc "Set the per-content-stream operator cap; returns the previous limit." def set_max_ops_per_stream(limit) when is_integer(limit), do: Native.set_max_ops_per_stream(limit) @doc "Toggle preserving unmapped glyphs (nonzero to enable); returns the previous value." def set_preserve_unmapped_glyphs(preserve) when is_integer(preserve), do: Native.set_preserve_unmapped_glyphs(preserve) defp wrap_doc({:ok, ref}), do: {:ok, %Document{ref: ref}} defp wrap_doc(other), do: other defp wrap_pdf({:ok, ref}), do: {:ok, %Pdf{ref: ref}} defp wrap_pdf(other), do: other defp wrap_editor({:ok, ref}), do: {:ok, %DocumentEditor{ref: ref}} defp wrap_editor(other), do: other defp wrap_box({:ok, {x, y, w, h}}), do: {:ok, %Bbox{x: x, y: y, width: w, height: h}} defp wrap_box(other), do: other defp wrap_image({:ok, {ref, width, height, data}}), do: {:ok, %RenderedImage{ref: ref, width: width, height: height, data: data}} defp wrap_image(other), do: other defp wrap_font({:ok, ref}), do: {:ok, %EmbeddedFont{ref: ref}} defp wrap_font(other), do: other defp wrap_doc_builder({:ok, ref}), do: {:ok, %DocumentBuilder{ref: ref}} defp wrap_doc_builder(other), do: other defp wrap_page_builder({:ok, ref}), do: {:ok, %PageBuilder{ref: ref}} defp wrap_page_builder(other), do: other defp wrap_certificate({:ok, ref}), do: {:ok, %Certificate{ref: ref}} defp wrap_certificate(other), do: other defp wrap_signature({:ok, ref}), do: {:ok, %SignatureInfo{ref: ref}} defp wrap_signature(other), do: other defp wrap_dss({:ok, ref}), do: {:ok, %Dss{ref: ref}} defp wrap_dss(other), do: other defp wrap_timestamp({:ok, ref}), do: {:ok, %Timestamp{ref: ref}} defp wrap_timestamp(other), do: other defp wrap_tsa({:ok, ref}), do: {:ok, %TsaClient{ref: ref}} defp wrap_tsa(other), do: other defp wrap_pdf_a({:ok, ref}), do: {:ok, %PdfAResult{ref: ref}} defp wrap_pdf_a(other), do: other defp wrap_pdf_ua({:ok, ref}), do: {:ok, %PdfUaResult{ref: ref}} defp wrap_pdf_ua(other), do: other defp wrap_pdf_x({:ok, ref}), do: {:ok, %PdfXResult{ref: ref}} defp wrap_pdf_x(other), do: other defp wrap_barcode({:ok, ref}), do: {:ok, %Barcode{ref: ref}} defp wrap_barcode(other), do: other defp wrap_ocr({:ok, ref}), do: {:ok, %OcrEngine{ref: ref}} defp wrap_ocr(other), do: other defp wrap_renderer({:ok, ref}), do: {:ok, %Renderer{ref: ref}} defp wrap_renderer(other), do: other defp wrap_element_list({:ok, ref}), do: {:ok, %ElementList{ref: ref}} defp wrap_element_list(other), do: other # Collect `count`/`get(index)` validation strings into a plain list. `count` # returns a bare int; each `get` returns {:ok, binary} | {:error, code}. defp result_strings(ref, count_fun, get_fun) do n = count_fun.(ref) n = if is_integer(n) and n > 0, do: n, else: 0 for i <- 0..(n - 1)//1 do case get_fun.(ref, i) do {:ok, s} -> s _ -> "" end end end end