Deterministic, pure-Go PDF text & structure extraction for LLM/RAG and document-ingestion pipelines — no CGo, no rendering, no OCR, no network.
GoPDF turns real-world PDFs into clean, inspectable structure — text, words and lines with bounding boxes, form fields, attachments, links, metadata, fonts, and image-draw signals — and reports, deterministically, when a page extracts cleanly versus when it should be routed to OCR or flagged for review. It opens modern encrypted files, survives malformed documents, and never silently guesses: every degraded extraction comes with a diagnostic you can act on.
Requires Go 1.25+ (go.mod directive).
package main
import (
"bytes"
"context"
"fmt"
"os"
"github.com/Detective-XH/gopdf"
)
func main() {
f, r, err := pdf.Open(os.Args[1])
if err != nil {
panic(err)
}
defer func() { _ = f.Close() }()
var buf bytes.Buffer
text, err := r.GetPlainText(context.Background())
if err != nil {
panic(err)
}
_, _ = buf.ReadFrom(text)
fmt.Println(buf.String())
}Why GoPDF · At a glance · Features · Install · Examples · API stability · Limitations · Accuracy · Releases
- Built for ingestion, not viewing. Every API answers an ingestion question — what is the text, where is it on the page, how confident is the decode, does this page need OCR?
- Confidence signals, not just bytes.
Page.ExtractionSignal()andReader.DocumentSummary()classify each page (text/image_only/empty/degraded) and quantify decode quality, so a pipeline can index, route to OCR, or flag low-confidence pages — without parsing logs. - Survives the real world. CJK and Cyrillic scripts, RC4 / AES-128 / AES-256 encryption, hybrid cross-references, object streams, rotated and vertical-writing pages, and malformed PDFs are all handled with bounded, panic-safe, deterministic extraction.
- Pure Go, drop-in. No CGo and no external services —
go getand ship. Safe for concurrent use after open.
| Need | API |
|---|---|
| Plain text (context/cancellation aware) | Reader.GetPlainText |
| Styled text runs (font, size, position) | Reader.GetStyledTexts / Page.Texts |
| Words / visual lines with bounding boxes | Page.Words / Page.Lines |
| Column-major visual blocks (RAG chunking unit, experimental) | Page.Blocks |
| Ruled-table reconstruction — fully-ruled + rect-bordered + per-cell-grid (experimental) | Page.Tables |
| Rows / columns of text (legacy, deprecated) | Page.Lines / Page.Words (Page.GetTextByRow / Page.GetTextByColumn are deprecated) |
| Form field values (AcroForms, read-only) | Reader.Fields |
| Embedded file attachments | Reader.Attachments |
| Link annotations / document-wide links | Page.Annotations / Reader.Links |
| Named destinations / outline | Reader.Dest / Reader.Outline |
| Image draw metadata (no decoding) | Page.Images |
| Fonts / XMP / document info | Reader.Fonts / Reader.XMP / Reader.Info |
| Printed page labels (roman front matter, offsets) | Reader.PageLabels |
Applied page rotation (/Rotate, upright display-space coords) |
Page.Rotate |
| Page extraction readiness + warnings | Page.ExtractionSummary / Reader.Warnings |
| Extraction routing signals (text / image / empty / degraded) | Page.ExtractionSignal / Reader.DocumentSummary |
| Structured JSON debug export (PyMuPDF-dict shape, experimental) | Page.DebugJSON / Reader.DebugJSON |
| Encrypted PDFs (RC4, AES-128, AES-256) | NewReaderEncrypted |
GoPDF is tuned for fast, deterministic text extraction. Extracting all text from a
22-page Traditional Chinese PDF takes ~25 ms (~870 pages/sec) on an Apple M4 Pro
(go test -bench=BenchmarkCJKColdOpenExtract).
In matched-scope benchmarking against common Python extractors on the same
documents, GoPDF was fastest at both plain-text and positioned-word extraction —
several times faster than pure-Python pypdf / pdfminer.six / pdfplumber, and
ahead of the C-backed pdftotext and PyMuPDF on this workload — while staying pure
Go. These are speed numbers only, covering text and word extraction: GoPDF does
not render pages or decode images, its ruled-table reconstruction (Page.Tables()) is
not part of these numbers, and its layout/word-grouping quality is a work in progress and
not benchmarked. See BENCHMARKS.md for methodology, full tables,
and caveats.
GoPDF began as a maintained repair fork of ledongthuc/pdf (itself derived from rsc/pdf). Early releases focused on closing long-standing extraction and robustness gaps in the upstream reader lineage — including CJK/Cyrillic decoding, ToUnicode priority, Form XObject text omission, TJ word-boundary handling, malformed-parser panics, AES-encrypted PDFs, and missing regression coverage. It is now an independent project, aimed at being a dependable, pure-Go upstream for PDF extraction and ingestion pipelines.
GoPDF stays API-compatible with the call sites the lineage readers expose — the set
a langchaingo PDF loader uses (NewReader, NewReaderEncrypted, NumPage,
Page, Page.Fonts, Page.Font, Page.GetPlainText) is signature-identical and
frozen in API-STABILITY.md, so adopting GoPDF from the older
readers is an import-path swap. Meanwhile the lineage packages have gone quiet and
GoPDF has closed extraction gaps still open upstream:
| GoPDF | ledongthuc/pdf | dslipak/pdf | |
|---|---|---|---|
| Maintenance | active | ~2–3 merges/yr | stalled since Jan 2024 |
| Form XObject text | extracted | open issue #67 | — |
CJK predefined CMaps (UniGB-UCS2-H …) |
decoded | open issue #55 | — |
— = not evaluated.
- Plain text extraction with context/cancellation support.
- Styled text extraction with font name, size, and position.
- Text grouped by row, plus word-level extraction with bounding boxes via
Page.Words(). - Cross-page diagonal watermarks (the same skew-rotated stamp printed on ~every page) are detected and dropped from the reading-order surfaces (
Words/Lines/Blocks/Tables) so a watermark glyph cannot fuse into a value (e.g.11144read as111Ê44); page-specific rotated content such as chart-axis labels is preserved, andPage.Content()/Page.Texts()keep every glyph unfiltered. Page.Blocks()(experimental) groups lines into column-major visual blocks — read down each detected column in full — as the chunking unit for RAG pipelines. See EXAMPLES.md.Page.Content()exposes the page's vector geometry — drawn rectangles (Rect) and stroked ruling lines / cell borders (Stroke, experimental) — in display space, the raw signal for table-grid detection without rendering. See EXAMPLES.md.Page.Tables()(experimental) reconstructs ruled tables — those drawn with visible cell borders — into a grid of cell strings (Table.Cells[row][col]), reading the ruling lines fromContent.Strokeand thinContent.Rect. Two table classes are locked by corpus regression gates: fully-ruled lattices and rect-bordered tables (interior cells closed by rules within an outer frame, ~98% cross-publisher); plus structurally-recovered half-open edge columns. Per-cell-grid tables (stroke-free, common in non-US/multilingual statistical PDFs) now reconstruct with column- and row-recovery, but this class is still stabilizing — best-effort, not yet a locked contract. Proportional Type0 / Identity-H text (Latin/Cyrillic wrapped in a CJK-subset font) now extracts with correct per-CID widths, fixing label↔value character-interleaving. Borderless and partially-ruled/banded tables remain out of documented scope — best-effort, not a contract: they may yield no table or an incomplete grid. Each detected table also carries a detection-relativeConfidenceand per-tableWarnings(for example a phantom-table flag, or alegacy_font_textflag when a table's labels were rendered through a legacy non-Unicode Indic font — canonical simple Kruti Dev 010 and subsetted Walkman-Chanakya 905 are now transcoded to real, searchable Unicode (Walkman-Chanakya 905 carries a known precomposed-ligature limitation — ~0.1% of recovered words may mis-decode; the orthographically-invalid subset is flagged vialegacy_font_textatConfidence Lowwithmisdecoded_clusters=Nin the detail), composite (Type0) Kruti Dev 010 on both a CFF- and a TrueType-descendant fixture is also now transcoded via a/ToUnicodeCID→keystroke bridge, while still-undecoded variants (other composite Kruti families, DevLys) keep the flag and stay gibberish rather than being silently returned as if trustworthy), andPage.TableRegions()returns page-space bounding boxes 1:1 withTables()— honest per-table quality signals and locations a RAG consumer can use to flag or fall back on a low-confidence grid. See EXAMPLES.md.- Nested Form XObject text is included and reported in page-space coordinates.
- TJ kerning arrays are interpreted as word gaps when spacing indicates a word boundary.
- Broad script coverage: Latin, Cyrillic, and CJK predefined CMaps.
- Legacy non-Unicode Indic fonts — government statistical PDFs that render Devanagari through a legacy Latin-mapped font (Kruti Dev / DevLys / Walkman-Chanakya) — are detected, and several families/variants are now transcoded to real, searchable Unicode Devanagari on every reading surface (
Words/Lines/Blocks/Tables/GetPlainText): canonical-coded simple Kruti Dev 010 (fully recovered), subsetted Walkman-Chanakya 905 (mostly recovered — ~99.9% of recovered words correct on the one tested statistical-appendix fixture; known limitation: precomposed-ligature glyphs ~0.1% of words may mis-decode, with the orthographically-invalid subset flagged vialegacy_font_textatConfidence Low), and composite (Type0/CID-keyed) Kruti Dev 010 (recovered on both a CFF- and a TrueType-descendant fixture via a/ToUnicodeCID→keystroke bridge — the same recovery mechanism, reused unchanged, just sourced from/ToUnicodeinstead of/Encoding /Differences). Still-undecoded variants (other composite legacy Indic fonts, DevLys) are flagged rather than silently returned. See EXAMPLES.md.
Page.ExtractionSignal()andReader.DocumentSummary()emit deterministic per-page and per-document routing signals for LLM/RAG pipelines: index text-bearing pages as-is, route image-only pages to OCR, and flag empty or degraded pages for review.DocumentSummaryalso carriesDecodeRatios— per-page and document decode-quality ratios (missing/ToUnicode, charset fallback, unmapped glyphs) to re-route text that is present but unreliable.Page.ExtractionSummary()reports page-level text/image readiness:HasText,WordCount,ImageCount,ImageCoverage(image bbox area / page area — distinguishes a full-bleed scan from a thumbnail), and page-scoped warnings includingsparse_text(a page whose only text is page furniture, e.g. a page number, so it still routes to OCR).Reader.Warnings()returns deterministic diagnostics for silently degraded extraction, including missing or broken/ToUnicode, fallback CJK encodings, unknown encodings, unmappable glyphs, legacy non-Unicode Indic fonts (Kruti Dev / DevLys / Walkman-Chanakya — canonical simple Kruti Dev 010, subsetted Walkman-Chanakya 905, and composite (Type0) Kruti Dev 010 are transcoded to real Unicode Devanagari; the warning is recovery-aware and stays silent for cleanly-recovered fonts; Walkman-Chanakya 905 has a known precomposed-ligature limitation (~0.1% of recovered words) detected at the Tables() level — see EXAMPLES.md; other composite Kruti families and DevLys still decode to Latin gibberish), and unsupported stream filters.Page.Images()reports image draw metadata — page-space bounds, declared dimensions, and declared filters — without decoding image content.Page.DebugJSON()andReader.DebugJSON()(experimental) emit a structured JSON snapshot of the extracted text geometry, shaped like PyMuPDF'sget_text("dict")(page → block → lines → word-spans with bounding boxes and a per-pagecoord_origin), for bbox-aware RAG chunking and citation. It is a thin projection over the stable primitives — only fields GoPDF actually computes — and carries the same in-band diagnostics (image_only_page,non_finite_geometry). See EXAMPLES.md.
Reader.Info()andReader.XMP()expose classic document metadata and raw XMP packets.Reader.Fonts()lists distinct document fonts, embedded-program presence, and pages where each font appears.Page.Annotations()extracts link and text annotations;Reader.Dest()resolves named destinations;Reader.Links()aggregates document links intoLinkRefentries.Reader.Fields()extracts AcroForm field values (text, checkbox, radio, choice) with page and bounding-box locations.Reader.Attachments()lists document-level embedded files (name, MIME type, declared size, decoded data).- Outlines expose resolved page numbers.
Reader.PageLabels()returns each page's printed label (roman-numeral front matter, an offset like "32", letter ranges) from the/PageLabelstree, so a citation can reference "page iv" rather than the 1-based position.Page.MediaBox()andPage.CropBox()resolve inherited page dimensions.
- Transparent Standard-security-handler decryption: RC4, AES-128, and AES-256, including owner-password unlocks, per-class crypt filters, cleartext metadata, and SASLprep-normalized passwords.
- Broad parser compatibility: PDF 2.0 headers, hybrid-reference files, object streams, and common stream filters including Flate, LZW, ASCII85, ASCIIHex, and RunLength.
- Resilient malformed-PDF behavior: bounded recursion/allocation, content-stream panic recovery, and deterministic best-effort extraction.
- Safe for concurrent use after open; repeated dereferencing is served from a bounded internal cache.
Pages() iter.Seq2[int, Page],Texts() iter.Seq[Text], andOpenBytes([]byte)support streaming and in-memory workflows.
- Japanese Shift-JIS (
90ms-RKSJ-H/V,90pv-RKSJ-H) - CJK UCS-2 BE (
UniGB-UCS2-H/V,UniCNS-UCS2-H/V,UniJIS-UCS2-H/V,UniKS-UCS2-H/V) - Simplified Chinese GBK / GB-EUC / GBKp-EUC (
GBK-EUC-H/V,GB-EUC-H/V,GBKp-EUC-H/V) - Traditional Chinese Big5-ETen / ETenms (
ETen-B5-H/V,ETenms-B5-H/V) - Korean UHC / KSC-EUC / UHC-HW (
KSCms-UHC-H/V,KSC-EUC-H/V,KSCms-UHC-HW-H/V)
go get github.com/Detective-XH/gopdfSee examples/ for runnable programs and EXAMPLES.md for API
cookbook snippets covering words, blocks, image metadata, extraction summaries,
diagnostics, XMP, fonts, annotations, named destinations, link aggregation,
form fields, attachments, encrypted PDFs, and a langchaingo-style RAG loader.
Runnable examples:
go run ./examples/read_plain_text
go run ./examples/read_text_with_styles
go run ./examples/langchaingo_loaderThe Quick start above is the smallest complete program; see EXAMPLES.md for the full API cookbook.
Exported APIs are tiered contracts — see API-STABILITY.md for what is frozen today, what will only grow additively, and the v1.0 freeze milestone. The geometry convention (PDF-native bottom-left, points) and the screen-space conversion recipe are documented there too.
- Extraction only — no PDF creation, modification, or rendering.
- Image content is not decoded;
Page.Images()reports draw metadata only. - AcroForms extraction is read-only field values — no form filling or appearance rendering.
Reader.Attachments()walks the document-level name tree only; page-level/FileAttachmentannotations are not scanned.- Table reconstruction (
Page.Tables(), experimental): two table classes are locked by corpus regression gates — fully-ruled lattices and rect-bordered tables (interior cells closed by rules within an outer frame, ~98% cross-publisher) — plus structurally-recovered half-open edge columns. Per-cell-grid tables (stroke-free, common in non-US/multilingual statistical PDFs) now reconstruct with column- and row-recovery, but remain best-effort, not yet a locked contract. Proportional Type0 / Identity-H text (Latin/Cyrillic wrapped in a CJK-subset font) now extracts with correct per-CID widths, fixing label↔value character-interleaving. Borderless and partially-ruled/banded tables remain out of documented scope (best-effort, not a contract — they may yield no table or an incomplete/merged grid). - Layout/word-position extraction (
Page.Words,Page.Lines,GetStyledTexts) is available and fast, but its grouping quality is a work in progress: it is tuned for speed and determinism and has not been benchmarked for layout fidelity against dedicated layout tools (on CJK it currently segments more aggressively). Validate it on your own documents.
Extraction quality is validated against a curated corpus of real, public-domain PDFs
spanning CJK scripts (Simplified Chinese, Traditional Chinese, Japanese, Korean),
Cyrillic, multi-column layouts, and numeric tables — each with a recorded provenance
and a verified golden output locked by the test suite. Negative fixtures (hard/)
document current extraction gaps honestly: documents that defeat decoding today are
committed without goldens so any future improvement is caught as a regression gate,
not a silent surprise. See testdata/corpus/README.md
for the full provenance table and fixture inventory.
Versions are published as signed git tags (mirrored as GitHub Releases — Go module resolution only needs the tag). Tags are signed with one of the maintainer's hardware-backed SSH keys; v0.8.7 was signed with:
256 SHA256:duCP4h22hb2oNAZMaFhUlpq0j8+qBbZuaXnS99yUhkY (ED25519-SK)
Verify a release tag — all of the maintainer's trusted keys are published on the GitHub account, so the recipe below works for any release regardless of which key signed it, and nothing needs to be copied from this README:
curl -s https://api.github.com/users/Detective-XH/ssh_signing_keys \
| python3 -c "import json,sys; [print('*', k['key']) for k in json.load(sys.stdin)]" > allowed_signers
git -c gpg.ssh.allowedSignersFile=./allowed_signers tag -v v0.8.7
# expect: Good "git" signature for * with ED25519-SK key SHA256:duCP4h2...Module integrity is independently guaranteed by the Go checksum database:
go mod download github.com/Detective-XH/gopdf@latest && go mod verifySee CHANGELOG.md for the full history of fixes and additions.