Skip to main content

Fast, layout-aware PDF text extraction in Rust with Python bindings

Project description

ferro-pdf

Fast, layout-aware PDF text extraction written in Rust, with Python bindings.

Most PDF text extractors are either slow (pdfminer.six is pure Python) or flat (they emit glyphs in content-stream order, which scrambles multi-column pages). ferro-pdf is built for RAG / document-ingestion pipelines: it reconstructs reading order (columns, lines, spacing) and is fast enough to not be the bottleneck.

Benchmark

Synthetic corpus (clean, standard fonts)

9 generated PDFs, 198 pages, single- and two-column. Machine: Apple M4 Pro (14 cores), macOS 26.5. Best-of-5, warm cache.

tool best (s) speedup vs pdfminer content (words vs pdfminer)
pdfminer.six 2.68 1.0× 100%
pypdfium2 (C++) 0.098 27.3× 100%
ferro-pdf 0.019 138.6× 100%

Real research papers (arXiv, embedded fonts, math, two-column)

6 papers (Attention, BERT, GPT-3, InstructGPT, LLaMA, ResNet), 213 pages total.

tool best (s) speedup vs pdfminer content (words vs pdfminer)
pdfminer.six 4.33 1.0× 100%
pypdfium2 (C++) 0.224 19.4× 100%
ferro-pdf 0.135 32.1× 98%

Per-paper: 19–43× faster, 91–100% word parity. The parity gap on dense two-column papers (ResNet 91%, BERT 95%) comes from math/figure/table content where word tokenization diverges from pdfminer — the next thing to harden.

Update: since this table was recorded, real-paper word parity improved to ~99% after implementing full font-encoding support (Identity-H CID, /Differences) and real-glyph-width spacing. Re-run bench.py papers to refresh these numbers on your machine.

Takeaway: on clean PDFs the win is ~140×; on messy real-world papers it's a still-excellent ~30× (and ~1.7× faster than C++ pypdfium2), at 98% content parity. Quote the real-paper number publicly — it survives scrutiny.

Reproduce:

python gen_corpus.py corpus      # synthetic PDFs
python bench.py corpus           # synthetic benchmark
python bench.py papers           # real-paper benchmark (after downloading PDFs)

The speedup is content-preserving: ferro-pdf extracts ~the same word count as pdfminer (the reference), so the win isn't from extracting less text.

How it works

PDF bytes
  │  lopdf            parse objects + decode content streams (don't reinvent this)
  ▼
glyph extraction      walk content operators, track CTM/text-matrix, decode
  │  (src/extract.rs)  Unicode via ToUnicode CMap, Identity-H CID, or simple-font
  │                    /Encoding + /Differences; measure real glyph widths
  ▼
layout engine ★       column detection (x-coverage histogram + gutter finding),
  │  (src/layout.rs)   line bucketing by y, gap-based spacing, reading order
  ▼
rayon                 lay out pages in parallel (pdfminer is single-threaded)
  ▼
clean reading-order text

The layout engine is the moat — it's what flat Rust crates (pdf-extract) and even C++ pypdfium2 don't do for you.

Usage

Python

pip install pyferro-pdf
import ferro_pdf
text = ferro_pdf.extract_text("paper.pdf")
text = ferro_pdf.extract_text_from_bytes(open("paper.pdf", "rb").read())

Rust

let text = ferro_pdf::extract_text_from_path("paper.pdf")?;

CLI

cargo build --release
./target/release/ferro-cli paper.pdf            # print text
./target/release/ferro-cli paper.pdf --time     # print timing to stderr

Build

# Rust library + CLI
cargo build --release

# Python wheel (into the active venv)
maturin develop --release

Honest limitations (current PoC)

  • Text-only: table reconstruction (ruling lines → Markdown grid) is not implemented yet — that's the next moat to build.
  • Scanned / image-only PDFs need an OCR fallback (not included).
  • Column detection is heuristic (gutter finding); pathological layouts may need tuning of the thresholds in detect_columns.
  • Font decoding handles ToUnicode CMaps (incl. array-form bfrange), Identity-H CID fonts, and simple fonts via WinAnsi/MacRoman + /Differences. Truly exotic encodings without any of these signals may still mis-decode.

These are the honest gaps to close before claiming production parity — but the core speed + reading-order story is real and reproducible above.

Performance: where the time goes & further scope

Stage profile (FERRO_PROFILE=1, GPT-3, 75 pages), after optimization:

stage time notes
load ~42 ms (~85%) lopdf parse + inflate — the bottleneck, single-threaded
extract ~4 ms content-stream + font decode (was ~31 ms before the font cache)
layout ~3 ms column/line reconstruction — negligible

Optimizations already applied:

  • Font/ToUnicode cache (parse each CMap once, not per page): ~7× on the extract stage (GPT-3 31 ms → 4 ms).
  • Parallel page extraction + parallel batch API (extract_text_batch): 9.2× scaling on 14 cores.

Throughput (60-PDF / 2,130-page batch, Apple M4 Pro):

mode time vs pdfminer notes
pdfminer.six (sequential) 43.9 s 1.0×
pypdfium2 — 1 core 2.32 s 18.9× C++; fastest single-core
pypdfium2 — threads crashes pdfium is not thread-safe
pypdfium2 — process pool 1.42 s 31× fork + IPC overhead
ferro — 1 core 5.63 s 7.8× lopdf parse/inflate-bound
ferro — parallel batch 0.59 s 74× no-GIL Rust threads

Honest read vs pypdfium2: per single core, pdfium (C++) is ~2.4× faster than ferro — it's heavily optimized and ferro is bound by lopdf. ferro wins at batch scale (2.4× faster than pdfium's process pool) because pdfium can't use threads, while ferro parallelizes freely with no GIL. ferro's edge is parallelism + pure-Rust embeddability + reading-order layout output, not raw single-core speed.

Remaining scope, ranked by expected payoff:

  1. Attack load (85% of single-core time). This is also exactly the gap to pypdfium2. SIMD inflate (flate2 zlib-ng, needs cmake) ≈ 2–3× on decompression; skip image/XObject streams we never use; or a lazier parser. Closing this would make ferro competitive with pdfium single-core too.
  2. Parallel granularity. 9.2× / 14 cores ≈ 66% efficiency; file-level-only parallelism for batch (avoid nested rayon overhead) could reach ~12×.
  3. Allocation trimming in the operator loop (arena / reuse buffers): modest.

Beyond ~2× more you'd need to drop lopdf for an FFI to pdfium/mupdf — at which point you lose the "pure Rust" story and pdfium would roughly match you.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyferro_pdf-0.1.0.tar.gz (36.2 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pyferro_pdf-0.1.0-cp38-abi3-win_amd64.whl (470.6 kB view details)

Uploaded CPython 3.8+Windows x86-64

pyferro_pdf-0.1.0-cp38-abi3-win32.whl (439.7 kB view details)

Uploaded CPython 3.8+Windows x86

pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_x86_64.whl (800.8 kB view details)

Uploaded CPython 3.8+musllinux: musl 1.2+ x86-64

pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_i686.whl (827.0 kB view details)

Uploaded CPython 3.8+musllinux: musl 1.2+ i686

pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_armv7l.whl (861.6 kB view details)

Uploaded CPython 3.8+musllinux: musl 1.2+ ARMv7l

pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_aarch64.whl (744.2 kB view details)

Uploaded CPython 3.8+musllinux: musl 1.2+ ARM64

pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (590.8 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ x86-64

pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl (627.7 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ s390x

pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl (649.8 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ ppc64le

pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl (586.9 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ ARMv7l

pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (567.7 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ ARM64

pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl (622.3 kB view details)

Uploaded CPython 3.8+manylinux: glibc 2.12+ i686

pyferro_pdf-0.1.0-cp38-abi3-macosx_11_0_arm64.whl (526.1 kB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

pyferro_pdf-0.1.0-cp38-abi3-macosx_10_12_x86_64.whl (550.7 kB view details)

Uploaded CPython 3.8+macOS 10.12+ x86-64

File details

Details for the file pyferro_pdf-0.1.0.tar.gz.

File metadata

  • Download URL: pyferro_pdf-0.1.0.tar.gz
  • Upload date:
  • Size: 36.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyferro_pdf-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f1439cbc23692c49ca3c29834d6fa0aff02aec742ab43c28f76d2c3c0bbc8382
MD5 6d0abc7623692ee7044e1c1666860ac0
BLAKE2b-256 69000f884448bba93c893626bb1da4b3a774a0c5d2e95634a282628b07f43aeb

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0.tar.gz:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-win_amd64.whl.

File metadata

  • Download URL: pyferro_pdf-0.1.0-cp38-abi3-win_amd64.whl
  • Upload date:
  • Size: 470.6 kB
  • Tags: CPython 3.8+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 9e694f765f21e660cd01106c3fb427e7aec0d6433f2d08026c086976a5a75b43
MD5 5b0ef091dc10531ededaace7b242b474
BLAKE2b-256 fd40d893f3f2697a6e4e67a7870578e32f663495eb0b65510d10ddf3a9b19dfe

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-win_amd64.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-win32.whl.

File metadata

  • Download URL: pyferro_pdf-0.1.0-cp38-abi3-win32.whl
  • Upload date:
  • Size: 439.7 kB
  • Tags: CPython 3.8+, Windows x86
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-win32.whl
Algorithm Hash digest
SHA256 d556023d501a1e94c92197c3f33410109a1be6c3f0c00563ce738c71ffa0f416
MD5 eb2d8d20d0feae0993492b19b837c338
BLAKE2b-256 37160d34f36aa83bc5e6afcd69ff9e70e6cc3e67f8ca5ac0f0f50ccbd6b5e46f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-win32.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 2a2c0273b4223af420ef47d954b039b2ac04d22d339145dcaa54e65fb8137ff6
MD5 cf06ca1c032e72010438c7d8024adc88
BLAKE2b-256 7b59d0bbec125c7d43c0041fb42349c69e2cfff2deff94547c223b3833c759b2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_x86_64.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_i686.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 d4aac624e154300f8394623b4655965ac99d4e5125df0113cd1e2e0201da48e9
MD5 17c13014050fc1b00d0b146bcc0d42c8
BLAKE2b-256 ca374bd20f0271d84f9f68d3283bc9f2dff449b00551d49ac3498a09d5e00026

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_i686.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_armv7l.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_armv7l.whl
Algorithm Hash digest
SHA256 cf466d0f7ae9c89830a01e39c6d522047b9b111c1d08afcab7e5ff8c03b79708
MD5 68ada40c3d50b653269210d26a8c85e7
BLAKE2b-256 2e129aa1ae858971095661081435487b14e0e4601224ce26c7bbfef8c8448333

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_armv7l.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 6382ba351a2e57ff760229e123a0957a39b47871b01f832d2320823ee7c46134
MD5 e70d020f2fbab7bed46bc1284a9ddf32
BLAKE2b-256 d003a34de29304a622253d58619e10d72e621ef66ebd451989b03da09a63cfec

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-musllinux_1_2_aarch64.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 d021fa67c52a65e487eb80dab35053efb2f8d293e711e604091c3bdfbe42742b
MD5 011a44c9b55d29c4381a9495684c3b7a
BLAKE2b-256 962ea0cb38a0c76268b7a58773f700f9ef8164952c096ae91631e27a8a21b891

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl
Algorithm Hash digest
SHA256 1f45ac3b24b1945fb69df658a00607864ca1ac4baa7a32fcbe1152971b9f7a10
MD5 9eb188b14af0c34a3cb94ef2191321be
BLAKE2b-256 483bb7b3f7c32d7dccbe014088a2c5f1c76b37c10ed06934db567b097c8df21f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl
Algorithm Hash digest
SHA256 71bc6d659de3326a48aba32e2a975e6de2bdbb2b7234c0885144ce549591645e
MD5 6ce999e1009bf2702eefbddff35fbcab
BLAKE2b-256 6d47b7a0df1173425659107b340e4960d3d694ac3fcaf5ad44966395046f915d

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
Algorithm Hash digest
SHA256 219342813aeb8e004dada20cf09446bade68b03049dd0b8e6baaf4473d92ba81
MD5 825e9e6ac0dc9247916a751b23b68984
BLAKE2b-256 065238ee00041694cf6615a42d5cfba9974579a1ee82c98aefa824463df1fdef

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 3a576fc7de53f18dbf09626b52bef8736f91c4819facfbb96e2aa2cb0f0c8795
MD5 37998d5d10636fc498f7027801560bd3
BLAKE2b-256 74fc13d57d1bb9a94b5d27bf487fa161c28613495a544ed5568e48bb18d73d7e

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
Algorithm Hash digest
SHA256 91f15170e557547b18ad8e6138342ca618be2f84a9749e04e486b802d47f0457
MD5 e4b4f8336569d11152489954b85eda10
BLAKE2b-256 08b3fe72d0a067d8e5e4dd62e48f175de5e364f7c13966a9a4933b3f2947e617

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d71ba487dca2901bf6f8a5aece796f50c83f8e1182f5c9f6d47802a4972d77a3
MD5 e80c3494f699a9cb2a5c0395c188704c
BLAKE2b-256 e027d16f29fa416eed7467044179f7feeeb692e349f0c828ab00bb32dfa297f8

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-macosx_11_0_arm64.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyferro_pdf-0.1.0-cp38-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pyferro_pdf-0.1.0-cp38-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 0c7aef5fbb443a73beca68aace92cb877eb1b42d584fef294973f1676706f6c2
MD5 4f1b92eff0eca9f4c83a3c1ae363d715
BLAKE2b-256 fb1feec84a04aef29abef95ff5a0dce3fe14c19fee6c3e58e136932bac82b375

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyferro_pdf-0.1.0-cp38-abi3-macosx_10_12_x86_64.whl:

Publisher: publish.yml on anshgoyalevil/ferro-pdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page