Elixir wrapper for the qpdf command-line tool.

Provides functions for PDF inspection, page selection, page extraction, and splitting. By default, operations automatically decrypt input PDFs if they are encrypted.

Input Types

All PDF processing functions accept input/0, which can be:

  • binary - in-memory PDF data
  • {:file, path} - path to an existing PDF file on disk

When passing {:file, path}, qpdf directly reads the file on disk, avoiding loading the entire document into BEAM memory or writing redundant temporary copies.

Summary

Functions

Embeds an attachment (file or binary data) into the PDF document.

Returns a list of all embedded attachments in the PDF.

Returns the version of the installed qpdf executable.

Checks whether the PDF file is syntactically valid.

Decrypts an encrypted PDF document, optionally using a password.

Inspects page geometry, bounding boxes, dimensions, orientation, and paper size.

Encrypts a PDF document with password protection and access permissions.

Checks whether the given PDF is encrypted.

Returns detailed encryption parameters, cipher methods, and access permissions for a PDF.

Returns the path to the qpdf executable, downloading and installing it if necessary.

Extracts the raw contents of an embedded attachment by key.

Extracts structural metadata, outlines, page details, and object trees as a decoded JSON map.

Optimizes a PDF for Fast Web View (linearization).

Checks whether the given PDF is linearized (optimized for Fast Web View).

Merges multiple PDFs into a single document.

Optimizes and compresses a PDF document to reduce file size.

Optimizes raster images within a PDF document using DCT (JPEG) compression.

Overlays pages from another PDF on top of the input document.

Returns the page count of a PDF without splitting it.

Returns a list with the byte size of each page in the PDF.

Extracts specified pages or page ranges from a PDF.

Checks whether the given password is valid to open the encrypted PDF.

Removes an embedded attachment from the PDF by key.

Checks whether the given PDF requires a password to open.

Rotates pages in a PDF by a specified angle.

Splits a PDF into pages or consecutive groups of pages.

Returns the base temporary directory used for PDF operations.

Underlays pages from another PDF behind the input document.

Types

input()

@type input() :: binary() | {:file, Path.t()}

Functions

add_attachment(input, attachment, opts \\ [])

@spec add_attachment(input(), input(), keyword()) ::
  {:ok, binary() | Path.t()} | {:error, any()}

Embeds an attachment (file or binary data) into the PDF document.

Outputs the resulting PDF directly to standard output or a destination file.

Options

  • :key - unique key for the attachment (defaults to filename or "attachment")
  • :filename - displayed filename in PDF viewers (defaults to key or basename)
  • :mimetype - MIME type (e.g., "application/xml", "text/plain")
  • :description - optional description string
  • :creation_date - creation date string
  • :mod_date - modification date string
  • :replace - boolean, replace existing attachment if key already exists (default false)
  • :into - destination: :memory (default) or path / {:file, path}

Examples

# Embed XML invoice for Factur-X / ZUGFeRD compliance
{:ok, embedded} = Qpdf.add_attachment(pdf, xml_binary,
  key: "factur-x.xml",
  filename: "factur-x.xml",
  mimetype: "text/xml"
)

# Embed a file from disk
{:ok, embedded} = Qpdf.add_attachment(pdf, {:file, "attachment.csv"}, into: "with_csv.pdf")

attachments(input)

@spec attachments(input()) :: {:ok, [map()]} | {:error, any()}

Returns a list of all embedded attachments in the PDF.

Extracts attachment metadata including key, filename, mime-type, description, creation/modification dates, and checksums.

Examples

{:ok, attachments} = Qpdf.attachments(pdf)
# => [%{key: "invoice.xml", filename: "invoice.xml", mimetype: "application/xml", ...}]

bin_version()

@spec bin_version() :: {:ok, String.t()} | :error

Returns the version of the installed qpdf executable.

Returns {:ok, version_string} on success or :error when the executable is not available or cannot be run.

Examples

case Qpdf.bin_version() do
  {:ok, version} -> IO.puts("Running qpdf #{version}")
  :error -> IO.puts("qpdf is not installed")
end

check(input)

@spec check(input()) :: :ok | {:error, any()}

Checks whether the PDF file is syntactically valid.

Uses qpdf --check. Returns :ok if valid, or {:error, reason} if the PDF is corrupt or invalid.

decrypt(input, opts \\ [])

@spec decrypt(input(), keyword()) :: {:ok, binary() | Path.t()} | {:error, any()}

Decrypts an encrypted PDF document, optionally using a password.

Outputs the unencrypted PDF directly to standard output without intermediate disk files.

Options

  • :password - password required to decrypt the document
  • :into - destination: :memory (default) or path / {:file, path}

Examples

{:ok, plain_pdf} = Qpdf.decrypt(encrypted_input, password: "secret")

dimensions(input, page_spec \\ :all)

@spec dimensions(input(), :all | integer() | Range.t() | list()) ::
  {:ok, map() | [map()]} | {:error, any()}

Inspects page geometry, bounding boxes, dimensions, orientation, and paper size.

Returns width and height in points (1/72 inch), visual orientation (:portrait, :landscape, :square), rotation angle (0, 90, 180, 270), and media/crop bounding boxes.

Parameters

  • input: The PDF as binary or {:file, path}
  • page_spec: Target page or pages:
    • :all (default) - returns a list of dimensions for all pages
    • integer (e.g. 1) - returns a single dimension map for that page
    • Range or list (e.g. 1..3) - returns a list of dimensions for specified pages

Examples

# Inspect all pages
{:ok, pages} = Qpdf.dimensions(pdf)
# => [%{page: 1, width: 595.28, height: 841.89, orientation: :portrait, paper_size: "A4", ...}]

# Inspect a single page
{:ok, page1} = Qpdf.dimensions(pdf, 1)
page1.orientation #=> :portrait
page1.paper_size  #=> "A4"

encrypt(input, opts \\ [])

@spec encrypt(input(), keyword()) :: {:ok, binary() | Path.t()} | {:error, any()}

Encrypts a PDF document with password protection and access permissions.

Outputs the encrypted PDF directly to standard output without intermediate disk files.

Options

  • :user_password - password required to open the PDF (default: "")
  • :owner_password - password required to modify permissions (required for 256-bit keys when :user_password is provided, unless allow_insecure: true; default: "")
  • :key_length - encryption key length: 40 (requires allow_weak_crypto: true), 128, or 256 (default: 256)
  • :use_aes - boolean, use AES encryption for 128-bit keys (default: true)
  • :allow_weak_crypto - boolean, allow writing insecure/legacy encryption (required for key_length: 40 or 128-bit RC4, default: false)
  • :allow_insecure - boolean, allow setting a user password with an empty owner password for 256-bit keys (insecure, allows passwordless opening; default: false)
  • :print - print permission: :none, :low, or :full
  • :modify - modification permission: :none, :assembly, :form, :annotate, or :all
  • :extract - boolean, allow text/graphic extraction
  • :annotate - boolean, allow annotations and commenting
  • :cleartext_metadata - boolean, keep metadata unencrypted
  • :into - destination: :memory (default) or path / {:file, path}

Examples

# Encrypt with user and owner passwords
{:ok, enc} = Qpdf.encrypt(input, user_password: "open", owner_password: "admin")

# Encrypt with restricted permissions
{:ok, enc} = Qpdf.encrypt(input, owner_password: "admin", print: :none, extract: false)

encrypted?(input)

@spec encrypted?(input()) :: boolean() | {:error, any()}

Checks whether the given PDF is encrypted.

Uses qpdf --is-encrypted. Returns true if encrypted, false if unencrypted, or {:error, reason} on failure.

encryption_info(input, opts \\ [])

@spec encryption_info(input(), keyword()) :: {:ok, map()} | {:error, any()}

Returns detailed encryption parameters, cipher methods, and access permissions for a PDF.

Uses qpdf --show-encryption. Returns {:ok, %{encrypted: false}} if the document is not encrypted. If encrypted, returns {:ok, map()} with encryption revision (:r), permission integer (:p), cipher methods (:stream_method, :string_method, :file_method), permissions map, and password details if supplied or recoverable.

Options

  • :password - optional password to test against the document

Examples

# Inspect an encrypted PDF
{:ok, info} = Qpdf.encryption_info(pdf)
info.encrypted     #=> true
info.r             #=> 6 (Revision)
info.stream_method #=> "AESv3"
info.permissions.print_high #=> false

# Test password type
{:ok, info} = Qpdf.encryption_info(pdf, password: "admin")
info.password_matched #=> :owner

executable_path()

@spec executable_path() :: String.t()

Returns the path to the qpdf executable, downloading and installing it if necessary.

extract_attachment(input, key, opts \\ [])

@spec extract_attachment(input(), String.t(), keyword()) ::
  {:ok, binary() | Path.t()} | {:error, any()}

Extracts the raw contents of an embedded attachment by key.

Outputs the attachment directly into memory as a binary, or into a file if :into is specified.

Options

  • :into - destination: :memory (default) or path / {:file, path}

Examples

{:ok, xml_bytes} = Qpdf.extract_attachment(pdf, "invoice.xml")
{:ok, path} = Qpdf.extract_attachment(pdf, "invoice.xml", into: "extracted.xml")

json(input, opts \\ [])

@spec json(input(), keyword()) :: {:ok, map()} | {:error, any()}

Extracts structural metadata, outlines, page details, and object trees as a decoded JSON map.

Uses qpdf --json. Decodes the JSON output into native Elixir maps and lists using the standard library JSON module.

Options

  • :version - JSON schema version (e.g. 1 or 2, default: 2)

Returns

  • {:ok, map} on success
  • {:error, any} on failure

linearize(input, opts \\ [])

@spec linearize(input(), keyword()) :: {:ok, binary() | Path.t()} | {:error, any()}

Optimizes a PDF for Fast Web View (linearization).

A linearized PDF enables viewers to display page 1 immediately over HTTP while the remainder of the document continues downloading.

Outputs the linearized PDF directly to standard output without intermediate disk files.

Parameters

  • input: The PDF as a binary or {:file, path}

@doc """ Optimizes a PDF for Fast Web View (linearization).

A linearized PDF enables viewers to display page 1 immediately over HTTP while the remainder of the document continues downloading.

Outputs the linearized PDF directly to standard output without intermediate disk files.

Options

  • :into - destination: :memory (default) or path / {:file, path}

Returns

  • {:ok, binary | Path.t()} on success

  • {:error, any} on failure

linearized?(input)

@spec linearized?(input()) :: boolean() | {:error, any()}

Checks whether the given PDF is linearized (optimized for Fast Web View).

Uses qpdf --check-linearization. Returns true if linearized, false if not linearized, or {:error, reason} on failure.

merge(inputs, opts \\ [])

@spec merge(
  [input() | {input(), integer() | Range.t() | list() | String.t()}],
  keyword()
) ::
  {:ok, binary() | Path.t()} | {:error, any()}

Merges multiple PDFs into a single document.

Accepts a list of inputs. Each item in the list can be:

  • an input (binary or {:file, path}) to include all its pages
  • a {input, page_spec} tuple to include only specific pages or ranges

Outputs the merged PDF directly to memory or a file specified with into:.

Examples

# Merge multiple binaries into memory
{:ok, merged} = Qpdf.merge([pdf1, pdf2])

# Merge files directly to a destination path without loading bytes into BEAM memory
{:ok, path} = Qpdf.merge([{:file, "cover.pdf"}, {:file, "body.pdf"}], into: "merged.pdf")

# Merge specific page selections from different documents
{:ok, merged} = Qpdf.merge([
  {{:file, "report.pdf"}, 1..5},
  {appendix_binary, "1-z:even"}
])

optimize(input, opts \\ [])

@spec optimize(input(), keyword()) :: {:ok, binary() | Path.t()} | {:error, any()}

Optimizes and compresses a PDF document to reduce file size.

Compresses uncompressed streams, generates object streams (packing objects into compressed stream containers), and recompresses Flate streams.

Outputs the optimized PDF directly to standard output without intermediate disk files.

Options

  • :stream_data - :compress (default), :uncompress, or :preserve
  • :object_streams - :generate (default), :preserve, or :disable
  • :recompress_flate - boolean, whether to recompress flate streams (default: true)
  • :into - destination: :memory (default) or path / {:file, path}

Examples

{:ok, compressed} = Qpdf.optimize(input)

optimize_images(input, opts \\ [])

@spec optimize_images(input(), keyword()) ::
  {:ok, binary() | Path.t()} | {:error, any()}

Optimizes raster images within a PDF document using DCT (JPEG) compression.

Combines qpdf --optimize-images with optional filtering by image dimensions, JPEG quality levels, inline image handling, and unreferenced resource cleanup.

Outputs the optimized PDF directly to standard output or a destination file.

Options

  • :jpeg_quality - integer from 0 (lowest) to 100 (highest)
  • :min_width - minimum image width in pixels to optimize
  • :min_height - minimum image height in pixels to optimize
  • :min_area - minimum image area (width * height in pixels) to optimize
  • :keep_inline_images - boolean, if true, exclude inline images from optimization (default false)
  • :externalize_inline_images - boolean, convert inline images to regular image objects (default false)
  • :remove_unreferenced - boolean, remove unreferenced fonts/images from page resource dictionaries (default false)
  • :into - destination: :memory (default) or path / {:file, path}

Examples

# Basic image optimization
{:ok, optimized_pdf} = Qpdf.optimize_images(pdf)

# Target images with quality and minimum area thresholds
{:ok, compact_pdf} = Qpdf.optimize_images(pdf,
  jpeg_quality: 80,
  min_area: 10_000,
  remove_unreferenced: true,
  into: "compact.pdf"
)

overlay(input, overlay_input, opts \\ [])

@spec overlay(input(), input(), keyword()) ::
  {:ok, binary() | Path.t()} | {:error, any()}

Overlays pages from another PDF on top of the input document.

Useful for applying stamps, watermarks, signatures, or foreground content. Outputs the result directly to standard output without intermediate disk files.

Options

  • :to - page specification on the destination document (e.g. 1..5, "even")
  • :from - page specification on the overlay document
  • :repeat - repeat overlay pages (e.g. "1-z" or true to repeat all pages)
  • :password - password for the overlay document if encrypted

Examples

# Apply a 1-page watermark across all pages
{:ok, watermarked} = Qpdf.overlay(doc, watermark_pdf, repeat: true)

# Apply a stamp to page 1 only
{:ok, stamped} = Qpdf.overlay(doc, stamp_pdf, to: 1)

page_count(input)

@spec page_count(input()) :: {:ok, pos_integer()} | {:error, any()}

Returns the page count of a PDF without splitting it.

Uses --show-npages, which only reads the page tree structure without writing per-page files, making it very fast even on large documents.

Returns

  • {:ok, pos_integer} on success
  • {:error, any} on failure

page_size_vector(input)

@spec page_size_vector(input()) :: {:ok, [non_neg_integer()]} | {:error, any()}

Returns a list with the byte size of each page in the PDF.

Uses the same default flags as split/1 but avoids loading full page binaries into memory by reading file metadata directly.

pages(input, page_spec, opts \\ [])

@spec pages(input(), integer() | Range.t() | list() | String.t(), keyword()) ::
  {:ok, binary() | Path.t()} | {:error, any()}

Extracts specified pages or page ranges from a PDF.

Accepts:

  • an integer page number (e.g. 1)
  • an Elixir range (e.g. 1..5)
  • a list of page numbers (e.g. [1, 3, 5])
  • a qpdf page selection string (e.g. "1-5", "1-z:even", "z-1")

Parameters

  • input: The PDF as a binary or {:file, path}
  • page_spec: An integer, range, list of pages, or selection string

Returns

  • {:ok, binary | Path.t()} on success

  • {:error, any} on failure

password_valid?(input, password)

@spec password_valid?(input(), String.t()) :: boolean() | {:error, any()}

Checks whether the given password is valid to open the encrypted PDF.

Uses qpdf --requires-password. Returns true if the password is valid (user or owner password), or false if the password is wrong or the document is not encrypted.

Parameters

  • input: The PDF as a binary or {:file, path}
  • password: The password string to test

Examples

Qpdf.password_valid?(pdf, "secret") #=> true
Qpdf.password_valid?(pdf, "wrong")  #=> false

remove_attachment(input, key, opts \\ [])

@spec remove_attachment(input(), String.t(), keyword()) ::
  {:ok, binary() | Path.t()} | {:error, any()}

Removes an embedded attachment from the PDF by key.

Outputs the resulting PDF directly to standard output or a destination file.

Options

  • :into - destination: :memory (default) or path / {:file, path}

Examples

{:ok, cleaned_pdf} = Qpdf.remove_attachment(pdf, "factur-x.xml")

requires_password?(input)

@spec requires_password?(input()) :: boolean() | {:error, any()}

Checks whether the given PDF requires a password to open.

Returns true if a user password is required to open the document, or false if the document is unencrypted or opens with an empty user password.

Examples

Qpdf.requires_password?(pdf) #=> true | false

rotate(input, angle, page_spec \\ :all, opts \\ [])

@spec rotate(input(), integer() | String.t(), any(), keyword()) ::
  {:ok, binary() | Path.t()} | {:error, any()}

Rotates pages in a PDF by a specified angle.

The angle must be a multiple of 90 (e.g. 90, 180, 270, -90). By default, all pages are rotated. A specific page, range, or page specification can optionally be provided.

Outputs the rotated PDF directly to standard output without intermediate disk files.

Examples

# Rotate all pages 90 degrees clockwise
{:ok, rotated} = Qpdf.rotate(input, 90)

# Rotate only page 2 by 180 degrees
{:ok, rotated} = Qpdf.rotate(input, 180, 2)

# Rotate a range of pages
{:ok, rotated} = Qpdf.rotate(input, 90, 1..5)

split_pages(input, opts)

Splits a PDF into pages or consecutive groups of pages.

Defaults to splitting into individual single-page documents (pages_per_group: 1). When pages_per_group > 1, splits into multi-page documents of at most pages_per_group pages.

Grouping happens inside a single qpdf execution, so splitting a large PDF into multiple chunks costs only one process invocation.

Options

  • :into - destination directory: :memory (default, returns [binary]) or path / {:dir, path} (returns [Path.t()] without loading files into memory)

Returns

  • {:ok, [binary] | [Path.t()]} on success, in page order

  • {:error, any} on failure

split_pages(input, pages_per_group \\ 1, opts \\ [])

@spec split_pages(input(), pos_integer() | keyword(), keyword()) ::
  {:ok, [binary()] | [Path.t()]} | {:error, any()}

tmp_dir()

@spec tmp_dir() :: String.t()

Returns the base temporary directory used for PDF operations.

Defaults to System.tmp_dir!(), but can be configured in your application:

config :qpdf, tmp_dir: "/path/to/custom/tmp"

underlay(input, underlay_input, opts \\ [])

@spec underlay(input(), input(), keyword()) ::
  {:ok, binary() | Path.t()} | {:error, any()}

Underlays pages from another PDF behind the input document.

Useful for applying digital letterheads, backgrounds, or stationery. Outputs the result directly to standard output without intermediate disk files.

Options

  • :to - page specification on the destination document
  • :from - page specification on the underlay document
  • :repeat - repeat underlay pages (e.g. "1-z" or true to repeat all pages)
  • :password - password for the underlay document if encrypted
  • :into - destination: :memory (default) or path / {:file, path}

Examples

# Apply a background letterhead across all pages
{:ok, with_letterhead} = Qpdf.underlay(doc, letterhead_pdf, repeat: true)