A PDF text extraction library written in Zig.
- Memory-mapped file reading, zero-copy where possible
- Streaming text extraction with efficient arena allocation
- Multiple decompression filters: FlateDecode, ASCII85, ASCIIHex, LZW, RunLength
- Font encoding support: WinAnsi, MacRoman, ToUnicode CMap
- XRef table and stream parsing (PDF 1.5+)
- Configurable error handling (strict or permissive)
- Structure tree extraction for tagged PDFs (PDF/UA)
- Optional geometric reading order for non-tagged PDFs
- Markdown export for structured PDFs
Build with zig build -Doptimize=ReleaseFast, then run:
zig build bench -- document.pdf
This runs five zpdf extractions and, when mutool is installed, one MuPDF
comparison. Treat the result as a local diagnostic rather than a controlled
cross-tool benchmark: record the zpdf revision, Zig and MuPDF versions,
hardware, input checksum, and run policy when publishing results. Additional
corpus and accuracy tools are documented in benchmark/README.md.
The full methodology uses
olmOCR-Bench, veraPDF, and PDF.js corpora to keep
ground-truth accuracy separate from compatibility and robustness. Initial
measured findings identify concrete reading-order
and dense-text gaps.
- Zig 0.15.2 or later
zig build # Build library and CLI zig build test # Run tests
const std = @import("std"); const zpdf = @import("zpdf"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); const doc = try zpdf.Document.open(allocator, "file.pdf"); defer doc.close(); var buf: [4096]u8 = undefined; var bw = std.fs.File.stdout().writer(&buf); const writer = &bw.interface; defer writer.flush() catch {}; for (0..doc.pageCount()) |page_num| { try doc.extractText(page_num, writer); } }
zpdf extract document.pdf # Extract all pages (uses structure tree for reading order) zpdf extract -p 1-10 document.pdf # Extract pages 1-10 zpdf extract -o out.txt document.pdf # Output to file zpdf info document.pdf # Show document info zpdf bench document.pdf # Run benchmark
import zpdf with zpdf.Document("file.pdf") as doc: print(doc.page_count) # Single page text = doc.extract_page(0) # All pages (structure-tree order when available; otherwise stream order) all_text = doc.extract_all() # Fast mode (higher throughput, stream-order extraction) fast_text = doc.extract_all(mode="fast") # Page info info = doc.get_page_info(0) print(f"{info.width}x{info.height}") # Zero-copy memory open (unsafe semantics for other language bindings) with zpdf.Document.open_memory_unsafe(open("file.pdf", "rb").read()) as doc: print(doc.page_count)
Build the shared library first:
zig build -Doptimize=ReleaseFast PYTHONPATH=python python3 examples/basic.py
Build an installable, platform-specific wheel from the current Zig library:
python3 -m pip install build python3 -m build --wheel python
When developing from a checkout, the Python loader prefers ZPDF_LIB and
zig-out/lib over any packaged library, so tests cannot silently use a stale
binary.
src/
├── root.zig # Document API and core types
├── main.zig # CLI entry point
├── capi.zig # C ABI exports for FFI
├── wapi.zig # WASM API exports
├── parser.zig # PDF object parser
├── xref.zig # XRef table/stream parsing
├── pagetree.zig # Page tree resolution
├── decompress.zig # Stream decompression filters
├── encoding.zig # Font encoding and CMap parsing
├── agl.zig # Adobe Glyph List mappings
├── cff.zig # CFF/Type1 font parsing
├── interpreter.zig # Content stream interpreter
├── structtree.zig # Structure tree parser (PDF/UA)
├── layout.zig # Text layout and bounding boxes
├── markdown.zig # Markdown export
└── simd.zig # SIMD-accelerated parsing
python/zpdf/ # Python bindings (cffi)
examples/ # Usage examples
The default extraction path prioritizes complete text extraction:
-
Structure Tree (preferred): For tagged PDFs, uses marked-content IDs in the document's semantic structure. If the structured result contains too little of the page's stream text, zpdf keeps the more complete stream-order result instead.
-
Stream Order (default fallback): Untagged content is extracted in raw PDF content-stream order. Python
extract_all(mode="fast")uses the same order while bypassing structure-tree processing. -
Geometric Layout (opt-in):
zpdf extract --reading-orderand Pythonextract_page(..., reading_order=True)analyze estimated span positions and columns to approximate visual order. This path is experimental.
| Method | Pros | Cons |
|---|---|---|
| Structure tree | Uses author-provided semantic order | Requires usable tagging and may be incomplete |
| Stream order | Fast and preserves content completeness | May not match visual order |
| Geometric layout | Can approximate visual and column order | Uses estimated bounds and may fail on complex layouts |
| Feature | zpdf | pdfium | MuPDF |
|---|---|---|---|
| Text Extraction | |||
| Stream order | Yes | Yes | Yes |
| Tagged/structure tree API | Yes | Yes | Yes |
| Visual reading order | Experimental | No | Yes |
| Text-span bounds | Estimated | Yes | Yes |
| Font Support | |||
| WinAnsi/MacRoman | Yes | Yes | Yes |
| ToUnicode CMap | Yes | Yes | Yes |
| CID fonts (Type0) | Partial* | Yes | Yes |
| Compression | |||
| FlateDecode, LZW, ASCII85/Hex | Yes | Yes | Yes |
| JBIG2, JPEG2000 | No | Yes | Yes |
| Other | |||
| Encrypted PDFs | No | Yes | Yes |
| Rendering | No | Yes | Yes |
*CID fonts: Works when CMap is embedded directly.
zpdf's span bounds use text positions plus an estimated width; they are not exact glyph or word bounds. Competitor capabilities refer to their public APIs and may vary by version.
Use zpdf when: Batch processing, tagged PDFs (PDF/UA), simple text extraction, Zig integration.
Use pdfium when: Browser integration, full PDF support, proven stability.
Use MuPDF when: Complex visual layouts, rendering needed.
CC0 - Public Domain