gopdf

module
v0.10.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: MIT

README

gopdf

CI Go Reference Go Report Card codecov

Pure Go table extraction from PDFs — plus positioned text, search, creation, merge, and editing. No CGo, no AGPL, no dependencies outside the standard library.

Sample Invoice Sample Report

Generated entirely from Go code — go run ./cmd/sample/

Getting structured rows and columns out of an invoice, bank statement, or report normally means shelling out to pdftotext -layout and regexing the columns back together, or leaving Go for Python's camelot/tabula. gopdf does it natively: tables are located by header anchors or auto-detected from column gaps, with multi-line cell merging and multi-page continuation.

doc, _ := pdf.OpenFile("statement.pdf")
tables, _ := doc.Page(0).Tables()
fmt.Println(tables[0].CellByName(0, "Amount")) // "1,204.50"

Why gopdf?

Table extraction in pure Go, under a permissive licence. unipdf extracts tables too, but is AGPL or commercial. ledongthuc/pdf gives you positioned text and groups it into rows and columns, but has no header anchoring, multi-line cell merging, or multi-page continuation.

gopdf unipdf pdfcpu ledongthuc/pdf MuPDF bindings
License MIT AGPL / Commercial Apache-2.0 BSD-3 AGPL
CGo required No No No No Yes
Text extraction Positioned (X/Y) Positioned Raw streams Positioned (X/Y) Positioned
Table detection Yes Yes No Row/column grouping No
Text search With rects Yes No No Yes
PDF merge Yes (size-constrained) Yes Yes No No
Text overlay Yes Yes Watermark No No
Visual redaction Yes Yes No No No
Text removal (true redaction) Yes Yes No No No
PDF creation Yes Yes Yes No Yes
Reads encrypted PDFs Yes Yes Yes No Yes
Dependencies 0 Many 0 0 System lib
When to use something else
  • pdfcpu — for document operations as your primary need: encryption, form filling, optimization, page manipulation, or a batch CLI. It is more mature and more broadly tested than gopdf on all of them.
  • unipdf — for breadth and commercial support, if AGPL or a paid licence works for you.
  • go-pdf/fpdf — for generating documents with embedded fonts and rich layout. gopdf's creator covers the standard 14 fonts only.
  • MuPDF bindings — for rendering fidelity, if CGo and AGPL are acceptable.

Features

  • Table detection with column/row extraction (explicit headers or auto-detection)
  • Multi-line cell merging for tables with wrapped content (bank statements, invoices)
  • Multi-page table support with automatic header re-detection
  • Text extraction with X/Y coordinates, font name, and font size
  • Line reconstruction with gap-derived word spacing
  • Text search returning bounding rectangles
  • PDF merge with page selection and size constraints (fail/truncate/shrink with JPEG recompression)
  • Text overlay (Helvetica, configurable size and color)
  • Visual redaction (filled rectangles with configurable color)
  • Text removal: glyphs deleted from the content stream, not covered over
  • Reads encrypted PDFs (RC4 40/128-bit, AES-128, AES-256; user or owner password)
  • Image overlay / watermark (PNG/JPEG, rotation, opacity, transparent SMask)
  • PDF creation with text, rectangles, lines, and multiple fonts
  • Pure Go — no CGo, no system dependencies

Installation

go get github.com/razvandimescu/gopdf@latest

Quick Start

package main

import (
    "fmt"
    "log"

    "github.com/razvandimescu/gopdf/pdf"
)

func main() {
    doc, err := pdf.OpenFile("document.pdf")
    if err != nil {
        log.Fatal(err)
    }
    text, err := doc.Text()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(text)
}

Examples

Positioned text lines
doc, err := pdf.OpenFile("document.pdf")
if err != nil {
    log.Fatal(err)
}
for i := 0; i < doc.NumPages(); i++ {
    lines, _ := doc.Page(i).TextLines()
    for _, line := range lines {
        fmt.Printf("Y=%.0f: %s\n", line.Y, line.Text)
    }
}
// Output:
// Y=756: Quotation Ref: QT10001
// Y=732: Quote Name: Northgate Academy
// Y=710: Company: Nova Facilities
// ...
Table extraction
// Auto-detect tables (no header hints needed)
tables, _ := doc.Page(0).Tables()
for _, tbl := range tables {
    for _, col := range tbl.Columns {
        fmt.Printf("%-20s", col.Name)
    }
    fmt.Println()
    for _, row := range tbl.Rows {
        for _, cell := range row.Cells {
            fmt.Printf("%-20s", cell.Text)
        }
        fmt.Println()
    }
}

With explicit header anchors (more precise):

spans, _ := doc.Page(0).TextSpans()
tbl := pdf.FindTable(spans, &pdf.TableOpts{
    Headers: []string{"Quantity", "Description"},
})
fmt.Println(tbl.CellByName(0, "Quantity"))    // "3"
fmt.Println(tbl.CellByName(0, "Description")) // "Widget Assembly"

Tables with multi-line cells (e.g., bank statements):

tbl := pdf.FindTable(spans, &pdf.TableOpts{
    Headers:   []string{"Date", "Description", "Debit", "Credit"},
    MergeGap:  16,  // merge rows within 16pt into one logical row
    MaxRowGap: 30,  // stop table when gap exceeds 30pt (footer boundary)
})

Multi-page tables:

var pages [][]pdf.TextSpan
for i := 0; i < doc.NumPages(); i++ {
    spans, _ := doc.Page(i).TextSpans()
    pages = append(pages, spans)
}
tbl := pdf.FindTableAcrossPages(pages, &pdf.TableOpts{
    Headers: []string{"Date", "Amount"},
})
// All rows across all pages in a single table
fmt.Printf("%d rows\n", len(tbl.Rows))
Search for text
results := doc.Search("Invoice Total")
for _, r := range results {
    fmt.Printf("Page %d at (%.0f, %.0f) size %.0fx%.0f\n",
        r.Page, r.Rect.X, r.Rect.Y, r.Rect.Width, r.Rect.Height)
}
// Output:
// Page 0 at (206, 691) size 70x12
Encrypted PDFs

Files encrypted with an empty user password — the common case for emailed statements — open through the ordinary entry points with no extra work:

doc, err := pdf.OpenFile("statement.pdf")

When a password is genuinely required, OpenFile reports pdf.ErrEncrypted so you can prompt for one rather than guessing at a parse failure:

doc, err := pdf.OpenFile("statement.pdf")
if errors.Is(err, pdf.ErrEncrypted) {
    doc, err = pdf.OpenFile("statement.pdf", pdf.WithPassword(os.Getenv("PDF_PASSWORD")))
}
if errors.Is(err, pdf.ErrWrongPassword) {
    log.Fatal("wrong password")
}

Either the user or the owner password is accepted. Everything reached through the opened document — text extraction, tables, search — behaves exactly as it does for an unencrypted file.

Merger and Editor take their own entry points and do not yet accept a password, so they handle encrypted input only when its user password is empty.

Merge PDFs
combined, err := pdf.MergeFiles("a.pdf", "b.pdf", "c.pdf")
if err != nil {
    log.Fatal(err)
}
os.WriteFile("merged.pdf", combined, 0644)

With page selection:

m := pdf.NewMerger()
m.AddFile("big.pdf", 0, 2, 5) // pages 0, 2, 5 only
m.Add(otherPDFBytes)           // all pages
result, err := m.Merge()
Size-constrained merge

Control output size with MergeWithOptions. Three oversize behaviors are available:

m := pdf.NewMerger()
m.AddFile("a.pdf")
m.AddFile("b.pdf")
m.AddFile("c.pdf")

res, err := m.MergeWithOptions(pdf.MergeOptions{
    MaxSize:          20 * 1024 * 1024, // 20 MB
    OversizeBehavior: pdf.OversizeShrink,
})
if err != nil {
    var ose *pdf.OversizeError
    if errors.As(err, &ose) {
        log.Fatalf("can't fit: %d bytes after optimization (limit %d)", ose.Size, ose.MaxSize)
    }
    log.Fatal(err)
}
os.WriteFile("merged.pdf", res.Data, 0644)
fmt.Printf("%d/%d pages, %d bytes\n", res.IncludedPages, res.TotalPages, len(res.Data))
Behavior What it does
OversizeFail Merges normally. Returns *OversizeError if output exceeds limit.
OversizeTruncate Dedup + metadata strip + JPEG recompress. Includes as many pages as fit.
OversizeShrink Two-pass: lossless optimization first, then JPEG recompression at ratio-derived quality to hit the target. Returns *OversizeError if still over.
Create a PDF from scratch
c := pdf.NewCreator()
page := c.NewPage(595, 842) // A4

page.SetFont("Helvetica-Bold", 24)
page.DrawText(72, 750, "Invoice #12345")

page.SetFont("Helvetica", 12)
page.DrawText(72, 720, "Date: 2026-03-26")
page.DrawText(72, 704, "Total: $500.00")

page.FillRect(72, 690, 200, 1, 0, 0, 0) // separator line

data, err := c.Build()
os.WriteFile("invoice.pdf", data, 0644)
Text overlay
ed := pdf.NewEditor(data)
ed.AddText(pdf.TextOverlay{
    Page: 0, X: 100, Y: 50,
    Text: "APPROVED", FontSize: 24,
    R: 0, G: 0.5, B: 0, // green
})
result, err := ed.Apply()
Redaction

Two separate operations, because they answer different needs. RedactText draws a box over the text and leaves it in the file. RemoveText deletes the glyphs from the content stream, so extraction, search and copy/paste have nothing left to find:

ed := pdf.NewEditor(data)
ed.RemoveText("Confidential")          // the glyphs are gone from the output
ed.RedactText("Confidential", 0, 0, 0) // and a black box marks where they were
result, err := ed.Apply()

Surviving text does not reflow: each removed run leaves behind a kerning number worth exactly the advance it had. RemoveRegion(page, rect) does the same for an area rather than a string.

What removal reaches, and what it leaves alone:

Location Removed
Page content stream glyphs Yes
Text inside Form XObjects Yes
Image XObjects No
Annotation text and appearance streams No
AcroForm / XFA field values No
Document information dictionary No
XMP metadata No
Embedded files No

Text is deleted from the page content stream. That is a narrower claim than "secure redaction", and deliberately so: a document can carry the same string in half a dozen other places, and which of them matter is yours to judge.

The claim is checked against real documents, not only ones this library wrote: the test suite redacts a corpus of PDFs from assorted producers and reads the output back with MuPDF and Poppler, which share no code with gopdf or with each other. Reading it back with gopdf alone would only show that its writer and its reader agree.

Combine redaction and overlay to replace text:

ed := pdf.NewEditor(data)
ed.RedactText("OLD-REF", 1, 1, 1)       // white box over old text
ed.AddText(pdf.TextOverlay{              // write new text
    Page: 0, X: 100, Y: 750,
    Text: "NEW-REF", FontSize: 12,
})
result, err := ed.Apply()
Image watermark
logo, _ := pdf.LoadImage("logo.png")     // PNG or JPEG; alpha becomes an SMask

ed := pdf.NewEditor(data)
for i := 0; i < doc.NumPages(); i++ {
    mb := doc.Page(i).MediaBox()
    pageW, pageH := mb[2]-mb[0], mb[3]-mb[1]
    // Size so the rotated watermark fills 85% of the page at any angle.
    w, h := logo.FitRotated(pageW, pageH, 45, 0.85)
    ed.AddImage(pdf.ImageOverlay{
        Page:     i,
        Image:    logo,
        CX:       mb[0] + pageW/2,        // page center
        CY:       mb[1] + pageH/2,
        Width:    w, Height: h,
        Rotation: 45,                     // diagonal
        Opacity:  0.15,
    })
}
result, err := ed.Apply()

The same image is written once and shared by every page that references it. The bundled cmd/watermark wraps this for one-shot usage:

go run ./cmd/watermark -i in.pdf -img logo.png -o out.pdf \
    -angle 45 -opacity 0.15 -scale 0.85

API Reference

Full reference on pkg.go.dev — every exported symbol is documented there.

Types
type TextSpan struct {
    X, Y     float64 // position on page
    EndX     float64 // X position after this span
    FontSize float64
    Font     string
    Text     string
}

type TextLine struct {
    Y     float64
    Spans []TextSpan
    Text  string // reconstructed line text with spacing
}

type Table struct {
    Columns []Column
    Rows    []Row
}

type Column struct {
    Name string
    X    float64
}

type Row struct {
    Y     float64
    Cells []Cell
}

type Cell struct {
    Column int // index into Table.Columns
    Text   string
    Spans  []TextSpan
}

type SearchResult struct {
    Page     int
    Text     string
    Rect     Rect
    FontSize float64
}

type Rect struct {
    X, Y, Width, Height float64
}

Supported PDF Features

Category Details
PDF versions 1.0–1.7, including xref streams (1.5+) and compressed object streams
Text encodings ToUnicode CMaps (bfchar + bfrange), WinAnsi, MacRoman, encoding differences, Adobe Glyph List (4200 names)
Font types Type1, TrueType, CIDFont/Type0 composite fonts, standard 14 fonts with built-in width tables
Compression FlateDecode, LZWDecode, ASCII85Decode, ASCIIHexDecode, PNG predictors, filter chains
Page features Resource inheritance from page tree, rotation (0/90/180/270), MediaBox/CropBox
Content streams All text operators (BT/ET/Tf/Tm/Td/TD/T*/TJ/Tj/'/"), graphics state stack (q/Q), CTM (cm)
XObjects Recursive text extraction from Form XObjects via Do operator
Marked content ActualText extraction (BMC/BDC/EMC) with UTF-16BE support
Encryption Standard security handler: RC4 40/128-bit (R2/R3), AES-128 (R4), AES-256 (R5/R6), crypt filters, /EncryptMetadata
Structure Linearized PDFs, incremental updates, indirect Length references

Limitations

  • Redaction covers two operations with different guarantees. RemoveText / RemoveRegion delete the glyphs from the page content streams and the Form XObjects those pages draw — and nothing else in the file (see the table above). RedactText / Redact only draw a rectangle: the text stays in the content stream and remains recoverable by copy/paste or any PDF parser.
  • Reading encrypted PDFs is supported; writing them is not. Output from merge, rewrite, and creation is always unencrypted, so an encrypted input is effectively decrypted by any operation that writes it back out. Public-key (certificate) security handlers are not supported.
  • No image extraction
  • PDF creation supports standard 14 fonts only (no font embedding)
  • Merge drops interactive features (forms, bookmarks, JS)
  • Text overlay uses Helvetica only

Architecture

pdf/
  document.go   Public API (Document, Page)
  reader.go     PDF structure: xref, streams, object resolution, fonts, CMap
  text.go       Content stream -> positioned text spans -> line reconstruction
  table.go      Table detection: header anchors, gap-based auto-detect, multi-page
  writer.go     PDF object serializer, xref generation, FlateDecode compression
  merge.go      PDF merge: size constraints (fail/truncate/shrink), stream dedup, JPEG recompression
  edit.go       Text search, text overlay, image overlay, visual redaction
  redact.go     Text removal: glyph-level content stream rewriting
  image.go      Image decoding (PNG/JPEG) → RGB + grayscale SMask streams
  creator.go    PDF creation from scratch (text, shapes, fonts)
  lexer.go      PDF byte stream tokenizer
  parser.go     Token -> object parser (dicts, arrays, refs)
  objects.go    Types: Dict, Array, Name, Ref, Stream; matrix math helpers
  glyphlist.go  Adobe Glyph List (generated, 4200 entries)
  stdfonts.go   Standard 14 font width tables

License

MIT — see LICENSE.

Directories

Path Synopsis
cmd
extract_tables command
merge command
sample command
watermark command
watermark applies an image as a diagonal watermark on every page of a PDF.
watermark applies an image as a diagonal watermark on every page of a PDF.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL