🔥 0
0
Lesson 2 of 5 25 min +100 XP

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:

TypeDescriptionExample
headingSection titles (level 1-6)Chapter titles, subsections
paragraphBody text blocksRegular text content
tableTabular dataData tables, grids
listOrdered/unordered listsBullet points, numbered items
imagePictures, diagramsPhotos, charts
captionImage/table descriptionsFigure 1: ...
formulaMathematical expressionsLaTeX 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)

🧠 Quick Quiz

Test your understanding of this lesson.

1

What coordinate system do PDF bounding boxes use?

2

What is XY-Cut++ used for?

3

When should you use use_struct_tree=True?

Introduction to OpenDataLoader