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
- π Get the number of pages and the size and rotation of each page
- πΌοΈ Render pages or areas of them to PNG images
- π Work with PDF data directly from memory (e.g., HTTP downloads)
- π Get PDF metadata like title, author, creation date
- π Leverages Python's powerful
pdfplumberlibrary - π 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.10.0"}
]
endThen 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
endUsage
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)Page Info
Use extract_page_info/3 to get the size of each page in points, the frame the areas above use,
and its rotation. The size of the map is the number of pages:
{:ok, pages} = PdfExtractor.extract_page_info("path/to/document.pdf")
# {:ok, %{0 => %{width: 612.0, height: 792.0, rotation: 0}, 1 => ...}}
map_size(pages)Rendering
Use render_pages/3 to get a PNG of each page, or of an area of it, for thumbnails and previews.
Give it a :width in pixels or a :resolution in pixels per inch:
{:ok, %{0 => png}} = PdfExtractor.render_pages("path/to/document.pdf", 0, width: 400)
File.write!("page-0.png", png)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, together with the bounding
box of each table. 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 => [
# %{
# rows: [["Item", "Quantity", "Price"], ["Work", "1", "1 500,00 β¬"]],
# left: 29.25, top: 190.5, right: 582.75, bottom: 211.5
# }
# ]
# }}
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},
# ...
# ]
# }}Pass font: true to also get the font_name and font_size of each word, which tells headings
from body text:
PdfExtractor.extract_words("path/to/document.pdf", 0, font: true)
# {:ok, %{0 => [%{text: "Text", font_name: "NimbusSans-Bold", font_size: 9.75, ...}, ...]}}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 the size and rotation of PDF pages.
Extracts the size and rotation of pages from PDF binary data. See extract_page_info/3 for details.
Extracts tables from PDF pages together with their position on the page.
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.
Renders PDF pages to PNG images.
Renders pages from PDF binary data to PNG images. See render_pages/3 for its options and
extract_text/3 for details on how to specify pages and areas.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
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"}}
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"}}
Extracts the size and rotation of PDF pages.
Each page maps to its :width and :height in PDF points and its :rotation in degrees, one of
0, 90, 180 or 270. The sizes are the frame used by the areas of the other functions, so read
them before choosing a bounding box. When all pages are requested, the size of the map is the
number of pages of the document.
Pages are an integer or a list of page numbers as in extract_text/3. Areas do not apply here.
Accepts the :timeout, :name and :password options.
Examples
Get the size of every page.
iex> PdfExtractor.extract_page_info("priv/fixtures/fatura.pdf")
{:ok,
%{
0 => %{width: 612.0, height: 792.0, rotation: 0},
1 => %{width: 595.2756, height: 841.8898, rotation: 0}
}}Count the pages of a document.
iex> {:ok, pages} =
...> PdfExtractor.extract_page_info("priv/fixtures/frankenstein-mary-shelley.pdf")
...>
...> map_size(pages)
17
Extracts the size and rotation of pages from PDF binary data. See extract_page_info/3 for details.
Examples
iex> content = File.read!("priv/fixtures/fatura.pdf")
...> PdfExtractor.extract_page_info_from_binary(content, 1)
{:ok, %{1 => %{width: 595.2756, height: 841.8898, rotation: 0}}}
Extracts tables from PDF pages together with their position on the page.
Each page maps to the list of tables found on it. A table is a map with its :rows, each one a
list of cells read left to right, and the edges of its bounding box, :left, :right, :top and
:bottom, in PDF points measured from the top left corner of the page. A cell is nil when the
table has a cell there but no text could be assigned to it. The bounding box can be used as an area
with the other functions, for example to read the text right below a table.
See extract_text/3 for details on how to specify pages, areas, and options.
Options
Besides :timeout, :name and :password, 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:linesbut 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 => [
%{
rows: [["Item", "Quantidade", "Avaliar", "Quantia"]],
left: 29.25,
top: 190.5,
right: 582.75,
bottom: 211.5
}
],
1 => []
}}Extract only the tables inside an area.
iex> PdfExtractor.extract_tables("priv/fixtures/fatura.pdf", %{0 => {0, 180, 595, 260}})
{:ok,
%{
0 => [
%{
rows: [["Item", "Quantidade", "Avaliar", "Quantia"]],
left: 29.25,
top: 190.5,
right: 582.75,
bottom: 211.5
}
]
}}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 => [
%{
rows: [
[nil, nil, "devedor:", "", "1 52", "5,59 β¬"],
[nil, nil, "", "", "", ""],
["I", "tem Quantidad", "e", "Avaliar", "", "Quantia"]
],
left: 44.25,
top: 126.75,
right: 568.0695,
bottom: 211.5
}
]
}}
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 => [
%{
rows: [["Item", "Quantidade", "Avaliar", "Quantia"]],
left: 29.25,
top: 190.5,
right: 582.75,
bottom: 211.5
}
]
}}
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.,
0for 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- whentrue, 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 β¬ "
}}
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"
]
}}
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.
Options
Besides :timeout, :name and :password, this function accepts:
:font- whentrue, adds the:font_nameand:font_sizeof each word. The subset tag that PDFs prepend to the names of embedded fonts, such asABCDEF+, is removed. A word written in more than one font is split where the font changes (default:false)
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
}
]
}}Get the font of each word to tell headings from body text.
iex> PdfExtractor.extract_words(
...> "priv/fixtures/frankenstein-mary-shelley.pdf",
...> %{
...> 1 => {0, 0, 612, 190}
...> },
...> font: true
...> )
{:ok,
%{
1 => [
%{
text: "Frankenstein",
left: 76.99218429199217,
right: 177.95707306012164,
top: 102.2249957406251,
bottom: 132.22499449062502,
font_name: "LeagueGothic-Regular",
font_size: 29.999998749999918
}
]
}}
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
}
]
}}
Renders PDF pages to PNG images.
Each page maps to the PNG binary of the page, or of the area when one is given, so the same function makes thumbnails of whole pages and crops of a single table or paragraph. Images are quantized to 256 colors, which keeps them small.
See extract_text/3 for details on how to specify pages and areas.
Options
Besides :timeout, :name and :password, this function accepts:
:width- the width of the image in pixels. The height follows the proportions of the page:resolution- pixels per inch (default:72, which gives one pixel per point):antialias- whentrue, smooths the edges of text and lines (default:false)
:width and :resolution cannot be given together.
Examples
Render a thumbnail of the first page.
iex> {:ok, %{0 => png}} = PdfExtractor.render_pages("priv/fixtures/fatura.pdf", 0, width: 200)
...> <<_::binary-size(16), width::32, height::32, _::binary>> = png
...> {width, height}
{200, 259}Render only the area of a table.
iex> {:ok, %{0 => png}} =
...> PdfExtractor.render_pages("priv/fixtures/fatura.pdf", %{0 => {0, 180, 595, 260}},
...> width: 200
...> )
...>
...> <<_::binary-size(16), width::32, height::32, _::binary>> = png
...> {width, height}
{200, 26}
Renders pages from PDF binary data to PNG images. See render_pages/3 for its options and
extract_text/3 for details on how to specify pages and areas.
Examples
iex> content = File.read!("priv/fixtures/fatura.pdf")
...> {:ok, %{0 => png}} = PdfExtractor.render_pages_from_binary(content, 0, width: 200)
...> <<_::binary-size(16), width::32, height::32, _::binary>> = png
...> {width, height}
{200, 259}