gopdf

module
v0.11.1 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 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"

Try it

go install github.com/razvandimescu/gopdf/cmd/gopdf@latest
gopdf tables statement.pdf -format csv
Date,Description,Debit,Credit
01-04-2025,Card payment,"94,19","0,00"
02-04-2025,Transfer received,"0,00","1.000,00"

One static binary, no toolchain, nothing to install alongside it. The same command handles multi-line cells and tables that continue across pages. See the CLI section for gopdf merge, gopdf pages and gopdf watermark.

Why gopdf?

go.mod has no require block — static binaries, FROM scratch containers, and cross-compilation without a C toolchain. Of the libraries below it is the only one that pairs that with table extraction, 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 10 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/GIF, rotation, opacity, transparent SMask)
  • PDF creation with text, rectangles, lines, images, and multiple fonts
  • Images as pages: PDFs and PNG/JPEG/GIF mixed into one document, detected by content, with baseline JPEGs embedded unre-encoded and EXIF orientation honoured
  • One CLI — gopdf tables, gopdf merge, gopdf pages, gopdf watermark
  • Pure Go — no CGo, no system dependencies

Installation

The CLI, as a single binary:

go install github.com/razvandimescu/gopdf/cmd/gopdf@latest

The library:

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
Text drawn as outlines

Some producers draw every glyph as a filled path, and such a page extracts as empty. Page.OutlineHint says when a page's fills repeat like glyphs, so an empty result comes with an explanation:

hint, _ := doc.Page(0).OutlineHint()
if hint.Possible() {
    // this page's text is drawn as outlines, not written with text operators
}

It is a hint, not a verdict: a page of varied repeated symbols satisfies it too.

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)
Images as pages

DrawImage places a decoded PNG/JPEG by its lower-left corner, scaled to the width and height you give it. FitRotated returns the largest such pair that keeps the image's aspect ratio inside a page:

img, err := pdf.LoadImage("scan.png")

c := pdf.NewCreator()
pageW, pageH := 595.0, 842.0 // A4
w, h := img.FitRotated(pageW-36, pageH-36, 0, 1) // fit inside an 18pt margin

page := c.NewPage(pageW, pageH)
page.DrawImage(img, (pageW-w)/2, (pageH-h)/2, w, h)

data, err := c.Build()

An image drawn on several pages is written to the file once. Transparency travels with it as a soft mask.

A JPEG's EXIF orientation is folded into the placement matrix, so a photo taken sideways lands upright without the pixels being touched, and DisplaySize reports the dimensions it will occupy. Image.DPI carries the resolution the file declares — EXIF or JFIF for JPEG, pHYs for PNG — and is 0 when it declares none.

A baseline JPEG is embedded in its original encoding, behind DCTDecode; re-encoding a photograph would multiply its size several-fold. Progressive, 12-bit, and CMYK JPEGs take the decode path, where viewer support behind DCTDecode is uneven.

Command line

One binary, gopdf, with a subcommand per capability. Installed with go install github.com/razvandimescu/gopdf/cmd/gopdf@latest:

gopdf tables invoice.pdf -format csv        # extract a table
gopdf merge report.pdf scan.png -o out.pdf  # combine PDFs and images
gopdf pages 1-2 report.pdf -o first-two.pdf # keep only some pages
gopdf watermark -img logo.png in.pdf -o out.pdf
gopdf tables

Detects a table and prints its rows as aligned text or CSV. Columns come from their headers when -headers is given, and from column geometry otherwise. When no table is found it says so on stderr and exits non-zero, rather than printing whatever the geometry happened to line up.

gopdf tables statement.pdf -format csv -o rows.csv
gopdf tables invoice.pdf -headers "Quantity,Product Code,Description"
gopdf tables statement.pdf -anchor Date -filter "Page ,Continued"
gopdf tables statement.pdf -format csv -delimiter ';'   # comma-decimal locales
Flag Default Meaning
-format text text or csv
-delimiter , field delimiter for -format csv; \t gives TSV
-o stdout output path
-headers — comma-separated header anchors
-anchor — column that signals a new row; rows where it is empty merge upwards
-filter — substrings whose rows are dropped
-require — columns of which at least one must be filled
-merge-gap, -max-row-gap auto row grouping distances; auto-tuned when unset
-col-width 30 maximum column width, for -format text
-password — password for an encrypted PDF

Where the decimal separator is a comma, -delimiter ';' is the difference between a file a spreadsheet opens correctly and one it does not: every 1.000,00 in the default output has to be quoted to survive the comma.

gopdf merge

Merges PDFs and images into one PDF. Each input is classified by its bytes, not its extension: a PDF header (tolerating prepended transport bytes, as the reader does) means PDF, anything else is decoded as an image. Images become one page each, in the order given.

gopdf merge -o out.pdf scan.png photo.jpg     # images only
gopdf merge -o out.pdf report.pdf scan.png    # append a scan
gopdf merge -page image -o out.pdf scan.png   # page follows the image
Flag Default Meaning
-o stdout output PDF path
-page a4 page size for image inputs: a4, letter, or image
-dpi 0 image resolution; 0 reads it from the file, falling back to 72
-margin 0 whitespace around an image, in points

Image pages default to paper, not to the image, unlike a converter: merge promises one document, and a phone photo taken at its declared 72 dpi would bind a 42×56 inch page next to an A4 one. Use -page image where the declared resolution means something, as it does for a 300 dpi scan. Fitting already letterboxes on one axis, so -margin defaults to 0.

PNG, JPEG and GIF are the formats Go's standard library decodes, so they are the formats gopdf reads. Hand it a HEIC, AVIF, WebP or TIFF and the error names the format rather than reporting an unknown one:

$ gopdf merge IMG_6407.HEIC -o out.pdf
gopdf merge: IMG_6407.HEIC: not a PDF, and HEIC is not supported (PNG, JPEG and GIF only)
gopdf pages

Keeps only the pages named by the range and drops the rest. Pages count from 1, and the output follows the order written, so a range can also reorder.

gopdf pages 1-2 report.pdf -o first-two.pdf  # split off the first two pages
gopdf pages 5- report.pdf > tail.pdf         # page 5 to the end
gopdf pages 1,3,5-7 report.pdf -o some.pdf   # a list of pages and ranges
Flag Default Meaning
-o stdout output PDF path

A range is a comma-separated list of N, A-B, or A- (to the end). Anything else is rejected before a byte is written, naming what went wrong:

$ gopdf pages 15 report.pdf
gopdf pages: page 15 is out of range; the document has 14 pages

The output is a rebuilt document: shared resources come across, but anything attached to the file as a whole — a digital signature above all — does not.

gopdf watermark

Stamps an image diagonally across every page, centred and scaled to the page it lands on — including pages rotated 90° or 270°, which are measured as the reader sees them rather than as they are stored.

gopdf watermark -img logo.png in.pdf -o out.pdf
gopdf watermark -img draft.png in.pdf -o out.pdf -angle 30 -opacity 0.12 -skip-first
Flag Default Meaning
-img — watermark image; PNG, JPEG or GIF (required)
-o stdout output PDF path
-angle 45 rotation in degrees, counter-clockwise
-opacity 0.15 opacity, in [0, 1]
-scale 0.85 size as a fraction of the page
-skip-first off leave the first page un-watermarked
-skip-last off leave the last page un-watermarked
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
Replacement text (ActualText, Alt, E) of marked content whose glyphs are removed Yes; a named property list keeps its entry in the resources, which the section stops naming
Replacement text in the structure tree of a tagged PDF No
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. gopdf watermark wraps this for one-shot usage.

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 // em height as drawn on the page, not the Tf operand
    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, Standard, 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
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.
  • Auto-detection judges a table by its column names. Running text is sliced into "columns" wherever word gaps line up, so auto-detection rejects candidates whose headings read as fragments rather than words. A real table whose columns are named in one or two characters therefore needs -headers (or TableOpts.Headers) to be found.
  • Text drawn as filled outlines is not extracted. Some producers (virtual printers re-printing a PDF, "convert text to outlines") draw every glyph as a path, and such a page extracts as empty. Page.OutlineHint reports when a page's fills repeat like glyphs, so an empty result can be told apart from an empty page. It is a hint: repeated icons can trigger it too (see Text drawn as outlines).
  • No image extraction
  • Images read as PNG, JPEG and GIF only — what the standard library decodes. HEIC and AVIF need an HEVC or AV1 decoder, available only through CGo or copyleft code; WebP and TIFF would need golang.org/x/image. Neither fits a CGo-free MIT library with no dependencies. Unsupported formats are named in the error.
  • 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
  outline.go    Filled-path capture and clustering; OutlineHint
  image.go      Image decoding (PNG/JPEG/GIF) → RGB + grayscale SMask streams
  creator.go    PDF creation from scratch (text, shapes, images, 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

cmd/
  gopdf         CLI: tables, merge, pages, watermark
  sample        generates the README's sample PDFs
  genglyphlist  regenerates glyphlist.go from the Adobe Glyph List

License

MIT — see LICENSE.

Directories

Path Synopsis
cmd
gopdf command
gopdf is the command-line interface to the gopdf library.
gopdf is the command-line interface to the gopdf library.
sample command

Jump to

Keyboard shortcuts

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