packages feed

hpdft-0.4.6.0: docs/library.md

# hpdft library guide

hpdft is a Haskell library (and CLI) for parsing PDF files: text extraction with geometry and tagged structure, page-level paragraph regions, document diff, image export, and Form XObject extraction.

## Installation

Add a dependency on Hackage (when published):

```cabal
build-depends: hpdft >= 0.4.6
```

Or build from this repository:

```bash
cabal install
# library + hpdft CLI + examples
```

Requires GHC 9.14+ (see `hpdft.cabal`).

## Opening a document

Load a file once and reuse the parsed `Document` for every operation:

```haskell
import PDF.Document (openDocument)
import PDF.Error (PdfError(..), PdfResult)

main :: IO ()
main = do
  result <- openDocument "paper.pdf" Nothing
  case result of
    Left err  -> print err
    Right doc -> ...
```

For encrypted PDFs, pass `Just password` as the second argument. Failures (broken xref, missing objects, bad password) are returned as `Left` values of type `PdfError` in `PdfResult` (`Either PdfError a`) instead of raising exceptions.

## Text extraction

### Streaming (viewer pipeline)

As of 0.4.6, the bare `hpdft FILE` viewer uses legacy stream-order extraction streamed page by page. Library callers use `pdfToTextStreamDoc`:

```haskell
import PDF.Text (pdfToTextStreamDoc)
import qualified Data.ByteString.Lazy as BSL

pdfToTextStreamDoc doc $ \page total bs -> do
  putStrLn ("page " ++ show page ++ "/" ++ show total)
  BSL.putStr bs
```

Concatenating all page outputs in order equals `fst (pdfToTextDoc doc)`.

### Default pipeline (tagged → geometry)

The CLI default (`hpdft text FILE`) matches `pdfToTextTaggedDoc`:

```haskell
import PDF.Text (pdfToTextTaggedDoc)
import qualified Data.ByteString.Lazy.Char8 as BSL

case pdfToTextTaggedDoc doc of
  Left err -> print err
  Right bs -> BSL.putStr bs
```

When the PDF has a usable tagged structure tree, text is assembled in logical order. Otherwise hpdft falls back to geometry-based paragraph reconstruction (same as `pdfToTextGeomDoc`).

### Geometry-only

```haskell
import PDF.Text (pdfToTextGeomDoc)
```

Equivalent to `hpdft text --geom FILE`.

### Tagged-only attempt

Use `pdfToTextTaggedDocWith` / `pdfToTextTaggedBSWith` with `LayoutOptions`. When no structure tree exists, these still fall back to geometry.

### Legacy stream-order extractor

```haskell
import PDF.Text (pdfToTextDoc, pdfToTextStreamDoc)
```

`pdfToTextDoc` returns `(ByteString, [PdfWarning])` for the full document in one pass (not `PdfResult`). `pdfToTextStreamDoc` invokes a callback per page in document order; see above. Same output style as `hpdft text --legacy FILE`. Prefer geometry or tagged paths when reading order matters.

### Layout options

Footnotes and ruby (Aozora bunko notation) are controlled by `PDF.Layout.LayoutOptions`:

```haskell
import PDF.Layout (LayoutOptions(..), defaultLayoutOptions)

opts = defaultLayoutOptions { optRuby = True, optFootnotes = False }
```

Pass `opts` to `pdfToTextGeomDocWith`, `pdfToTextTaggedDocWith`, page APIs, and diff.

## Page-level API

`PDF.Page` exposes stable page enumeration and structured extraction:

| Function | Role |
|----------|------|
| `pageCount` | Number of pages |
| `pageRefAt` | Resolve 1-based page number to internal page reference |
| `pageItems` | Raw interpreted page items (glyphs, graphics) |
| `pageGlyphs` | Glyph list for one page |
| `pageLines` | Layout lines with `LayoutOptions` |
| `pageParagraphs` | Paragraph texts for one page |
| `pageRegions` | Paragraph text plus bounding box and indices |

Example:

```haskell
import PDF.Layout (defaultLayoutOptions)
import PDF.Page (pageCount, pageRefAt, pageRegions)

n <- pageCount doc
ref <- pageRefAt doc 1
regions <- pageRegions doc ref defaultLayoutOptions
```

See `examples/page-api/Main.hs` for a runnable program.

## Document diff

`PDF.Diff.compareDocuments` aligns pages by number and diffs paragraph text:

```haskell
import PDF.Diff (compareDocuments, TextChange(..))
import PDF.Layout (defaultLayoutOptions)

changes <- compareDocuments defaultLayoutOptions docA docB
```

Each `TextChange` records old/new paragraph text and optional page/paragraph indices. A `PageCountMismatch` entry appears when page counts differ.

CLI equivalent: `hpdft diff FILE_A FILE_B`.

## Image extraction

`PDF.Image.extractPageImages` returns `/Image` XObjects on a page (including nested forms):

```haskell
import PDF.Image (extractPageImages, extractPageImagesToDir)

images <- extractPageImages doc pageNum
paths  <- extractPageImagesToDir doc pageNum "out/"
```

JPEG streams are passed through; 8-bit DeviceRGB/Gray become minimal PNG. Inline images (`BI` … `EI`) are not extracted yet.

## Form extraction

`PDF.FormExtract` exports a named Form XObject to a standalone vector PDF:

```haskell
import PDF.FormExtract (pageFormNames, extractFormPdf, extractFormToFile)

names <- pageFormNames doc pageNum
bytes <- extractFormPdf doc pageNum "Fm42"
path  <- extractFormToFile doc pageNum "Fm42" "out/"
```

`pageFormNames` lists top-level form names on the page resources (not nested forms).

## Example programs

From the repository root:

```bash
cabal run extract-text -- data/fixtures/classic.pdf
cabal run page-api -- data/fixtures/paragraphs.pdf
```

With no argument, each example walks up from the current directory to find `data/fixtures/…`.

Sources: `examples/extract-text/Main.hs`, `examples/page-api/Main.hs`.

## API reference (Haddock)

Generate HTML documentation locally:

```bash
cabal haddock --haddock-all
```

Open `dist/doc/html/hpdft/index.html` (path may vary slightly by cabal version).

Primary public modules:

| Module | Purpose |
|--------|---------|
| `PDF.Document` | `openDocument`, `Document` |
| `PDF.Error` | `PdfResult`, `PdfError`, `PdfWarning` |
| `PDF.Text` | Full-document text extraction |
| `PDF.Layout` | `LayoutOptions`, paragraph heuristics |
| `PDF.Page` | Page count, paragraphs, regions |
| `PDF.Diff` | Paragraph-level diff |
| `PDF.Image` | Image XObject extraction |
| `PDF.FormExtract` | Form XObject export |

Lower-level modules (`PDF.Interpret`, `PDF.Structure`, `PDF.DocumentStructure`, …) are exposed for advanced use but may change more often.

## Known limitations

- RTL horizontal text is unsupported.
- Inline images are not extracted (see CHANGELOG 0.4.3).
- Some image color spaces are written as `.raw` plus JSON sidecar.
- Legacy stream-order extraction remains for comparison only.
- Details and roadmap: [CHANGELOG](../CHANGELOG.md). Developer notes: [dev/](../dev/).