PDF Structure & Bounding Boxes
Understanding PDF structure is crucial for effective data extraction. In this lesson, you'll learn how OpenDataLoader represents document elements and their positions.
Element Types
OpenDataLoader detects and classifies these element types:
| Type | Description | Example |
|---|---|---|
heading | Section titles (level 1-6) | Chapter titles, subsections |
paragraph | Body text blocks | Regular text content |
table | Tabular data | Data tables, grids |
list | Ordered/unordered lists | Bullet points, numbered items |
image | Pictures, diagrams | Photos, charts |
caption | Image/table descriptions | Figure 1: ... |
formula | Mathematical expressions | LaTeX equations |
The Bounding Box
Every element has a bounding box - four coordinates that define its rectangle:
bounding box: [left, bottom, right, top]
PDF Coordinate System
PDF uses a coordinate system with:
- Origin: Bottom-left corner of the page
- Units: Points (72 points = 1 inch)
- Y-axis: Increases upward (not downward like screen coordinates)
Page
┌─────────────────────────┐
│ │ top
│ ┌─────────────┐ │
│ │ Element │ │
│ └─────────────┘ │ bottom
│ left right │
│ │
└─────────────────────────┘
Origin (0,0)
Working with Coordinates
import json
# Load JSON output
with open('output/document.json', 'r') as f:
data = json.load(f)
# Extract all headings with their positions
for element in data['kids']:
if element['type'] == 'heading':
bbox = element['bounding box']
print(f"Heading: {element['content'][:50]}")
print(f" Position: left={bbox[0]:.1f}, bottom={bbox[1]:.1f}")
print(f" Size: width={bbox[2]-bbox[0]:.1f}, height={bbox[3]-bbox[1]:.1f}")
Reading Order
PDFs don't store text in reading order. A two-column document might have text physically stored as:
Column 1, Line 1
Column 2, Line 1
Column 1, Line 2
Column 2, Line 2
XY-Cut++ Algorithm
OpenDataLoader uses XY-Cut++ to determine correct reading order:
- Recursively splits the page into regions
- Analyzes whitespace patterns
- Orders elements as a human would read them
This works automatically - no configuration needed.
# Reading order is automatic
opendataloader_pdf.convert(
input_path="two-column-paper.pdf",
output_dir="output/",
format="markdown" # Text comes out in reading order
)
Tagged PDFs
Some PDFs have built-in structure tags (created by authors/publishers). These provide the exact intended layout.
Detecting Tagged PDFs
# Check if PDF is tagged (in JSON output)
if data.get('is_tagged'):
print("This is a tagged PDF")
Using Structure Tags
opendataloader_pdf.convert(
input_path="tagged-document.pdf",
output_dir="output/",
use_struct_tree=True # Use native PDF structure
)
When use_struct_tree=True:
- Headings, lists, tables come directly from PDF tags
- Reading order is author-defined
- Higher accuracy for well-tagged documents
Element Metadata
Beyond bounding boxes, elements have rich metadata:
{
"type": "heading",
"id": 42,
"level": 1,
"page number": 1,
"bounding box": [72.0, 700.0, 540.0, 730.0],
"heading level": 1,
"font": "Helvetica-Bold",
"font size": 24.0,
"text color": "[0.0]",
"content": "Introduction"
}
Font Information
# Find all fonts used in the document
fonts = set()
for element in data['kids']:
if 'font' in element:
fonts.add(element['font'])
print("Fonts used:", fonts)
Heading Hierarchy
# Build document outline from headings
outline = []
for element in data['kids']:
if element['type'] == 'heading':
level = element.get('heading level', 1)
outline.append({
'level': level,
'title': element['content'],
'page': element['page number']
})
Challenge: Visual Element Mapper
Create a script that:
- Converts a PDF to JSON and annotated PDF
- Extracts all element positions
- Generates a summary of element distribution across pages
import opendataloader_pdf
import json
def analyze_pdf(pdf_path):
# Convert with annotated output for visualization
opendataloader_pdf.convert(
input_path=pdf_path,
output_dir="output/",
format="json,pdf" # pdf = annotated version
)
# Load and analyze JSON
with open(f'output/{pdf_path.replace(".pdf", ".json")}', 'r') as f:
data = json.load(f)
# Your analysis code here:
# 1. Count elements per page
# 2. Calculate average element sizes
# 3. Find largest/smallest elements
# 4. Identify element clusters
return analysis
# Run analysis
result = analyze_pdf("sample.pdf")
print(result)
Expected output:
Page Analysis:
- Page 1: 15 elements (3 headings, 10 paragraphs, 2 images)
- Page 2: 22 elements (2 headings, 18 paragraphs, 1 table, 1 image)
Size Statistics:
- Average heading height: 18.5 points
- Average paragraph width: 468.0 points
- Largest element: Table on page 2 (12,540 sq points)