Current section
Files
Jump to
Current section
Files
lib/popplex.ex
defmodule Popplex do
@moduledoc """
Popplex - Elixir NIF wrapper for Poppler PDF library.
This module provides a high-level API for working with PDF files using
the Poppler library through a Native Implemented Function (NIF).
## Features
- Get page count from PDF files
- Extract text content from PDF files (by page or entire document)
- Combine multiple PDF files
- Render PDF pages to images (PNG, JPEG)
## Examples
# Get the number of pages in a PDF
{:ok, count} = Popplex.get_page_count("document.pdf")
# Extract text from all pages
{:ok, text} = Popplex.get_text("document.pdf")
# Extract text from a specific page (0-indexed)
{:ok, text} = Popplex.get_text("document.pdf", page: 0)
# Render a page to PNG
{:ok, png_data} = Popplex.render_page("document.pdf", page: 0)
File.write!("page.png", png_data)
"""
alias Popplex.NIF
@type page_count :: non_neg_integer()
@type error_reason :: String.t()
@type image_format :: :png | :jpeg
@type render_opts :: [
page: non_neg_integer(),
all: boolean(),
format: image_format(),
dpi: pos_integer(),
quality: 1..100
]
@doc """
Gets the total number of pages in a PDF file.
## Parameters
- `path`: Path to the PDF file (string or charlist)
## Returns
- `{:ok, page_count}` on success
- `{:error, reason}` on failure
## Examples
Popplex.get_page_count("my_document.pdf")
# => {:ok, 42}
Popplex.get_page_count("nonexistent.pdf")
# => {:error, "Failed to open PDF document"}
"""
@spec get_page_count(Path.t()) :: {:ok, page_count()} | {:error, error_reason()}
def get_page_count(path) when is_binary(path) do
path
|> to_charlist()
|> NIF.get_page_count_nif()
end
def get_page_count(path) when is_list(path) do
NIF.get_page_count_nif(path)
end
@doc """
Extracts text content from a PDF file.
## Parameters
- `path`: Path to the PDF file (string or charlist)
- `opts`: Options keyword list
- `:page` - Page number to extract (0-indexed). If not provided, extracts all pages.
- `:all` - If true, extracts all pages (default behavior)
## Returns
- `{:ok, text}` on success, where text is a binary string
- `{:error, reason}` on failure
## Examples
# Extract text from all pages
Popplex.get_text("document.pdf")
# => {:ok, "Full document text..."}
# Extract text from page 1 (0-indexed)
Popplex.get_text("document.pdf", page: 0)
# => {:ok, "First page text..."}
# Explicitly extract all pages
Popplex.get_text("document.pdf", all: true)
# => {:ok, "Full document text..."}
"""
@spec get_text(Path.t(), keyword()) :: {:ok, binary()} | {:error, error_reason()}
def get_text(path, opts \\ []) when is_binary(path) or is_list(path) do
charlist_path = if is_binary(path), do: to_charlist(path), else: path
page_num =
cond do
Keyword.has_key?(opts, :page) -> Keyword.get(opts, :page)
Keyword.get(opts, :all, false) -> -1
# Default to all pages
true -> -1
end
NIF.get_text_nif(charlist_path, page_num)
end
@doc """
Combines multiple PDF files into a single output file.
Uses the `pdfunite` command-line tool (part of Poppler utilities) to merge PDFs.
## Parameters
- `input_files`: List of PDF file paths to combine (minimum 2 files)
- `output_file`: Path where the combined PDF should be saved
## Returns
- `{:ok, output_path}` on success
- `{:error, reason}` on failure
## Examples
Popplex.combine_pdfs(["file1.pdf", "file2.pdf"], "combined.pdf")
# => {:ok, "combined.pdf"}
Popplex.combine_pdfs(["page1.pdf", "page2.pdf", "page3.pdf"], "book.pdf")
# => {:ok, "book.pdf"}
## Requirements
This function requires `pdfunite` to be installed on your system:
- **macOS**: `brew install poppler` (included with Poppler)
- **Ubuntu/Debian**: `sudo apt-get install poppler-utils`
- **Fedora/RHEL**: `sudo dnf install poppler-utils`
"""
@spec combine_pdfs([Path.t()], Path.t()) :: {:ok, Path.t()} | {:error, error_reason()}
def combine_pdfs(input_files, output_file)
when is_list(input_files) and is_binary(output_file) do
cond do
length(input_files) < 2 ->
{:error, "At least 2 input files are required"}
not Enum.all?(input_files, &File.exists?/1) ->
missing = Enum.reject(input_files, &File.exists?/1)
{:error, "Input files not found: #{Enum.join(missing, ", ")}"}
true ->
execute_pdfunite(input_files, output_file)
end
end
defp execute_pdfunite(input_files, output_file) do
args = input_files ++ [output_file]
case System.cmd("pdfunite", args, stderr_to_stdout: true) do
{_, 0} ->
{:ok, output_file}
{error_msg, _exit_code} ->
error_msg = String.trim(error_msg)
{:error, "Failed to combine PDFs: #{error_msg}"}
end
rescue
e in ErlangError ->
case e.original do
:enoent -> {:error, "pdfunite command not found. Please install poppler-utils."}
_ -> {:error, "System error: #{Exception.message(e)}"}
end
end
@doc """
Renders PDF pages to images.
## Parameters
- `path`: Path to the PDF file (string or charlist)
- `opts`: Options keyword list
- `:page` - Page number to render (0-indexed). If not provided, renders all pages.
- `:all` - If true, renders all pages (default behavior)
- `:format` - Output format: `:png` (default) or `:jpeg`
- `:dpi` - Resolution in dots per inch (default: 150)
- `:quality` - JPEG quality 1-100 (default: 90, ignored for PNG)
## Returns
- `{:ok, binary}` for single page - raw image binary data
- `{:ok, [binary]}` for all pages - list of raw image binary data
- `{:error, reason}` on failure
## Examples
# Render first page as PNG at 150 DPI
{:ok, png_data} = Popplex.render_page("document.pdf", page: 0)
File.write!("page1.png", png_data)
# Render all pages as JPEG at 300 DPI
{:ok, images} = Popplex.render_page("document.pdf", format: :jpeg, dpi: 300)
# Render specific page as high-quality JPEG
{:ok, jpeg_data} = Popplex.render_page("document.pdf", page: 2, format: :jpeg, quality: 95)
## Requirements
This function requires Poppler to be compiled with the Splash rendering backend.
Most standard Poppler installations include this support.
"""
@spec render_page(Path.t(), render_opts()) ::
{:ok, binary()} | {:ok, [binary()]} | {:error, error_reason()}
def render_page(path, opts \\ []) when is_binary(path) or is_list(path) do
charlist_path = if is_binary(path), do: to_charlist(path), else: path
page_num =
cond do
Keyword.has_key?(opts, :page) -> Keyword.get(opts, :page)
Keyword.get(opts, :all, false) -> -1
# Default to all pages
true -> -1
end
format =
case Keyword.get(opts, :format, :png) do
:png -> 0
:jpeg -> 1
_ -> 0
end
dpi = Keyword.get(opts, :dpi, 150)
quality = Keyword.get(opts, :quality, 90)
NIF.render_page_nif(charlist_path, page_num, format, dpi, quality)
end
end