Skip to content

Repository files navigation

zpdf (alpha stage - early version)

A PDF text extraction library written in Zig.

Features

  • 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

Benchmarking

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.

Requirements

  • Zig 0.15.2 or later

Building

zig build # Build library and CLI
zig build test# Run tests

Usage

Library

conststd=@import("std");
constzpdf=@import("zpdf");
pubfnmain() !void {
vargpa=std.heap.GeneralPurposeAllocator(.{}){};
defer_=gpa.deinit();
constallocator=gpa.allocator();
constdoc=tryzpdf.Document.open(allocator, "file.pdf");
deferdoc.close();
varbuf: [4096]u8=undefined;
varbw=std.fs.File.stdout().writer(&buf);
constwriter=&bw.interface;
deferwriter.flush() catch {};
for (0..doc.pageCount()) |page_num| {
trydoc.extractText(page_num, writer);
}
}

CLI

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

Python

importzpdfwithzpdf.Document("file.pdf") asdoc:
print(doc.page_count)
# Single pagetext=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 infoinfo=doc.get_page_info(0)
print(f"{info.width}x{info.height}")
# Zero-copy memory open (unsafe semantics for other language bindings)withzpdf.Document.open_memory_unsafe(open("file.pdf", "rb").read()) asdoc:
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.

Project Structure

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

Reading Order

The default extraction path prioritizes complete text extraction:

  1. 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.

  2. 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.

  3. Geometric Layout (opt-in): zpdf extract --reading-order and Python extract_page(..., reading_order=True) analyze estimated span positions and columns to approximate visual order. This path is experimental.

MethodProsCons
Structure treeUses author-provided semantic orderRequires usable tagging and may be incomplete
Stream orderFast and preserves content completenessMay not match visual order
Geometric layoutCan approximate visual and column orderUses estimated bounds and may fail on complex layouts

Comparison

FeaturezpdfpdfiumMuPDF
Text Extraction
Stream orderYesYesYes
Tagged/structure tree APIYesYesYes
Visual reading orderExperimentalNoYes
Text-span boundsEstimatedYesYes
Font Support
WinAnsi/MacRomanYesYesYes
ToUnicode CMapYesYesYes
CID fonts (Type0)Partial*YesYes
Compression
FlateDecode, LZW, ASCII85/HexYesYesYes
JBIG2, JPEG2000NoYesYes
Other
Encrypted PDFsNoYesYes
RenderingNoYesYes

*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.

License

CC0 - Public Domain

About

Zero-copy PDF text extraction library written in Zig. High-performance, memory-mapped parsing with SIMD acceleration.

Topics

Resources

Stars

920 stars

Watchers

4 watching

Forks

Releases

Packages

Contributors

Languages