PdfExtractor (PdfExtractor v0.9.0)

Copy Markdown View Source

A powerful and easy-to-use Elixir library for extracting text and metadata from PDF files.

PdfExtractor leverages Python's pdfplumber library through seamless integration to provide robust PDF text extraction capabilities. It supports both file-based and binary-based operations, making it suitable for various use cases from local file processing to web-based PDF handling.

Features

  • πŸ” Extract text from single or multiple PDF pages
  • πŸ“ Area-based extraction using bounding boxes
  • πŸ“ Extract words together with their position on the page
  • πŸ“‹ Extract tables as rows of cells
  • πŸ” Open encrypted PDFs with a password
  • 🌐 Work with PDF data directly from memory (e.g., HTTP downloads)
  • πŸ“Š Get PDF metadata like title, author, creation date
  • 🐍 Leverages Python's powerful pdfplumber library
  • πŸš€ Simple and intuitive API
  • βœ… Comprehensive test coverage
  • πŸ“š Full documentation

Installation

Add pdf_extractor to your list of dependencies in mix.exs:

def deps do
  [
    {:pdf_extractor, "~> 0.9.0"}
  ]
end

Then start it in your application start function:

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
        PdfExtractor,
        ...
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Usage

Extract text from specific regions using bounding boxes {x0, y0, x1, y1}:

pages = %{
  0 => {0, 0, 300, 200},    # Top-left area of page 0
  1 => [
        {200, 300, 600, 500}, # Bottom-right area of page 1
        {0, 0, 200, 250}, # Top-left area of page 1
       ]
}
PdfExtractor.extract_text("path/to/document.pdf", pages)

Return Format

The function returns a map where keys are page numbers and values are the extracted text:

%{
  0 => "Text from page 0...",
  1 => ["Text from page 1 (first area)...", "Text from page 1 (second area)..."],
  2 => "Text from page 2..."
}

Keeping the Layout

Pass layout: true to pad the text with spaces and blank lines so it keeps its position on the page. Columns of statements and tables stay aligned instead of being flattened into single lines:

PdfExtractor.extract_text("path/to/document.pdf", 0, layout: true)

Encrypted Files

Every function takes a :password option for encrypted PDFs. Without it, or with a wrong one, the function returns {:error, exception}:

PdfExtractor.extract_text("path/to/document.pdf", [], password: "secret")

Tables

Use extract_tables/3 to get the tables of each page as rows of cells. By default the cell borders come from the lines drawn on the page; use the :text strategy for tables drawn without lines:

PdfExtractor.extract_tables("path/to/document.pdf", 0)
# {:ok, %{0 => [[["Item", "Quantity", "Price"], ["Work", "1", "1 500,00 €"]]]}}

PdfExtractor.extract_tables("path/to/document.pdf", 0,
  vertical_strategy: :text,
  horizontal_strategy: :text
)

Words With Positions

Use extract_words/3 to get every word with its bounding box in PDF points, measured from the top left corner of the page. It takes the same pages and areas as extract_text/3:

PdfExtractor.extract_words("path/to/document.pdf", 0)
# {:ok,
#  %{
#    0 => [
#      %{text: "Text", left: 48.0, right: 67.99875, top: 26.64224999999999, bottom: 36.39224999999999},
#      %{text: "Example", left: 70.734375, right: 110.757, top: 26.64224999999999, bottom: 36.39224999999999},
#      ...
#    ]
#  }}

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Built on top of the excellent pdfplumber Python library
  • Uses pythonx for seamless Python integration

Summary

Functions

Returns a specification to start this module under a supervisor.

Extracts metadata from a PDF file info trailers. Typically includes "CreationDate", "ModDate", "Producer", et cetera.

Extracts metadata from PDF binary data. Similar to extract_metadata/1 but works with PDF data in memory instead of files.

Extracts tables from PDF pages.

Extracts tables from PDF binary data. See extract_tables/3 for the shape of each table and its options, and extract_text/3 for details on how to specify pages and areas.

Extracts text from PDF pages.

Extracts text from PDF binary data. See extract_text/2 for details on how to specify pages, areas, and options.

Extracts words from PDF pages together with their position on the page.

Extracts words from PDF binary data. See extract_words/3 for the shape of each word and extract_text/3 for details on how to specify pages, areas, and options.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

extract_metadata(file_path, opts \\ [])

Extracts metadata from a PDF file info trailers. Typically includes "CreationDate", "ModDate", "Producer", et cetera.

Examples

iex> PdfExtractor.extract_metadata("priv/fixtures/frankenstein-mary-shelley.pdf")
{:ok, %{"CreationDate" => "D:20260403141150", "Creator" => "PDFium", "Producer" => "PDFium"}}

extract_metadata_from_binary(binary, opts \\ [])

Extracts metadata from PDF binary data. Similar to extract_metadata/1 but works with PDF data in memory instead of files.

Examples

iex> content = File.read!("priv/fixtures/frankenstein-mary-shelley.pdf")
...> PdfExtractor.extract_metadata_from_binary(content)
{:ok, %{"CreationDate" => "D:20260403141150", "Creator" => "PDFium", "Producer" => "PDFium"}}

extract_tables(file_path, pages \\ [], opts \\ [])

Extracts tables from PDF pages.

Each page maps to the list of tables found on it. A table is a list of rows and a row a list of cells, read left to right and top to bottom. A cell is nil when the table has a cell there but no text could be assigned to it.

See extract_text/3 for details on how to specify pages, areas, and options.

Options

Besides :timeout and :name, this function accepts how the edges of the tables are found:

  • :vertical_strategy - how to find the columns (default: :lines)
  • :horizontal_strategy - how to find the rows (default: :lines)

Each strategy is one of:

  • :lines - use the lines drawn on the page, including the sides of rectangles, as cell borders
  • :lines_strict - like :lines but ignore the sides of rectangles
  • :text - infer the borders from the alignment of the words, for tables drawn without lines

Examples

Extract the tables of every page.

iex> PdfExtractor.extract_tables("priv/fixtures/fatura.pdf")
{:ok, %{0 => [[["Item", "Quantidade", "Avaliar", "Quantia"]]], 1 => []}}

Extract only the tables inside an area.

iex> PdfExtractor.extract_tables("priv/fixtures/fatura.pdf", %{0 => {0, 180, 595, 260}})
{:ok, %{0 => [[["Item", "Quantidade", "Avaliar", "Quantia"]]]}}

Find the columns from the alignment of the words instead of the drawn lines.

iex> PdfExtractor.extract_tables("priv/fixtures/fatura.pdf", 0,
...>   vertical_strategy: :text,
...>   horizontal_strategy: :lines
...> )
{:ok,
 %{
   0 => [
     [
       [nil, nil, "devedor:", "", "1 52", "5,59 €"],
       [nil, nil, "", "", "", ""],
       ["I", "tem Quantidad", "e", "Avaliar", "", "Quantia"]
     ]
   ]
 }}

extract_tables_from_binary(binary, pages \\ [], opts \\ [])

Extracts tables from PDF binary data. See extract_tables/3 for the shape of each table and its options, and extract_text/3 for details on how to specify pages and areas.

Examples

iex> content = File.read!("priv/fixtures/fatura.pdf")
...> PdfExtractor.extract_tables_from_binary(content, 0)
{:ok, %{0 => [[["Item", "Quantidade", "Avaliar", "Quantia"]]]}}

extract_text(file_path, pages \\ [], opts \\ [])

Extracts text from PDF pages.

It supports extracting from single pages, multiple pages, and specific areas within pages.

Page Numbers

  • Integer: Extract from single page (e.g., 0 for first page)
  • List: Extract from multiple pages (e.g., [0, 1, 2])
  • Empty list []: Extract from all pages (default)

Areas Format

Areas are specified as a map where keys are page numbers and values are bounding boxes:

  • Single area: %{0 => {x0, y0, x1, y1}}
  • Multiple areas: %{0 => [{x0, y0, x1, y1}, {x2, y2, x3, y3}]}
  • Mixed: %{0 => {x0, y0, x1, y1}, 1 => [{x2, y2, x3, y3}, {x4, y4, x5, y5}]}

Options

All extraction functions accept these options:

  • :timeout - timeout in milliseconds for the GenServer call (default: #{@default_timeout})
  • :name - name or PID of the server to call (default: PdfExtractor)
  • :password - the password of an encrypted PDF. Without it, or with a wrong one, extraction returns an error (default: nil)

extract_text/3 and extract_text_from_binary/3 also accept:

  • :layout - when true, keeps the position of the text on the page by padding it with spaces and blank lines, so columns stay aligned. Useful for statements and tables that the default extraction flattens into single lines (default: false)

Examples

Extract text from all pages.

iex> PdfExtractor.extract_text("priv/fixtures/fatura.pdf")
{:ok,
 %{
   0 =>
     "Text Example Bill FATURA\n# 2025010002\nData: Jun 21, 2025\nProjeto de lei para:\nSaldo devedor: 1 525,59 €\nElixir Company\nItem Quantidade Avaliar Quantia\nTrabalho 1 1 500,00 € 1 500,00 €\nMais trabalho 1 25,59 € 25,59 €\nSubtotal: 1 525,59 €\nImposto (0%): 0,00 €\nTotal: 1 525,59 €",
   1 =>
     "βœ‚\nReceipt Payment part Account / Payable to\nCH4431999123000889012\nβœ‚\nMax Muster & SΓΆhne\nAccount / Payable to\nCH4431999123000889012 Musterstrasse 123\nMax Muster & SΓΆhne 8000 Seldwyla\nMusterstrasse 123\n8000 Seldwyla\nReference\n210000000003139471430009017\nReference\n210000000003139471430009017\nAdditional information\nBestellung vom 15.10.2020\nPayable by (name/address)\nSimon Muster\nPayable by (name/address)\nMusterstrasse 1\nCurrency Amount\nSimon Muster\n8000 Seldwyla\nCHF 1 949.75 Musterstrasse 1\n8000 Seldwyla\nCurrency Amount\nCHF 1 949.75\nAcceptance point"
 }}

Extract text from only some pages.

iex> PdfExtractor.extract_text("priv/fixtures/fatura.pdf", [0])
{:ok,
 %{
   0 =>
     "Text Example Bill FATURA\n# 2025010002\nData: Jun 21, 2025\nProjeto de lei para:\nSaldo devedor: 1 525,59 €\nElixir Company\nItem Quantidade Avaliar Quantia\nTrabalho 1 1 500,00 € 1 500,00 €\nMais trabalho 1 25,59 € 25,59 €\nSubtotal: 1 525,59 €\nImposto (0%): 0,00 €\nTotal: 1 525,59 €"
 }}

Extract only the titles in the book.

iex> PdfExtractor.extract_text("priv/fixtures/frankenstein-mary-shelley.pdf", %{
...>   1 => {0, 0, 612, 190},
...>   2 => {0, 0, 612, 190},
...>   8 => {0, 0, 612, 190},
...>   12 => {0, 0, 612, 190},
...>   13 => {0, 0, 612, 190}
...> })
{:ok,
 %{
   1 => "Frankenstein",
   2 => "Introduction",
   8 => "Preface",
   12 => "Frankenstein",
   13 => "Letter I"
 }}

Extract multiple areas from a single page.

iex> PdfExtractor.extract_text("priv/fixtures/frankenstein-mary-shelley.pdf", %{
...>   13 => [{0, 200, 600, 300}, {0, 400, 612, 440}]
...> })
{:ok,
 %{
   13 => [
     "To Mrs. Saville, England.\nSt. Petersburgh, Dec. 11th, 17 β€”.",
     "I am already far north of London; and as I walk in the streets of\nPetersburgh, I feel a cold northern breeze play upon my cheeks, which"
   ]
 }}

Extract with a custom timeout for large files.

iex> PdfExtractor.extract_text("priv/fixtures/frankenstein-mary-shelley.pdf", 11,
...>   timeout: 30_000
...> )
{:ok,
 %{
   11 =>
     "β€œDid I request thee, Maker, from my clay\nTo mould me Man, did I solicit thee\nFrom darkness to promote me?”\nP L , X, 743 – 45\nARADISE OST"
 }}

Open an encrypted file with its password.

iex> PdfExtractor.extract_text("priv/fixtures/fatura-encrypted.pdf", %{0 => {0, 0, 300, 200}},
...>   password: "secret"
...> )
{:ok, %{0 => "Text Example Bill\nProjeto de lei para:\nElixir Company"}}

Keep the layout of the page so the columns of a table stay aligned.

iex> PdfExtractor.extract_text("priv/fixtures/fatura.pdf", %{0 => {0, 220, 595, 250}})
{:ok, %{0 => "Trabalho 1 1 500,00 € 1 500,00 €\nMais trabalho 1 25,59 € 25,59 €"}}

iex> PdfExtractor.extract_text("priv/fixtures/fatura.pdf", %{0 => {0, 220, 595, 250}},
...>   layout: true
...> )
{:ok,
 %{
   0 =>
     "      Trabalho                                     1          1 500,00 € 1 500,00 €\n      Mais trabalho                                1            25,59 €   25,59 € "
 }}

extract_text_from_binary(binary, pages \\ [], opts \\ [])

Extracts text from PDF binary data. See extract_text/2 for details on how to specify pages, areas, and options.

This function allows you to extract text from PDF data that's already in memory, such as data downloaded from a URL or received via an API. This avoids the need to write the PDF to the filesystem.

Examples

Extract text from all pages.

iex> content = File.read!("priv/fixtures/fatura.pdf")
...> PdfExtractor.extract_text_from_binary(content)
{:ok,
 %{
   0 =>
     "Text Example Bill FATURA\n# 2025010002\nData: Jun 21, 2025\nProjeto de lei para:\nSaldo devedor: 1 525,59 €\nElixir Company\nItem Quantidade Avaliar Quantia\nTrabalho 1 1 500,00 € 1 500,00 €\nMais trabalho 1 25,59 € 25,59 €\nSubtotal: 1 525,59 €\nImposto (0%): 0,00 €\nTotal: 1 525,59 €",
   1 =>
     "βœ‚\nReceipt Payment part Account / Payable to\nCH4431999123000889012\nβœ‚\nMax Muster & SΓΆhne\nAccount / Payable to\nCH4431999123000889012 Musterstrasse 123\nMax Muster & SΓΆhne 8000 Seldwyla\nMusterstrasse 123\n8000 Seldwyla\nReference\n210000000003139471430009017\nReference\n210000000003139471430009017\nAdditional information\nBestellung vom 15.10.2020\nPayable by (name/address)\nSimon Muster\nPayable by (name/address)\nMusterstrasse 1\nCurrency Amount\nSimon Muster\n8000 Seldwyla\nCHF 1 949.75 Musterstrasse 1\n8000 Seldwyla\nCurrency Amount\nCHF 1 949.75\nAcceptance point"
 }}

Extract text from only some pages.

iex> content = File.read!("priv/fixtures/fatura.pdf")
...> PdfExtractor.extract_text_from_binary(content, [0])
{:ok,
 %{
   0 =>
     "Text Example Bill FATURA\n# 2025010002\nData: Jun 21, 2025\nProjeto de lei para:\nSaldo devedor: 1 525,59 €\nElixir Company\nItem Quantidade Avaliar Quantia\nTrabalho 1 1 500,00 € 1 500,00 €\nMais trabalho 1 25,59 € 25,59 €\nSubtotal: 1 525,59 €\nImposto (0%): 0,00 €\nTotal: 1 525,59 €"
 }}

Extract only the titles in the book.

iex> content = File.read!("priv/fixtures/frankenstein-mary-shelley.pdf")
...>
...> PdfExtractor.extract_text_from_binary(content, %{
...>   1 => {0, 0, 612, 190},
...>   2 => {0, 0, 612, 190},
...>   8 => {0, 0, 612, 190},
...>   12 => {0, 0, 612, 190},
...>   13 => {0, 0, 612, 190}
...> })
{:ok,
 %{
   1 => "Frankenstein",
   2 => "Introduction",
   8 => "Preface",
   12 => "Frankenstein",
   13 => "Letter I"
 }}

Extract multiple areas from a single page.

iex> content = File.read!("priv/fixtures/frankenstein-mary-shelley.pdf")
...>
...> PdfExtractor.extract_text_from_binary(content, %{
...>   13 => [{0, 200, 600, 300}, {0, 400, 612, 440}]
...> })
{:ok,
 %{
   13 => [
     "To Mrs. Saville, England.\nSt. Petersburgh, Dec. 11th, 17 β€”.",
     "I am already far north of London; and as I walk in the streets of\nPetersburgh, I feel a cold northern breeze play upon my cheeks, which"
   ]
 }}

extract_words(file_path, pages \\ [], opts \\ [])

Extracts words from PDF pages together with their position on the page.

Each word is a map with its :text and the edges of its bounding box, :left, :right, :top and :bottom, in PDF points measured from the top left corner of the page. Words come in the order pdfplumber reads them, which is the same order extract_text/3 uses to build its text.

See extract_text/3 for details on how to specify pages, areas, and options.

Examples

Extract the words of a single page.

iex> {:ok, %{0 => words}} = PdfExtractor.extract_words("priv/fixtures/fatura.pdf", 0)
...> Enum.take(words, 3)
[
  %{
    text: "Text",
    left: 48.0,
    right: 67.99875,
    top: 26.64224999999999,
    bottom: 36.39224999999999
  },
  %{
    text: "Example",
    left: 70.734375,
    right: 110.757,
    top: 26.64224999999999,
    bottom: 36.39224999999999
  },
  %{
    text: "Bill",
    left: 113.49609375,
    right: 128.63240625,
    top: 26.64224999999999,
    bottom: 36.39224999999999
  }
]

Extract only the words inside an area.

iex> PdfExtractor.extract_words("priv/fixtures/frankenstein-mary-shelley.pdf", %{
...>   1 => {0, 0, 612, 190}
...> })
{:ok,
 %{
   1 => [
     %{
       text: "Frankenstein",
       left: 76.99218429199217,
       right: 177.95707306012164,
       top: 102.2249957406251,
       bottom: 132.22499449062502
     }
   ]
 }}

extract_words_from_binary(binary, pages \\ [], opts \\ [])

Extracts words from PDF binary data. See extract_words/3 for the shape of each word and extract_text/3 for details on how to specify pages, areas, and options.

Examples

Extract only the words inside an area.

iex> content = File.read!("priv/fixtures/frankenstein-mary-shelley.pdf")
...> PdfExtractor.extract_words_from_binary(content, %{1 => {0, 0, 612, 190}})
{:ok,
 %{
   1 => [
     %{
       text: "Frankenstein",
       left: 76.99218429199217,
       right: 177.95707306012164,
       top: 102.2249957406251,
       bottom: 132.22499449062502
     }
   ]
 }}

start_link(opts \\ [])