# Editing

`PdfElixide.Editor` changes page structure, rotates and crops pages, covers
regions and adds attachments. Changes stay in memory until you write the
document. The [Forms](forms.md) guide covers filling fields and flattening
annotations; the [Encryption](encryption.md) guide covers password-protected
output; [Merging and splitting](merging-and-splitting.md) covers combining
documents and writing parts of one to new files.

## Saving edits

Use `PdfElixide.Editor.save/3` with its default `incremental: false`, or
`PdfElixide.Editor.to_binary/2`, to write page edits and attachments. Writing
leaves the editor open for further changes; `PdfElixide.Editor.close/1` discards
any unsaved edits.

**An incremental save carries field values and document information, and refuses
everything else.** `PdfElixide.Editor.save(editor, path, incremental: true)`
appends an update to a verbatim copy of the original file. Only two kinds of
change reach that update: form field values written with
`PdfElixide.Form.put_value/3` — see [Saving](forms.md#saving) for that workflow
— and the `/Info` entries the `PdfElixide.Editor.set_title/2` family sets.
Everything else counts as pending:

  * page deletions and moves
  * page rotations
  * page media and crop boxes
  * erased regions and queued redaction regions
  * redaction marks
  * annotation and form flatten marks
  * attachments

If the editor holds any of those, the save returns
`{:error, %PdfElixide.Error{reason: :unsupported}}` naming them, and writes no
file.

To withdraw pending changes, use `PdfElixide.Editor.clear_erase_regions/2` for a
page's erased regions, `PdfElixide.Editor.unmark_redactions/2` for its redaction
mark, or move pages back into their original order. Incremental saving becomes
available once no unsupported changes remain. Deletions and the other changes
listed above cannot be withdrawn; reopen the source or write a full rewrite.

**Setting rotation or boxes counts as pending even if the value is unchanged.**
Restoring the original value or calling `PdfElixide.Editor.rotate_page_by/3`
with `0` still prevents incremental saving. Only page order is compared with the
source.

Marks also count when there is nothing to draw: `PdfElixide.Form.flatten/1` on
a document with no AcroForm and `PdfElixide.Editor.mark_redactions/2` on a page
without `/Redact` annotations both prevent incremental saving.

**A full rewrite in between does not lift the refusal.** An incremental update
is appended to the *original* file whatever was written since, so the pending
change would still be missing from it.

**An editor built with `PdfElixide.Editor.from_binary/2` cannot save
incrementally**, and neither can one created with
`PdfElixide.Editor.from_markdown/2`, `PdfElixide.Editor.from_html/2`,
`PdfElixide.Editor.from_plain_text/2` or `PdfElixide.Editor.from_images/2`.
None of them has a source file to copy, so `incremental: true` returns
`{:error, %PdfElixide.Error{reason: :unsupported}}` even with no edits or only
field values. Use `PdfElixide.Editor.open/2` for incremental saving, or write a
full rewrite.

**An editor that has merged another document, or been converted to PDF/A, cannot
save incrementally.** Both rebuild the editor from the resulting document, which
has no source file to append to; see
[Merging documents](merging-and-splitting.md#merging-documents) and the
[PDF/A conversion](pdf-a-conversion.md) guide.

**An encrypted source cannot be saved incrementally.** The call returns
`{:error, %PdfElixide.Error{reason: :unsupported}}`; see
[Incremental saves](encryption.md#incremental-saves).

**Destructive redaction requires a full rewrite.** Once
`PdfElixide.Editor.apply_redactions/1` or `PdfElixide.Editor.sanitize/1` has
run, `incremental: true` is refused because it would leave removed content
readable. A write with `garbage_collect: false` after a sanitize is also
refused. See [What is refused, and why](redaction.md#what-is-refused-and-why).

`PdfElixide.Editor.to_binary/2` refuses `incremental: true` with
`{:error, %PdfElixide.Error{reason: :invalid_pdf}}` regardless of pending edits.
After `PdfElixide.Editor.sanitize/1`, requesting `garbage_collect: false`
takes precedence and returns `:unsupported` instead. Neither writing function
accepts encryption with `incremental: true`; see [Encryption](encryption.md).

## Page structure

`PdfElixide.Editor.delete_page/2` and `PdfElixide.Editor.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 `PdfElixide.Editor.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:

```elixir
alias PdfElixide.Document
alias PdfElixide.Editor

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

`PdfElixide.Editor.keep_pages/2` does several of those at once: it keeps the
listed pages in the order given and drops the rest. Pending edits such as a
rotation stay with their pages. To write some pages to a new document instead of
changing this one, see
[Splitting a document](merging-and-splitting.md#splitting-a-document).

There are two further limitations:

**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. The same is true of a page `PdfElixide.Editor.keep_pages/2`
drops. Do not use either, or extraction, to remove confidential content; see
[Extraction is not redaction](merging-and-splitting.md#extraction-is-not-redaction).
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.

## Page rotation

`PdfElixide.Editor.set_rotation/3` turns a page to an absolute angle,
`PdfElixide.Editor.rotate_page_by/3` adds a relative rotation, and
`PdfElixide.Editor.rotate_all_by/2` does that to every page.
`PdfElixide.Editor.rotation/2` reads the angle back, pending changes included:

```elixir
"path/to/scan.pdf"
|> Editor.open!()
|> Editor.rotate_all_by!(90)
|> Editor.set_rotation!(0, 0)
|> Editor.save!("upright.pdf")
|> Editor.close()
#=> :ok
```

**Angles are quadrants.** `PdfElixide.Editor.set_rotation/3` takes `0`, `90`,
`180` or `270`, and the two relative calls take any multiple of `90` — negative
to turn anticlockwise, past `360` to wrap. Anything else raises
`FunctionClauseError` rather than being rounded to the nearest quadrant. An
invalid stored `/Rotate` reads as `0` before the delta is added, so turning a
`45` page by `90` lands on `90`, not `135`.

A rotation belongs to the page rather than to the position, so it follows the
page through `PdfElixide.Editor.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. See [Saving edits](#saving-edits) for
the incremental-save refusal.

## Page boxes

A page has two boxes that matter to a viewer: the `/MediaBox` is the sheet the
page is imposed on, and the optional `/CropBox` is the part of that sheet a
viewer displays and prints. `PdfElixide.Editor.media_box/2` and
`PdfElixide.Editor.crop_box/2` read them, pending changes included;
`PdfElixide.Editor.set_media_box/3` and `PdfElixide.Editor.set_crop_box/3` set
one page's, and `PdfElixide.Editor.crop_margins/2` sets every page's crop box by
insetting its media box:

```elixir
"path/to/scan.pdf"
|> Editor.open!()
|> Editor.crop_margins!(left: 36, right: 36, top: 24, bottom: 24)
|> Editor.save!("trimmed.pdf")
|> Editor.close()
#=> :ok
```

Every box is a `PdfElixide.Geometry.Rect` in the page's raw, unrotated user
space, so a box read from `PdfElixide.Document.Page.media_box/1`, or a `bbox`
from an extractor that reports in that space, can be handed straight back. For
rotated extractor boxes, see [Rotated pages and extracted
geometry](page-geometry.md#rotated-pages-and-extracted-geometry); only boxes
reported in the displayed frame should pass through
`PdfElixide.Geometry.Rect.to_user_space/3`. A reversed rectangle is normalized
before it is written, and the getters report the box as it will land in the
file.

**Cropping hides content; it does not remove it.** Whatever lies outside the
crop box is still in the written file, and anything that reads the content
stream still finds it. Text extraction is not such a reader — see [What the crop
box hides from
extraction](page-geometry.md#what-the-crop-box-hides-from-extraction) — so
cropping is a way to hide text from extraction but not a way to delete it; use
[Redaction](redaction.md) for that. A viewer clips the crop box to the media
box, so a crop box larger than the media box shows the whole page, and nothing
here checks one against the other.

**Setting the media box leaves an existing crop box alone.** A page whose crop
box was declared by the document keeps it after
`PdfElixide.Editor.set_media_box/3`, wherever the new media box lands; call
`PdfElixide.Editor.set_crop_box/3` as well when both should change. Once a page
has a crop box it cannot be removed — set it equal to the media box to show the
whole page. The page's content is neither moved nor scaled by either setter.

`PdfElixide.Editor.crop_margins/2` measures from each page's media box as
`PdfElixide.Editor.media_box/2` reports it, so a media box set earlier in the
same session counts, and it replaces any crop box a page already has. It reads
every page before changing any: if one page has no readable `/MediaBox` the call
returns `{:error, %PdfElixide.Error{reason: :invalid_pdf}}`, and if the margins
would leave a page with no area it returns
`{:error, %PdfElixide.Error{reason: :other}}` naming the page — in both cases
with nothing changed. Give such a page a media box with
`PdfElixide.Editor.set_media_box/3` first.

A box belongs to the page rather than to the position, so it follows the page
through `PdfElixide.Editor.move_page/3` and survives the deletion of another
page. Where a box is inherited from the page tree, see [Which ancestor an
inherited box comes
from](page-geometry.md#which-ancestor-an-inherited-box-comes-from). See [Saving
edits](#saving-edits) for the incremental-save refusal.

## Erasing regions

`PdfElixide.Editor.erase_region/3` paints a white rectangle over part of a page
as the file is written, and `PdfElixide.Editor.erase_regions/3` does that for
several rectangles in one call. A `PdfElixide.Geometry.Rect` from an extractor
can be handed straight back:

```elixir
doc = Document.open!("path/to/report.pdf")
[span | _] = Document.spans!(doc, 0)
Document.close(doc)

"path/to/report.pdf"
|> Editor.open!()
|> Editor.erase_region!(0, span.bbox)
|> Editor.save!("covered.pdf")
|> Editor.close()
#=> :ok
```

**Erasing is not redaction.** Covered text and images remain in the written
file; `PdfElixide.Document.text/1` still returns the covered words. Do not use
it to remove confidential content — `PdfElixide.Editor.apply_redactions/1` is
the call that removes any, and it removes covered *text* only, leaving images
and vector graphics where they were. The [Redaction](redaction.md) guide covers
what each one does and does not take.

**An erase on a page that `PdfElixide.Editor.apply_redactions/1` then rewrites
is silently dropped** — the whiteout never reaches the output and whatever it
covered stays visible. Erase in a separate editor, after the redactions have
been written; see
[It discards other pending overlays on the page](redaction.md#it-discards-other-pending-overlays-on-the-page).

The rectangle covers page content only. Annotations — form widgets, stamps,
links — remain above it, even when flattened on the same editor. To hide an
annotation whose rectangle is `rect`, flatten, write, reopen what was written,
and erase there:

```elixir
flattened =
  "path/to/form.pdf"
  |> Editor.open!()
  |> Editor.flatten_annotations!()

bytes = Editor.to_binary!(flattened)
Editor.close(flattened)

bytes
|> Editor.from_binary!()
|> Editor.erase_region!(0, rect)
|> Editor.save!("covered.pdf")
|> Editor.close()
#=> :ok
```

A page with no content stream is left as it is. A page whose content streams are
stored as an indirect array — a `/Contents` entry that refers to an array object
rather than holding one — cannot take an overlay, and
`PdfElixide.Editor.erase_region/3` returns
`{:error, %PdfElixide.Error{reason: :unsupported}}` for it. Such a page can
still be rotated, moved, deleted and saved.

Coordinates usually use the page's raw, unrotated user space, as reported by
`PdfElixide.Document.chars/1`, `PdfElixide.Document.spans/1` and
`PdfElixide.Document.paths/1`. Rotated extractor boxes follow the same
conversion rules as crop boxes above.

The whiteout inherits the graphics state left by the page content. An active
transformation can move it, a clipping path can hide part or all of it, and an
unfinished path can cause extra content to be covered. These cases still report
success. A redaction block does not behave this way; see
[Where the block lands](redaction.md#where-the-block-lands).
`PdfElixide.Document.rects/2` shows placement, not visibility, and may omit a
fill combined with an unfinished path; `PdfElixide.Document.paths/2` reports the
combined shapes. For a document you did not produce, render the written result
to verify coverage.

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

`PdfElixide.Editor.clear_erase_regions/2` discards the regions pending on a
page. It does not reset `PdfElixide.Editor.modified?/1`, but it does lift the
incremental-save refusal for that page. See [Saving edits](#saving-edits).

## Attachments

`PdfElixide.Editor.embed_file/4` attaches a file to the document — a spreadsheet
behind a report, the source data behind a chart — and
`PdfElixide.Editor.embedded_files/1` lists what the document will carry, pending
attachments included:

```elixir
"path/to/report.pdf"
|> Editor.open!()
|> Editor.embed_file!("figures.csv", csv, description: "Chart data")
|> Editor.save!("report-with-data.pdf")
|> Editor.close()
#=> :ok
```

**A document that already has a name tree is refused** with
`{:error, %PdfElixide.Error{reason: :unsupported}}`, because attaching a file
cannot preserve that tree's existing attachments, named destinations or
document-level JavaScript. To attach several files, add them in the same editing
session. The error message names the entries that would be lost.

A `PdfElixide.Editor.sanitize/1` that emptied the name tree lifts the refusal,
so a document's attachments can be replaced rather than only removed:

```elixir
editor = Editor.open!("path/to/received.pdf")
Editor.sanitize!(editor)
Editor.embed_file!(editor, "figures.csv", csv)
Editor.save!(editor, "resupplied.pdf")
Editor.close(editor)
```

This works only when nothing is left in the tree. A sanitize that kept an entry
— one run with `remove_javascript: false`, say — still refuses, and names the
entry it is protecting. A name tree that cannot be read is refused either way.

See [Saving edits](#saving-edits) for the incremental-save refusal.

No media type is written for an attachment. `PdfElixide.Document.EmbeddedFile`
reads one when another producer declared it, but this editor cannot set one.

## Document information

`PdfElixide.Editor.set_title/2`, `PdfElixide.Editor.set_author/2`,
`PdfElixide.Editor.set_subject/2`, `PdfElixide.Editor.set_keywords/2`,
`PdfElixide.Editor.set_creator/2`, `PdfElixide.Editor.set_producer/2`,
`PdfElixide.Editor.set_creation_date/2` and `PdfElixide.Editor.set_mod_date/2`
change the `/Info` dictionary, and `PdfElixide.Editor.metadata/1` reads it with
pending changes applied:

```elixir
"path/to/report.pdf"
|> Editor.open!()
|> Editor.set_title!("Quarterly report")
|> Editor.set_author!("Ada Lovelace")
|> Editor.set_mod_date!(DateTime.utc_now())
|> Editor.save!("titled.pdf")
|> Editor.close()
#=> :ok
```

Passing `nil` removes an entry. Each setter takes one string; keywords are
stored as a single comma-separated string, which is how
`PdfElixide.Document.Metadata` reads them back.

### What a write carries

Every write, full rewrite or incremental, carries the source's title, author,
subject, keywords, creator, producer and both dates whether or not you set any
of them, re-encoded as described below — `/Info` is one of the two things an
incremental update carries, per [Saving edits](#saving-edits). Two things do not
survive: `/Trapped`, which cannot be set — `PdfElixide.Editor.metadata/1`
reports the source's value until something clears it — and any non-standard
`/Info` key.

`PdfElixide.Editor.sanitize/1` is what clears it. With its default
`scrub_metadata: true` none of those values are carried, `/Trapped` included,
and `PdfElixide.Editor.metadata/1` answers `nil` for every field afterwards;
see [Sanitizing](redaction.md#sanitizing). Nothing is stamped for you: the
producer stays whatever the source named, and `/ModDate` is only what you set,
so set it yourself when a reader will look at it. A whitespace-only value is
written but reads back as `nil`.

### Text encoding

ASCII is written as it stands. Anything else is written as UTF-8 behind a
byte-order mark, the PDF 2.0 spelling (ISO 32000-2 §7.9.2.2), which
`PdfElixide.Document.metadata/1` decodes and PDF 2.0-aware readers such as
Poppler decode too. The file's declared version is not raised, so a reader that
predates PDF 2.0 may show the mark and the raw bytes instead. UTF-16 cannot be
produced.

### Dates

`PdfElixide.Editor.set_creation_date/2` and `PdfElixide.Editor.set_mod_date/2`
take a `DateTime` or a PDF date string.

A `DateTime` is written as `D:YYYYMMDDHHMMSS` followed by `Z` for UTC or by the
offset as `+HH'mm'` / `-HH'mm'`. Fractional seconds are dropped. An offset that
is not a whole number of minutes cannot be spelled, so such a value is written
in UTC and the instant is preserved rather than the wall clock.

A string must be a well-formed PDF date in full; a value read from
`PdfElixide.Editor.metadata/1` qualifies. A malformed string, or one with
anything after the date, raises `ArgumentError`.

### XMP and document identifiers

The XMP packet is not updated: `PdfElixide.Document.xmp_metadata/1` reads back
what the source carried, and a viewer that prefers XMP over `/Info` shows those
values rather than the ones you set. The exception is an encrypted write, which
the [Encryption](encryption.md) guide covers.

The trailer's `/ID` is dropped by a full rewrite, and a write emits one only
when `:encryption` is given.
