Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Positioned Text Extraction** — `Page.ExtractTextElements()` and `Document.ExtractTextElementsFromPage()` return text runs with X, Y, Width, Height, FontName, FontSize (#68)
- **In-Memory PDF Opening** — `OpenFromBytes()`, `OpenFromBytesWithPassword()`, and context-aware variants for server-side workflows without temp file I/O (#68)

### Changed
- **Parser Reader Refactor** — Internal `Reader` refactored from `*os.File` to `io.ReadSeeker` interface, enabling in-memory PDF support
- **Test Coverage** — Project-wide coverage raised from 53.7% to 86.5%
- 11,200+ lines of new enterprise-grade tests across 12 packages
- All non-example packages now above 80% coverage threshold
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,36 @@ for _, page := range doc.Pages() {
}
```

### Positioned Text Extraction (NEW)

```go
doc, _ := gxpdf.Open("document.pdf")
defer doc.Close()

// Get text with positions, sizes, and font info
elements, _ := doc.ExtractTextElementsFromPage(1)
for _, e := range elements {
fmt.Printf("%q at (%.1f, %.1f) font=%s size=%.1f\n",
e.Text, e.X, e.Y, e.FontName, e.FontSize)
}
```

### Opening PDFs from Memory (NEW)

```go
// Read PDF from HTTP request, database, or any byte source
data, _ := io.ReadAll(httpResponse.Body)

doc, err := gxpdf.OpenFromBytes(data)
if err != nil {
log.Fatal(err)
}
defer doc.Close()

// Works with encrypted PDFs too
doc, _ = gxpdf.OpenFromBytesWithPassword(data, "secret")
```

### Extracting Tables from PDFs

```go
Expand Down
66 changes: 37 additions & 29 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,39 +130,42 @@ Page sizes, custom dimensions, landscape orientation, and text rotation:

## Current Development

### Unreleased (on main)
### v0.7.0 "Builder & Signatures"

**Released**: March 2026

- **Arc Drawing** (#59) — elliptical/circular arcs with wedge/chord fill modes
- **Declarative Builder API** — QuestPDF-inspired layout with 12-col grid, auto-pagination
- **Enterprise Tables** — colspan, rowspan, header repeat, page split
- **Rich Text** — multi-style inline text with mixed bold/italic/color
- **Digital Signatures** — PAdES B-B/B-T, RSA + ECDSA, zero external deps
- **Rich Text** — mixed-style inline text with baseline alignment, justify
- **Digital Signatures** — PAdES B-B + B-T, CMS/PKCS#7, RFC 3161, zero deps
- **Arc Drawing** (#59) — elliptical/circular arcs with wedge/chord fill modes
- **Text Measurement API** — exported font metrics (Standard 14 + TTF)

### Planned
### Unreleased (on main)

#### v0.7.0 - "Builder & Signatures"
- **Positioned Text Extraction** (#68) — `ExtractTextElements()` with X, Y, Width, Height, FontName, FontSize
- **In-Memory PDF Opening** (#68) — `OpenFromBytes()` for server-side workflows
- **Embedded Font Extraction** (#67) — extract TTF/OTF font binaries for round-trip preservation
- **Vector Graphics Extraction** (#66) — extract paths, bezier curves, colors, opacity with CTM support

**Done (unreleased on main)**
### Planned

- **Declarative Builder API** — QuestPDF-inspired layout with 12-col grid, auto-pagination
- `layout/` pure computation engine (85.7% coverage)
- `builder/` user-facing API (80.6% coverage)
- Own types (Value, Color, Size) — no layout/ import leak
- Font measurement bridge (Standard 14 + TTF)
- **Enterprise Tables** — colspan, rowspan, header repeat on overflow, page split, auto/fixed/fr columns
- **Rich Text** — mixed-style inline text with baseline alignment, justify
- **Digital Signatures** — PAdES B-B + B-T, CMS/PKCS#7, RFC 3161, zero deps (80.7% coverage)
- **Text Measurement API** — exported font metrics (Standard 14 + TTF)
- **Half-leading** — optically centered text in line boxes
#### v0.8.0 - "Extraction & Access"

- **Positioned Text Extraction** (#68) — public API for text elements with positions
- **In-Memory PDF Opening** (#68) — `OpenFromBytes()` without temp files
- **Embedded Font Extraction** (#67) — TTF round-trip extraction
- **Vector Graphics Extraction** (#66) — path verbs, CTM, q/Q stack, opacity
- **Scientific Paper Text Extraction** — two-column layout detection, reading order
- **Scientific Metadata Extraction** — title, authors, abstract, DOI via font heuristics

#### v0.8.0 - "Generation Platform"
#### v0.9.0 - "Generation Platform"

- **HTML to PDF** — render HTML/CSS into PDF via Builder API
- **QR Code + Barcode** — QR, Code128, EAN-13 generation
- **PDF/A Compliance** — archival format (A-1b, A-2b)
- **PDF/UA Accessibility** — tagged PDF for screen readers
- **Ready Components** — invoice, report, letter templates
- **HTML to PDF** - Render WYSIWYG HTML into PDF (may be separate library)

#### v1.0.0 - Stable Release

Expand Down Expand Up @@ -212,16 +215,21 @@ Page sizes, custom dimensions, landscape orientation, and text rotation:
| Text Opacity | Done | v0.5.0 |
| Gradient Rendering (PDF Shading) | Done | v0.6.0 |
| Encrypted PDF Reading | Done | v0.6.0 |
| Arc Drawing (elliptical/circular) | Done | unreleased |
| Declarative Builder API | Done | unreleased |
| Tables with ColSpan/RowSpan | Planned | v0.7.0 |
| Rich Text (mixed inline styles) | Planned | v0.7.0 |
| Digital Signatures | Planned | v0.7.0 |
| HTML to PDF | Planned | v0.8.0 |
| PDF/A Compliance | Planned | v0.8.0 |
| PDF Render to Image | Planned | v0.8.0 |
| SVG Import | Planned | v0.8.0 |
| Barcode / QR Code | Planned | v0.8.0 |
| Arc Drawing (elliptical/circular) | Done | v0.7.0 |
| Declarative Builder API | Done | v0.7.0 |
| Tables with ColSpan/RowSpan | Done | v0.7.0 |
| Rich Text (mixed inline styles) | Done | v0.7.0 |
| Digital Signatures (PAdES) | Done | v0.7.0 |
| Positioned Text Extraction | Done | unreleased |
| In-Memory PDF Opening | Done | unreleased |
| Embedded Font Extraction | Done | unreleased |
| Vector Graphics Extraction | Done | unreleased |
| Scientific Paper Text Extraction | Planned | v0.8.0 |
| HTML to PDF | Planned | v0.9.0 |
| PDF/A Compliance | Planned | v0.9.0 |
| PDF Render to Image | Planned | v0.9.0 |
| SVG Import | Planned | v0.9.0 |
| Barcode / QR Code | Planned | v0.9.0 |

## Backlog

Expand Down
23 changes: 23 additions & 0 deletions document.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,29 @@ func (d *Document) ExtractTextFromPage(pageNum int) (string, error) {
return result, nil
}

// ExtractTextElementsFromPage extracts positioned text elements from a specific page.
//
// pageNum is 1-based. Returns an error if pageNum is out of range.
//
// This is a convenience wrapper around Page.ExtractTextElements.
//
// Example:
//
// elements, err := doc.ExtractTextElementsFromPage(1)
// if err != nil {
// log.Fatal(err)
// }
// for _, e := range elements {
// fmt.Printf("%q at (%.1f, %.1f)\n", e.Text, e.X, e.Y)
// }
func (d *Document) ExtractTextElementsFromPage(pageNum int) ([]TextElement, error) {
if pageNum < 1 || pageNum > d.PageCount() {
return nil, fmt.Errorf("gxpdf: page %d out of range (1-%d)", pageNum, d.PageCount())
}
page := d.Page(pageNum - 1)
return page.ExtractTextElements()
}

// ExtractTablesFromPage extracts tables from a specific page (1-based).
//
// Errors are logged via slog. For error handling, use Page.ExtractTablesWithOptions.
Expand Down
79 changes: 79 additions & 0 deletions gxpdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ import (
// Version is the current version of the gxpdf library.
const Version = "0.1.0-alpha"

// pathFromBytes is the path sentinel reported by Documents opened from in-memory bytes.
const pathFromBytes = "<bytes>"

// ErrPasswordRequired is returned when a password is needed to open an encrypted PDF.
// Use OpenWithPassword to provide a password.
var ErrPasswordRequired = security.ErrPasswordRequired
Expand Down Expand Up @@ -150,3 +153,79 @@ func OpenWithPasswordAndContext(ctx context.Context, path, password string) (*Do
path: path,
}, nil
}

// OpenFromBytes opens a PDF document from an in-memory byte slice.
//
// This is equivalent to Open but reads from memory instead of the filesystem.
// It is useful when the PDF data has been received over the network, read from
// a database, or produced in-process without writing to disk.
//
// The returned Document must be closed after use (Close is a no-op for
// in-memory documents but should still be called for future compatibility).
//
// Example:
//
// data, err := os.ReadFile("document.pdf")
// if err != nil {
// log.Fatal(err)
// }
// doc, err := gxpdf.OpenFromBytes(data)
// if err != nil {
// log.Fatal(err)
// }
// defer doc.Close()
//
// fmt.Printf("Pages: %d\n", doc.PageCount())
func OpenFromBytes(data []byte) (*Document, error) {
return OpenFromBytesWithContext(context.Background(), data)
}

// OpenFromBytesWithContext opens a PDF document from an in-memory byte slice
// with a custom context.
//
// The context can be used for cancellation and timeouts during table or text
// extraction operations.
func OpenFromBytesWithContext(ctx context.Context, data []byte) (*Document, error) {
reader, err := parser.OpenPDFFromBytes(data)
if err != nil {
return nil, fmt.Errorf("gxpdf: failed to open PDF from bytes: %w", err)
}

return &Document{
reader: reader,
ctx: ctx,
path: pathFromBytes,
}, nil
}

// OpenFromBytesWithPassword opens a password-protected PDF from an in-memory byte slice.
//
// For PDFs with an empty user password (permissions-only encryption),
// OpenFromBytes handles them transparently.
//
// Example:
//
// data, _ := os.ReadFile("encrypted.pdf")
// doc, err := gxpdf.OpenFromBytesWithPassword(data, "secret")
// if err != nil {
// log.Fatal(err)
// }
// defer doc.Close()
func OpenFromBytesWithPassword(data []byte, password string) (*Document, error) {
return OpenFromBytesWithPasswordAndContext(context.Background(), data, password)
}

// OpenFromBytesWithPasswordAndContext opens a password-protected in-memory PDF
// with a custom context.
func OpenFromBytesWithPasswordAndContext(ctx context.Context, data []byte, password string) (*Document, error) {
reader, err := parser.OpenPDFFromBytesWithPassword(data, password)
if err != nil {
return nil, fmt.Errorf("gxpdf: failed to open encrypted PDF from bytes: %w", err)
}

return &Document{
reader: reader,
ctx: ctx,
path: pathFromBytes,
}, nil
}
Loading
Loading