# Install required packages
!pip install --upgrade --quiet 'natural-pdf[ai]>=0.5.0'
print('✓ Packages installed!')
Slides: slides.pdf
Sometimes you have data that flows over multiple columns, or pages, or just... isn't arranged in a "normal" top-to-bottom way.
from natural_pdf import PDF
pdf = PDF("https://github.com/jsoma/natural-pdf/raw/refs/heads/main/pdfs/multicolumn.pdf")
page = pdf.pages[0]
page.show(width=800)
Natural PDF deals with these through reflowing pages, where you grab specific regions of a page and then paste them back together either vertically or horizontally.
In this example we're splitting the page into three columns.
left = page.region(left=0, right=page.width/3, top=0, bottom=page.height)
mid = page.region(left=page.width/3, right=page.width/3*2, top=0, bottom=page.height)
right = page.region(left=page.width/3*2, right=page.width, top=0, bottom=page.height)
mid.show()
Now let's stack them on top of each other.
from natural_pdf.flows import Flow
stacked = [left, mid, right]
flow = Flow(segments=stacked, arrangement="vertical")
flow.show()
flow.show(in_context=False)
Now any time we want to use spatial comparisons, like "find something below this," it just works.
region = (
flow
.find('text:contains("Table one")')
.below(
until='text:contains("Table two")',
include_endpoint=False
)
)
region.show()
It works for text, it works for tables, it works for anything. Let's see how we can get both tables on the page.
First we find the bold headers – we need to say width > 10 because otherwise it pulls some weird tiny empty boxes.
(
flow
.find_all('text[width>10]:bold')
.show()
)
Then we take each of those headers, and go down down down until we either hit another bold header or the "Here is a bit more text" text.
regions = (
flow
.find_all('text[width>10]:bold')
.below(
until='text[width>10]:bold|text:contains("Here is a bit")',
include_endpoint=False
)
)
regions.show()
Now we can use .extract_table() on each individual region to give us a bunch of tables.
regions[1].extract_table().to_df()
Similar to how we have feelings about what things are on a page - headers, tables, graphics – computers also have opinions! Just like some AI models have been trained to do things like identify pictures of cats and dogs or spell check, others are capable of layout analysis - YOLO, surya, etc etc etc. There are a million! TATR is one of the useful ones for us, it's just for table detection.
But honestly: they're mostly trained on academic papers, so they aren't very good at the kinds of awful documents that journalists have to deal with. And with Natural PDF, you're probably selecting text[size>12]:bold in order to find headlines, anyway. But if your page has no readable text, they might be able to provide some useful information.
Let's start with YOLO, the default.
from natural_pdf import PDF
pdf = PDF("https://github.com/jsoma/natural-pdf/raw/refs/heads/main/pdfs/needs-ocr.pdf")
page = pdf.pages[0]
page.analyze_layout('yolo')
(
page
.find_all('region')
.show(group_by='type', width=800)
)
page.find('table').apply_ocr()
text = page.extract_text()
print(text)
There are other options in Natural PDF - TATR, Microsoft's table transformer - VLM options... but in the end, guides are the answer. You just point it at text, lines, whitespace... and it finds the table for you.
from natural_pdf import PDF
pdf = PDF("https://github.com/jsoma/natural-pdf/raw/refs/heads/main/pdfs/needs-ocr.pdf")
page = pdf.pages[0]
page.apply_ocr()
table_area = (
page
.find("text:contains(Violations)")
.below(until='text:contains(Jungle Health)', include_endpoint=False)
.trim()
)
table_area.show(crop=True, width=700)
Let's tell Natural PDF to split columns based on the headers - we OCR'd them but they seem reliable.
guide = table_area.guides()
guide.vertical.from_headers(["Statute", "Description", "Level", "Repeat?"])
guide.show(width=700)
The big problem is that in trying to evenly divide up the columns, it went right through our text! We can migrate the lines to the nearest empty area by snapping to whitespace.
guide.snap_to_whitespace()
guide.show(width=700)
Sometimes you don't have lines to split up rows, but once you've split the columns you can pick one that functions as an anchor. We'll take the first one.
rows = guide.column(0).find_all('text')
rows.show(crop=True, width=50)
We can tell Natural PDF to use those as row dividers. The top of each will count as a new row.
guide.horizontal.from_content(rows)
guide.show()
Now we just need to detect those checkboxes...
page.detect_checkboxes()
And we can ask the guide for the table!
guide.extract_table().to_df()
That was overly complex for pedagogical reasons, we could have also just used the lines.... Natural PDF counts up the pixels and sees what's likely to be dark vertical/horizontal lines. You can pass extra parameters to specify custom approaches like we need more than 40% filled (threshold=0.4) or I only want 5 guidelines (n=5).
from natural_pdf.analyzers.guides import Guides
guide = table_area.guides()
guide.horizontal.from_lines(detection_method='pixels')
guide.vertical.from_lines(detection_method='pixels')
guide.show()
page.detect_checkboxes()
guide.extract_table().to_df()