PdfElixide.Editor (pdf_elixide v0.23.0)

Copy Markdown View Source

Mutable, in-memory PDF editor.

Open a document, apply changes, and write the result:

"form.pdf"
|> PdfElixide.Editor.open!()
|> PdfElixide.Form.put_value!("name", "Ada")
|> PdfElixide.Editor.save!("filled.pdf")
|> PdfElixide.Editor.close()
#=> :ok

Nothing is written until save/3 or to_binary/2 runs, and neither consumes the editor. Mutating calls return the editor so they compose in a pipeline, except apply_redactions/1,2, sanitize/1,2 and PdfElixide.Compliance.convert/3, which return a report of what they did. close/1 discards unsaved edits.

Incremental saves support only field values and document information. Other pending changes require a full rewrite; see Saving edits for restrictions and errors.

An editor is a mutable handle: rebinding does not fork it. Earlier bindings see later edits too. The Forms guide covers both bang pipelines and tuple-returning calls, plus filling and deferred flattening.

Creating documents

from_markdown/2, from_html/2 and from_plain_text/2 lay text out on new pages and return an editor holding them. They are simple typesetters rather than round-trip conversions: lines are not wrapped, and only a limited Markdown or HTML vocabulary is recognised. from_images/2 places one JPEG or PNG image on each new page. See the Creating documents guide for examples, supported input and character limitations.

Page structure

delete_page/2, move_page/3 and keep_pages/2 use zero-based indices in the current page order. Deleting a page is not redaction, and bookmarks and links are not remapped. See Page structure.

Merging and splitting

merge/2 and merge_binary/2 append another document's pages, and refuse a document they cannot carry intact. extract_pages/2 and extract_page_ranges/2 write chosen pages to new binaries without changing the editor, and split_by_bookmarks/2 writes one per bookmarked section. See the Merging and splitting guide.

Page rotation

set_rotation/3, rotate_page_by/3 and rotate_all_by/2 change how a viewer displays pages; rotation/2 includes pending changes. See Page rotation for examples and page geometry.

Page boxes

media_box/2 and crop_box/2 read a page's boxes, pending changes included; set_media_box/3, set_crop_box/3 and crop_margins/2 change them. Setting the media box leaves an existing crop box alone. See Page boxes for what a viewer does with each box and the limitations.

Erasing regions

erase_region/3 and erase_regions/3 paint white rectangles when the document is written. Erasing is not redaction: covered content remains recoverable. Annotations remain above the overlay, and the page's graphics state can change its coverage even when the call succeeds. See Erasing regions for coordinates, unsupported pages, verifying coverage, and flattening before erasing.

Redaction

mark_redactions/1,2 schedules the page's /Redact annotations to be painted over when the document is written — cosmetic only, the text stays extractable. apply_redactions/1,2 removes covered text from the page's streams immediately and irreversibly, leaving covered images and vector graphics in the file. sanitize/1,2 separately strips document-level metadata, JavaScript and attachments without changing page content. See the Redaction guide for workflows and limitations.

Attachments

embed_file/4 adds an attachment and embedded_files/1 includes pending ones. Attaching to a document with an existing name tree is refused unless a sanitize/1,2 emptied it. See Attachments for the workflow and metadata limits.

Document information

set_title/2, set_author/2, set_subject/2, set_keywords/2, set_creator/2, set_producer/2, set_creation_date/2 and set_mod_date/2 change the /Info dictionary; metadata/1 reads it with pending changes. See Document information for what writes preserve and how text, dates, XMP and document identifiers behave.

Encryption

save/3 and to_binary/2 accept :encryption; open/2 and from_binary/2 accept the password of an already-encrypted source. See the Encryption guide for the workflow, supported algorithms and operations refused on an encrypted source.

PDF/A

PdfElixide.Compliance.convert/3 rewrites the editor's document in place and restricts later writes, such as incremental saves and encryption. See the PDF/A conversion guide for which edits to make first.

Concurrency

Every call that writes or mutates takes the handle's lock exclusively, so concurrent editing of a single editor serializes; give each process its own editor if you need them to work at once. Most accessors take the lock shared, but not all of them — redaction_count/2 is one that does not. The Concurrency guide lists which calls fall on which side.

Summary

Opening and lifecycle

Releases the editor's native memory without waiting for garbage collection.

Returns whether the editor has been released with close/1.

Opens a PDF document for editing from the given binary data.

Opens a PDF document for editing from the given binary data, raising an error if it fails.

Returns whether the editor holds changes that have not been written out.

Opens a PDF document for editing from the specified file path.

Opens a PDF document for editing from the specified file path, raising an error if it fails.

Returns the number of pages the editor currently holds.

Returns the number of pages the editor currently holds, raising an error if it fails.

Returns the file path the editor was opened from, or nil if it has no source file: one built with from_binary/2, from_markdown/2, from_html/2, from_plain_text/2 or from_images/2.

Returns the PDF specification version of the document being edited, as a {major, minor} tuple.

Creating documents

Creates a new document from HTML and opens it for editing.

Creates a new document from HTML and opens it for editing, raising an error if it fails.

Creates a new document from JPEG and PNG images and opens it for editing.

Creates a new document from JPEG and PNG images and opens it for editing, raising an error if it fails.

Creates a new document from Markdown and opens it for editing.

Creates a new document from Markdown and opens it for editing, raising an error if it fails.

Creates a new document from plain text and opens it for editing.

Creates a new document from plain text and opens it for editing, raising an error if it fails.

Saving

Writes the editor to a PDF file at the given path, and returns the editor.

Writes the editor to a PDF file at the given path, raising an error if it fails.

Serialises all in-memory changes into a PDF binary.

Serialises all in-memory changes into a PDF binary, raising an error if it fails.

Page structure

Deletes the page at the given zero-based index, and returns the editor.

Deletes the page at the given zero-based index, raising an error if it fails.

Keeps only the pages at the given zero-based indices, in the order given, and returns the editor.

Keeps only the pages at the given zero-based indices, in the order given, raising an error if it fails.

Moves the page at zero-based index from so that it sits at index to, and returns the editor.

Moves the page at zero-based index from so that it sits at index to, raising an error if it fails.

Merging and splitting

Plans a split of the editor's pages at the document's bookmarks, without writing anything, as PdfElixide.Document.bookmark_segments/2 does.

Plans a split of the editor's pages at the document's bookmarks, raising an error if it fails.

Writes each range of zero-based page indices to its own PDF binary, as extract_pages/2 does, and returns the binaries in the order of ranges.

Writes each range of zero-based page indices to its own PDF binary, raising an error if it fails.

Writes the pages at the given zero-based indices, in the order given, to a new PDF binary. The editor's pages and pending edits are not changed, but an editor with no unsaved edits can report modified?/1 as true afterwards if its document has an information dictionary, or if an attachment added with embed_file/4 has already been written.

Writes the pages at the given zero-based indices to a new PDF binary, raising an error if it fails.

Appends every page of the PDF file at path to the end of the editor, and returns the editor.

Appends every page of the PDF file at path to the end of the editor, raising an error if it fails.

Appends every page of a PDF binary to the end of the editor, and returns the editor. Behaves as merge/2 does for a file.

Appends every page of a PDF binary to the end of the editor, raising an error if it fails.

Splits the editor's pages at the document's bookmarks, writing each part to its own PDF binary as extract_page_ranges/2 does.

Splits the editor's pages at the document's bookmarks, raising an error if it fails.

Page rotation

Turns every page a further degrees clockwise from where it already is, and returns the editor.

Turns every page a further degrees clockwise, raising an error if it fails.

Turns the page at the given zero-based index a further degrees clockwise from where it already is, and returns the editor.

Turns the page at the given zero-based index a further degrees clockwise, raising an error if it fails.

Returns the clockwise display rotation of the page at the given zero-based index, as 0, 90, 180 or 270.

Returns the clockwise display rotation of the page at the given zero-based index, raising an error if it fails.

Sets the page at the given zero-based index to rotate by degrees clockwise, and returns the editor.

Sets the page at the given zero-based index to rotate by degrees clockwise, raising an error if it fails.

Page boxes

Returns the /CropBox of the page at the given zero-based index — the region a viewer displays and prints — including pending changes. For an unchanged page it behaves like PdfElixide.Document.Page.crop_box/1 and may return nil.

Returns the /CropBox of the page at the given zero-based index, or nil, raising an error if it fails.

Sets every page's /CropBox to its media box inset by margins, and returns the editor.

Sets every page's /CropBox to its media box inset by margins, raising an error if it fails.

Returns the /MediaBox of the page at the given zero-based index — the sheet it is imposed on — including pending changes. For an unchanged page it behaves like PdfElixide.Document.Page.media_box/1.

Returns the /MediaBox of the page at the given zero-based index, raising an error if it fails.

Sets the /CropBox of the page at the given zero-based index to rect, and returns the editor.

Sets the /CropBox of the page at the given zero-based index, raising an error if it fails.

Sets the /MediaBox of the page at the given zero-based index to rect, and returns the editor.

Sets the /MediaBox of the page at the given zero-based index, raising an error if it fails.

Erasing regions

Discards the regions pending on the page at the given zero-based index, so the next write paints nothing over it, and returns the editor.

Discards the regions pending on the page at the given zero-based index, raising an error if it fails.

Paints a white rectangle over rect on the page at the given zero-based index when the document is written, and returns the editor.

Paints a white rectangle over rect on the page at the given zero-based index, raising an error if it fails.

Paints a white rectangle over each of rects on the page at the given zero-based index when the document is written, and returns the editor.

Paints a white rectangle over each of rects on the page at the given zero-based index, raising an error if it fails.

Redaction

Queues rect on the page at the given zero-based index for removal by apply_redactions/1,2, and returns the editor.

Queues a region for removal by apply_redactions/1,2, raising an error if it fails.

Removes covered page text from marked pages and pages with queued regions, and returns a PdfElixide.RedactionReport. Glyphs whose boxes touch a region are deleted and an overlay is drawn over the cleared area.

Removes the content covered by every queued region, raising an error if it fails.

Marks every page so its redaction annotations are painted over when the document is written, and returns the editor.

Marks the page at the given zero-based index so each of its /Redact annotations is painted over when the document is written, and returns the editor.

Marks every page for redaction, raising an error if it fails.

Marks the page at the given zero-based index for redaction, raising an error if it fails.

Returns whether the page at the given zero-based index is marked for redaction.

Same as marked_for_redaction/2 but returns the bare boolean, raising on failure — a page that does not exist included. See the "Errors versus exceptions" section of PdfElixide.Error.

Returns how many redaction regions the page at the given zero-based index carries.

Returns how many redaction regions a page carries, raising an error if it fails.

Strips document-level metadata, JavaScript and embedded files, and returns a PdfElixide.SanitizeReport.

Strips document-level metadata, JavaScript and embedded files, raising an error if it fails.

Removes the redaction mark from the page at the given zero-based index, so the next write paints nothing over it, and returns the editor.

Removes the redaction mark from the page at the given zero-based index, raising an error if it fails.

Attachments

Attaches data to the document under name, and returns the editor.

Attaches data to the document under name, raising an error if it fails.

Lists the file attachments the edited document will carry, in name-tree order.

Lists the file attachments the edited document will carry, raising an error if it fails.

Document information

Reads the document information the edited document will carry: values given to the setters below over the source's /Info dictionary.

Reads the document information the edited document will carry, raising an error if it fails.

Sets the document's /Author and returns the editor. nil removes the entry.

Sets the document's /Author, raising an error if it fails.

Sets the document's /CreationDate and returns the editor.

Sets the document's /CreationDate, raising an error if it fails.

Sets the document's /Creator, the application that made the original document, and returns the editor. nil removes the entry.

Sets the document's /Creator, raising an error if it fails.

Sets the document's /Keywords and returns the editor. nil removes the entry.

Sets the document's /Keywords, raising an error if it fails.

Sets the document's /ModDate and returns the editor.

Sets the document's /ModDate, raising an error if it fails.

Sets the document's /Producer, the application that produced the PDF, and returns the editor. nil removes the entry.

Sets the document's /Producer, raising an error if it fails.

Sets the document's /Subject and returns the editor. nil removes the entry.

Sets the document's /Subject, raising an error if it fails.

Sets the document's /Title and returns the editor. nil removes the entry.

Sets the document's /Title, raising an error if it fails.

Flattening

Marks every page's annotations for flattening.

Marks the annotations of the page at the given zero-based index for flattening.

Marks every page's annotations for flattening, raising an error if it fails.

Marks the annotations of the page at the given zero-based index for flattening, raising an error if it fails.

Lists the warnings collected while flattening.

Lists the warnings collected while flattening, raising an error if it fails.

Types

Options accepted by from_markdown/2, from_html/2 and their bang variants.

Options for embed_file/4.

The :encryption option of save_opts/0.

A DateTime or complete PDF date string for set_creation_date/2 and set_mod_date/2. See Dates for formatting details.

Margins for crop_margins/2, in points.

Options accepted by open/2, open!/2, from_binary/2, and from_binary!/2.

The :permissions option of encryption_opts/0.

Options for apply_redactions/1,2 and apply_redactions!/1,2. See apply_redactions/2 for defaults and allowed values.

Options for sanitize/1,2 and sanitize!/1,2. See sanitize/2 for defaults and allowed values.

Options accepted by save/3, save!/3, to_binary/2, and to_binary!/2.

t()

An open editor.

Opening and lifecycle

close(editor)

@spec close(t()) :: :ok

Releases the editor's native memory without waiting for garbage collection.

An editor holds the source document plus its pending edits in native memory, normally freed only when the BEAM garbage-collects the handle. close/1 frees it early, which matters for long-lived processes that open many documents. Calling it is optional and idempotent. It waits for an in-flight call on the same editor — a save can hold the handle's lock for seconds — and releases the memory as soon as the handle is idle, not preemptively.

Unsaved edits are discarded — call save/3 or to_binary/2 first. Afterwards, functions that read or mutate the editor return {:error, %PdfElixide.Error{reason: :closed}}, and their bang variants raise it. source_path/1 and version/1 keep working, since they read the struct rather than the native handle.

"form.pdf"
|> PdfElixide.Editor.open!()
|> PdfElixide.Form.put_value!("name", "Ada")
|> PdfElixide.Editor.save!("filled.pdf")
|> PdfElixide.Editor.close()
#=> :ok

closed?(editor)

@spec closed?(t()) :: boolean()

Returns whether the editor has been released with close/1.

from_binary(bytes, opts \\ [])

@spec from_binary(binary(), open_opts()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Opens a PDF document for editing from the given binary data.

Takes bytes you already have — an HTTP response body, a database blob — so no path is involved; use open/2 to read a file.

Encrypted bytes need a password, as in open/2.

Incremental saves are unsupported; see Saving edits.

from_binary!(bytes, opts \\ [])

@spec from_binary!(binary(), open_opts()) :: t()

Opens a PDF document for editing from the given binary data, raising an error if it fails.

Takes bytes you already have — an HTTP response body, a database blob — so no path is involved; use open!/2 to read a file.

Raises for encrypted bytes opened without their password, as in open/2.

Incremental saves are unsupported, as in from_binary/2.

modified?(editor)

@spec modified?(t()) :: boolean()

Returns whether the editor holds changes that have not been written out.

false for a freshly opened editor, and true once something has changed it — PdfElixide.Form.put_value/3, say.

A full rewrite clears it again, so save/3 and to_binary/2 both leave the editor unmodified — to_binary/2 included, even though it writes no file. An incremental save/3 does not: after save(editor, path, incremental: true) the flag stays true. A refused save changes nothing at all.

open(path, opts \\ [])

@spec open(Path.t(), open_opts()) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Opens a PDF document for editing from the specified file path.

An encrypted document needs its password, as open_opts/0 describes; without one the call returns {:error, %PdfElixide.Error{reason: :encrypted}}.

The path is handed to the operating system unchanged — see the "File paths" section of PdfElixide.

open!(path, opts \\ [])

@spec open!(Path.t(), open_opts()) :: t()

Opens a PDF document for editing from the specified file path, raising an error if it fails.

Raises for an encrypted document opened without its password; open/2 describes why.

The path is handed to the operating system unchanged — see the "File paths" section of PdfElixide.

page_count(editor)

@spec page_count(t()) :: {:ok, non_neg_integer()} | {:error, PdfElixide.Error.t()}

Returns the number of pages the editor currently holds.

Counts the pages as edited rather than as found on disk, so unlike version/1 this asks the editor on every call.

Returns {:error, %PdfElixide.Error{reason: :closed}} after close/1.

page_count!(editor)

@spec page_count!(t()) :: non_neg_integer()

Returns the number of pages the editor currently holds, raising an error if it fails.

source_path(editor)

@spec source_path(t()) :: Path.t() | nil

Returns the file path the editor was opened from, or nil if it has no source file: one built with from_binary/2, from_markdown/2, from_html/2, from_plain_text/2 or from_images/2.

version(editor)

@spec version(t()) :: {non_neg_integer(), non_neg_integer()}

Returns the PDF specification version of the document being edited, as a {major, minor} tuple.

This is the version of the document the editor was opened from, which editing does not change. It is read from the struct, so it keeps working after close/1.

Encryption does not raise the output version; see The declared version is not raised to match.

Creating documents

from_html(content, opts \\ [])

@spec from_html(String.t(), create_opts()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Creates a new document from HTML and opens it for editing.

Only a small set of bare tags is recognised, and it is not a browser: see Creating documents for what is kept and what appears as it was written.

"<h1>Report</h1><p>All <b>good</b>.</p>"
|> PdfElixide.Editor.from_html!()
|> PdfElixide.Editor.to_binary!()

from_html!(content, opts \\ [])

@spec from_html!(String.t(), create_opts()) :: t()

Creates a new document from HTML and opens it for editing, raising an error if it fails.

from_images(images, opts \\ [])

@spec from_images([binary()], image_create_opts()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Creates a new document from JPEG and PNG images and opens it for editing.

Each binary in images becomes one page, in list order. Every page has the same size, and each image is scaled, keeping its proportions, to fill the area inside the margins and is centred there. Pages are not sized to their images.

A JPEG is embedded without re-encoding. A PNG's pixels are stored losslessly, with transparency kept and 16-bit channels reduced to 8 bits. An EXIF orientation tag is not applied, and most CMYK JPEGs come out with inverted colours. See Images for these, and for making a page that matches one image.

jpeg = File.read!("scan.jpg")

[jpeg]
|> PdfElixide.Editor.from_images!(margin_top: 36, margin_bottom: 36)
|> PdfElixide.Editor.save!("scan.pdf")
|> PdfElixide.Editor.close()

These return {:error, %PdfElixide.Error{reason: :unsupported}}:

  • an image that is neither JPEG nor PNG;
  • an image over 128 million pixels, or 32 million on a 32-bit system, or a 16-bit colour PNG over about 89 million (67 million with transparency). Scale it down first.

An image that cannot be decoded, including a 12-bit, arithmetic-coded or lossless JPEG, returns {:error, %PdfElixide.Error{reason: :other}}. Each error message gives the image's zero-based position in the list. An empty list, or an element that is not a binary, raises ArgumentError.

The images are held in memory several times over while the document is built, and the editor keeps the whole document until close/1. Close it once it has been written.

from_images!(images, opts \\ [])

@spec from_images!([binary()], image_create_opts()) :: t()

Creates a new document from JPEG and PNG images and opens it for editing, raising an error if it fails.

from_markdown(content, opts \\ [])

@spec from_markdown(String.t(), create_opts()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Creates a new document from Markdown and opens it for editing.

Headings, emphasis, lists, quotes, code and tables are laid out on as many pages as the text needs. The result is an editor like one from from_binary/2: write it with save/3 or to_binary/2, or edit it first.

"# Report\n\nAll **good**."
|> PdfElixide.Editor.from_markdown!(title: "Report")
|> PdfElixide.Editor.save!("report.pdf")

Only part of Markdown is recognised, and lines are not wrapped; see Creating documents.

from_markdown!(content, opts \\ [])

@spec from_markdown!(String.t(), create_opts()) :: t()

Creates a new document from Markdown and opens it for editing, raising an error if it fails.

from_plain_text(content, opts \\ [])

@spec from_plain_text(String.t(), plain_text_create_opts()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Creates a new document from plain text and opens it for editing.

Each line of content becomes a line on the page, in 12-point Helvetica, with no markup interpreted.

Only Windows-1252 characters can be set this way. Text containing any other character, such as Cyrillic or Greek, returns {:error, %PdfElixide.Error{reason: :unsupported}} naming the first one. A leading byte order mark counts, so strip it from text read from a file. from_markdown/2 renders many of those characters; see Creating documents for which.

from_plain_text!(content, opts \\ [])

@spec from_plain_text!(String.t(), plain_text_create_opts()) :: t()

Creates a new document from plain text and opens it for editing, raising an error if it fails.

Saving

save(editor, path, opts \\ [])

@spec save(t(), Path.t(), save_opts()) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Writes the editor to a PDF file at the given path, and returns the editor.

A full rewrite is the default. incremental: true returns {:error, %PdfElixide.Error{reason: :unsupported}} without writing if the editor has no source file — it came from from_binary/2, from_markdown/2, from_html/2, from_plain_text/2 or from_images/2 — or holds unsupported changes. See Saving edits for supported changes and recovery.

Writing does not consume the editor: you can keep editing and write again.

Pass :encryption to write a password-protected PDF. It cannot be combined with incremental: true, and a successful return does not by itself prove every object was encrypted — see A failed encryption is not reported.

A source that stores its XMP metadata unencrypted is refused; see What an encrypted source cannot do.

The path is handed to the operating system unchanged — see the "File paths" section of PdfElixide.

save!(editor, path, opts \\ [])

@spec save!(t(), Path.t(), save_opts()) :: t()

Writes the editor to a PDF file at the given path, raising an error if it fails.

Uses the same write modes and refusals as save/3; see Saving edits.

The path is handed to the operating system unchanged — see the "File paths" section of PdfElixide.

to_binary(editor, opts \\ [])

@spec to_binary(t(), save_opts()) :: {:ok, binary()} | {:error, PdfElixide.Error.t()}

Serialises all in-memory changes into a PDF binary.

The result is a fully self-contained PDF that can be written to disk, stored in a database, or streamed over HTTP.

Accepts the same save_opts/0 keyword list as save/3, except incremental: true, which returns {:error, %PdfElixide.Error{reason: :invalid_pdf}}: an incremental update is an append to the original file, so there is nothing to append to in memory. Use save/3 for an incremental write; see Saving edits.

That includes :encryption, which encrypts the returned binary exactly as it encrypts a file — including the caveat in A failed encryption is not reported.

A source that stores its XMP metadata unencrypted is refused, as in save/3.

The whole document is serialised in native memory before being copied into the returned binary, so peak usage is roughly twice the output size (on top of the editor itself). For very large documents prefer save/3, which streams to the file without that second buffer.

to_binary!(editor, opts \\ [])

@spec to_binary!(t(), save_opts()) :: binary()

Serialises all in-memory changes into a PDF binary, raising an error if it fails.

Page structure

delete_page(editor, page_index)

@spec delete_page(t(), non_neg_integer()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Deletes the page at the given zero-based index, and returns the editor.

Every later page moves down one index, and page_count/1 reflects the removal at once — no save is needed. See Page structure and Saving edits for the deletion's security limitation and the incremental-save refusal.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist.

delete_page!(editor, page_index)

@spec delete_page!(t(), non_neg_integer()) :: t()

Deletes the page at the given zero-based index, raising an error if it fails.

keep_pages(editor, pages)

@spec keep_pages(t(), [non_neg_integer(), ...]) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Keeps only the pages at the given zero-based indices, in the order given, and returns the editor.

keep_pages(editor, [2, 0]) on a three-page document leaves two pages: the old last page, then the old first. Pending edits stay with their pages. Dropping a page is not redaction; see Page structure.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if an index does not exist, or :invalid_pdf if a listed page cannot be read. Raises ArgumentError for an empty list or a repeated index.

keep_pages!(editor, pages)

@spec keep_pages!(t(), [non_neg_integer(), ...]) :: t()

Keeps only the pages at the given zero-based indices, in the order given, raising an error if it fails.

move_page(editor, from, to)

@spec move_page(t(), non_neg_integer(), non_neg_integer()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Moves the page at zero-based index from so that it sits at index to, and returns the editor.

to is where the page ends up once it has been lifted out, so move_page(editor, 0, 2) on a three-page document leaves the first page last. The pages it passes over shift by one to fill the gap; nothing else changes.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if either index does not exist. See Page structure for what a move does not update and Saving edits for the incremental-save refusal.

move_page!(editor, from, to)

@spec move_page!(t(), non_neg_integer(), non_neg_integer()) :: t()

Moves the page at zero-based index from so that it sits at index to, raising an error if it fails.

Merging and splitting

bookmark_segments(editor, opts \\ [])

@spec bookmark_segments(t(), PdfElixide.Document.bookmark_opts()) ::
  {:ok, [PdfElixide.Document.BookmarkSegment.t()]}
  | {:error, PdfElixide.Error.t()}

Plans a split of the editor's pages at the document's bookmarks, without writing anything, as PdfElixide.Document.bookmark_segments/2 does.

Segments follow the editor's current page order, and bookmarks on removed pages are skipped. split_by_bookmarks/2 writes the same parts. See Splitting at bookmarks.

bookmark_segments!(editor, opts \\ [])

Plans a split of the editor's pages at the document's bookmarks, raising an error if it fails.

extract_page_ranges(editor, ranges)

@spec extract_page_ranges(t(), [Range.t(), ...]) ::
  {:ok, [binary()]} | {:error, PdfElixide.Error.t()}

Writes each range of zero-based page indices to its own PDF binary, as extract_pages/2 does, and returns the binaries in the order of ranges.

Each range is inclusive with a step of 1, so [0..9, 10..19] splits a twenty-page document in two. Ranges may overlap. All ranges are checked before anything is written.

Every binary is held in memory until the call returns. To keep only one at a time, call extract_pages/2 once per range. See Parts and chunks.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if a range reaches past the last page, or :invalid_pdf if a listed page cannot be read. Raises ArgumentError for an empty list, an empty range or a range whose step is not 1.

extract_page_ranges!(editor, ranges)

@spec extract_page_ranges!(t(), [Range.t(), ...]) :: [binary()]

Writes each range of zero-based page indices to its own PDF binary, raising an error if it fails.

extract_pages(editor, pages)

@spec extract_pages(t(), [non_neg_integer(), ...]) ::
  {:ok, binary()} | {:error, PdfElixide.Error.t()}

Writes the pages at the given zero-based indices, in the order given, to a new PDF binary. The editor's pages and pending edits are not changed, but an editor with no unsaved edits can report modified?/1 as true afterwards if its document has an information dictionary, or if an attachment added with embed_file/4 has already been written.

The result includes pending edits, like to_binary/2 with its default options, and is never encrypted. Extraction is not a way to remove confidential content; see Extraction is not redaction.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if an index does not exist, or :invalid_pdf if a listed page cannot be read. Raises ArgumentError for an empty list or a repeated index. A source that stores its XMP metadata unencrypted is refused, as in save/3.

extract_pages!(editor, pages)

@spec extract_pages!(t(), [non_neg_integer(), ...]) :: binary()

Writes the pages at the given zero-based indices to a new PDF binary, raising an error if it fails.

merge(editor, path)

@spec merge(t(), Path.t()) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Appends every page of the PDF file at path to the end of the editor, and returns the editor.

A merge incorporates pending edits into the combined document. Afterwards, modified?/1 reports true until the next full write, and an incremental save/3 is refused. Documents whose pages cannot be carried safely return :unsupported without changing the editor; encrypted documents return :encrypted. See Merging documents for the complete behavior and limitations.

The path is handed to the operating system unchanged — see the "File paths" section of PdfElixide.

merge!(editor, path)

@spec merge!(t(), Path.t()) :: t()

Appends every page of the PDF file at path to the end of the editor, raising an error if it fails.

merge_binary(editor, bytes)

@spec merge_binary(t(), binary()) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Appends every page of a PDF binary to the end of the editor, and returns the editor. Behaves as merge/2 does for a file.

merge_binary!(editor, bytes)

@spec merge_binary!(t(), binary()) :: t()

Appends every page of a PDF binary to the end of the editor, raising an error if it fails.

split_by_bookmarks(editor, opts \\ [])

@spec split_by_bookmarks(t(), PdfElixide.Document.bookmark_opts()) ::
  {:ok, [{PdfElixide.Document.BookmarkSegment.t(), binary()}]}
  | {:error, PdfElixide.Error.t()}

Splits the editor's pages at the document's bookmarks, writing each part to its own PDF binary as extract_page_ranges/2 does.

Returns a {segment, binary} pair per part, where each PdfElixide.Document.BookmarkSegment is the one bookmark_segments/2 plans, and {:ok, []} when there are no bookmarks or none matches opts; see PdfElixide.Document.bookmark_opts/0. The editor's pages and pending edits are not changed, but modified?/1 can report true afterwards, as after extract_pages/2.

Parts may retain pages outside their visible page range through the document's bookmarks. Do not use splitting to separate confidential content; see Extraction is not redaction.

Every binary is held in memory until the call returns. See Splitting at bookmarks, which also shows how to write one part at a time.

Returns {:error, %PdfElixide.Error{reason: :invalid_pdf}} if a page cannot be read. A source that stores its XMP metadata unencrypted is refused, as in save/3.

split_by_bookmarks!(editor, opts \\ [])

Splits the editor's pages at the document's bookmarks, raising an error if it fails.

Page rotation

rotate_all_by(editor, degrees)

@spec rotate_all_by(t(), integer()) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Turns every page a further degrees clockwise from where it already is, and returns the editor.

Each page is turned from its own current rotation, so a document whose pages disagree keeps them disagreeing. degrees is a delta and is accepted on the same terms as rotate_page_by/3. On a document with no pages this changes nothing and succeeds.

If any page's rotation cannot be read, the call fails without changing the editor.

See Page rotation.

rotate_all_by!(editor, degrees)

@spec rotate_all_by!(t(), integer()) :: t()

Turns every page a further degrees clockwise, raising an error if it fails.

rotate_page_by(editor, page_index, degrees)

@spec rotate_page_by(t(), non_neg_integer(), integer()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Turns the page at the given zero-based index a further degrees clockwise from where it already is, and returns the editor.

degrees is a delta rather than an absolute angle, so rotate_page_by(e, 0, 90) takes a page already at 180 to 270. It must be a multiple of 90 — anything else raises FunctionClauseError. A negative delta turns anticlockwise and one past 360 wraps.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist. See Page rotation.

rotate_page_by!(editor, page_index, degrees)

@spec rotate_page_by!(t(), non_neg_integer(), integer()) :: t()

Turns the page at the given zero-based index a further degrees clockwise, raising an error if it fails.

rotation(editor, page_index)

@spec rotation(t(), non_neg_integer()) ::
  {:ok, PdfElixide.Document.Page.rotation()} | {:error, PdfElixide.Error.t()}

Returns the clockwise display rotation of the page at the given zero-based index, as 0, 90, 180 or 270.

It reflects pending edits immediately. For unchanged pages, it matches PdfElixide.Document.Page.rotation/1, including inheritance and normalization.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist.

rotation!(editor, page_index)

Returns the clockwise display rotation of the page at the given zero-based index, raising an error if it fails.

set_rotation(editor, page_index, degrees)

@spec set_rotation(t(), non_neg_integer(), PdfElixide.Document.Page.rotation()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the page at the given zero-based index to rotate by degrees clockwise, and returns the editor.

degrees is absolute, not a delta, and must be 0, 90, 180 or 270 — anything else raises FunctionClauseError. Use rotate_page_by/3 to turn a page relative to where it already is.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist. See Page rotation.

set_rotation!(editor, page_index, degrees)

@spec set_rotation!(t(), non_neg_integer(), PdfElixide.Document.Page.rotation()) ::
  t()

Sets the page at the given zero-based index to rotate by degrees clockwise, raising an error if it fails.

Page boxes

crop_box(editor, page_index)

@spec crop_box(t(), non_neg_integer()) ::
  {:ok, PdfElixide.Geometry.Rect.t() | nil} | {:error, PdfElixide.Error.t()}

Returns the /CropBox of the page at the given zero-based index — the region a viewer displays and prints — including pending changes. For an unchanged page it behaves like PdfElixide.Document.Page.crop_box/1 and may return nil.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist. See Page boxes.

crop_box!(editor, page_index)

@spec crop_box!(t(), non_neg_integer()) :: PdfElixide.Geometry.Rect.t() | nil

Returns the /CropBox of the page at the given zero-based index, or nil, raising an error if it fails.

crop_margins(editor, margins)

@spec crop_margins(t(), margins()) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets every page's /CropBox to its media box inset by margins, and returns the editor.

It uses the normalized boxes from media_box/2, including pending changes, and replaces existing crop boxes. All results are computed before any page is changed, so a failure leaves every page unchanged. See Page boxes.

crop_margins!(editor, margins)

@spec crop_margins!(t(), margins()) :: t()

Sets every page's /CropBox to its media box inset by margins, raising an error if it fails.

media_box(editor, page_index)

@spec media_box(t(), non_neg_integer()) ::
  {:ok, PdfElixide.Geometry.Rect.t()} | {:error, PdfElixide.Error.t()}

Returns the /MediaBox of the page at the given zero-based index — the sheet it is imposed on — including pending changes. For an unchanged page it behaves like PdfElixide.Document.Page.media_box/1.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist. See Page boxes.

media_box!(editor, page_index)

@spec media_box!(t(), non_neg_integer()) :: PdfElixide.Geometry.Rect.t()

Returns the /MediaBox of the page at the given zero-based index, raising an error if it fails.

set_crop_box(editor, page_index, rect)

@spec set_crop_box(t(), non_neg_integer(), PdfElixide.Geometry.Rect.t()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the /CropBox of the page at the given zero-based index to rect, and returns the editor.

Reversed corners are normalized. A rectangle whose corners do not fit a 32-bit float raises ArgumentError.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist. See Page boxes.

set_crop_box!(editor, page_index, rect)

@spec set_crop_box!(t(), non_neg_integer(), PdfElixide.Geometry.Rect.t()) :: t()

Sets the /CropBox of the page at the given zero-based index, raising an error if it fails.

set_media_box(editor, page_index, rect)

@spec set_media_box(t(), non_neg_integer(), PdfElixide.Geometry.Rect.t()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the /MediaBox of the page at the given zero-based index to rect, and returns the editor.

Reversed corners are normalized. A rectangle whose corners do not fit a 32-bit float raises ArgumentError.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist. See Page boxes.

set_media_box!(editor, page_index, rect)

@spec set_media_box!(t(), non_neg_integer(), PdfElixide.Geometry.Rect.t()) :: t()

Sets the /MediaBox of the page at the given zero-based index, raising an error if it fails.

Erasing regions

clear_erase_regions(editor, page_index)

@spec clear_erase_regions(t(), non_neg_integer()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Discards the regions pending on the page at the given zero-based index, so the next write paints nothing over it, and returns the editor.

modified?/1 is left as it was, but the page stops blocking an incremental save/3. Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist. See Erasing regions.

clear_erase_regions!(editor, page_index)

@spec clear_erase_regions!(t(), non_neg_integer()) :: t()

Discards the regions pending on the page at the given zero-based index, raising an error if it fails.

erase_region(editor, page_index, rect)

@spec erase_region(t(), non_neg_integer(), PdfElixide.Geometry.Rect.t()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Paints a white rectangle over rect on the page at the given zero-based index when the document is written, and returns the editor.

See Erasing regions for the coordinate system and coverage limitations. Reversed corners are normalized. A rectangle whose corners do not fit a 32-bit float raises ArgumentError.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist, and {:error, %PdfElixide.Error{reason: :unsupported}} if the page stores its content streams as an indirect array. Nothing is recorded in the latter case.

erase_region!(editor, page_index, rect)

@spec erase_region!(t(), non_neg_integer(), PdfElixide.Geometry.Rect.t()) :: t()

Paints a white rectangle over rect on the page at the given zero-based index, raising an error if it fails.

erase_regions(editor, page_index, rects)

@spec erase_regions(t(), non_neg_integer(), [PdfElixide.Geometry.Rect.t(), ...]) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Paints a white rectangle over each of rects on the page at the given zero-based index when the document is written, and returns the editor.

Accepts a nonempty list of PdfElixide.Geometry.Rect with the same validation, errors and coverage limitations as erase_region/3. An empty list raises ArgumentError.

erase_regions!(editor, page_index, rects)

@spec erase_regions!(t(), non_neg_integer(), [PdfElixide.Geometry.Rect.t(), ...]) ::
  t()

Paints a white rectangle over each of rects on the page at the given zero-based index, raising an error if it fails.

Redaction

add_redaction(editor, page_index, rect, fill \\ nil)

@spec add_redaction(
  t(),
  non_neg_integer(),
  PdfElixide.Geometry.Rect.t(),
  PdfElixide.Color.RGB.t() | nil
) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Queues rect on the page at the given zero-based index for removal by apply_redactions/1,2, and returns the editor.

fill is the colour of the block drawn over the cleared area, each component between 0.0 and 1.0; nil uses the :default_fill given to apply_redactions/2.

rect is in raw, unrotated page space, as returned by PdfElixide.Document.chars/1, PdfElixide.Document.spans/1 and PdfElixide.Document.paths/1. For other extractors, see Rotated pages and extracted geometry; a rectangle in the wrong frame is accepted without error.

Saving alone does not apply the queued rectangle. Queuing also marks the page: if it has /Redact annotations, a full write paints their rectangles and removes all annotations. Without them, the mark changes nothing. See Queuing your own regions.

A queued region cannot be withdrawn, including with unmark_redactions/2. Reopen the source to start again. An incremental save/3 is refused for the life of the editor from the first queued region, since nothing can take it back; see Saving edits.

Reversed corners are normalized. A rectangle whose corners do not fit a 32-bit float raises ArgumentError, and so does a fill component outside 0.0..1.0. Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist, and {:error, %PdfElixide.Error{reason: :unsupported}}, queuing nothing, if the page stores its content in an indirect object that is not itself a content stream, or if the editor was opened from an encrypted document. Unlike mark_redactions/2, both restrictions apply even when the page has no redaction annotations. See What an encrypted source cannot do.

add_redaction!(editor, page_index, rect, fill \\ nil)

@spec add_redaction!(
  t(),
  non_neg_integer(),
  PdfElixide.Geometry.Rect.t(),
  PdfElixide.Color.RGB.t() | nil
) :: t()

Queues a region for removal by apply_redactions/1,2, raising an error if it fails.

apply_redactions(editor, opts \\ [])

@spec apply_redactions(t(), redaction_opts()) ::
  {:ok, PdfElixide.RedactionReport.t()} | {:error, PdfElixide.Error.t()}

Removes covered page text from marked pages and pages with queued regions, and returns a PdfElixide.RedactionReport. Glyphs whose boxes touch a region are deleted and an overlay is drawn over the cleared area.

Immediate and irreversible. The call rewrites content in the editor; saving is not required to apply it. Queue every region first: only one pass is allowed per editor. A second call returns {:error, %PdfElixide.Error{reason: :unsupported}} without changes.

Refused on an encrypted source; nothing is changed and the editor stays usable. See What an encrypted source cannot do.

Only glyphs drawn by the page itself are removed. Covered images, vector graphics, form XObject text and /ActualText remain recoverable. Redact the source document if those contain sensitive content. An unfinished path can extend the overlay beyond its rectangle; see Where the block lands and Verifying.

A /Redact annotation alone does not schedule a page: use mark_redactions/1,2 or add_redaction/3,4 first. Deleting a page does not cancel its marks or regions, and its results still count in the report. Mark and queue after deletions, or reopen the source.

Rewritten pages lose all annotations, whose objects remain recoverable; see Annotation removal. They also lose pending erase and annotation-flatten overlays; see Pending overlays.

Incremental saves are refused afterwards: the update would leave the removed content readable. Metadata, JavaScript and embedded files require a separate sanitize/1,2 call.

Returns {:error, %PdfElixide.Error{reason: :unsupported}} for undefined fonts, composite fonts other than horizontal Identity-H, or text measured with state discarded by a q … Q restore. A font refusal can leave earlier pages rewritten: discard the editor and reopen the source. The text-state check leaves the editor unchanged. See Refused pages.

Options

  • :edge_padding — how far past a region's edge to clear, in points, as a floor on a proportional padding. Must be a non-negative number. Defaults to 0.5.
  • :default_fill — the PdfElixide.Color.RGB drawn over a region queued with add_redaction/3,4 and given no fill. Each component must be between 0.0 and 1.0. Defaults to black. It never reaches a region taken from a /Redact annotation: that carries a colour of its own, its /IC or black where it declares none.
  • :draw_default_overlay — draw :default_fill over such a region at all. false leaves the cleared area blank. The same scope: an annotation's region is painted either way. Defaults to true.

apply_redactions!(editor, opts \\ [])

@spec apply_redactions!(t(), redaction_opts()) :: PdfElixide.RedactionReport.t()

Removes the content covered by every queued region, raising an error if it fails.

mark_redactions(editor)

@spec mark_redactions(t()) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Marks every page so its redaction annotations are painted over when the document is written, and returns the editor.

Equivalent to mark_redactions/2 on each page. Returns {:error, %PdfElixide.Error{reason: :unsupported}}, marking nothing, if any page would be refused there.

mark_redactions(editor, page_index)

@spec mark_redactions(t(), non_neg_integer()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Marks the page at the given zero-based index so each of its /Redact annotations is painted over when the document is written, and returns the editor.

This is not redaction. The annotation's rectangle is filled with its /IC colour, or black if it declares none, over the existing page content — the covered text and images remain in the written file and PdfElixide.Document.text/1 still returns the covered words. Use apply_redactions/1,2 to remove covered page text.

The mark is deferred: nothing happens until the next full write, save/3 without :incremental or to_binary/2. An incremental save/3 is refused while the mark is pending, since it could only write the original back unmarked. unmark_redactions/2 takes the mark back, and with it the refusal.

A page with no redaction annotations is marked and paints nothing. A page that has them writes without any annotations at all — links and form widgets go with them — so read the Redaction guide before marking a page whose annotations matter.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist, and {:error, %PdfElixide.Error{reason: :unsupported}} if it has redaction annotations and stores its content streams in an indirect object that is not itself a content stream. Nothing is recorded in the latter case.

mark_redactions!(editor)

@spec mark_redactions!(t()) :: t()

Marks every page for redaction, raising an error if it fails.

mark_redactions!(editor, page_index)

@spec mark_redactions!(t(), non_neg_integer()) :: t()

Marks the page at the given zero-based index for redaction, raising an error if it fails.

marked_for_redaction(editor, page_index)

@spec marked_for_redaction(t(), non_neg_integer()) ::
  {:ok, boolean()} | {:error, PdfElixide.Error.t()}

Returns whether the page at the given zero-based index is marked for redaction.

mark_redactions/1,2 and add_redaction/3,4 both mark a page, and so does apply_redactions/1,2 for every page it actually redacted — that one answers true afterwards whether or not it was marked before the pass.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist.

marked_for_redaction?(editor, page_index)

@spec marked_for_redaction?(t(), non_neg_integer()) :: boolean()

Same as marked_for_redaction/2 but returns the bare boolean, raising on failure — a page that does not exist included. See the "Errors versus exceptions" section of PdfElixide.Error.

redaction_count(editor, page_index)

@spec redaction_count(t(), non_neg_integer()) ::
  {:ok, non_neg_integer()} | {:error, PdfElixide.Error.t()}

Returns how many redaction regions the page at the given zero-based index carries.

Counts each of the page's /Redact annotations that carries a /Rect — one without it is not a region and is not counted — plus the regions added with add_redaction/3,4. An annotation contributes one region however many quadrilaterals it declares.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist.

redaction_count!(editor, page_index)

@spec redaction_count!(t(), non_neg_integer()) :: non_neg_integer()

Returns how many redaction regions a page carries, raising an error if it fails.

sanitize(editor, opts \\ [])

@spec sanitize(t(), sanitize_opts()) ::
  {:ok, PdfElixide.SanitizeReport.t()} | {:error, PdfElixide.Error.t()}

Strips document-level metadata, JavaScript and embedded files, and returns a PdfElixide.SanitizeReport.

This removes nothing from any page. Text, images and annotations are left as they are; only catalog-level and /Info entries go. Use apply_redactions/1,2 to remove page content.

Like apply_redactions/1,2 this takes effect immediately and cannot be undone, and an incremental save/3 is refused afterwards: the update would leave the removed content readable.

garbage_collect: false on save/3 or to_binary/2 is refused afterwards with {:error, %PdfElixide.Error{reason: :unsupported}}: a full rewrite with garbage collection is required to remove the scrubbed objects.

With :scrub_metadata, an indirect /Info value returns {:error, %PdfElixide.Error{reason: :unsupported}} without changes. Use scrub_metadata: false to strip the rest, or rewrite the metadata first. See Sanitization refusals.

Options

  • :scrub_metadata — clear /Info and the catalog's XMP metadata. Defaults to true.
  • :remove_javascript — remove document JavaScript, /OpenAction and /AA. Defaults to true.
  • :remove_embedded_files — remove the embedded-file name tree, and discard any file queued with embed_file/4 that has not been written yet. Emptying the tree also lets embed_file/4 attach to a document that would otherwise be refused; see Attachments. A /FileAttachment annotation is left alone and its bytes stay in the file, and nothing in this library removes them; see Sanitizing. Defaults to true.

sanitize!(editor, opts \\ [])

@spec sanitize!(t(), sanitize_opts()) :: PdfElixide.SanitizeReport.t()

Strips document-level metadata, JavaScript and embedded files, raising an error if it fails.

unmark_redactions(editor, page_index)

@spec unmark_redactions(t(), non_neg_integer()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Removes the redaction mark from the page at the given zero-based index, so the next write paints nothing over it, and returns the editor.

modified?/1 is left as it was, but the page stops blocking an incremental save/3. A region added with add_redaction/3,4 is not withdrawn — the page keeps it, apply_redactions/1,2 still removes its content, and it goes on blocking an incremental save/3. Nothing can withdraw a queued region; reopen the source instead.

Returns {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist.

unmark_redactions!(editor, page_index)

@spec unmark_redactions!(t(), non_neg_integer()) :: t()

Removes the redaction mark from the page at the given zero-based index, raising an error if it fails.

Attachments

embed_file(editor, name, data, opts \\ [])

@spec embed_file(t(), String.t(), binary(), embed_opts()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Attaches data to the document under name, and returns the editor.

The attachment is written into the document's /Names /EmbeddedFiles name tree by the next full write — save/3 without :incremental, or to_binary/2 — and read back with PdfElixide.Document.embedded_files/1. It is a file the document carries, not page content: nothing about the pages changes and nothing displays it, though a viewer will offer it for saving.

name must be a non-empty UTF-8 string; anything else raises. Attaching two files under the same name is allowed and produces a document declaring both, which readers resolve inconsistently — use distinct names.

Returns {:error, %PdfElixide.Error{reason: :unsupported}} for a document that already has a name tree, naming the entries that would be lost. A sanitize/1,2 that emptied the tree lifts the refusal; one that left an entry in it does not. See Attachments for this restriction and the media-type limitation, and Saving edits for the incremental-save refusal.

Attachment data is copied into native memory and increases peak memory during writes, so measure large attachments before adding several.

embed_file!(editor, name, data, opts \\ [])

@spec embed_file!(t(), String.t(), binary(), embed_opts()) :: t()

Attaches data to the document under name, raising an error if it fails.

embedded_files(editor)

@spec embedded_files(t()) ::
  {:ok, [PdfElixide.Document.EmbeddedFile.t()]} | {:error, PdfElixide.Error.t()}

Lists the file attachments the edited document will carry, in name-tree order.

This reflects pending edits: an attachment added with embed_file/4 appears here before any save, in the order a full write will place it, and a sanitize/1,2 that removed the attachments empties the list. Its :size, :checksum, :created and :modified remain nil, even after a save. Read those fields with PdfElixide.Document.embedded_files/1 on the written document.

Malformed or excessively complex name trees return an error. See PdfElixide.Document.EmbeddedFile for the fields and for the memory the result holds.

embedded_files!(editor)

@spec embedded_files!(t()) :: [PdfElixide.Document.EmbeddedFile.t()]

Lists the file attachments the edited document will carry, raising an error if it fails.

Document information

metadata(editor)

@spec metadata(t()) ::
  {:ok, PdfElixide.Document.Metadata.t()} | {:error, PdfElixide.Error.t()}

Reads the document information the edited document will carry: values given to the setters below over the source's /Info dictionary.

:trapped is the source's value until sanitize/1,2 clears it. It cannot be set, and any write that emits /Info drops it. A sanitize/1,2 that scrubbed the metadata leaves every field here nil. See Document information.

metadata!(editor)

@spec metadata!(t()) :: PdfElixide.Document.Metadata.t()

Reads the document information the edited document will carry, raising an error if it fails.

set_author(editor, author)

@spec set_author(t(), String.t() | nil) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the document's /Author and returns the editor. nil removes the entry.

set_author!(editor, author)

@spec set_author!(t(), String.t() | nil) :: t()

Sets the document's /Author, raising an error if it fails.

set_creation_date(editor, date)

@spec set_creation_date(t(), info_date() | nil) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the document's /CreationDate and returns the editor.

Takes a info_date/0 or nil, which removes the entry. A malformed date string, or anything else, raises ArgumentError.

set_creation_date!(editor, date)

@spec set_creation_date!(t(), info_date() | nil) :: t()

Sets the document's /CreationDate, raising an error if it fails.

set_creator(editor, creator)

@spec set_creator(t(), String.t() | nil) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the document's /Creator, the application that made the original document, and returns the editor. nil removes the entry.

set_creator!(editor, creator)

@spec set_creator!(t(), String.t() | nil) :: t()

Sets the document's /Creator, raising an error if it fails.

set_keywords(editor, keywords)

@spec set_keywords(t(), String.t() | nil) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the document's /Keywords and returns the editor. nil removes the entry.

PDF stores keywords as one string, comma-separated by convention, and PdfElixide.Document.Metadata reads it back unsplit.

set_keywords!(editor, keywords)

@spec set_keywords!(t(), String.t() | nil) :: t()

Sets the document's /Keywords, raising an error if it fails.

set_mod_date(editor, date)

@spec set_mod_date(t(), info_date() | nil) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the document's /ModDate and returns the editor.

Takes a info_date/0 or nil, which removes the entry. A malformed date string, or anything else, raises ArgumentError.

set_mod_date!(editor, date)

@spec set_mod_date!(t(), info_date() | nil) :: t()

Sets the document's /ModDate, raising an error if it fails.

set_producer(editor, producer)

@spec set_producer(t(), String.t() | nil) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the document's /Producer, the application that produced the PDF, and returns the editor. nil removes the entry.

set_producer!(editor, producer)

@spec set_producer!(t(), String.t() | nil) :: t()

Sets the document's /Producer, raising an error if it fails.

set_subject(editor, subject)

@spec set_subject(t(), String.t() | nil) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the document's /Subject and returns the editor. nil removes the entry.

set_subject!(editor, subject)

@spec set_subject!(t(), String.t() | nil) :: t()

Sets the document's /Subject, raising an error if it fails.

set_title(editor, title)

@spec set_title(t(), String.t() | nil) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Sets the document's /Title and returns the editor. nil removes the entry.

Anything but a string or nil raises ArgumentError. See Document information.

set_title!(editor, title)

@spec set_title!(t(), String.t() | nil) :: t()

Sets the document's /Title, raising an error if it fails.

Flattening

flatten_annotations(editor)

@spec flatten_annotations(t()) :: {:ok, t()} | {:error, PdfElixide.Error.t()}

Marks every page's annotations for flattening.

Flattening draws each annotation's appearance into the page content. Nothing happens until the next full write: save/3 without :incremental, or to_binary/2. An incremental save/3 is refused once a page is marked, and because the mark cannot be removed it stays refused for the life of the editor — reopen the source for an unflattened document.

On a page where at least one annotation appearance can be produced, this removes every annotation entry, including ones it could not draw and form field widgets. A skipped annotation can therefore be deleted without being rendered or reported. If the page produces no appearances, the write creates no flatten data for it and draws or removes nothing.

Refused on an encrypted source; see What an encrypted source cannot do.

Do not flatten annotations on a page whose form fields you also flatten with PdfElixide.Form.flatten/1,2: where appearances are produced, the two marks are applied independently and fields can be drawn twice.

Returns the editor. See the "Flattening" section of the Forms guide.

flatten_annotations(editor, page_index)

@spec flatten_annotations(t(), non_neg_integer()) ::
  {:ok, t()} | {:error, PdfElixide.Error.t()}

Marks the annotations of the page at the given zero-based index for flattening.

Deferred until the next full write, with the same all-or-nothing page behavior around appearance production as flatten_annotations/1, and refused on an encrypted source for the same reason.

Returns the editor, or {:error, %PdfElixide.Error{reason: :out_of_range}} if the page does not exist. See the "Flattening" section of the Forms guide.

flatten_annotations!(editor)

@spec flatten_annotations!(t()) :: t()

Marks every page's annotations for flattening, raising an error if it fails.

flatten_annotations!(editor, page_index)

@spec flatten_annotations!(t(), non_neg_integer()) :: t()

Marks the annotations of the page at the given zero-based index for flattening, raising an error if it fails.

flatten_warnings(editor)

@spec flatten_warnings(t()) :: {:ok, [String.t()]} | {:error, PdfElixide.Error.t()}

Lists the warnings collected while flattening.

Flattening is deferred, so warnings cannot appear before a full write processes a flatten mark. Each entry describes a problem encountered while flattening — most importantly a newly set non-Latin or emoji field value that this library cannot draw into an appearance, which is written with wrong glyphs or none while the PDF stays otherwise valid. The warning is the only signal that happened.

Warnings accumulate for the life of the editor and are never cleared, so a second write reports the first one's entries again. Read the list after the write you care about.

The list is a best effort, not an inventory. Some losses are recorded nowhere, so an empty list is not proof that the written document matches the original. The "Flattening" section of the Forms guide says which, and what each warning means.

flatten_warnings!(editor)

@spec flatten_warnings!(t()) :: [String.t()]

Lists the warnings collected while flattening, raising an error if it fails.

Types

create_opts()

@type create_opts() :: [
  title: String.t() | nil,
  author: String.t() | nil,
  subject: String.t() | nil,
  page_size: page_size(),
  margin_top: number(),
  margin_bottom: number(),
  margin_left: number(),
  font_size: number(),
  line_height: number()
]

Options accepted by from_markdown/2, from_html/2 and their bang variants.

  • :title, :author, :subject — written to the document information dictionary. Each defaults to nil, which leaves the entry out.
  • :page_size — a page_size/0. Defaults to :letter.
  • :margin_top, :margin_bottom, :margin_left — margins in points. Each defaults to 72. There is no right margin, since lines are not wrapped; see Creating documents.
  • :font_size — body text size in points. Defaults to 12. Headings are set larger in proportion to it.
  • :line_height — line spacing as a multiple of the text size. Defaults to 1.5.

An unknown key, or a value of the wrong type, raises ArgumentError naming the key. So does a non-positive page dimension, font size or line height, a negative margin, a left margin as wide as the page, a page too short for two of the largest supported lines, or line spacing too small for the page.

embed_opts()

@type embed_opts() :: [
  description: String.t(),
  relationship: PdfElixide.Document.EmbeddedFile.relationship()
]

Options for embed_file/4.

  • :description — a human-readable note about the attachment, shown by viewers beside its name. Absent by default.
  • :relationship — how the attachment relates to the document, one of the values in PdfElixide.Document.EmbeddedFile.relationship/0. Absent by default.

Unknown keys and invalid values raise ArgumentError naming the key.

encryption_opts()

@type encryption_opts() :: [
  user_password: String.t(),
  owner_password: String.t(),
  algorithm: :aes128 | :rc4_128,
  permissions: permission_opts()
]

The :encryption option of save_opts/0.

  • :user_password — password required to open the document. Defaults to "", which produces a document that opens without a prompt but still carries the permission flags.
  • :owner_password — password granting full access and the right to change security settings. Defaults to "", which makes the user password serve as the owner password too.
  • :algorithm — :aes128 (the default) or :rc4_128.
  • :permissions — what a reader may do with the document, as a permission_opts/0 keyword list. Defaults to granting everything.

Both passwords are UTF-8 String.t/0, unlike PdfElixide.Document.open/2's :password, which is a byte string. See Passwords for their length limit, interoperability and what each empty value means, and Algorithms for the cipher choice and its consequences for the output.

An unknown key here or in :permissions raises ArgumentError naming that key. A declared key given a value of the wrong type raises naming :encryption.

image_create_opts()

@type image_create_opts() :: [
  title: String.t() | nil,
  author: String.t() | nil,
  subject: String.t() | nil,
  page_size: page_size(),
  margin_top: number(),
  margin_bottom: number(),
  margin_left: number(),
  margin_right: number()
]

Options accepted by from_images/2 and from_images!/2.

  • :title, :author, :subject — written to the document information dictionary. Each defaults to nil, which leaves the entry out.
  • :page_size — a page_size/0, used for every page. Defaults to :letter.
  • :margin_top, :margin_bottom, :margin_left, :margin_right — margins in points. Each defaults to 72. Each image is fitted inside the area they leave.

An unknown key, or a value of the wrong type, raises ArgumentError naming the key. So does a non-positive page dimension, a negative margin, or margins that leave no room on the page.

info_date()

@type info_date() :: DateTime.t() | String.t()

A DateTime or complete PDF date string for set_creation_date/2 and set_mod_date/2. See Dates for formatting details.

margins()

@type margins() :: [left: number(), right: number(), top: number(), bottom: number()]

Margins for crop_margins/2, in points.

  • :left, :right, :top, :bottom — how far the crop box is inset from that edge of the media box. Each is a non-negative number that fits a 32-bit float, and defaults to 0.

Unknown keys and invalid values raise ArgumentError naming the key.

open_opts()

@type open_opts() :: [{:password, binary() | nil}]

Options accepted by open/2, open!/2, from_binary/2, and from_binary!/2.

  • :password — password used to authenticate against an encrypted PDF. It has the byte-string semantics of PdfElixide.Document.open_opts/0. A wrong password returns :wrong_password; when omitted or nil, only the empty password is tried and a document that needs another returns :encrypted.

This authenticates the source. The :user_password of encryption_opts/0 encrypts the output and is a String.t/0.

An unknown key, or a :password that is neither a binary nor nil, raises ArgumentError — see the "Errors versus exceptions" section of PdfElixide.Error.

page_size()

@type page_size() :: :letter | :a4 | :legal | :a3 | {number(), number()}

A page size for from_markdown/2, from_html/2, from_plain_text/2 and from_images/2.

:letter is 612 × 792 points, :a4 595 × 842, :legal 612 × 1008 and :a3 842 × 1190. {width, height} gives any other size in points (1/72 inch).

permission_opts()

@type permission_opts() :: [
  print_low_res: boolean(),
  print_high_res: boolean(),
  modify: boolean(),
  copy: boolean(),
  annotate: boolean(),
  fill_forms: boolean(),
  accessibility: boolean(),
  assemble: boolean()
]

The :permissions option of encryption_opts/0.

The keys are those of PdfElixide.Document.Permissions, which describes what each one grants. Every key defaults to true.

See Permissions for how they interact and what they do and do not promise.

plain_text_create_opts()

@type plain_text_create_opts() :: [
  title: String.t() | nil,
  author: String.t() | nil,
  page_size: page_size(),
  margin_top: number(),
  margin_bottom: number(),
  margin_left: number(),
  line_height: number()
]

Options accepted by from_plain_text/2 and from_plain_text!/2.

The keys of create_opts/0 without :subject and :font_size: plain text is always set at 12 points, and its output carries no subject.

redaction_opts()

@type redaction_opts() :: [
  edge_padding: number(),
  default_fill: PdfElixide.Color.RGB.t(),
  draw_default_overlay: boolean()
]

Options for apply_redactions/1,2 and apply_redactions!/1,2. See apply_redactions/2 for defaults and allowed values.

Unknown keys, invalid types and out-of-range values raise ArgumentError naming the offending key.

sanitize_opts()

@type sanitize_opts() :: [
  scrub_metadata: boolean(),
  remove_javascript: boolean(),
  remove_embedded_files: boolean()
]

Options for sanitize/1,2 and sanitize!/1,2. See sanitize/2 for defaults and allowed values.

save_opts()

@type save_opts() :: [
  incremental: boolean(),
  compress: boolean(),
  garbage_collect: boolean(),
  encryption: encryption_opts() | nil
]

Options accepted by save/3, save!/3, to_binary/2, and to_binary!/2.

  • :incremental — write an incremental update instead of a full rewrite. Defaults to false. See save/3 and to_binary/2 for restrictions.
  • :compress — compress streams. Defaults to true. An editor converted to PDF/A-1 must be written with compress: false; the default returns {:error, %PdfElixide.Error{reason: :unsupported}}. See the PDF/A conversion guide.
  • :garbage_collect — drop unreferenced objects. Defaults to true. false is refused after sanitize/1,2 with {:error, %PdfElixide.Error{reason: :unsupported}}, since the write would carry the scrubbed values back into the file; see the Redaction guide.
  • :encryption — encrypt the written document, as a encryption_opts/0 keyword list. Defaults to nil, which writes an unencrypted PDF. Cannot be combined with incremental: true; see the Encryption guide. An editor converted to PDF/A refuses it; see the PDF/A conversion guide.

An unknown key, or a declared key given a value of the wrong type, raises ArgumentError naming the offending key; see the "Errors versus exceptions" section of PdfElixide.Error.

t()

@type t() :: %PdfElixide.Editor{
  ref: reference(),
  source_path: Path.t() | nil,
  version: {non_neg_integer(), non_neg_integer()}
}

An open editor.

:version is cached when the editor is opened and served from the struct thereafter: it is the version of the document the editor was opened from, and no editing operation changes it.

:source_path is nil for an editor with no source file: one built with from_binary/2, from_markdown/2, from_html/2, from_plain_text/2 or from_images/2.

:ref is an internal handle; do not read it or construct the struct directly.