Fast, light, cgo-free document parsing for Go. Spatial text with bounding boxes, Markdown reconstruction, page screenshots, OCR routing and recursive extraction of files inside files — no cloud, no LLM required.
ParseRails is the Go counterpart to run-llama/liteparse.
It wraps the same engine liteparse uses — Google's PDFium — through
klippa-app/go-pdfium, running it in
pure-Go WebAssembly (wazero) mode. That means no CGO_ENABLED=1, no native
libraries to install in your Docker image, and clean cross-compilation.
Most Go PDF text extractors give you a flat string and lose layout. ParseRails keeps spatial structure — every word with its bounding box, page, and font — which is what downstream RAG, table reconstruction, and LLM-vision pipelines actually need. On top of that it reconstructs the structure those pipelines read: headings, tables and lists as Markdown.
It also handles what arrives around the document. Real input is a scan, or a
.docx with a spreadsheet pasted into it, or an invoice attached to an e-mail
inside a ZIP — so ParseRails detects formats from their
bytes, walks files inside
files, and says which pages need OCR
before you pay for it.
| Capability | Engine | Status |
|---|---|---|
| PDF text + per-word bounding boxes | PDFium (WASM) | ✅ |
| Word / line granularity, opt-in font size | PDFium (WASM) | ✅ |
Page screenshot rendering (RenderPage) |
PDFium (WASM) | ✅ |
| Pluggable OCR + automatic fallback | interface | ✅ |
| Figure OCR on text pages, merged with native words | interface | ✅ |
| Standalone images (PNG/JPEG/TIFF/BMP/WebP/GIF) | OCR backend | ✅ |
| Tesseract OCR adapter (cgo-free, subprocess) | tesseract CLI |
✅ |
| HTTP OCR adapter (LiteParse OCR API compatible) | net/http |
✅ |
| Office formats (DOCX/PPTX/XLSX/...) | LibreOffice headless → PDF | ✅ |
| Native DOCX/XLSX/PPTX reading (no LibreOffice) | archive/zip + encoding/xml |
✅ |
Magic-byte format detection (Sniff/Detect) |
— | ✅ |
Encrypted PDFs, page ranges (ReadOptions) |
PDFium | ✅ |
Complexity routing (Inspect, is-complex) |
PDFium | ✅ |
| Recursive containers (ZIP/EML/MSG/OLE/attachments) | — | ✅ |
| Concurrent batch parsing | — | ✅ |
Per-document timeouts (WithTimeout) |
— | ✅ |
| CLI: parse/batch/extract/render/is-complex, stdin | — | ✅ |
ExtractText whole-page text fast path |
PDFium | ✅ |
| Markdown output (headings, tables, lists, figures) | — | ✅ |
Layout blocks as data (Blocks) |
— | ✅ |
Native cgo backend (-tags parserails_cgo) |
libpdfium | ✅ |
| Task | Go | CLI |
|---|---|---|
| PDF/Office/image text with coordinates | ParseData, ParseFile |
parse --format json |
| PDF text layer only, without OCR | ExtractText, ExtractTextData |
— |
| Headings, tables and lists | Document.Markdown, Document.Blocks |
parse --format markdown |
| DOCX/XLSX/PPTX structure without LibreOffice | ReadOfficeDocument |
parse --native-office --format markdown |
| Archives, emails and embedded attachments | Extract, ExtractFile |
extract |
| Independent files in a batch | ParseFiles |
batch |
| OCR candidates and page screenshots | Inspect, RenderPage |
is-complex, render |
For a complete walkthrough with runnable Go code, start with the Usage Guide. The CLI reference lists flags and exit codes.
Requires Go 1.27 or later.
As a library:
go get github.com/promptrails/parserailsAs a command:
go install github.com/promptrails/parserails/cmd/parserails@latest
parserails parse invoice.pdf # reconstructed text
parserails parse --format markdown doc.pdf # headings, tables, lists
parserails parse --json report.docx # JSON with boxes
parserails batch ./corpus ./out # a directory, concurrently
parserails extract bundle.zip # everything inside a file
parserails is-complex scan.pdf # which pages need OCR?
parserails render --dpi 150 doc.pdf # page 0 → doc-p0.png
curl -sL https://example.com/report.pdf | parserails parse -No system dependencies for PDF or native DOCX/XLSX/PPTX text reading. Office
page layout and word boxes need libreoffice/soffice on PATH. OCR needs
the tesseract binary or a configured HTTP OCR server.
@latest installs a published version. To test the code in a local checkout:
go build -o ./bin/parserails ./cmd/parserails
./bin/parserails parse --native-office --format markdown report.docxPlace CLI flags before positional arguments. --pages selections are 1-based;
render --page and result page indexes are 0-based.
package main
import (
"context"
"fmt"
"os"
"github.com/promptrails/parserails"
)
func main() {
p, err := parserails.New() // initializes a pooled PDFium WASM runtime
if err != nil {
panic(err)
}
defer p.Close()
pdf, err := os.ReadFile("invoice.pdf")
if err != nil {
panic(err)
}
doc, err := p.Parse(context.Background(), pdf)
if err != nil {
panic(err)
}
for _, w := range doc.Words() {
fmt.Printf("p%d %q [%.1f %.1f %.1f %.1f]\n",
w.Page, w.Text, w.X0, w.Y0, w.X1, w.Y1)
}
}type Word struct {
Text string
Page int
X0, Y0, X1, Y1 float64 // bounding box, PDF user-space coordinates
FontSize float64 // 0 unless New(WithFontInfo()) is used
Confidence float64 // OCR score, 0-1; 0 for natively extracted text
}Word-level boxes are precise but extract every character across the WASM boundary. For per-line boxes at ~6× the speed:
p, _ := parserails.New(parserails.WithGranularity(parserails.GranularityLine))Structure, reconstructed from the geometry — headings, tables, lists, figures, in reading order:
md := doc.Markdown()
for _, b := range doc.Blocks() {
fmt.Printf("%-10s p%d %q\n", b.Kind, b.Page, b.Text)
}For PDF-derived blocks, Go coordinate fields map a heading or table cell back
to the page. Use WithFontInfo() and WithImages() to supply heading metrics
and figure regions; the CLI enables these for Markdown automatically. Figure
links are placeholders, not image files written to disk. See
docs/markdown.md.
c, _ := p.Inspect(ctx, data, parserails.ReadOptions{})
if c.NeedsOCR() {
fmt.Println("pages needing OCR:", c.OCRPages()) // reasons: scanned, garbled, ...
}A text-layer pass — nothing is rendered, no OCR runs — so a pipeline can route or price a batch for a fraction of a parse. See docs/complexity.md.
node, err := p.ExtractFile(ctx, "bundle.zip", parserails.ExtractOptions{
MaxFiles: 100,
MaxBytes: 64 << 20,
})
if err != nil {
panic(err)
}
node.Walk(func(n *parserails.Node) {
if n.Err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", n.Name, n.Err)
}
})
fmt.Println(node.AllText())PDF attachments, ZIP entries, e-mail and Outlook attachments, OLE objects
embedded in office documents — recursively, bounded by depth, file count and
unpacked bytes. One unreadable file is recorded on its own node instead of
failing the walk. Check every Node.Err before treating the result as complete.
Extract returns an inventory/text tree; it does not save the original
attachment files. See docs/containers.md.
Scanned/image-only pages have no extractable text. ParseRails renders those pages and falls back to an OCR backend you provide — automatically:
import "github.com/promptrails/parserails/ocr/tesseract"
p, _ := parserails.New(
parserails.WithOCR(tesseract.New(tesseract.Config{Lang: "eng"})),
)The bundled ocr/tesseract adapter shells out to the tesseract binary (no cgo,
no libtesseract), and ocr/httpocr talks to a remote server over the
LiteParse OCR API, so the OCR server images published for LiteParse
(EasyOCR, PaddleOCR, RapidOCR, Surya) work unchanged. Implement the one-method
OCR interface for anything else.
WithImageOCR goes further than the fallback: it also reads the figures on
pages that do have text — charts, pasted screenshots — and merges the result
with the native words.
img, _ := p.RenderPage(ctx, pdf, parserails.RenderRequest{Page: 0, DPI: 150})
png.Encode(out, img) // standard image.ImageWhen you need just the text (RAG ingestion, search indexing) and not boxes, use
ExtractText — it uses PDFium's whole-page text API (one call per page) and is
several times cheaper than Parse:
text, _ := p.ExtractText(ctx, pdf) // plain string, no boxes
text, _ = p.ExtractFileText(ctx, "x.docx") // PDF or office docThe PDF fast path never runs OCR, even with WithOCR configured. Use
Parse/ParseData for scanned PDFs and call doc.Text() on the result.
Same API, two build-time backends:
go build . # PDFium as WASM — cgo-free, portable (default)
go build -tags parserails_cgo . # PDFium native — much faster/lighter, needs libpdfiumThe default is cgo-free and needs no system libraries. The parserails_cgo
backend links libpdfium for far higher throughput on controlled hosts (e.g. a
Dockerized worker). parserails.Backend reports which is active. See
docs/architecture.md.
- cgo-free by default — PDFium runs as WASM under wazero; opt into the native
parserails_cgobackend for maximum throughput. - Pooled runtime — instances are reused across requests (PDF parsing is CPU-bound); one pool per process.
- Layered —
domain → parser → ocr → service, mirroring the PromptRails service conventions so it drops cleanly into a standalone parse service.
ParseRails is benchmarked against ledongthuc/pdf, pdfcpu/pdfcpu, and
unidoc/unipdf in benchmark/ — a separate Go module, so
those (heavy, partly AGPL/commercial) dependencies never enter this module's
graph.
cd benchmark && go test -bench=. -benchmem ./...ParseRails is the only one of the four that returns per-word bounding boxes;
the pure-Go readers are far faster but give flat text only. See
benchmark/README.md for numbers and the (important)
caveats on what each library actually measures.
doc, _ := p.ParseFile(ctx, "report.docx") // PDF or office doc
results := p.ParseFiles(ctx, paths, 4) // concurrent batchParseFile converts Office documents to PDF via LibreOffice, then parses.
ParseFiles captures errors in each FileResult and preserves input order;
pass a positive concurrency value. WithNativeOffice applies to text
extraction and the container walk, not these spatial APIs.
For CLI input discovery, output collisions, retries and examples, see Batch Processing.
Extraction defaults to depth 8, 512 file attempts and 256 MiB of content,
including the root. MaxBytes is not a process memory cap: PDFium materializes
PDF attachments before their sizes can be checked. WithTimeout stops the
caller waiting for an operation but cannot kill an in-progress PDFium call.
Use a context deadline for a whole request spanning multiple operations.
CLI extract can exit successfully with errors on individual nodes. Use tree
or JSON output when completeness matters; text output alone omits those errors.
See Limits & Timeouts for accounting, cancellation and native
Office timeout scope.
Full documentation lives in docs/:
- Getting Started and Usage Guide — installation and complete workflows.
- CLI and API Reference — commands, flags, options and result types.
- OCR, Native Office, Containers and Batch Processing — format-specific usage.
- Limits & Timeouts and Troubleshooting — quotas, partial results and operational behavior.
The documentation site deploys from main. Feature-branch pages can be read
in the checkout or previewed locally as described in
Troubleshooting.
SKILL.md is a ready-made skill file for coding agents: which
command fits which task, how to route scanned documents, and how to read the
output. Copy it into your agent's skills directory.
Runnable, self-contained examples (each its own module + Dockerfile) live in
examples/:
extract-text— the basics, fully static cgo-free binaryparse-server— HTTP service with OCR + office supportnative-cgo— native PDFium backend for max throughput
See docs/examples.md.
The PDF core is complete — spatial text, rendering, OCR, office formats, CLI,
batch — with both a cgo-free WASM backend and a native parserails_cgo
backend. Around it sit the layers a real pipeline needs: format detection,
Markdown and block reconstruction, OCR
routing, recursive container
extraction and native office
reading.
See docs/roadmap.md for the detailed status and what is
still being considered (ruled-table detection from vector graphics, layout
complexity signals, published quality benchmark numbers).
MIT © PromptRails