# `PdfElixide.Editor`
[🔗](https://github.com/r8/pdf_elixide/blob/v0.16.0/lib/pdf_elixide/editor.ex#L1)

Mutable, in-memory PDF editor.

Where `PdfElixide.Document` only reads, an editor accumulates changes in
memory and writes them out on demand. The shape is open, mutate, write:

    "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 — you can keep editing and write again. `close/1` **discards
unsaved edits**, so write before you close. Flattening is deferred to that same
write: `flatten_annotations/1,2` and `PdfElixide.Form.flatten/1,2` mark what to
flatten and the drawing happens as the file is written.

Every function that changes an editor returns the editor, as above, and the
tuple-returning half is uniform in the same way, so both compose as one
pipeline. `to_binary/2` and `close/1` are the two ways such a pipeline ends.

**An editor is a mutable handle, not a value, and rebinding does not fork it**
— the editor a mutating call returns is the one that went in, so an earlier
binding will not give you the document as it was before the edit. The
[Forms](guides/forms.md) guide has both shapes and the full account.

## Page structure

`delete_page/2` and `move_page/3` change which pages the document has and in
what order. Indices are zero-based and count the pages as currently edited, so
`page_count/1` is what they are bounded by, and it moves as soon as a page is
deleted rather than waiting for a save:

    "report.pdf"
    |> PdfElixide.Editor.open!()
    |> PdfElixide.Editor.move_page!(0, 2)
    |> PdfElixide.Editor.delete_page!(0)
    |> PdfElixide.Editor.save!("reordered.pdf")
    |> PdfElixide.Editor.close()
    #=> :ok

Three things they do not do, none of them visible from the call:

**Deleting a page is not redaction.** It removes the page from the document's
page tree, so the written file has one fewer page and nothing displays it —
but the page's objects and content stream are still in that file as
unreferenced data, with `garbage_collect: true` as much as without. Anyone
reading the bytes can recover them. Do not use `delete_page/2` to remove
confidential content; write the pages you want to keep to a new document
instead. If every page is deleted, reopening the written file can discover
those orphaned page objects again, so this is not a way to create a safely
page-less PDF either.

**Bookmarks and links are not remapped.** Nothing updates the outline, link
annotations, named destinations, page labels, the structure tree or a form
field's widget references, so entries pointing at a page that was deleted or
moved are left pointing where they were.

**An incremental save carries neither of them, nor a page rotation.**
`save(editor, path, incremental: true)` appends an update to the original
file, whose page tree is still there, so the written file has the pages it
started with, in the order and at the rotation it started with — and reports
no error. Write with `save/3` without `:incremental`, or with `to_binary/2`.

## Page rotation

`set_rotation/3` turns a page to an absolute angle, `rotate_page_by/3` turns it
a further so many degrees from where it already is, and `rotate_all_by/2` does
that to every page. `rotation/2` reads the angle back, pending changes
included:

    "scan.pdf"
    |> PdfElixide.Editor.open!()
    |> PdfElixide.Editor.rotate_all_by!(90)
    |> PdfElixide.Editor.set_rotation!(0, 0)
    |> PdfElixide.Editor.save!("upright.pdf")
    |> PdfElixide.Editor.close()
    #=> :ok

A rotation belongs to the page rather than to the position, so it follows the
page through `move_page/3` and survives the deletion of another page.

Rotation only turns the page as a viewer displays it. Nothing re-lays out the
content, and the page's `/MediaBox` is not swapped, so a `90`-rotated portrait
page still reports portrait dimensions. The "Page structure" section above
describes the incremental-save limitation.

Every call that writes or mutates takes the handle's lock exclusively — and so
does `PdfElixide.Form.fields/1`, which only reads — so concurrent *editing* of
a single editor serializes. `page_count/1`, `modified?/1`, `rotation/2`,
`flatten_warnings/1` and `closed?/1` take the lock shared, as do the
`PdfElixide.Signature` reads given an editor, which reach the document it was
opened from. Give each process its own editor if you need them to work at
once; see the [Concurrency](guides/concurrency.md) guide.

# `save_opts`

```elixir
@type save_opts() :: [
  incremental: boolean(),
  compress: boolean(),
  garbage_collect: boolean()
]
```

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`.
  * `:compress` — compress streams. Defaults to `true`.
  * `:garbage_collect` — drop unreferenced objects. Defaults to
    `true`.

An unknown key, or a declared key given a value that is not a boolean,
raises `ArgumentError` naming the offending key; see the "Errors versus
exceptions" section of `PdfElixide.Error`.

# `t`

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

An open editor.

`:version` arrives with the handle, from the same native call that opens the
editor, and is 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 built with `from_binary/1`.

# `close`

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

Releases the editor's native memory immediately.

An editor holds the source document plus its pending edits in memory on the
Rust side, normally freed only when the BEAM garbage-collects the handle.
`close/1` frees it now, 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 — so
*immediately* means 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?`

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

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

# `delete_page`

```elixir
@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 the "Page structure" section of this module
for the deletion's security and writing limitations.

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

# `delete_page!`

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

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

# `flatten_annotations`

```elixir
@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 ignores the mark entirely. The mark cannot
be removed — 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.

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](guides/forms.md) guide.

# `flatten_annotations`

```elixir
@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`.

Returns the editor, or `{:error, %PdfElixide.Error{reason: :out_of_range}}` if
the page does not exist. See the "Flattening" section of the
[Forms](guides/forms.md) guide.

# `flatten_annotations!`

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

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

# `flatten_annotations!`

```elixir
@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`

```elixir
@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 the shipped
appearance path cannot render faithfully, 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](guides/forms.md) guide says
which, and what each warning means.

# `flatten_warnings!`

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

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

# `from_binary`

```elixir
@spec from_binary(binary()) :: {: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/1` to read a file.

# `from_binary!`

```elixir
@spec from_binary!(binary()) :: 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!/1` to read a file.

# `modified?`

```elixir
@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`.

# `move_page`

```elixir
@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 the "Page structure" section of this module for what a move
does not update, and why an incremental `save/3` does not carry it.

# `move_page!`

```elixir
@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.

# `open`

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

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

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

# `open!`

```elixir
@spec open!(Path.t()) :: t()
```

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

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

# `page_count`

```elixir
@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!`

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

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

# `rotate_all_by`

```elixir
@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.

Every page's current rotation is read before any page is turned, so if one of
them cannot be read the call fails having turned none of them and left the
editor unmodified.

See the "Page rotation" section of this module.

# `rotate_all_by!`

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

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

# `rotate_page_by`

```elixir
@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`. Any integer is accepted: a negative one
turns anticlockwise, one past `360` wraps, and one that is not a multiple of 90
is rounded to the nearest quadrant — `45` and `134` both add `90`.

Returns `{:error, %PdfElixide.Error{reason: :out_of_range}}` if the page does
not exist. See the "Page rotation" section of this module.

# `rotate_page_by!`

```elixir
@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`

```elixir
@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!`

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

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

# `save`

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

Writes all in-memory changes to a PDF file at the given path, and returns the
editor.

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

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

# `save!`

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

Writes all in-memory changes to a PDF file at the given path, raising an error if it fails.

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

# `set_rotation`

```elixir
@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 the "Page rotation" section of this module.

# `set_rotation!`

```elixir
@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.

# `source_path`

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

Returns the file path from which the editor was loaded, or `nil` if it
was loaded from binary data.

# `to_binary`

```elixir
@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 `t:save_opts/0` keyword list as `save/3`, except
`:incremental` — an incremental update is an append to the original file, so
there is nothing to append to in memory and passing `incremental: true` here
returns `{:error, %PdfElixide.Error{reason: :invalid_pdf}}`. Use `save/3` for
an incremental write.

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!`

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

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

# `version`

```elixir
@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`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
