Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Filter by Categories
About Article
AI Tools
Analyze Data
Archive
Best Practices
Better Outputs
Blog
Code Optimization
Code Quality
Command Line
Course
Daily tips
Dashboard
Data Analysis & Manipulation
Data Engineer
Data Visualization
DataFrame
Delta Lake
DevOps
DuckDB
Environment Management
Feature Engineer
Git
Jupyter Notebook
LLM
LLM Tools
Machine Learning
Machine Learning & AI
Machine Learning Tools
Manage Data
MLOps
Natural Language Processing
Newsletter Archive
NumPy
Pandas
Polars
PySpark
Python Helpers
Python Tips
Python Utilities
Scrape Data
SQL
Testing
Time Series
Tools
Visualization
Visualization & Reporting
Workflow & Automation
Workflow Automation

pdf

Auto-created tag for pdf

olmOCR-2 vs PaddleOCR-VL: Which Extracts PDF Tables Better?

Table of Contents

Introduction
The Test Document
Runtime Setup
olmOCR-2: Qwen2.5-VL Fine-Tune
PaddleOCR-VL 1.6: Pipeline VLM
Summary
Try It Yourself

Introduction
In a previous article, we tested three Python tools for PDF table extraction: Docling, Marker, and LlamaParse. None of them handled the test document perfectly: Docling hallucinated values, Marker merged columns on borderless rows, and LlamaParse added a duplicate empty column.
After publishing part 1, I came across two more tools that target the same problem and wanted to see how they perform compared to the ones we already tested:

olmOCR-2 from Allen Institute for AI, a 7B fine-tune of Qwen2.5-VL
PaddleOCR-VL 1.6 from Baidu, a 1B model with a layout-detection pipeline

Both claim state-of-the-art table extraction. We’ll test them on a Mac (Apple M5 Pro), using the same PDF as part 1, to see if they fix the failures we saw there.

💻 Get the Code: Open the notebook in Google Colab to run it in your browser, or grab the source from GitHub.

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-wrap {
display: flex;
justify-content: center;
}

.codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
border: none;
border-radius: 8px;
padding: 12px 28px;
font-family: inherit;
font-size: 16px;
font-weight: 700;
cursor: pointer;
text-decoration: none !important;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover,
.codecut-subscribe-btn:focus {
background: #5aa8e8 !important;
color: #2F2D2E !important;
text-decoration: none !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-subscribe-btn {
width: 100%;
text-align: center;
}
}

Subscribe for free

The Test Document
For a fair comparison, we will use the same PDF as part 1: the Docling Technical Report from arXiv:
import urllib.request

source = "https://arxiv.org/pdf/2408.09869"
local_pdf = "docling_report.pdf"
urllib.request.urlretrieve(source, local_pdf)

Runtime Setup
Neither olmOCR-2 nor PaddleOCR-VL ships with native Apple Silicon support in its official Python package. Both rely on CUDA-only inference stacks. To run them on a Mac, we have two options:

Rent a cloud GPU (RunPod, Modal, Lambda) and use the official inference path
Use community GGUF quantizations with llama.cpp. GGUF is a file format that packages compressed model weights into a single file. llama.cpp is an inference engine that can load GGUF files and run them on Apple Silicon’s GPU, bypassing the CUDA dependency entirely.

In this article, we will use the GGUF + llama.cpp path for the rest of this article because the compressed model files fit on a laptop and the setup runs free on Apple Silicon.
Install llama.cpp:
brew install llama.cpp

This article uses llama.cpp build 9380.
olmOCR-2: Qwen2.5-VL Fine-Tune
olmOCR-2 is Allen AI’s open-weight OCR model. It stands out for three reasons:

A 7B fine-tune of Qwen2.5-VL reads each PDF page as an image
Cheap to run at scale: on a rented NVIDIA H100, olmOCR-2 processes a few pages per second, working out to about $2 per 10,000 pages in cloud costs
Strongest table benchmark: scores 84.9 on tables on its own olmOCR-Bench, the highest among open VLM-OCR models at release

olmOCR-2 takes the whole PDF page as an image and produces structured output in a single step. This is the same architecture as Docling’s VLM pipeline from part 1, just with a different model.
PDF page rendered as image
┌─────────────────────┐
│ Text paragraph… │
│ Name Score │
│ Alice 92 │
│ Bob 85 │
└─────────────────────┘


One model reads the page
and writes the output


| Name | Score |
|——-|——-|
| Alice | 92 |
| Bob | 85 |

Download the GGUF and vision projector
To use olmOCR-2 with llama.cpp, download two files: the model weights and the vision projector (mmproj).
# Language model (Q8_0, ~8 GB)
curl -L -O https://huggingface.co/lmstudio-community/olmOCR-2-7B-1025-GGUF/resolve/main/olmOCR-2-7B-1025-Q8_0.gguf

# Vision projector (F16, ~1.4 GB)
curl -L -O https://huggingface.co/lmstudio-community/olmOCR-2-7B-1025-GGUF/resolve/main/mmproj-olmOCR-2-7B-1025-F16.gguf

Table extraction
olmOCR-2 reads images, not PDFs, so we’ll extract tables in three steps:

Convert each PDF page to an image
Run olmOCR-2 on each image and collect the output
Extract the tables from the combined output with a regex

For step 1, we will use pdf2image, which depends on the poppler system binary. Install both:
brew install poppler
pip install pdf2image

Now convert each page to a JPEG:
import subprocess
from pathlib import Path
from pdf2image import convert_from_path

images_dir = Path("images")
images_dir.mkdir(exist_ok=True)

pages = convert_from_path(local_pdf, dpi=200)
for i, page in enumerate(pages):
page.save(images_dir / f"page_{i}.jpg")

olmOCR-2 doesn’t have a pure-Python API that runs on Apple Silicon, so we shell out to llama-mtmd-cli via subprocess for each page. The command for one page looks like this:
llama-mtmd-cli \
-m olmOCR-2-7B-1025-Q8_0.gguf \
–mmproj mmproj-olmOCR-2-7B-1025-F16.gguf \
–image page_0.jpg \
-p "Convert this page to markdown. Preserve tables exactly. Output tables in HTML format." \
–n-predict 3072

What each flag does:

-m: the language model weights (the .gguf we downloaded)
–mmproj: the vision encoder (the mmproj we downloaded)
–image: the input image to process
-p: the prompt sent to the model
–n-predict: the maximum number of tokens to generate (3072 is enough for most table-heavy pages)

Wrap it in a Python helper so we can loop over pages:
import re

def extract_with_olmocr(page_path: str) -> str:
result = subprocess.run(
[
"llama-mtmd-cli",
"-m", "olmOCR-2-7B-1025-Q8_0.gguf",
"–mmproj", "mmproj-olmOCR-2-7B-1025-F16.gguf",
"–image", page_path,
"-p", "Convert this page to markdown. Preserve tables exactly. Output tables in HTML format.",
"–n-predict", "3072",
],
capture_output=True,
text=True,
)
return result.stdout

Run the helper on every page and combine the outputs:
%%time
olmocr_output = "\n".join(
extract_with_olmocr(str(images_dir / f"page_{i}.jpg")) for i in range(len(pages))
)

OutputWall time: 5min 34s

olmOCR-2’s output is mostly Markdown but tables come out as HTML blocks. Extract them with a regex:
all_tables = re.findall(r"<table>.*?</table>", olmocr_output, re.DOTALL)
print(f"Items tagged as table: {len(all_tables)}")

OutputItems tagged as table: 4

Not every block tagged <table> is actually a table. olmOCR-2 misreads the author block on the title page as a table and outputs two copies of it. We filter both out:
incorrect_table_indices = (1, 2)

tables = [t for i, t in enumerate(all_tables) if i not in incorrect_table_indices]
print(f"Actual tables: {len(tables)}")

OutputActual tables: 2

The output is HTML, so use IPython.display.HTML to see it rendered:
from IPython.display import display, HTML

Let’s look at the first table. Here’s the original from the PDF:

And here’s what olmOCR-2 extracted:
display(HTML(tables[0]))

Worked:

The two-tier header matches the original: “native backend” and “pypdfium backend” each sit above their three sub-columns (TTS, Pages/s, Mem)
All numeric values match the original
CPU names like “Apple M3 Max (16 cores)” stay in a single cell

Didn’t work:

Merged cells (6.20 GB, 6.16 GB, and the CPU names) only appear in the first row of each CPU group, leaving the continuation row blank. The original PDF shows these values spanning both rows.

Now the second table. Here’s the original:

This is the hardest table in the document: 12 rows of similar-looking numbers and no cell borders to mark column boundaries. And here’s what olmOCR-2 extracted:
display(HTML(tables[1]))

Worked:

All 12 row labels (Caption, Footnote, …, All) preserved
12 data rows extracted with one numeric value per cell

Didn’t work:

Two column headers are missing: Only 4 of the 6 columns have headers, so the class-label column and one of the model columns appear unlabeled.
MRCNN R101 is dropped from the header row: The numeric values in that column still appear, but they sit under the wrong header name.
Hyphenated ranges become decimals: Every entry in the “human” range column is wrong: 84-89 becomes 84.89, 83-91 becomes 83.91, and so on.
Numeric values drift in several cells: Most rows have at least one digit substitution (Page-footer 61.6 → 74.6, List-item 81.2 → 81.6, All-row 72.4 → 77.4).

Conclusion: olmOCR-2’s output looks clean but can be quietly wrong. It handles structured tables with merged cells correctly (table 1), but introduces character-level errors on dense numeric tables (table 2). Verify numeric values before trusting them.
Performance
olmOCR-2 took 5 min 34 s for the 9-page PDF on an Apple M5 Pro (64 GB RAM), about 37 seconds per page through GGUF + llama.cpp.
For production on a Mac, switch to the native MLX build (mlx-community/olmOCR-2-7B-1025-8bit), which runs about 20% faster than GGUF.
PaddleOCR-VL 1.6: Pipeline VLM
PaddleOCR-VL is Baidu’s open-weight document parser. It stands out for three reasons:

A 1B fine-tune of ERNIE-4.5, the smallest model of the new VLM-OCR generation
Strong multilingual support including Chinese ancient documents, scans, and stamps (not tested in this article)
Mature ecosystem: PaddleOCR has 78.9k stars on GitHub and a long history of production deployment

Unlike olmOCR-2’s single-pass approach, PaddleOCR-VL splits table extraction into two stages:

Layout detection locates each text block, table, and figure on the page
Element-level VL recognition reads each detected region and converts it to text or structured Markdown

PDF page
┌─────────────────────┐
│ Text paragraph… │
│ Name Score │
│ Alice 92 │
│ Bob 85 │
└─────────────────────┘


1. Layout detection identifies [TABLE] region


2. Element-level VL reads only the table region


| Name | Score |
|——-|——-|
| Alice | 92 |
| Bob | 85 |

Install
Pick the install that matches your hardware.
Apple Silicon (Mac):
pip install paddlepaddle
pip install -U "paddleocr[doc-parser]>=3.6.0"

Linux / Windows (NVIDIA):
pip install paddlepaddle-gpu==3.2.1
pip install -U "paddleocr[doc-parser]>=3.6.0"

This article uses PaddleOCR v3.6.0.
Table extraction
Unlike olmOCR-2, PaddleOCR-VL accepts a PDF path directly and returns a result object per page. No PDF-to-image conversion or subprocess loop required:
from paddleocr import PaddleOCRVL

pipeline = PaddleOCRVL(pipeline_version="v1.6")

Run the pipeline on the PDF:
%%time
results = pipeline.predict(local_pdf)

OutputWall time: 7min 56s

Each entry in results corresponds to one page of the PDF. Loop through them and collect the tables:
# Create an output directory for the per-page markdown files
paddle_output_dir = Path("paddle_output")
paddle_output_dir.mkdir(exist_ok=True)

# Save each page's markdown to disk
for res in results:
res.save_to_markdown(save_path=str(paddle_output_dir))

# Find every HTML table block
all_paddle_tables = []
for md_file in sorted(paddle_output_dir.glob("*.md")):
content = md_file.read_text()
all_paddle_tables.extend(re.findall(r"<table[^>]*>.*?</table>", content, re.DOTALL))

print(f"Items tagged as table: {len(all_paddle_tables)}")

OutputItems tagged as table: 3

Not every block PaddleOCR-VL tagged as a table is a unique table. The third item is a malformed near-duplicate of the second. Let’s filter it out:
incorrect_table_indices = (2,)

paddle_tables = [t for i, t in enumerate(all_paddle_tables) if i not in incorrect_table_indices]
print(f"Actual tables: {len(paddle_tables)}")

OutputActual tables: 2

Let’s look at the first table. Here’s the original from the PDF:

And here’s what PaddleOCR-VL extracted:
display(HTML(paddle_tables[0]))

Worked:

The two-tier header matches the original: “native backend” and “pypdfium backend” each sit above their three sub-columns, with CPU and Thread budget extending across both header rows
Merged cells appear correctly: “Apple M3 Max (16 cores)” spans both of its thread-budget rows, and “6.20 GB” spans both Mem rows (no blank continuation rows like olmOCR-2 had)
All numeric values match the source

Didn’t work:

Multi-line column labels (CPU names, Thread budget) render on a single line; the original PDF had them on two lines

Now the second table. Here’s the original from the PDF:

And here’s what PaddleOCR-VL extracted:
display(HTML(paddle_tables[1]))

Worked:

All 12 class-label rows plus the Total row are present (truncated above for space)
Hyphenated ranges preserved correctly as “84-89”, “40-61”, exactly where olmOCR-2 misread them as decimals
“n/a” entries preserved
All numeric values match the source

Didn’t work:

Header grouping is wrong: The two parent headers in the original PDF get split into three in the extraction: “Count” is absorbed into “% of Total”, and “triple inter-annotator mAP @ 0.5-0.95 (%)” is split into two separate parents.

Conclusion: PaddleOCR-VL is 7x smaller than olmOCR-2 (1B vs 7B parameters) and still more accurate on this PDF. All numeric values match the source, merged cells render correctly, and the only real flaw is the mis-grouped multi-tier headers.
Performance
PaddleOCR-VL 1.6 took about 7 min 56 s for the full 9-page PDF on an Apple M5 Pro running CPU PaddlePaddle, roughly 53 seconds per page.
Even though the model is smaller than olmOCR-2, the pipeline overhead (layout detection plus element-level recognition) makes it slower per page than olmOCR-2 on this hardware.
Summary
Stack-ranking all five tools tested across both articles on the same PDF:

Feature
Docling
Marker
LlamaParse
olmOCR-2
PaddleOCR-VL 1.6

Approach
Vision-language model (local)
Pipeline (local)
LLM agent (cloud)
Vision-language model (local)
Pipeline (local)

Tables detected (3 in PDF)
2
3
3
3
2

Accuracy overall
Poor: hallucinates values on dense tables
Mixed: column collapse on borderless tables
High: values correct, structure flattened
Mixed: silent character errors (digit drift, hyphen→decimal)
High: values correct, header grouping mis-aligned

Speed (M5 Pro, 9-page PDF)
~1 min 50s
~47s
~8.54s
~5 min 34s
~7 min 56s

Pricing
Free (MIT)
Free (GPL-3.0)
Free tier (10k credits/month)
Free (Apache 2.0)
Free (Apache 2.0)

In short, neither of the new VLM-OCR tools beats LlamaParse on this PDF:

LlamaParse: all 3 tables, all values correct
olmOCR-2: all 3 tables, but silent character errors on the dense numeric grid
PaddleOCR-VL 1.6: clean merged cells on 2 of 3 tables, missed the dense numeric one

Try It Yourself
These benchmarks are based on a single academic PDF tested on an Apple M5 Pro (64 GB RAM) using GGUF Q8_0 quantizations via llama.cpp. Table complexity, document language, scan quality, and hardware all affect the results. The best way to pick the right tool is to run each one on a sample of your own PDFs.
Related Tutorials

PDF Table Extraction: Docling vs Marker vs LlamaParse Compared: Part 1 of this comparison, covering three earlier tools on the same test PDF
Transform Any PDF into Searchable AI Data with Docling: Docling’s full document processing capabilities including chunking and RAG integration
Turn Receipt Images into Spreadsheets with LlamaIndex: Extracting structured data from images using the same LlamaIndex ecosystem

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-wrap {
display: flex;
justify-content: center;
}

.codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
border: none;
border-radius: 8px;
padding: 12px 28px;
font-family: inherit;
font-size: 16px;
font-weight: 700;
cursor: pointer;
text-decoration: none !important;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover,
.codecut-subscribe-btn:focus {
background: #5aa8e8 !important;
color: #2F2D2E !important;
text-decoration: none !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-subscribe-btn {
width: 100%;
text-align: center;
}
}

Subscribe for free

olmOCR-2 vs PaddleOCR-VL: Which Extracts PDF Tables Better? Read More »

PDF Table Extraction: Docling vs Marker vs LlamaParse Compared

Table of Contents

Introduction
The Test Document
Docling: TableFormer Deep Learning
Marker: Vision Transformer Pipeline
LlamaParse: LLM-Guided Extraction
Summary
Try It Yourself

Introduction
Have you ever copied a table from a PDF into a spreadsheet only to find the formatting completely broken? These issues include cells shifting, values landing in the wrong columns, and merged headers losing their structure.
This happens because PDFs do not store tables as structured data. They simply place text at specific coordinates on a page.
For example, a table that looks like this on screen:
┌───────┬───────┐
│ Name │ Score │
├───────┼───────┤
│ Alice │ 92 │
│ Bob │ 85 │
└───────┴───────┘

is stored in the PDF as a flat list of positioned text:
"Name" at (x=72, y=710)
"Score" at (x=200, y=710)
"Alice" at (x=72, y=690)
"92" at (x=200, y=690)
"Bob" at (x=72, y=670)
"85" at (x=200, y=670)

A table extraction tool must analyze those positions, determine which text belongs in each cell, and rebuild the table structure.
The challenge becomes even greater with multi-level headers, merged cells, or tables that span multiple pages. Many tools struggle with at least one of these scenarios.
While doing research, I came across three Python tools for extracting tables from PDFs: Docling, Marker, and LlamaParse. To compare them fairly, I ran each tool on the same PDF and evaluated the results.
In this article, I’ll walk through what I found and help you decide which tool may work best for your needs.

💻 Get the Code: Open the notebook in Google Colab to run it in your browser, or grab the source from GitHub.

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-wrap {
display: flex;
justify-content: center;
}

.codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
border: none;
border-radius: 8px;
padding: 12px 28px;
font-family: inherit;
font-size: 16px;
font-weight: 700;
cursor: pointer;
text-decoration: none !important;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover,
.codecut-subscribe-btn:focus {
background: #5aa8e8 !important;
color: #2F2D2E !important;
text-decoration: none !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-subscribe-btn {
width: 100%;
text-align: center;
}
}

Subscribe for free

The Test Document
All examples use the same PDF: the Docling Technical Report from arXiv. This paper contains tables with the features that make extraction difficult:

Multi-level headers with sub-columns
Merged cells spanning multiple rows
Numeric data that is easy to misalign

source = "https://arxiv.org/pdf/2408.09869"

Some tools require a local file path instead of a URL, so let’s download the PDF first:
import urllib.request

# Download PDF locally (used by Marker later)
local_pdf = "docling_report.pdf"
urllib.request.urlretrieve(source, local_pdf)

Docling: Vision-Language Model Pipeline
Docling is IBM’s open-source document converter built specifically for structured extraction. It ships with two pipelines:

Default pipeline uses two small AI models trained specifically for tables. One spots tables on the page, the other reads the grid inside
VLM pipeline uses one larger AI model that can understand images, similar to how ChatGPT can describe a photo. It reads the whole page and outputs the table structure directly

The default pipeline is fast, but it can struggle with complex layouts like multi-level headers and merged cells. The VLM pipeline trades some speed for better accuracy on tricky tables, which is what we want for this comparison.
We’ll use GraniteDocling, IBM’s vision model built specifically for documents.
PDF page with mixed content
┌─────────────────────┐
│ Text paragraph… │
│ Name Score │
│ Alice 92 │
│ Bob 85 │
│ (figure) │
└─────────────────────┘


AI reads the whole page
and extracts the table


┌───────┬───────┐
│ Name │ Score │
├───────┼───────┤
│ Alice │ 92 │
│ Bob │ 85 │
└───────┴───────┘

The result is a pandas DataFrame for each table, ready for analysis.

For Docling’s full document processing capabilities beyond tables, including chunking and RAG integration, see Transform Any PDF into Searchable AI Data with Docling.

To install Docling, pick the variant that matches your hardware:

Platform
Install command
Model spec

Apple Silicon (M1+)
pip install "docling[vlm]" mlx-vlm
GRANITEDOCLING_MLX

Linux / Windows (CUDA or CPU)
pip install "docling[vlm]"
GRANITEDOCLING_TRANSFORMERS

This article uses docling v2.93.0.
Table Extraction
To use the VLM pipeline, we configure DocumentConverter with VlmPipeline and select GraniteDocling as the model:
from docling.datamodel import vlm_model_specs
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import VlmPipelineOptions
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.pipeline.vlm_pipeline import VlmPipeline

pipeline_options = VlmPipelineOptions(
vlm_options=vlm_model_specs.GRANITEDOCLING_MLX, # Apple Silicon
# vlm_options=vlm_model_specs.GRANITEDOCLING_TRANSFORMERS, # Linux / Windows
)

converter = DocumentConverter(
format_options={
InputFormat.PDF: PdfFormatOption(
pipeline_cls=VlmPipeline,
pipeline_options=pipeline_options,
)
}
)

Now we can convert the PDF and measure how long it takes:
%%time
result = converter.convert(source)

OutputWall time: 1min 50s

Once we have the Docling document, we can loop through all detected tables and export each one as a pandas DataFrame:
for i, table in enumerate(result.document.tables):
df = table.export_to_dataframe(doc=result.document)
print(f"Table {i + 1}: {df.shape[0]} rows × {df.shape[1]} columns")

Table 1: 6 rows × 8 columns
Table 2: 12 rows × 6 columns

The PDF contains 5 tables, but Docling detected only 2 with the VLM pipeline.
Let’s look at the first table. Here’s the original from the PDF:

And here’s what Docling extracted:
# Export the first table as a DataFrame
table_1 = result.document.tables[0]
df_1 = table_1.export_to_dataframe(doc=result.document)
df_1

0
1
2
3
4
5
6
7

0
CPU
Thread budget
native backend
native backend
native backend
pypdfium backend
pypdfium backend
pypdfium backend

1

TTS
Pages/s
Mem
TTS
Pages/s
Mem

2
Apple M3 Max
4
177 s
1.27
6.20 GB
103 s
2.18
2.56 GB

3
(16 cores)
16
167 s
1.34
92 s
92 s
2.45
2.56

4
Intel(R) Xeon
4
375 s
0.60
6.16 GB
239 s
0.94
2.42 GB

5
E5-2690
16
244 s
0.92
143 s
1.57
1.57
2.42

The VLM pipeline handled values well but tripped on structure.
Worked:

Each thread budget stays on its own row (4 and 16 are separate)
Individual timing values appear in their own cells (177 s and 167 s are not concatenated)
Most numeric values match the original

Didn’t work:

CPU names got split across rows: “Apple M3 Max” sits in one row and “(16 cores)” in the next
The merged Mem cells caused values from adjacent columns to leak in (e.g., “92 s” appears in the native Mem column on row 3)
Row 5 has “1.57” duplicated in both the pypdfium TTS and Pages/s columns

Now the second table. Here’s the original from the PDF:

And here’s what Docling extracted:
# Export the second table as a DataFrame
table_2 = result.document.tables[1]
df_2 = table_2.export_to_dataframe(doc=result.document)
df_2

0
1
2
3
4
5

0
Caption
human
R-CNN
R-CNN10-FPRN 3x
V1S
V2S

1
Footnote
70.1
70.1
70.1
70.1
70.1

2
Formula
73.8
73.7
73.7
72.2
72.2

3
List-item
81.8
81.8
81.8
80.1
80.1

4
Page-footer
61.9
61.9
61.9
59.7
59.7

5
Page-header
64.4
64.4
64.4
64.4
64.4

6
Picture
69.8
69.8
69.8
64.4
64.4

7
Section-header
68.7
68.7
68.7
64.4
68.7

8
Table
82.8
82.8
82.8
64.4
82.8

9
Text
85.8
85.8
85.8
64.4
85.8

10
Title
86.8
86.8
86.8
64.4
86.8

11
All
86.8
86.8
86.8
64.4
86.8

The VLM pipeline struggled badly with this denser table.
Worked:

The 12 row labels (Caption, Footnote, …, Title, All) match the original

Didn’t work:

Column headers are hallucinated: the original has “MRCNN R50”, “MRCNN R101”, “FRCNN R101”, “YOLO v5x6”, but the VLM output shows “R-CNN”, “R-CNN10-FPRN 3x”, “V1S”, “V2S”
Numeric values don’t match the original. The Footnote row reads “70.1 70.1 70.1 70.1 70.1” instead of “83-91 70.9 71.8 73.7 77.2”
Column 4 shows “64.4” repeating across 7 consecutive rows

This happens because the VLM writes cells one at a time, similar to how ChatGPT writes a response word by word. When the table has many similar-looking numbers, the model can get stuck and keep repeating the same value, which is why “64.4” appears 7 times in a row.
Conclusion: Docling’s VLM pipeline handles simple tables well, but produces unreliable results on dense numeric data, where it can hallucinate column names, repeat values across rows, and lose track of merged cells.
Performance
Docling took about 1 minute 50 seconds for the full 6-page PDF on an Apple M5 Pro (64 GB RAM). Most of that time is spent on the GPU: GraniteDocling reads each page as an image and generates the table structure one token at a time, which pins the GPU at near-full utilization.
Marker: Vision Transformer Pipeline
Marker is an open-source PDF-to-Markdown converter built on the Surya layout engine. Unlike Docling’s two-stage pipeline, Marker runs five stages for table extraction:

Layout detection: a Vision Transformer identifies table regions on each page
OCR error detection: flags misrecognized text
Bounding box detection: locates individual cell boundaries
Table recognition: reconstructs row/column structure from detected cells
Text recognition: extracts text from all detected regions

Here is how the five stages work together:
PDF page
┌─────────────────────┐
│ Text paragraph… │
│ Name Score │
│ Alice 92 │
│ Bob 85 │
└─────────────────────┘


1. Layout detection → finds [TABLE] region
2. OCR error detection → fixes misread text


3. Bounding box detection
┌──────────────────┐
│ [Name] [Score] │
│ [Alice] [92] │
│ [Bob] [85] │
└──────────────────┘


4. Table recognition → maps cells to rows/columns
5. Text recognition → extracts final text


| Name | Score |
|——-|——-|
| Alice | 92 |
| Bob | 85 |

To install Marker, run:
pip install marker-pdf

This article uses marker v1.10.2.
Table Extraction
Marker provides a dedicated TableConverter that extracts only tables from a document, returning them as Markdown:
from marker.converters.table import TableConverter
from marker.models import create_model_dict
from marker.output import text_from_rendered

models = create_model_dict()
converter = TableConverter(artifact_dict=models)

Convert the PDF and measure how long it takes:
%%time
rendered = converter(local_pdf)
table_md, _, images = text_from_rendered(rendered)

OutputWall time: 47.1 s

Since TableConverter returns all tables as a single Markdown string, we split them on blank lines:
tables = table_md.strip().split("\n\n")
print(f"Tables found: {len(tables)}")

Tables found: 3

Let’s look at the first table. Here’s the original from the PDF:

And here’s what Marker extracted:
print(tables[0])

CPU
Thread<br>budget
native backend

pypdfium backend

TTS
Pages/s
Mem
TTS
Pages/s
Mem

Apple M3 Max<br>(16 cores)
4<br>16
177 s<br>167 s
1.27<br>1.34
6.20 GB
103 s<br>92 s
2.18<br>2.45
2.56 GB

Intel(R) Xeon<br>E5-2690<br>(16 cores)
4<br>16
375 s<br>244 s
0.60<br>0.92
6.16 GB
239 s<br>143 s
0.94<br>1.57
2.42 GB

Marker handled this table well.
Worked:

The two-tier header is preserved across two rows: “native backend” and “pypdfium backend” sit on the first row, with their sub-columns (TTS, Pages/s, Mem) on the second
Multi-line CPU names stay in one cell using <br> tags (e.g., “Apple M3 Max(16 cores)”)
Multi-value cells preserve individual numbers with <br> separators (e.g., “177 s167 s”), so each value is easy to split programmatically later
Merged Mem cells correctly show a single value (6.20 GB) without duplication
All numeric values match the original

Didn’t work:

The two-tier header takes up two rows instead of being flattened, so reading this into pandas requires extra handling

Let’s look at the second table. Here’s the original from the PDF:

And here’s what Marker extracted:
print(tables[1])

human
MRCNN

FRCNN YOLO

R50 R101
R101
v5x6

Caption
84-89 68.4 71.5

70.1
77.7

Footnote
83-91 70.9 71.8

73.7
77.2

Formula
83-85 60.1 63.4

63.5
66.2

List-item
87-88 81.2 80.8

81.0
86.2

Page-footer
93-94 61.6 59.3

58.9
61.1

Page-header
85-89 71.9 70.0

72.0
67.9

Picture
69-71 71.7 72.7

72.0
77.1

Section-header 83-84 67.6 69.3

68.4
74.6

Table
77-81 82.2 82.9

82.2
86.3

Text
84-86 84.6 85.8

85.4
88.1

Title
60-72 76.7 80.4

79.9
82.7

All
82-83 72.4 73.5

73.4
76.8

Marker struggled with this denser table.
Worked:

All 12 row labels are preserved (Caption, Footnote, …, Title, All)
Values for the FRCNN R101 and YOLO v5x6 columns extracted correctly

Didn’t work:

Header parents merged: “human” and “MRCNN” share a column header, “FRCNN” and “YOLO” merged into one cell
The human, MRCNN R50, and MRCNN R101 values are packed into one cell per row (e.g., “84-89 68.4 71.5”), leaving the MRCNN columns empty
The Section-header row label merged with its data (“Section-header 83-84 67.6 69.3”), breaking that row’s alignment

Let’s look at the third table. Here’s the original from the PDF:

And here’s what Marker extracted:
print(tables[2])

human
MRCNN
MRCNN
FRCNN
YOLO

human
R50
R101
R101
v5x6

Caption
84-89
68.4
71.5
70.1
77.7

Footnote
83-91
70.9
71.8
73.7
77.2

Formula
83-85
60.1
63.4
63.5
66.2

List-item
87-88
81.2
80.8
81.0
86.2

Page-footer
93-94
61.6
59.3
58.9
61.1

Page-header
85-89
71.9
70.0
72.0
67.9

Picture
69-71
71.7
72.7
72.0
77.1

Section-header
83-84
67.6
69.3
68.4
74.6

Table
77-81
82.2
82.9
82.2
86.3

Text
84-86
84.6
85.8
85.4
88.1

Title
60-72
76.7
80.4
79.9
82.7

All
82-83
72.4
73.5
73.4
76.8

This table has clear visual separation between rows and columns, while the previous one did not. The visible gaps give Marker’s vision model exact boundaries to read, so all 12 rows and 5 columns extract correctly.
Conclusion: Marker’s pipeline handles tables with clear visual separation well, but struggles when rows and columns are packed close together without visible borders.
Performance
Marker took about 47 seconds for the full 6-page PDF on an Apple M5 Pro (64 GB RAM), more than twice as fast as Docling’s VLM pipeline. The speed difference comes down to architecture:

Docling runs a single large vision-language model that reads each page as an image and generates the table structure one token at a time. Large models take time per token, so the total runtime adds up.
Marker runs a 5-stage pipeline of smaller specialized models that mostly do classification or detection, avoiding the slow token-by-token generation that VLMs need.

LlamaParse: LLM-Guided Extraction
LlamaParse is a cloud-hosted document parser by LlamaIndex that takes a different approach:

Cloud-based: the PDF is uploaded to LlamaCloud instead of being processed locally
LLM-guided: an LLM interprets each page and identifies tables, returning structured row data

Here is how it works:
PDF file
┌─────────────────────┐
│ Name Score │
│ Alice 92 │
│ Bob 85 │
└─────────────────────┘

▼ upload
┌─────────────────────┐
│ LlamaCloud │
│ │
│ LLM reads the page │
│ and identifies │
│ table structure │
└─────────────────────┘

▼ response
┌───────┬───────┐
│ Name │ Score │
├───────┼───────┤
│ Alice │ 92 │
│ Bob │ 85 │
└───────┴───────┘

For extracting structured data from images like receipts using the same LlamaIndex ecosystem, see Turn Receipt Images into Spreadsheets with LlamaIndex.

To install LlamaParse, run:
pip install llama-parse

This article uses llama-parse v0.6.54.
LlamaParse requires an API key from LlamaIndex Cloud. The free tier includes 10,000 credits per month (basic parsing costs 1 credit per page; advanced modes like parse_page_with_agent cost more).
Create a .env file with your API key:
LLAMA_CLOUD_API_KEY=llx-…

from dotenv import load_dotenv

load_dotenv()

Table Extraction
To extract tables, we create a LlamaParse instance with two key settings:

parse_page_with_agent: tells LlamaCloud to use an LLM agent that reads each page and returns structured items (tables, text, figures)
output_tables_as_HTML=True: returns tables as HTML instead of Markdown, which better preserves multi-level headers

from llama_cloud_services import LlamaParse

parser = LlamaParse(
parse_mode="parse_page_with_agent",
output_tables_as_HTML=True,
)

Now let’s convert the PDF and measure how long it takes:
%%time
result = parser.parse(local_pdf)

OutputWall time: 8.54 s

We can then iterate through each page’s items and collect only the tables:
all_tables = []
for page in result.pages:
for item in page.items:
if item.type == "table":
all_tables.append(item)

print(f"Items tagged as table: {len(all_tables)}")

Items tagged as table: 5

Not every item LlamaParse tagged as a table is actually a table. The second item is the paper’s title page, and the fourth is a figure. We’ll filter both out and keep only the real tables.
incorrect_table_indices = (1, 3)

tables = [t for i, t in enumerate(all_tables) if i not in incorrect_table_indices]
print(f"Actual tables: {len(tables)}")

Actual tables: 3

Let’s look at the first table. Here’s the original from the PDF:

And here’s what LlamaParse extracted:
print(tables[0].md)

CPU
Thread budget
native backend<br/>TTS
native backend<br/>Pages/s
native backend<br/>Mem
pypdfium backend<br/>TTS
pypdfium backend<br/>Pages/s
pypdfium backend<br/>Mem
pypdfium backend<br/>Mem

Apple M3 Max<br/>(16 cores)
4
177 s
1.27
6.20 GB
103 s
2.18
2.56 GB

16
167 s
1.34

92 s
2.45

Intel(R) Xeon<br/>E5-2690<br/>(16 cores)
4
375 s
0.60
6.16 GB
239 s
0.94
2.42 GB

16
244 s
0.92

143 s
1.57

LlamaParse handled this table well, with one minor quirk.
Worked:

All numeric values land in their correct cells (177 s, 1.27, 6.20 GB, etc.)
Multi-line CPU names stay in one cell (“Apple M3 Max(16 cores)”), and thread budget values sit on separate rows
The two-tier header is flattened into combined names like “native backendTTS”, with merged Mem cells correctly shown once per CPU group

Didn’t work:

The output includes a duplicate empty “pypdfium backendMem” column at the end

Let’s look at the second table. Here’s the original from the PDF:

And here’s what LlamaParse extracted:
print(tables[1].md)

human
MRCNN R50
MRCNN R101
FRCNN R101
YOLO v5x6

Caption
84-89
68.4
71.5
70.1
77.7

Footnote
83-91
70.9
71.8
73.7
77.2

Formula
83-85
60.1
63.4
63.5
66.2

List-item
87-88
81.2
80.8
81.0
86.2

Page-footer
93-94
61.6
59.3
58.9
61.1

Page-header
85-89
71.9
70.0
72.0
67.9

Picture
69-71
71.7
72.7
72.0
77.1

Section-header
83-84
67.6
69.3
68.4
74.6

Table
77-81
82.2
82.9
82.2
86.3

Text
84-86
84.6
85.8
85.4
88.1

Title
60-72
76.7
80.4
79.9
82.7

All
82-83
72.4
73.5
73.4
76.8

LlamaParse handled this table perfectly:

All 12 row labels match the original (Caption, Footnote, …, All)
All 5 columns are correctly named: human, MRCNN R50, MRCNN R101, FRCNN R101, YOLO v5x6
All numeric values match the source, including the “human” inter-annotator range column (84-89, 83-91, etc.)

Let’s look at the third table. Here’s the original from the PDF:

And here’s what LlamaParse extracted:
print(tables[2].md)

class label
Count
% of Total<br/>Train
% of Total<br/>Test
% of Total<br/>Val
triple inter-annotator mAP @ 0.5-0.95 (%)<br/>All
triple inter-annotator mAP @ 0.5-0.95 (%)<br/>Fin
triple inter-annotator mAP @ 0.5-0.95 (%)<br/>Man
triple inter-annotator mAP @ 0.5-0.95 (%)<br/>Sci
triple inter-annotator mAP @ 0.5-0.95 (%)<br/>Law
triple inter-annotator mAP @ 0.5-0.95 (%)<br/>Pat
triple inter-annotator mAP @ 0.5-0.95 (%)<br/>Ten

Caption
22524
2.04
1.77
2.32
84-89
40-61
86-92
94-99
95-99
69-78
n/a

Footnote
6318
0.60
0.31
0.58
83-91
n/a
100
62-88
85-94
n/a
82-97

Formula
25027
2.25
1.90
2.96
83-85
n/a
n/a
84-87
86-96
n/a
n/a

List-item
185660
17.19
13.34
15.82
87-88
74-83
90-92
97-97
81-85
75-88
93-95

Page-footer
70878
6.51
5.58
6.00
93-94
88-90
95-96
100
92-97
100
96-98

Page-header
58022
5.10
6.70
5.06
85-89
66-76
90-94
98-100
91-92
97-99
81-86

Picture
45976
4.21
2.78
5.31
69-71
56-59
82-86
69-82
80-95
66-71
59-76

Section-header
142884
12.60
15.77
12.85
83-84
76-81
90-92
94-95
87-94
69-73
78-86

Table
34733
3.20
2.27
3.60
77-81
75-80
83-86
98-99
58-80
79-84
70-85

Text
510377
45.82
49.28
45.00
84-86
81-86
88-93
89-93
87-92
71-79
87-95

Title
5071
0.47
0.30
0.50
60-72
24-63
50-63
94-100
82-96
68-79
24-56

Total
1107470
941123
99816
66531
82-83
71-74
79-81
89-94
86-91
71-76
68-85

LlamaParse correctly extracted this complex table:

All 12 data rows plus the Total row appear with correct values, including n/a entries
The two-tier headers use <br/> to preserve the parent-child relationship (e.g., “% of TotalTrain”)
The “triple inter-annotator mAP @ 0.5-0.95 (%)” prefix is repeated for every sub-column (All, Fin, Man, etc.), making headers verbose but unambiguous

Conclusion: LlamaParse produces the most accurate extraction of the three tools across simple and complex tables alike, with only occasional column hallucinations.
Performance
LlamaParse finished in 8.54 seconds, the fastest of the three tools (Docling took 1 min 50s, Marker took 47s).
Unlike Docling and Marker, LlamaParse runs no models on your machine. It uploads the PDF to LlamaCloud, an LLM agent reads each page, and the result comes back:
%%{init: {“theme”: “dark”}}%%
sequenceDiagram
participant A as Your Machine
participant B as LlamaCloud
A->>B: Upload PDF
B–>>A: Return extracted tables
The runtime is mostly network upload time and server processing, so it depends on your internet speed and current LlamaCloud load rather than your local hardware.
Summary
The table below summarizes the key differences we found after testing all three tools on the same PDF:

Feature
Docling
Marker
LlamaParse

Table detection
Vision-language model (local)
5-stage specialized pipeline (local)
LLM agent (cloud)

Multi-level headers
Returns integer column names; mishandles parent groups
Keeps as separate rows with <br> tags
Flattens with <br/> tags, preserves grouping

Dense numeric tables
Hallucinates values, repetition loops
Merges columns, packs values into single cells
Extracts all values correctly

Speed (6-page PDF)
~1 min 50s
~47s
~8.54s

Dependencies
docling[vlm] + mlx-vlm (Apple) or transformers
marker-pdf
API key

Pricing
Free (MIT)
Free (GPL-3.0)
Free tier (10k credits/month)

In short:

LlamaParse wins on speed and accuracy. It’s the fastest overall and produces the cleanest output, but it requires sending PDFs to LlamaCloud.
Marker is the best local option. It’s faster than Docling and handles simple tables well, but it merges columns on dense layouts.
Docling is the slowest of the three and prone to hallucinating values on dense tables.

When to use each:

Use LlamaParse if your documents aren’t sensitive and you want the best accuracy.
Use Marker if you must stay local.
Use Docling for its broader document conversion features beyong just table extraction like chunking and RAG.

Try It Yourself
These benchmarks are based on a single academic PDF tested on an Apple M5 Pro (64 GB RAM). Table complexity, document length, and hardware all affect the results. The best way to pick the right tool is to run each one on a sample of your own PDFs.
Docling and Marker are completely free, and LlamaParse’s free tier gives you 10,000 credits per month to experiment with.
Related Tutorials

From CSS Selectors to Natural Language: Web Scraping with ScrapeGraphAI: Use LLM-guided web scraping to extract structured data from HTML pages without manual selector maintenance
Structured Output Tools for LLMs Compared: Compare tools for enforcing schemas and structured formats on LLM outputs

📚 Want to go deeper? My book shows you how to build data science projects that actually make it to production. Get the book →

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-wrap {
display: flex;
justify-content: center;
}

.codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
border: none;
border-radius: 8px;
padding: 12px 28px;
font-family: inherit;
font-size: 16px;
font-weight: 700;
cursor: pointer;
text-decoration: none !important;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover,
.codecut-subscribe-btn:focus {
background: #5aa8e8 !important;
color: #2F2D2E !important;
text-decoration: none !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-subscribe-btn {
width: 100%;
text-align: center;
}
}

Subscribe for free

pre.mermaid { background: transparent !important; padding: 0 !important; } pre.mermaid svg { background: transparent !important; } pre.mermaid .cluster rect { fill: transparent !important; stroke: #555 !important; }

PDF Table Extraction: Docling vs Marker vs LlamaParse Compared Read More »

Transform Any PDF into Searchable AI Data with Docling

Table of Contents

Setting Up Your Document Processing Pipeline
What is Docling?
What is RAG?

Quick Start: Your First Document Conversion
Export Options for Different Use Cases
Configuring PdfPipelineOptions for Advanced Processing
Enable Image Extraction
Table Recognition Enhancement
AI-Powered Content Understanding
Performance and Memory Management

Building Your RAG Pipeline
Tools for RAG Pipelines
Document Processing
Chunking
Creating a Vector Store

Conclusion

What if complex research papers could be transformed into AI-searchable data using fewer than 10 lines of Python?
Financial reports, research documents, and analytical papers often contain vital tables and formulas that traditional PDF tools fail to extract properly. This results in the loss of structured data that could inform key decisions.
Docling, developed by IBM Research, is an AI-first document processing tool that preserves the relationships between text, tables, and formulas. With just three lines of code, you can convert any document into structured data.
Key Takeaways
Here’s what you’ll learn:

Convert any PDF into structured data with just 3 lines of Python code
Extract tables, formulas, and text while preserving relationships between elements
Build complete RAG pipelines that process 50 chunks in under 60 seconds
Use AI-powered image descriptions to make diagrams searchable
Optimize processing speed by 10x with parallel processing and selective extraction

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-wrap {
display: flex;
justify-content: center;
}

.codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
border: none;
border-radius: 8px;
padding: 12px 28px;
font-family: inherit;
font-size: 16px;
font-weight: 700;
cursor: pointer;
text-decoration: none !important;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover,
.codecut-subscribe-btn:focus {
background: #5aa8e8 !important;
color: #2F2D2E !important;
text-decoration: none !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-subscribe-btn {
width: 100%;
text-align: center;
}
}

Subscribe for free

Setting Up Your Document Processing Pipeline
What is Docling?
Docling is an AI-first document processing tool developed by IBM Research. It transforms complex documents (like PDFs, Excel spreadsheets, and Word files) into structured data while preserving their original structure, including text, tables, and formulas.
To install Docling, run the following command:
pip install docling

What is RAG?
RAG (Retrieval-Augmented Generation) is an AI technique that combines document retrieval with language generation. Instead of relying solely on training data, RAG systems search through external documents to find relevant information, then use that context to generate accurate, up-to-date responses.
This process requires converting documents into structured, searchable chunks. Docling handles this conversion seamlessly.
Quick Start: Your First Document Conversion
Docling transforms any document into structured data with just three lines of code. Let’s see this in action by converting a PDF document – specifically, Docling’s own technical report from arXiv. This is a good example because it contains a lot of different types of elements, including tables, formulas, and text.
from docling.document_converter import DocumentConverter
import pandas as pd

# Initialize converter with default settings
converter = DocumentConverter()

# Convert any document format – we'll use the Docling technical report itself
source_url = "https://arxiv.org/pdf/2408.09869"
result = converter.convert(source_url)

# Access structured data immediately
doc = result.document
print(f"Successfully processed document from: {source_url}")

To iterate through each document element, we will use the doc.iterate_items() method. This method returns tuples of (item, level). For example:

(TextItem(label='paragraph', text='Introduction text…'), 0) – top-level paragraph
(TableItem(label='table', text='| Col1 | Col2 |…'), 1) – table at depth 1
(TextItem(label='heading', text='Section 2'), 0) – section heading

from collections import defaultdict

# Create a dictionary to categorize all document elements by type
element_types = defaultdict(list)

# Iterate through all document elements and group them by label
for item, _ in doc.iterate_items():
element_type = item.label
element_types[element_type].append(item)

# Display the breakdown of document structure
print("Document structure breakdown:")
for element_type, items in element_types.items():
print(f" {element_type}: {len(items)} elements")

The output shows the different types of elements Docling extracted from the document.
Document structure breakdown:
picture: 13 elements
section_header: 31 elements
text: 102 elements
list_item: 22 elements
code: 2 elements
footnote: 1 elements
caption: 3 elements
table: 5 elements

Let’s look specifically for structured elements like tables and formulas that are crucial for RAG applications:
first_table = element_types["table"][0]
print(first_table.export_to_dataframe(doc=doc).to_markdown())

CPU.
Thread budget.
native backend.TTS
native backend.Pages/s
native backend.Mem
pypdfium backend.TTS
pypdfium backend.Pages/s
pypdfium backend.Mem

0
Apple M3 Max
4
177 s 167 s
1.27 1.34
6.20 GB
103 s 92 s
2.18 2.45
2.56 GB

1
(16 cores) Intel(R) E5-2690
16 4 16
375 s 244 s
0.60 0.92
6.16 GB
239 s 143 s
0.94 1.57
2.42 GB

Here is how the table looks in the original PDF:

The extracted table shows Docling’s accuracy and structural differences from the original PDF. Docling captured all numerical data and text perfectly but flattened the merged cell structure into separate columns.
While this loses visual formatting, it benefits RAG applications since each row contains complete information without complex cell merging logic.
Next, look at the first list item element:
first_list_items = element_types["list_item"][0:6]
for list_item in first_list_items:
print(list_item.text)

· Converts PDF documents to JSON or Markdown format, stable and lightning fast
· Understands detailed page layout, reading order, locates figures and recovers table structures
· Extracts metadata from the document, such as title, authors, references and language
· Optionally applies OCR, e.g. for scanned PDFs
· Can be configured to be optimal for batch-mode (i.e high throughput, low time-to-solution) or interactive mode (compromise on efficiency, low time-to-solution)
· Can leverage different accelerators (GPU, MPS, etc).

This matches the original PDF list item.

Look at the first caption element:
first_caption = element_types["caption"][0]
print(first_caption.text)

This matches the image caption in the original PDF.

This matches the image caption in the original PDF.
Export Options for Different Use Cases
Docling provides multiple ways to export the document data, including Markdown, JSON, and dictionary formats.
For human review and documentation, Markdown format preserves the document structure beautifully.
# Human-readable markdown for review
markdown_content = doc.export_to_markdown()
print(markdown_content[:500] + "…")

<!– image –>

## Docling Technical Report

Version 1.0

Christoph Auer Maksym Lysak Ahmed Nassar Michele Dolfi Nikolaos Livathinos Panos Vagenas Cesar Berrospi Ramis Matteo Omenetti Fabian Lindlbauer Kasper Dinkla Lokesh Mishra Yusik Kim Shubham Gupta Rafael Teixeira de Lima Valery Weber Lucas Morin Ingmar Meijer Viktor Kuropiatnyk Peter W. J. Staar

AI4K Group, IBM Research R¨ uschlikon, Switzerland

## Abstract

This technical report introduces Docling , an easy to use, self-contained, MITli…

Compare this to the original PDF:

Docling preserves all original content while converting complex PDF formatting into clean markdown. Every author name, title, and abstract text remains intact, creating searchable structure perfect for RAG applications.
For programmatic processing and API integrations, JSON format provides structured access to all document elements:
import json

# JSON for programmatic processing
json_dict = doc.export_to_dict()

print('JSON keys:', json_dict.keys())

JSON keys: dict_keys(['schema_name', 'version', 'name', 'origin', 'furniture', 'body', 'groups', 'texts', 'pictures', 'tables', 'key_value_items', 'form_items', 'pages'])

The JSON structure reveals Docling’s comprehensive document analysis. Key sections include texts for paragraphs, tables for structured data, pictures for images, and pages for layout information.
For Python development workflows, the dictionary format enables immediate access to all document elements.
# Python dictionary for immediate use
dict_repr = doc.export_to_dict()

# Preview the structure
num_texts = len(dict_repr['texts'])
num_tables = len(dict_repr['tables'])

print(f"Text elements: {num_texts}")
print(f"Table elements: {num_tables}")

Text elements: 985
Table elements: 5

Configuring PdfPipelineOptions for Advanced Processing
The default Docling configuration works well for most documents, but PdfPipelineOptions unlocks advanced processing capabilities. These options control OCR engines, table recognition, AI enrichments, and performance settings.
PdfPipelineOptions becomes essential when working with scanned documents, complex layouts, or specialized content requiring AI-powered understanding.
Enable Image Extraction
By default, Docling does not extract images from the document. However, you can enable image extraction by setting the generate_picture_images option to True.
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.document_converter import PdfFormatOption

pipeline_options = PdfPipelineOptions(generate_picture_images=True)

# Create converter with enhanced table processing
converter_enhanced = DocumentConverter(
format_options={
InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)
}
)

result_enhanced = converter_enhanced.convert("https://arxiv.org/pdf/2408.09869")
doc_enhanced = result_enhanced.document

Display the first image:
# Extract and display the first image
from IPython.display import Image, display

for item, _ in doc_enhanced.iterate_items():
if item.label == "picture":
image_data = item.image

# Get the image URI
uri = str(image_data.uri)

# Display the image using IPython
display(Image(url=uri))
break

The output image matches the first image of the PDF.
Table Recognition Enhancement
To use the more sophisticated AI model for table extraction instead of the default fast model, you can set the table_structure_options.mode to TableFormerMode.ACCURATE.
from docling.datamodel.pipeline_options import PdfPipelineOptions, TableFormerMode
from docling.datamodel.base_models import InputFormat
from docling.document_converter import PdfFormatOption

# Enhanced table processing for complex layouts
pipeline_options = PdfPipelineOptions()
pipeline_options.table_structure_options.mode = TableFormerMode.ACCURATE

# Create converter with enhanced table processing
converter_enhanced = DocumentConverter(
format_options={
InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)
}
)

result_enhanced = converter_enhanced.convert("https://arxiv.org/pdf/2408.09869")
doc_enhanced = result_enhanced.document

AI-Powered Content Understanding
AI enrichments enhance extracted content with semantic understanding. Picture descriptions, formula detection, and code parsing improve RAG accuracy by adding crucial context.
In the code below, we:

Set the do_picture_description option to True to enable picture description extraction
Set the picture_description_options option to use the SmolVLM-256M-Instruct model from Hugging Face.

from docling.datamodel.pipeline_options import PictureDescriptionVlmOptions

# AI-powered content enrichment
pipeline_options = PdfPipelineOptions(
do_picture_description=True, # AI-generated image descriptions
picture_description_options=PictureDescriptionVlmOptions(
repo_id="HuggingFaceTB/SmolVLM-256M-Instruct",
prompt="Describe this picture. Be precise and concise.",
),
generate_picture_images=True,
)

converter_enhanced = DocumentConverter(
format_options={
InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)
}
)

result_enhanced = converter_enhanced.convert("https://arxiv.org/pdf/2408.09869")
doc_enhanced = result_enhanced.document

Extract the picture description from the second picture:
second_picture = doc_enhanced.pictures[1]

print(f"Caption: {second_picture.caption_text(doc=doc_enhanced)}")

# Check for annotations
for annotation in second_picture.annotations:
print(annotation.text)

Caption: Figure 1: Sketch of Docling's default processing pipeline. The inner part of the model pipeline is easily customizable and extensible.
### Image Description

The image is a flowchart that depicts a sequence of steps from a document, likely a report or a document. The flowchart is structured with various elements such as text, icons, and arrows. Here is a detailed description of the flowchart:

#### Step 1: Parse
– **Description:** The first step in the process is to parse the document. This involves converting the text into a format that can be easily understood by the user.

#### Step 2: Ocr
– **Description:** The second step is to perform OCR (Optical Character Recognition) on the document. This involves converting the text into a format that can be easily read by the OCR software.

#### Step 3: Layout Analysis
– **Description:** The third step is to analyze the document's layout. This involves examining the document's structure, including the layout of the text, the alignment of the text, and the alignment of the document's content

Here is the original image:

The detailed description shows how Docling’s picture analysis transforms visual content into text that can be indexed and searched, making diagrams accessible to RAG systems.
Performance and Memory Management
Processing a large document can be time-consuming. To speed up the process, we can use:

The page_range option to process only a specific page range.
The max_num_pages option to limit the number of pages to process.
The images_scale option to reduce the image resolution for speed.
The generate_page_images option to skip page images to save memory.
The do_table_structure option to skip table structure extraction.
The enable_parallel_processing option to use multiple cores.

# Optimized for large documents
pipeline_options = PdfPipelineOptions(
max_num_pages=4, # Limit processing to first 4 pages
page_range=[1, 3], # Process specific page range
generate_page_images=False, # Skip page images to save memory
do_table_structure=False, # Skip table structure extraction
enable_parallel_processing=True # Use multiple cores
)

Building Your RAG Pipeline
We’ll build our RAG pipeline in five steps:

Document Processing: Use Docling to convert documents into structured data
Chunking: Break documents into smaller, searchable pieces
Create Embeddings: Convert text chunks into vector representations
Store in Vector Database: Save embeddings in FAISS for fast similarity search
Query: Retrieve relevant chunks and generate contextual responses

Tools for RAG Pipelines
Building RAG pipelines requires four essential tools:

Docling: converts documents into structured data
LangChain: manages document workflows, chain orchestration, and provides embedding models
FAISS: stores and retrieves document chunks

These tools work together to create complete RAG pipelines that can process, store, and retrieve document content intelligently.
LangChain
LangChain simplifies building AI applications by providing components for document loading, text processing, and chain orchestration. It integrates seamlessly with vector stores and language models.
For a comprehensive introduction to LangChain fundamentals and local AI workflows, see our LangChain and Ollama guide.
FAISS
FAISS (Facebook AI Similarity Search) is a library for efficient similarity search in high-dimensional spaces. It enables fast retrieval of the most relevant document chunks based on embedding similarity.
For production use cases requiring robust database integration, consider implementing semantic search with pgvector in PostgreSQL or using Pinecone for cloud-based vector search as alternatives to FAISS.
Let’s install the additional packages for RAG functionality:
# Install additional packages for RAG functionality
pip install docling sentence-transformers langchain-community langchain-huggingface faiss-cpu
# Note: Use faiss-gpu if you have CUDA support

Document Processing
Convert the document into structured data using Docling.
from docling.document_converter import DocumentConverter

# Initialize converter with default settings
converter = DocumentConverter()

# Convert the document into structured data
source_url = "https://arxiv.org/pdf/2408.09869"
result = converter.convert(source_url)

# Access structured data immediately
doc = result.document

Chunking
AI models have limited context windows that can’t process entire documents at once. Chunking solves this by breaking documents into smaller, searchable pieces that fit within these constraints. This improves retrieval accuracy by finding the most relevant sections rather than entire documents.
Docling provides two main chunking strategies:

HierarchicalChunker: Focuses purely on document structure, creating chunks based on headings and sections
HybridChunker: Combines structure-aware chunking with token-based limits, preserving document hierarchy while respecting model constraints

Let’s compare how these chunkers process the same document.
First, create a helper function to print the chunk content:
def print_chunk(chunk):
print(f"Chunk length: {len(chunk.text)} characters")
if len(chunk.text) > 30:
print(f"Chunk content: {chunk.text[:30]}…{chunk.text[-30:]}")
else:
print(f"Chunk content: {chunk.text}")
print("-" * 50)

Next, process the document with the HierarchicalChunker:
from docling.chunking import HierarchicalChunker

# Process with HierarchicalChunker (structure-based)
hierarchical_chunker = HierarchicalChunker()
hierarchical_chunks = list(hierarchical_chunker.chunk(doc))

print(f"HierarchicalChunker: {len(hierarchical_chunks)} chunks")

# Print the first 3 chunks
for chunk in hierarchical_chunks[:5]:
print_chunk(chunk)

HierarchicalChunker: 114 chunks
Chunk length: 11 characters
Chunk content: Version 1.0
————————————————–
Chunk length: 295 characters
Chunk content: Christoph Auer Maksym Lysak Ah… Kuropiatnyk Peter W. J. Staar
————————————————–
Chunk length: 50 characters
Chunk content: AI4K Group, IBM Research R¨ us…arch R¨ uschlikon, Switzerland
————————————————–
Chunk length: 431 characters
Chunk content: This technical report introduc…on of new features and models.
————————————————–
Chunk length: 792 characters
Chunk content: Converting PDF documents back … gap to proprietary solutions.
————————————————–

Compare this to the HybridChunker:
from docling.chunking import HybridChunker

# Process with HybridChunker (token-aware)
hybrid_chunker = HybridChunker(max_tokens=512, overlap_tokens=50)
hybrid_chunks = list(hybrid_chunker.chunk(doc))

print(f"HybridChunker: {len(hybrid_chunks)} chunks")

# Print the first 3 chunks
for chunk in hybrid_chunks[:5]:
print_chunk(chunk)

HybridChunker: 50 chunks
Chunk length: 358 characters
Chunk content: Version 1.0
Christoph Auer Mak…arch R¨ uschlikon, Switzerland
————————————————–
Chunk length: 431 characters
Chunk content: This technical report introduc…on of new features and models.
————————————————–
Chunk length: 1858 characters
Chunk content: Converting PDF documents back … accelerators (GPU, MPS, etc).
————————————————–
Chunk length: 1436 characters
Chunk content: To use Docling, you can simply…and run it inside a container.
————————————————–
Chunk length: 796 characters
Chunk content: Docling implements a linear pi…erialized to JSON or Markdown.
————————————————–

The comparison shows key differences:

HierarchicalChunker: Creates many small chunks by splitting at every section boundary
HybridChunker: Creates fewer, larger chunks by combining related sections within token limits

We will use HybridChunker because it respects document boundaries (won’t split tables inappropriately) while ensuring chunks fit within embedding model constraints.
from docling.chunking import HybridChunker

# Initialize the chunker
chunker = HybridChunker(max_tokens=512, overlap_tokens=50)

# Create the chunks
rag_chunks = list(chunker.chunk(doc))

print(f"Created {len(rag_chunks)} intelligent chunks")

Created 50 intelligent chunks

Creating a Vector Store
A vector store is a database that converts text into numerical vectors called embeddings. These vectors capture semantic meaning, allowing the system to find related content even when different words are used.
When you search for “document processing,” the vector store finds chunks about “PDF parsing” or “text extraction” because their embeddings are mathematically close. This enables semantic search beyond exact keyword matching.
Create the vector store for semantic search across your document chunks:
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings

# Create embeddings
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")

# Create the vector store
texts = [chunk.text for chunk in rag_chunks]
vectorstore = FAISS.from_texts(texts, embeddings)

print(f"Built vector store with {len(texts)} chunks")

Built vector store with 50 chunks

Now you can search your knowledge base with semantic similarity:
# Search the knowledge base
query = "How does document processing work?"
relevant_docs = vectorstore.similarity_search(query, k=3)

print(f"Query: '{query}'")
print(f"Found {len(relevant_docs)} relevant chunks:")

for i, doc in enumerate(relevant_docs, 1):
print(f"\nResult {i}:")
print(f"Content: {doc.page_content[:150]}…")

Query: 'How does document processing work?'
Found 3 relevant chunks:

Result 1:
Content: Docling implements a linear pipeline of operations, which execute sequentially on each given document (see Fig. 1). Each document is first parsed by a…

Result 2:
Content: In the final pipeline stage, Docling assembles all prediction results produced on each page into a well-defined datatype that encapsulates a converted…

Result 3:
Content: Docling is designed to allow easy extension of the model library and pipelines. In the future, we plan to extend Docling with several more models, suc…

The search results show effective semantic retrieval. The vector store found relevant chunks about Docling’s architecture and design when searching for “document processing” – demonstrating how RAG systems match meaning, not just keywords.
Conclusion
This tutorial demonstrated building a robust document processing pipeline that handles complex, real-world documents. Your pipeline preserves critical elements like tables, mathematical formulas, and document structure while generating semantically meaningful chunks for retrieval-augmented generation systems.
The capability to transform any document format into AI-ready data using minimal code at no cost represents a significant advancement in document processing workflows. For enhanced reasoning capabilities in your RAG workflows, explore our guide on building data science workflows with DeepSeek and LangChain which combines advanced language models with document processing pipelines.

📚 Want to go deeper? Learning new techniques is the easy part. Knowing how to structure, test, and deploy them is what separates side projects from real work. My book shows you how to build data science projects that actually make it to production. Get the book →

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-wrap {
display: flex;
justify-content: center;
}

.codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
border: none;
border-radius: 8px;
padding: 12px 28px;
font-family: inherit;
font-size: 16px;
font-weight: 700;
cursor: pointer;
text-decoration: none !important;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover,
.codecut-subscribe-btn:focus {
background: #5aa8e8 !important;
color: #2F2D2E !important;
text-decoration: none !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-subscribe-btn {
width: 100%;
text-align: center;
}
}

Subscribe for free

Transform Any PDF into Searchable AI Data with Docling Read More »

Scroll to Top

Work with Khuyen Tran

Work with Khuyen Tran