Skip to main content

Text & Content Extraction

paperjam provides several levels of text extraction: raw strings, line-level objects with bounding boxes, and individual positioned spans. It also supports structured extraction that identifies headings, paragraphs, list items, and tables.

:::tip Multi-Format Support All extraction methods work across formats. Use paperjam.open() with any supported file:

# Works with PDF, DOCX, XLSX, PPTX, HTML, EPUB
doc = paperjam.open("report.docx")
text = doc.extract_text()
tables = doc.extract_tables()
structure = doc.extract_structure()

:::

Plain text

The simplest way to get text out of a page is extract_text(). It returns a plain string with newlines preserved:

import paperjam

doc = paperjam.open("report.pdf")
text = doc.pages[0].extract_text()
print(text)

Text lines

extract_text_lines() returns a list of TextLine objects. Each TextLine carries the full text of the line, its bounding box (x1, y1, x2, y2) in PDF points (origin at bottom-left), and a tuple of TextSpan objects that make up the line:

for line in doc.pages[0].extract_text_lines():
x1, y1, x2, y2 = line.bbox
print(f"[{x1:.1f},{y1:.1f},{x2:.1f},{y2:.1f}] {line.text}")
for span in line.spans:
print(f" span: {span.text!r} font={span.font_name} size={span.font_size}")

Text spans

extract_text_spans() returns every individual positioned text fragment without grouping them into lines. This is useful when you need the exact (x, y) position, width, font name, and font size of each fragment:

for span in doc.pages[0].extract_text_spans():
print(span.text, span.x, span.y, span.font_name, span.font_size, span.width)

TextSpan attributes:

AttributeTypeDescription
textstrThe text content
xfloatLeft edge in PDF points
yfloatBaseline y in PDF points
widthfloatWidth of the span
font_sizefloatFont size in points
font_namestrFont name as embedded in the PDF

Structured content

extract_structure() recognises headings (by relative font size), paragraphs, list items, and tables. Each block is returned as a ContentBlock:

blocks = doc.extract_structure(
heading_size_ratio=1.2, # font must be 1.2× median to count as heading
detect_lists=True,
include_tables=True,
layout_aware=False, # set True for multi-column docs
)

for block in blocks:
if block.type == "heading":
print(f"{'#' * (block.level or 1)} {block.text} (page {block.page})")
elif block.type == "paragraph":
print(block.text)
elif block.type == "list_item":
indent = " " * (block.indent_level or 0)
print(f"{indent}- {block.text}")
elif block.type == "table":
print(block.table.to_csv())

ContentBlock attributes:

AttributeTypeDescription
typestr"heading", "paragraph", "list_item", "table"
pageint1-based page number
textstr | NoneText content (None for tables)
levelint | NoneHeading level 1–6 (headings only)
indent_levelint | NoneNesting depth (list items only)
bboxtuple | NoneBounding box (x1, y1, x2, y2)
tableTable | NoneThe Table object (tables only)

Image extraction

Embedded images can be extracted from any page:

for img in doc.pages[0].extract_images():
print(f"{img.width}×{img.height} {img.color_space} {img.bits_per_component}bpc")
print(f" filters: {img.filters}")
img.save("extracted.png") # writes raw bytes to disk

Image attributes:

AttributeTypeDescription
widthintWidth in pixels
heightintHeight in pixels
color_spacestr | Nonee.g. "DeviceRGB", "DeviceCMYK"
bits_per_componentint | NoneBit depth
filterslist[str]PDF stream filters applied, e.g. ["DCTDecode"]
databytesRaw encoded image data

Searching

search() is available on both Document and Page:

# Search the whole document
results = doc.search("invoice", case_sensitive=False, max_results=50)

# Search a single page
results = doc.pages[2].search("total", case_sensitive=False)

# Regex search
results = doc.search(r"\$[\d,]+\.\d{2}", use_regex=True)

for r in results:
print(f"Page {r.page}, line {r.line_number}: {r.text!r}")
if r.bbox:
print(f" at {r.bbox}")

SearchResult attributes:

AttributeTypeDescription
pageint1-based page number
textstrFull line text containing the match
line_numberint1-based line index on the page
bboxtuple | NoneBounding box of the matching line
# All links across all pages
links = doc.extract_links()

# Links on a single page
links = doc.pages[0].extract_links()

for link in links:
print(f"Page {link.page} url={link.url} dest={link.destination}")

Link attributes:

AttributeTypeDescription
pageint1-based page number
recttupleClickable area (x1, y1, x2, y2)
urlstr | NoneExternal URL, if any
destinationdict | NoneInternal destination (page jump), if any
contentsstr | NoneAlternative text

PDF to Markdown

For LLM and RAG pipelines, converting a PDF to Markdown is often more useful than plain text:

# From a Document
md = doc.to_markdown(
heading_offset=1, # shift heading levels by N
include_page_numbers=True, # insert <!-- page N --> comments
html_tables=False, # use pipe-style tables by default
layout_aware=True, # respect multi-column layout
)

# From a single page
md = doc.pages[0].to_markdown()

# Convenience function — no need to open the document first
md = paperjam.to_markdown("report.pdf")