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 (planned feature) ## 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) """ alias Popplex.NIF @type page_count :: non_neg_integer() @type error_reason :: String.t() @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 end