experimental

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 3, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

README

gomupdf/experimental

A question-first layer over gomupdf.

The core library hands you a bag of positioned fragments — spans, words, quads, blocks, bounding boxes — and leaves the geometry to you. That translation (question → hand-rolled geometry → primitives → y-flip → tolerance fudge → answer) is paid on every extraction. This package lets you ask the page questions instead.

Status: experimental. Heuristics are best-effort and tuned for structured, layout-rich documents. Signatures may change.

Two models, one coordinate system

Everything uses one Rect (origin top-left, y grows down, units = PDF points). Conversions from the core library's two rect shapes (gomupdf.Rect{X,Y,W,H} and geometry.Rect{X0,Y0,X1,Y1}) live inside this package so you never juggle both.

1. The row/word model — "rows of words"

The substrate most document code actually thinks in. Cluster words into visual lines, sort by x, filter by x-band, cluster x-positions into columns.

doc, _ := experimental.Open("document.pdf")
defer doc.Close()
page, _ := doc.Page(0)

words, _ := page.Words()                 // []Word with unified Rect + Top/Bottom/Height
rows, _  := page.Rows()                  // visual lines, y-clustered, x-sorted
for _, r := range rows {
    fmt.Println(r.Text())                // words joined left-to-right
}

col   := rows[3].Band(120, 180)          // x-band filter (one column)
lanes := experimental.ClusterFloats(words.Lefts(), 8) // detect column lanes
clean := words.DropOutliers(2.2)         // strip oversized outliers (e.g. watermarks)
2. The spatial-query model — "ask the page"

Key/value, region cropping, located regex — built on the row/word substrate.

order, ok, _ := page.ValueRightOf("Order No")        // value beside a label
addr,  ok, _ := page.ValueBelow("Address")           // value under a heading
header       := page.TextIn(experimental.R(0, 0, 600, 120)) // crop a region
hits, _      := page.Find(`\d{4}-\d{2}-\d{2}`)       // regex WITH locations
for _, h := range hits {
    fmt.Println(h.Text, h.Rect, h.Context()) // text, box, and the line it sat on
}

CollectBlock captures the gnarliest reused geometry — walking rows downward inside an x-band until a stop keyword or a gap (multi-line postal addresses, label-anchored blocks):

lines := experimental.CollectBlock(rows, labelRow+1, experimental.BlockOptions{
    Lo: 200, Hi: 410, Stop: stopKeywords, GuardLeft: true,
})

Use cases, ranked

# Question you ask API
1 value for this label? page.ValueRightOf / ValueBelow
2 text inside this region? page.TextIn(rect) / WordsIn(rect)
3 where does this pattern appear? page.Find(regex) → located []Match
4 lines / columns, in order, with boxes? page.Rows(), Row.Band, ClusterFloats
5 the table rows? (+ region-scoped) page.Tables() (auto), TablesIn(rect)
6 render / thumbnail? page.PNG/Image/SavePNG, doc.SavePNGs/Thumbnail
7 embedded images? page.Images(), doc.Images/SaveImages
8 quick facts (pages, title, encrypted)? doc.Info(), doc.Outline(), page.Links()
9 merge / combine? experimental.Merge, doc.AppendPDF/AppendImage

Reading & opening

Open accepts a file path, []byte, or io.Reader, with transparent encryption handling:

doc, err := experimental.Open(src, experimental.Password("1234"))

doc.Raw() and page.Raw() expose the underlying gomupdf types as escape hatches for anything this layer does not wrap.

Tables

Auto-strategy: tries word-alignment, falls back to vector ruling when alignment finds nothing — so you need not know the layout up front.

tables, _ := doc.Tables()              // every page, auto, page-tagged
t := tables[0]
fmt.Println(t.NumRows(), t.NumCols(), t.Rows, t.Region())
inBox, _ := page.TablesIn(box, experimental.TableLines()) // force lines, region-scoped

Rendering

png, _   := page.PNG(experimental.DPI(150))
paths, _ := doc.SavePNGs("out/", experimental.DPI(200))   // page-1.png, page-2.png, ...
thumb, _ := doc.Thumbnail(experimental.Zoom(0.3))

Merging (PDFs + images)

experimental.Merge("combined.pdf", "scan.pdf", "receipt.jpg", "appendix.pdf")

doc, _ := experimental.NewDoc()
defer doc.Close()
doc.AppendPDF("a.pdf")
doc.AppendImage("photo.png")   // becomes one full-bleed page sized to the image
doc.Save("out.pdf")

Design notes

  • Word edges map directly onto the names layout code uses: Left/Top/ Right/Bottom, with Height (bottom - top) doubling as a font-size proxy for outlier filtering.
  • Row clustering sweeps words top-to-bottom and starts a new line when the vertical gap exceeds RowTolerance (default 3.5 pt) — robust where native line breaks are unreliable across columns.
  • ClusterFloats is the shared 1-D clustering primitive behind column-lane and row detection.

Documentation

Overview

Package experimental is a question-first layer over gomupdf.

The base library hands you a bag of positioned fragments (spans, words, quads, blocks) and leaves the geometry to you. This package lets you ask the page questions instead:

v, _ := page.ValueRightOf("Total")   // key/value extraction
t    := page.TextIn(box)             // crop text by region
hits := page.Find(`\d{4}-\d{4}`)     // regex with locations

One coordinate model is used everywhere: Rect, origin top-left, y grows down, units are PDF points. Conversions from the base library's two rect shapes (gomupdf.Rect{X,Y,W,H} and geometry.Rect{X0,Y0,X1,Y1}) live here so callers never juggle both.

Status: experimental. Heuristics are best-effort and tuned for structured, layout-rich documents. Signatures may change.

Index

Constants

This section is empty.

Variables

View Source
var ErrPassword = errors.New("experimental: document is encrypted and the password is missing or incorrect")

ErrPassword is returned by Open when a document is encrypted and the supplied password is missing or incorrect. Test for it with errors.Is.

Functions

func ClusterFloats

func ClusterFloats(values []float64, tol float64) [][]float64

ClusterFloats groups sorted values into runs where each value is within tol of the previous one. It is the building block for detecting vertical lanes (columns) from a bag of word x-positions, and rows from y-positions:

xs := words.Lefts()
lanes := experimental.ClusterFloats(xs, 8) // 8pt column tolerance

The input is not mutated; output groups are in ascending order.

func CollectBlock

func CollectBlock(rows []Row, start int, opt BlockOptions) []string

CollectBlock walks rows downward from start, collecting the text of words inside the x-band [Lo, Hi] until it hits a Stop keyword, runs past MaxGap empty lines, or reaches MaxLines. It captures the gnarly multi-line region reads (postal addresses, label-anchored blocks) that layout-aware extraction otherwise hand-rolls every time.

func Mean

func Mean(values []float64) float64

Mean returns the arithmetic mean of values (0 for an empty slice). Handy for turning a position cluster into a single lane center.

func Merge

func Merge(out string, sources ...any) error

Merge stitches any mix of PDFs and images into a single PDF at out. Each source (path, []byte, or io.Reader) is sniffed: PDFs have all their pages appended; images each become one page.

experimental.Merge("combined.pdf", "scan.pdf", "receipt.jpg", "appendix.pdf")

Types

type BlockOptions

type BlockOptions struct {
	Lo, Hi    float64        // x-band the block lives in
	MaxLines  int            // cap on collected lines (default 7)
	MaxGap    int            // empty lines tolerated mid-block (default 1)
	Stop      *regexp.Regexp // stop when a band word matches (e.g. next field label)
	GuardLeft bool           // also stop when a word left of the band matches Stop
}

BlockOptions configures CollectBlock.

type Bookmark

type Bookmark struct {
	Level int // 1-based nesting depth
	Title string
	Page  int // 0-based target page, or -1 if external/unresolved
}

Bookmark is one entry of the document outline (table of contents).

type Doc

type Doc struct {
	// contains filtered or unexported fields
}

Doc is a thin, lifecycle-managed handle over a gomupdf.Document. It exists so callers stop juggling Open/Authenticate/Close ceremony and page handles.

doc, err := experimental.Open("document.pdf", experimental.Password("1234"))
if err != nil { ... }
defer doc.Close()
for _, page := range doc.Pages() { ... }

func NewDoc

func NewDoc() (*Doc, error)

NewDoc creates a new, empty PDF document ready for AppendPDF / AppendImage.

func Open

func Open(src any, opts ...OpenOption) (*Doc, error)

Open opens a PDF from any of: a file path (string), raw bytes ([]byte), or an io.Reader (read fully into memory). Encryption is handled transparently when a Password option is given; otherwise an encrypted PDF returns an error.

func (*Doc) AppendImage

func (d *Doc) AppendImage(src any) error

AppendImage appends the image (path, []byte, or io.Reader) as a new full-bleed page sized to the image's pixel dimensions (1px = 1pt). Encoded JPEG/PNG/GIF bytes are accepted as-is.

func (*Doc) AppendPDF

func (d *Doc) AppendPDF(src any) error

AppendPDF appends every page of a source PDF (path, []byte, or io.Reader).

func (*Doc) Bytes

func (d *Doc) Bytes() ([]byte, error)

Bytes serializes the document to PDF bytes (with garbage collection).

func (*Doc) Close

func (d *Doc) Close()

Close releases native resources. Safe to call more than once.

func (*Doc) Find

func (d *Doc) Find(pattern string, opts ...QueryOption) ([]Match, error)

Find scans every page and returns all matches, tagged by page.

func (*Doc) Images

func (d *Doc) Images() ([]Image, error)

Images extracts embedded images across every page, tagged by page.

func (*Doc) Info

func (d *Doc) Info() (Info, error)

Info returns a typed summary of the document.

func (*Doc) Lines

func (d *Doc) Lines() ([]string, error)

Lines returns every page's cleaned lines (soft hyphens stripped, blanks dropped) as one flat stream — a flat line stream suitable for line-oriented parsing.

func (*Doc) NumPages

func (d *Doc) NumPages() int

NumPages returns the page count.

func (*Doc) Outline

func (d *Doc) Outline() ([]Bookmark, error)

Outline returns the document's table of contents as a flat, depth-first list.

func (*Doc) Page

func (d *Doc) Page(i int) (*Page, error)

Page returns the 0-based page i as an experimental.Page.

func (*Doc) Pages

func (d *Doc) Pages() iter.Seq2[int, *Page]

Pages iterates pages in order: for i, page := range doc.Pages() { ... }. Pages that fail to load are skipped.

func (*Doc) Raw

func (d *Doc) Raw() *gomupdf.Document

Raw exposes the underlying gomupdf.Document as an escape hatch for features this layer does not wrap (write/modify, metadata, TOC, drawings, ...).

func (*Doc) Save

func (d *Doc) Save(path string) error

Save writes the document to a PDF file (with garbage collection).

func (*Doc) SaveImages

func (d *Doc) SaveImages(dir string) ([]string, error)

SaveImages extracts every image and writes them into dir as page<P>-img<I>.<ext>, returning the written paths. dir is created if needed.

func (*Doc) SavePNGs

func (d *Doc) SavePNGs(dir string, opts ...RenderOption) ([]string, error)

SavePNGs renders every page into dir as page-1.png, page-2.png, ... and returns the written paths. dir is created if needed.

func (*Doc) Tables

func (d *Doc) Tables(opts ...TableOption) ([]Table, error)

Tables detects tables across every page, tagged by page.

func (*Doc) Text

func (d *Doc) Text() (string, error)

Text returns the whole document's text, pages separated by form feed.

func (*Doc) Thumbnail

func (d *Doc) Thumbnail(opts ...RenderOption) (image.Image, error)

Thumbnail renders the first page to an image.Image (default options unless overridden, e.g. experimental.Zoom(0.3)).

func (*Doc) ValueBelow

func (d *Doc) ValueBelow(label string, opts ...QueryOption) (string, bool, error)

ValueBelow scans pages in order and returns the first label/value hit.

func (*Doc) ValueRightOf

func (d *Doc) ValueRightOf(label string, opts ...QueryOption) (string, bool, error)

ValueRightOf scans pages in order and returns the first label/value hit.

type Image

type Image struct {
	Page   int
	Index  int
	Ext    string // jpeg, png, jpx, ...
	Bytes  []byte // original encoded bytes
	Width  int    // source pixel width
	Height int    // source pixel height
	Region Rect   // placement rectangle on the page
}

Image is an embedded image extracted from a page, tagged with its page and fill-order index, carrying its original encoded bytes and placement region.

func (Image) Save

func (im Image) Save(path string) error

Save writes the image's encoded bytes to path.

type Info

type Info struct {
	Pages     int
	Encrypted bool
	Title     string
	Author    string
	Subject   string
	Keywords  string
	Creator   string
	Producer  string
	Format    string
	Created   string // raw creationDate string
	Modified  string // raw modDate string
	Metadata  map[string]string
}

Info is a typed snapshot of document-level facts, sparing callers the stringly-typed metadata map and separate count/encryption calls.

type Link struct {
	Rect Rect
	URI  string
}

Link is a clickable link on a page.

type Match

type Match struct {
	Page int
	Text string
	Rect Rect
	// contains filtered or unexported fields
}

Match is a located text hit: the matched text and its bounding rect, plus the page it came from and the visual line it sat on (for context).

func (Match) Context

func (m Match) Context() string

Context returns the full text of the line the match sat on — a cheap snippet of surrounding context.

type OpenOption

type OpenOption func(*openConfig)

OpenOption configures Open.

func Password

func Password(pw string) OpenOption

Password supplies the password for an encrypted PDF.

type Page

type Page struct {
	// contains filtered or unexported fields
}

Page is a positioned-text view of one PDF page. It loads the page's words once (lazily) and serves every spatial query from that cache.

func (*Page) Bound

func (p *Page) Bound() (Rect, error)

Bound returns the page's bounding rect in points.

func (*Page) Find

func (p *Page) Find(pattern string, opts ...QueryOption) ([]Match, error)

Find returns every regex match on the page, each with its bounding rect. Matching runs per visual line, so a pattern spanning adjacent words on the same line is found and its rect is the union of those words.

hits := page.Find(`\d{4}-\d{2}-\d{2}`) // dates, with their locations

func (*Page) Image

func (p *Page) Image(opts ...RenderOption) (image.Image, error)

Image renders the page to a standard library image.Image.

func (*Page) Images

func (p *Page) Images() ([]Image, error)

Images extracts every embedded image on the page, preserving original encoding where available.

func (*Page) Lines

func (p *Page) Lines() ([]string, error)

Lines returns the page's non-empty lines (soft hyphens stripped) as produced by gomupdf's reading-order extractor.

func (p *Page) Links() ([]Link, error)

Links returns the page's clickable links.

func (*Page) PNG

func (p *Page) PNG(opts ...RenderOption) ([]byte, error)

PNG renders the page and returns encoded PNG bytes.

func (*Page) Raw

func (p *Page) Raw() *gomupdf.Page

Raw exposes the underlying gomupdf.Page for unwrapped features (pixmap, images, drawings, tables, search quads, ...).

func (*Page) Rows

func (p *Page) Rows(opts ...RowOption) ([]Row, error)

Rows groups the page's words into visual lines by vertical position: words are swept top-to-bottom and a new line starts when a word's top exceeds the current line's first word by more than the tolerance. Each row's words are sorted left-to-right. This is the workhorse for table-style and multi-column layouts where native line breaks are unreliable across columns.

func (*Page) SavePNG

func (p *Page) SavePNG(path string, opts ...RenderOption) error

SavePNG renders the page and writes it to path as PNG.

func (*Page) Search

func (p *Page) Search(needle string, opts ...QueryOption) ([]Match, error)

Search finds a literal substring on the page and returns each hit's location. Use Find for regular expressions.

func (*Page) Tables

func (p *Page) Tables(opts ...TableOption) ([]Table, error)

Tables detects tables on the page. By default it tries the text strategy and falls back to the lines strategy when text finds nothing — so callers need not know the document's layout up front. Force a strategy with TableText / TableLines.

func (*Page) TablesIn

func (p *Page) TablesIn(r Rect, opts ...TableOption) ([]Table, error)

TablesIn returns the page's tables whose bounding box overlaps region r.

func (*Page) Text

func (p *Page) Text() (string, error)

Text returns the page's reading-order plain text.

func (*Page) TextIn

func (p *Page) TextIn(r Rect) (string, error)

TextIn returns the text inside region r, clustered into reading-order lines (rows joined by newline, words within a row by space). This is region/area cropping: hand it a header box, a column, a cell — get just that text.

func (*Page) ValueBelow

func (p *Page) ValueBelow(label string, opts ...QueryOption) (string, bool, error)

ValueBelow finds the label on the page and returns the text on the nearest line below it that sits within the label's horizontal band — for values printed under their heading rather than beside it.

func (*Page) ValueRightOf

func (p *Page) ValueRightOf(label string, opts ...QueryOption) (string, bool, error)

ValueRightOf finds the label on the page and returns the text immediately to its right on the same line — the bread-and-butter of key/value extraction:

v, ok := page.ValueRightOf("Order No")
v, ok := page.ValueRightOf("Total", experimental.MaxGap(40))

The label may be a regular expression. ok is false if the label is not found.

func (*Page) Words

func (p *Page) Words() (Words, error)

Words returns every positioned word on the page.

func (*Page) WordsIn

func (p *Page) WordsIn(r Rect) (Words, error)

WordsIn returns the words whose center lies inside the region r.

type QueryOption

type QueryOption func(*queryConfig)

QueryOption configures Find / ValueRightOf / ValueBelow.

func CaseSensitive

func CaseSensitive() QueryOption

CaseSensitive makes label/pattern matching case-sensitive (default: insensitive).

func MaxGap

func MaxGap(pts float64) QueryOption

MaxGap stops a right-of value scan when the horizontal gap between two words exceeds pts, so a far-away column is not swept into the value.

func Pad

func Pad(pts float64) QueryOption

Pad widens the x-band used by ValueBelow by pts on each side (default 2).

type Rect

type Rect struct {
	X0, Y0, X1, Y1 float64
}

Rect is an axis-aligned rectangle in PDF points. Origin is top-left and y grows downward, matching the page coordinate system used throughout gomupdf.

func R

func R(x0, y0, x1, y1 float64) Rect

R is a terse constructor for a Rect from its two corners.

func (Rect) CenterX

func (r Rect) CenterX() float64

func (Rect) CenterY

func (r Rect) CenterY() float64

func (Rect) Contains

func (r Rect) Contains(s Rect) bool

Contains reports whether s lies entirely within r.

func (Rect) ContainsPoint

func (r Rect) ContainsPoint(x, y float64) bool

ContainsPoint reports whether (x, y) lies within r.

func (Rect) Empty

func (r Rect) Empty() bool

Empty reports whether the rect has no area.

func (Rect) Expand

func (r Rect) Expand(d float64) Rect

Expand grows the rect by d points on every side (negative shrinks it).

func (Rect) Geometry

func (r Rect) Geometry() geometry.Rect

Geometry converts back to the base library's corner rect, for callers that need to hand a region to gomupdf APIs (e.g. InsertImage, AddRectAnnot).

func (Rect) Height

func (r Rect) Height() float64

func (Rect) Overlaps

func (r Rect) Overlaps(s Rect) bool

Overlaps reports whether r and s share any area.

func (Rect) Union

func (r Rect) Union(s Rect) Rect

Union returns the smallest rect enclosing both r and s. The zero Rect is treated as "nothing", so unioning onto it yields s unchanged.

func (Rect) Width

func (r Rect) Width() float64

type RenderOption

type RenderOption func(*renderConfig)

RenderOption configures rasterization.

func DPI

func DPI(dpi float64) RenderOption

DPI sets the render resolution. 72 DPI == zoom 1.0.

func Grayscale

func Grayscale() RenderOption

Grayscale renders in grayscale instead of RGB.

func Zoom

func Zoom(z float64) RenderOption

Zoom sets the render scale factor directly (1.0 == 72 DPI).

type Row

type Row struct {
	Top   float64
	Words Words
}

Row is a visual line of words sharing roughly the same vertical position, already sorted left-to-right.

func ClusterRows added in v0.1.1

func ClusterRows(ws Words, tolerance float64) []Row

ClusterRows groups an arbitrary set of words into visual lines by vertical position, identically to Rows. Use it when you need to cluster a filtered or combined word set (e.g. after DropOutliers, or merging words from several sources) rather than a whole page.

func (Row) Band

func (r Row) Band(lo, hi float64) Words

Band returns the row's words whose left edge falls within [lo, hi].

func (Row) Bounds

func (r Row) Bounds() Rect

Bounds returns the row's bounding rect.

func (Row) Text

func (r Row) Text() string

Text joins the row's words left-to-right with single spaces.

type RowOption

type RowOption func(*rowConfig)

RowOption configures Rows.

func RowTolerance

func RowTolerance(pts float64) RowOption

RowTolerance sets the max vertical gap (points) between a word and the current line before a new line starts. Default 3.5.

type Table

type Table struct {
	Page int
	gomupdf.Table
}

Table is a detected table tagged with its source page. It embeds gomupdf.Table, so Rows, ColX, RowY, NumRows() and NumCols() are available directly; Region() gives the bounding box in the unified Rect type.

func (Table) Region

func (t Table) Region() Rect

Region returns the table's bounding box as a unified Rect.

type TableOption

type TableOption func(*tableConfig)

TableOption configures table detection.

func TableLines

func TableLines() TableOption

TableLines forces the vector-drawing ("lines") strategy.

func TableText

func TableText() TableOption

TableText forces the word-alignment ("text") strategy.

type Word

type Word struct {
	Text  string
	Rect  Rect
	Block int // source block index (reading-order hint from MuPDF)
	Line  int // source line index within the block
}

Word is a single positioned word. Its geometry is a unified Rect (top-left origin, y down, PDF points). The accessors below name the edges the way most layout-aware code refers to them.

func (Word) Bottom

func (w Word) Bottom() float64

Bottom is the word's lower edge (larger y).

func (Word) Height

func (w Word) Height() float64

Height is the word box height (bottom - top), useful for filtering oversized outliers such as watermarks.

func (Word) Left

func (w Word) Left() float64

Left is the word's left edge.

func (Word) Right

func (w Word) Right() float64

Right is the word's right edge.

func (Word) Top

func (w Word) Top() float64

Top is the word's upper edge (smaller y).

type Words

type Words []Word

Words is a slice of Word with chainable spatial filters. Methods return new slices and never mutate the receiver, so they compose:

row.Words.Band(lo, hi).Text()

func (Words) Band

func (ws Words) Band(lo, hi float64) Words

Band keeps words whose left edge (x0) falls within [lo, hi]. This is the x-band filter for reading a single column.

func (Words) Bounds

func (ws Words) Bounds() Rect

Bounds returns the union rect of all words (zero Rect if empty).

func (Words) DropOutliers

func (ws Words) DropOutliers(factor float64) Words

DropOutliers removes words whose height exceeds factor × the median word height — the standard trick for stripping oversized diagonal watermarks. A factor <= 0 defaults to 2.2.

func (Words) In

func (ws Words) In(r Rect) Words

In keeps words whose center point lies inside r.

func (Words) LeftOf

func (ws Words) LeftOf(x float64) Words

LeftOf keeps words whose left edge is strictly left of x.

func (Words) Lefts

func (ws Words) Lefts() []float64

Lefts returns the left edge (x0) of every word — feed straight into ClusterFloats to find column lanes.

func (Words) RightOf

func (ws Words) RightOf(x float64) Words

RightOf keeps words whose left edge is at or right of x.

func (Words) SortByX

func (ws Words) SortByX() Words

SortByX returns the words sorted left-to-right.

func (Words) SortReading

func (ws Words) SortReading() Words

SortReading returns the words in reading order (top-to-bottom, then left-to-right), tolerating small vertical jitter within a line.

func (Words) Text

func (ws Words) Text() string

Text joins the words left-to-right with single spaces.

func (Words) Tops

func (ws Words) Tops() []float64

Tops returns the top edge (y0) of every word.

Jump to

Keyboard shortcuts

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