Merging combines several PDF files into one, such as a cover and the chapters
that follow it. Splitting cuts one document into parts, such as a report into
one file per section. Both work on an open PdfElixide.Editor: a merge brings
pages into the editor and changes it, and a split writes pages out of it as new
binaries and leaves it alone. Either way, the editor's pending edits travel with
its pages.
Choosing the right call
Three calls keep only some of a document's pages, and they differ in what they change and what they leave behind:
PdfElixide.Editor.keep_pages/2 | PdfElixide.Editor.extract_pages/2 | PdfElixide.Editor.delete_page/2 | |
|---|---|---|---|
| Changes the editor | yes | no | yes |
| Result | the editor, written later | a new PDF binary | the editor, written later |
| Order of the kept pages | as listed | as listed | unchanged |
| Content of the pages left out | stays in the written file, unreferenced | left out, unless something still refers to it | stays in the written file, unreferenced |
PdfElixide.Editor.keep_pages/2 and PdfElixide.Editor.delete_page/2 rearrange
the open document and are covered under
Page structure. None of the three is a reliable way
to remove content; see
Extraction is not redaction.
Merging documents
PdfElixide.Editor.merge/2 appends every page of a PDF file to the end of the
editor, and PdfElixide.Editor.merge_binary/2 does the same for a PDF binary:
alias PdfElixide.Document
alias PdfElixide.Editor
"path/to/cover.pdf"
|> Editor.open!()
|> Editor.merge!("path/to/chapter_1.pdf")
|> Editor.merge!("path/to/chapter_2.pdf")
|> Editor.save!("book.pdf")
|> Editor.close()
#=> :okA merge incorporates the editor's pending edits into the combined document. Afterwards, the editor behaves as if it had been opened on a file that already held every page:
- Merged pages are ordinary pages:
PdfElixide.Editor.delete_page/2,PdfElixide.Editor.move_page/3, rotation and page boxes work on them as on any other. - Pending edits become part of the document. Flatten marks and erased regions
are drawn, and their warnings stay available from
PdfElixide.Editor.flatten_warnings/1. Pending redactions are refused instead; see below. - Attachments become part of the document, so
PdfElixide.Editor.embed_file/4is refused from then on; see Attachments. PdfElixide.Editor.modified?/1reportstrueuntil the next full write, and an incremental save is refused.
Repeated merges become more expensive as the combined document grows.
What a merge keeps
Merged pages keep their content, rotation, media box and crop box. A page that had no crop box may report an explicit crop box equal to its media box after merging. What belongs to the merged document as a whole is not carried, such as its form fields and signatures, bookmarks, named destinations, page labels, structure tree, attachments and document information. A resource that several of its pages share, such as an embedded font, is written once for each page that uses it, so the result can be larger than the two documents together.
The combined document keeps this editor's declared PDF version, even when the merged document declares a later one; see The declared version is not raised to match.
When a merge is refused
Some documents are refused with
{:error, %PdfElixide.Error{reason: :unsupported}}, and the editor is left as
it was:
- A page with annotations. Links, comments and form fields refer to other
pages or to the document's form, and a merge would leave them pointing
outside the result. Flattening them in that document first with
PdfElixide.Editor.flatten_annotations/1andPdfElixide.Form.flatten/1, then merging the written result, usually removes them; a page where any remain is refused again. - A page that refers to other pages in any other way, such as an article thread that continues on another page, a page action that goes to one (by page or by a named destination) or to an article thread, a color separation group or presentation steps. The error names what the page holds.
- A page action that runs JavaScript or acts on form fields or
attachments, such as one that resets, submits or imports form data, hides
a field by name, or opens an attachment of that document. Once merged it
would act on this document's fields and attachments instead.
PdfElixide.Editor.sanitize/2removes only document-level JavaScript, so it cannot prepare such a page for merging. - A page that uses layers (optional content). The merged document's layer settings are not carried, so a hidden layer would show and the list of layers would be lost.
- A page that takes its fonts, images or other resources from its page tree rather than declaring them itself. The merged page would lose them.
- A page that declares no resources at all, when this document's page tree declares some. The merged page would take this document's fonts and images as its own. Such a page merges into a document whose page tree declares none.
- A tagged page, when this document is tagged too. The page's structure entries would attach its content to this document's structure elements, so assistive technology would read it in the wrong place. A tagged page merges into an untagged document, where its structure is not carried.
- A page with an excessively nested object graph.
- An editor with a pending redaction: a region queued with
PdfElixide.Editor.add_redaction/3, or a page marked withPdfElixide.Editor.mark_redactions/2that has redaction annotations. The merge would lose it, and a laterPdfElixide.Editor.apply_redactions/1would remove nothing. Apply it first, or withdraw a mark withPdfElixide.Editor.unmark_redactions/2. A region queued afterPdfElixide.Editor.apply_redactions/1has run cannot be applied in that editor at all: write it withPdfElixide.Editor.to_binary/2, open the result, and redact it there before merging. An editor whosePdfElixide.Editor.apply_redactions/1failed is refused too: discard it and reopen the source. - An editor with no pages. Merge before deleting the last page.
- An editor converted to PDF/A-1 with
PdfElixide.Compliance.convert/3. The merge would compress the document's XMP metadata, which PDF/A-1 forbids. Merge first, then convert; see the PDF/A conversion guide.
Merging an unencrypted document with no pages changes nothing and succeeds.
An encrypted document returns {:error, %PdfElixide.Error{reason: :encrypted}},
even one that opens without a password. Open it as an editor, with its password
if it needs one, write it with PdfElixide.Editor.to_binary/2, and merge the
result with PdfElixide.Editor.merge_binary/2.
Taking only some pages from another document
A merge takes every page of the other document. To take only some, extract them from that document first and merge the result:
source = Editor.open!("path/to/appendix.pdf")
second_page = Editor.extract_pages!(source, [1])
Editor.close(source)
"path/to/report.pdf"
|> Editor.open!()
|> Editor.merge_binary!(second_page)
|> Editor.save!("report-with-appendix.pdf")
|> Editor.close()
#=> :okThe extracted pages carry the other editor's pending edits, and the merge refuses them for the same reasons it would refuse the whole document.
Splitting a document
PdfElixide.Editor.extract_pages/2 writes the listed pages, in the order given,
to a new PDF binary without changing the editor's pages or pending edits.
File.write!/2 saves one:
editor = Editor.open!("path/to/report.pdf")
File.write!("summary.pdf", Editor.extract_pages!(editor, [0, 1]))The examples below reuse this editor until the next one opens its own.
Extracted documents include pending edits but are not encrypted. Open one with
PdfElixide.Editor.from_binary/2 and write it with :encryption if it needs a
password. An empty list or a repeated index raises ArgumentError, and a page
the document cannot resolve returns
{:error, %PdfElixide.Error{reason: :invalid_pdf}} rather than being left out.
Parts and chunks
PdfElixide.Editor.extract_page_ranges/2 does the same for each of several
inclusive ranges, and returns the binaries in the order of the ranges. Ranges
may overlap:
[front, back] = Editor.extract_page_ranges!(editor, [0..9, 10..19])To cut a document into parts of a fixed size, build the ranges from the page count:
chunks =
0..(Editor.page_count!(editor) - 1)//1
|> Enum.chunk_every(10)
|> Enum.map(fn pages -> List.first(pages)..List.last(pages) end)
editor
|> Editor.extract_page_ranges!(chunks)
|> Enum.with_index(1)
|> Enum.each(fn {bytes, n} -> File.write!("part-#{n}.pdf", bytes) end)
:ok = Editor.close(editor)All the binaries from one call are in memory together. To hold only one at a
time, call PdfElixide.Editor.extract_pages/2 once per part.
Splitting at bookmarks
PdfElixide.Editor.split_by_bookmarks/2 cuts the document where its bookmarks
point, so a book with one bookmark per chapter becomes one binary per chapter.
It returns each part with the PdfElixide.Document.BookmarkSegment describing
it:
editor = Editor.open!("path/to/book.pdf")
editor
|> Editor.split_by_bookmarks!()
|> Enum.each(fn {segment, bytes} -> File.write!("#{segment.file_stem}.pdf", bytes) end)A part runs from its bookmark's page to the page before the next bookmark's; the last runs to the end. Parts include pending edits and document information, and are not encrypted.
Every part also keeps the whole document's bookmarks, and with them every page a bookmark points to, including the first page of each other part that starts at a bookmark. Those bookmarks lead to pages the part does not show. That adds to each part's size, and anyone reading a part's bytes can recover those pages; see Extraction is not redaction.
Plan the parts without writing them with
PdfElixide.Document.bookmark_segments/2 on a read-only document, or
PdfElixide.Editor.bookmark_segments/2 on an editor. By default each top-level
bookmark starts a part, and the pages before the first become a part of their
own, with a nil title and the stem "front-matter". Options choose deeper
bookmarks, filter them by title, or leave the front matter out; see
PdfElixide.Document.bookmark_opts/0:
book = Document.open!("path/to/book.pdf")
segments = Document.bookmark_segments!(book, depth: 2, title_prefix: "Chapter")
:ok = Document.close(book)Bookmarks that do not point to a page are skipped. When several point to the
same page, the first starts the part. No matching bookmarks returns {:ok, []};
use Parts and chunks instead.
On an editor, the parts follow the pages the editor would write now. After
PdfElixide.Editor.move_page/3, PdfElixide.Editor.delete_page/2 or
PdfElixide.Editor.keep_pages/2, each bookmark follows its page to its new
position, and a bookmark whose page is gone is skipped.
PdfElixide.Document.bookmark_segments/2 always plans the pages as they are in
the file.
Every part is in memory until PdfElixide.Editor.split_by_bookmarks/2 returns.
To hold one at a time, plan on the editor and extract each part yourself:
for segment <- Editor.bookmark_segments!(editor) do
[bytes] = Editor.extract_page_ranges!(editor, [segment.pages])
File.write!("#{segment.file_stem}.pdf", bytes)
end
:ok = Editor.close(editor)Extraction is not redaction
PdfElixide.Editor.extract_pages/2 leaves out the pages you did not list, but
only when nothing else in the document still refers to them. Anything that does
— a bookmark, link or named destination targeting the page, or a form field
whose widget sits on it, for example — keeps that page and its content stream in
the extracted file, where anyone reading the bytes can recover it. Do not rely
on extraction to keep content confidential; see the Redaction
guide for removing it.