# pdf

> Write PDF documents with flowing text, tables, shapes, images, bookmarks and watermarks, and merge and split PDFs you wrote.

This is the Markdown version of https://osl-lang.org/packages/pdf. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:pdf"
```

`std:pdf` writes PDF files. A document is a stack of pages. You add text that flows from the top margin down and onto new pages, tables, lines and shapes at exact positions, and images from [img](/packages/img.md). Then you save the file or take its bytes.

Reach for it for invoices, reports, tickets and anything a person will print or attach to an email. It only reads back PDFs it wrote itself, so it is not a general PDF parser.

## A first document

`pdf.new()` returns a document with one blank page. `doc.text(content)` writes a line where the flow has reached, and `doc.save(path)` writes the file:

```osl
import "std:pdf"
import "std:fs"

fs.remove("hello.pdf", {force: true}).unwrap()

auto doc = pdf.new()
doc.text("Hello, PDF")
doc.text("A second line.")
doc.save("hello.pdf").unwrap()

auto back = pdf.decodeFile("hello.pdf").unwrap()
log back.pageCount()
log back.pageText(1)
fs.remove("hello.pdf").unwrap()
```

Output:

```text
1
Hello, PDF
A second line.
```

`pageText(n)` returns the text written to page `n`, one line per line. It is also a quick way to test what a document says. `doc.encode()` returns the file as bytes instead, for sending it over the network.

## Page setup

Options to `pdf.new` choose the page: `pageSize` (`"letter"`, the default, `"legal"`, `"tabloid"`, `"a3"`, `"a4"` or `"a5"`), `landscape`, or a custom `width` and `height` in points (72 to an inch). `margin` defaults to 72 points, `fontSize` to 12, and `title`, `author` and `subject` set the document's properties:

```osl
import "std:pdf"

auto doc = pdf.new({pageSize: "a4", title: "Report", author: "Ada"})
log doc.pageSize()
log doc.margin() doc.fontSize()
log doc.title() doc.author()

auto wide = pdf.new({pageSize: "a4", landscape: true})
log wide.pageSize()
```

Output:

```text
{"height":842,"width":595}
72 12
Report Ada
{"height":595,"width":842}
```

Change settings for text written from now on with `setFontSize(points)`, `setMargin(points)`, `setTitle`, `setAuthor` and `setSubject`.

## Flowing text

Text starts at the top margin and moves down. A line that would pass the bottom margin starts a new page. `newLine()` leaves a blank line, `addPage()` starts a page on purpose, and `wrap: true` breaks a long line at the margins:

```osl
import "std:pdf"

auto doc = pdf.new({pageSize: "a5"})
doc.setFontSize(20)
doc.text("Quarterly report")
doc.setFontSize(12)
doc.newLine()
string summary = [
  "Sales rose in every region this quarter,",
  "and the table below shows how the numbers break down by product."
].join(" ")
doc.text(summary, {wrap: true})
log doc.pageText(1)

doc.addPage()
doc.text("Second page")
log doc.pageCount()
```

Output:

```text
Quarterly report
Sales rose in every region this quarter, and the
table below shows how the numbers break down by
product.

2
```

Without `wrap`, a long line stays on one line, so wrap prose. Characters outside Windows-1252 (the Western European set) print as `?`. `doc.textWidth(text)` tells you how wide a string is in points, for lining things up.

Text can be placed at an exact spot with `x` and `y`, counted in points from the top-left corner, which is `{x: 1, y: 1}`. Give both or neither.

## Tables

`table(headers, rows)` draws a table where the flow has reached, with equal-width columns, a rule under the headers and under each row. The flow carries on below it, and a row that doesn't fit moves to a new page:

```osl
import "std:pdf"

auto doc = pdf.new()
doc.text("Stock")
doc.table(["Product", "Units"], [["Tea", 120], ["Coffee", 80]])
doc.text("Total: 200")

log doc.pageText(1)
```

Output:

```text
Stock
Product
Units
Tea
120
Coffee
80
Total: 200
```

Text too wide for its column is cut short with "…", and cells beyond the last header are left out. Each row must be an array.

## Lines and shapes

`line`, `rectangle` and `circle` draw at exact positions, in points from the top-left. Colours are `"#rrggbb"`, `"#rrggbbaa"` or `{r, g, b, a}`. A rectangle with neither `fill` nor `stroke` is outlined in black:

```osl
import "std:pdf"

auto doc = pdf.new()
doc.rectangle({x: 72, y: 100, width: 100, height: 50}, {fill: "#2f6feb"})
doc.circle({x: 300, y: 125}, 25, {stroke: "#ff0000", lineWidth: 2})
doc.line({x: 72, y: 200}, {x: 300, y: 200}, {color: "#808080"})
log doc.encode().len > 500
```

Output:

```text
true
```

Shapes don't move the text flow, so keep them clear of it or place text with `x` and `y`.

## Images

`doc.drawImage(image, rect)` draws an `*img.Image` stretched to fill `{x, y, width, height}` in points. Anything you draw or filter with [img](/packages/img.md), a [qr](/packages/qr.md) code included, goes in as it is. Translucent pixels are blended onto white:

```osl
import "std:pdf"
import "std:img"
import "std:qr"

auto doc = pdf.new()
*img.Image swatch = img.new(20, 20, {fill: "orange"})
doc.drawImage(swatch, {x: 72, y: 100, width: 40, height: 40})

*img.Image code = qr.code("https://rotur.dev").unwrap()
doc.drawImage(code, {x: 72, y: 160, width: 100, height: 100})
log doc.encode().len > 1000
```

Output:

```text
true
```

`doc.image(bytes, rect)` and `doc.imageFile(path, rect)` do the same from PNG, JPEG or GIF bytes or a file, and return a result.

## Bookmarks and watermarks

`addBookmark(title, page, options)` adds an entry to the viewer's bookmark panel. `level` nests it under the closest earlier entry one level up. `setWatermark(text)` draws light grey text diagonally across every page, including pages already written:

```osl
import "std:pdf"

auto doc = pdf.new()
doc.text("Introduction")
doc.addPage()
doc.text("Details")

doc.addBookmark("Introduction", 1)
doc.addBookmark("Details", 2, {level: 1})
doc.setWatermark("DRAFT")
log doc.watermark()

doc.setWatermark("")
log doc.watermark() == ""
```

Output:

```text
DRAFT
true
```

## Merging and splitting

`pdf.mergeFiles(paths)` returns a new document with every page of the given files in order. `pdf.splitFile(path, dir)` writes each page to `dir/page_N.pdf` and returns the paths. Both work only on files this package wrote:

```osl
import "std:pdf"
import "std:fs"

fs.remove("book.pdf", {force: true}).unwrap()
fs.remove("pages", {force: true}).unwrap()

auto doc = pdf.new()
doc.text("One")
doc.addPage()
doc.text("Two")
doc.save("book.pdf").unwrap()

auto parts = pdf.splitFile("book.pdf", "pages").unwrap()
log parts

auto joined = pdf.mergeFiles(parts.reverse()).unwrap()
log joined.pageCount()
log joined.pageText(1)

log pdf.decode([1, 2, 3]).unwrapErr().kind
```

Output:

```text
["pages/page_1.pdf","pages/page_2.pdf"]
2
Two

ParseError
```

A PDF from other software is an `UnsupportedError`, and bytes that aren't a PDF are a `ParseError`.

**Quiz.** Where does `doc.text("Hi")` put the text?

- [x] Where the flow has reached: from the top margin down, then onto a new page.
- [ ] At the bottom of the page.
- [ ] At `{x: 1, y: 1}`, always.

Answer: Give `x` and `y` to place text at an exact point instead.

### Exercise: A two-page document

Write "One" on the first page and "Two" on a second page, then print the page count.

Starting code:

```osl
import "std:pdf"

auto doc = pdf.new()
doc.text("One")

log doc.pageCount()
```

It should print:

```text
2
```

Hint 1: `doc.addPage()` starts a new page.

A solution:

```osl
import "std:pdf"

auto doc = pdf.new()
doc.text("One")
doc.addPage()
doc.text("Two")

log doc.pageCount()
```

## API reference

## API reference

Every member of `std:pdf`, generated from the source of OSL.

### Functions

#### `pdf.create() *pdf.PDF`

**Deprecated.** Deprecated: use pdf.new() instead, which returns a *pdf.Document.

create returns an empty US Letter document.

#### `pdf.createCustom(any width, any height) *pdf.PDF`

**Deprecated.** Deprecated: use pdf.new({width: width, height: height}) instead.

createCustom returns an empty document width x height points, or US
Letter when the size is invalid.

#### `pdf.decode(byte[] data) result<*pdf.Document, error>`

decode reads a PDF this package wrote back into a document, so it can be
inspected, added to and saved again. Text written to it flows onto a new
page; bookmarks are not kept. Fails with ParseError when data isn't a PDF
and UnsupportedError for a PDF written by other software.

#### `pdf.decodeFile(string path) result<*pdf.Document, error>`

decodeFile reads the PDF at path like pdf.decode. Fails with
NotFoundError, PermissionError or IOError when the file can't be read.

#### `pdf.getInfo(any filePath) object`

**Deprecated.** Deprecated: use fs.stat(path) for file details, or pdf.decodeFile(path) to read the PDF.

getInfo returns the name, size, dates and extension of the file at path,
or {} when it doesn't exist.

#### `pdf.merge(array pdfFiles) *pdf.PDF`

**Deprecated.** Deprecated: use pdf.mergeFiles(paths) instead, which fails for a file it can't read.

merge builds a document from the pages of PDF files this package wrote,
skipping files it can't read.

#### `pdf.mergeFiles(string[] paths) result<*pdf.Document, error>`

mergeFiles returns a new document with the pages of each PDF at paths in
order, keeping each page's size. The files must have been written by this
package. Fails, naming the file, with the errors of pdf.decodeFile. Throws
ValueError when paths is empty.

#### `pdf.new(object? options) *pdf.Document`

new returns an empty document with one blank page. Options: pageSize
("letter", the default, "legal", "tabloid", "a3", "a4" or "a5", in any
case); landscape (boolean) turns the page sideways; width and height
(points, both or neither, up to 14400) set a custom page size instead of
pageSize; margin (points, default 72, or a quarter of the shorter side on
smaller pages) and fontSize (points, default 12);
title, author and subject (strings) for the document's properties.
Throws ValueError for an unknown page size or an invalid number.

#### `pdf.split(any pdfPath, any outputDir) boolean`

**Deprecated.** Deprecated: use pdf.splitFile(path, dir) instead.

split writes each page of a PDF this package wrote to outputDir/page_N.pdf,
returning false on failure.

#### `pdf.splitFile(string path, string dir) result<string[], error>`

splitFile writes each page of the PDF at path, which must have been
written by this package, to its own file dir/page_N.pdf (N from 1),
creating dir if needed, and returns the paths written. Fails with the
errors of pdf.decodeFile, or with PermissionError or IOError when a file
can't be written.

### `*pdf.Document`

#### `document.addBookmark(string title, int page, object? options)`

addBookmark adds an entry titled title to the viewer's bookmarks panel
that opens page (1-based). Options: level (whole number, default 0) nests
it: 0 is top level and level n goes under the closest earlier entry of
level n-1 (a deeper jump is treated as one level). Throws ValueError for
a page that doesn't exist or a negative level.

#### `document.addPage()`

addPage finishes the current page and starts a new blank one the size of
the document's pages; flowing text continues at its top margin.

#### `document.author() string`

author returns the document's author property, or "" when it has none.

#### `document.circle(object center, number radius, object? options)`

circle draws a circle around center, {x, y} in points from the top-left
corner ({x: 1, y: 1}), with radius in points. Options: fill, stroke and lineWidth as for
rectangle. Throws ValueError for a malformed center, a negative radius or
an invalid option.

#### `document.drawImage(*img.Image image, object rect)`

drawImage draws an img.Image from std:img stretched to fill rect, {x,
y, width, height} in points with x and y its top-left corner, so
anything drawn or filtered with img can go in the document as it is,
without encoding it first. Translucent pixels are blended onto white, the
page's color. Later changes to the image don't change the document.
Throws ValueError for a malformed rectangle, TypeError for null and
StateError when the image is closed.

#### `document.encode() byte[]`

encode returns the document as PDF file bytes; the document is unchanged
and can still be added to.

#### `document.fontSize() number`

fontSize returns the font size in points for text written from now on.

#### `document.image(byte[] data, object rect) result<unit, error>`

image draws PNG, JPEG or GIF image bytes stretched to fill rect, {x, y,
width, height} in points with x and y its top-left corner, {x: 1, y: 1}
for the page's. Transparent
pixels are drawn without their transparency. Fails with ParseError when
data isn't an image it can decode. Throws ValueError for a malformed
rectangle.

#### `document.imageFile(string path, object rect) result<unit, error>`

imageFile draws the image file at path like image. Fails with
NotFoundError, PermissionError or IOError when the file can't be read
and ParseError when it isn't an image.

#### `document.line(object from, object to, object? options)`

line draws a straight line between the points from and to, each {x, y} in
points from the top-left corner, which is {x: 1, y: 1}. Options: color (an OSL colour, default
black) and lineWidth (points, default 1). Throws ValueError for a
malformed point or option.

#### `document.margin() number`

margin returns the page margin in points.

#### `document.newLine()`

newLine moves the flowing text down one line, leaving a blank line.

#### `document.pageCount() int`

pageCount returns the number of pages, the current one included; a new
document has 1.

#### `document.pageSize() {height: number, width: number}`

pageSize returns the size in points of the current page, which new pages
share, as {width, height}.

#### `document.pageText(int page) string`

pageText returns the text written to page (1-based), one line per text
line, each ending in a newline. Throws ValueError for a page that doesn't
exist.

#### `document.rectangle(object rect, object? options)`

rectangle draws the rectangle rect, {x, y, width, height} in points with
x and y its top-left corner ({x: 1, y: 1} starts at the page's). Options: fill and stroke (OSL colours)
fill it and outline it; without either it is outlined in black;
lineWidth (points, default 1). Throws ValueError for a malformed
rectangle, a negative size or an invalid option.

#### `document.save(string path) result<unit, error>`

save writes the document to path as a PDF file; the document is
unchanged and can still be added to. Fails with NotFoundError,
PermissionError or IOError when the file can't be written.

#### `document.setAuthor(string author)`

setAuthor sets the document's author property; "" removes it.

#### `document.setFontSize(number size)`

setFontSize sets the font size in points for text written from now on;
on a page with nothing on it yet, flowing text starts one line of the new
size below the top margin. Throws ValueError for a size that isn't more than 0 and at most 1000.

#### `document.setMargin(number margin)`

setMargin sets the margin in points that flowing text, wrapping and
tables keep from the page edges, from the next page (or this one, if
nothing is on it yet). Throws ValueError for a negative margin or one
that leaves no room on the page.

#### `document.setSubject(string subject)`

setSubject sets the document's subject property; "" removes it.

#### `document.setTitle(string title)`

setTitle sets the document's title property, which PDF viewers show
as the window title; "" removes it.

#### `document.setWatermark(string text)`

setWatermark draws text diagonally in light grey across every page,
including pages already written; "" removes it.

#### `document.subject() string`

subject returns the document's subject property, or "" when it has none.

#### `document.table(array headers, array rows)`

table draws headers and rows (an array of arrays of cells) as a table
where the flowing text has reached, with equal-width columns across the
space between the margins, a rule under the headers and under each row,
and the flow continuing below it. A row that doesn't fit starts a new
page; text too wide for its column is cut short with "…" and cells past
the last header are left out. Throws ValueError when headers is empty
and TypeError when a row isn't an array.

#### `document.text(string content, object? options)`

text writes content, starting a new page at the bottom margin. Options:
x and y (the top-left corner of the text, {x: 1, y: 1} being the page's),
wrap and color. Characters outside Windows-1252 print as "?".
Throws ValueError when only one of x and y is given.

#### `document.textWidth(string text) number`

textWidth returns how wide text is in points in the document's font
size, for lining text up.

#### `document.title() string`

title returns the document's title property, or "" when it has none.

#### `document.watermark() string`

watermark returns the text drawn across every page, or "" when there is
none.

### `*pdf.PDF`

#### `pDF.addBookmark(any level, any title, any page) boolean`

**Deprecated.** Deprecated: use pdf.new() and doc.addBookmark(title, page, {level}) instead.

addBookmark adds an outline entry titled title opening page (1-based) at
level (0 is top level), returning false for a page that doesn't exist or
a negative level.

#### `pDF.addPage()`

**Deprecated.** Deprecated: use pdf.new() and doc.addPage() instead.

addPage starts a new page.

#### `pDF.addWatermark(any text) string`

**Deprecated.** Deprecated: use pdf.new() and doc.setWatermark(text) instead.

addWatermark draws text diagonally across every page and returns it.

#### `pDF.create() *pdf.PDF`

**Deprecated.** Deprecated: use pdf.new() instead.

create returns a new empty US Letter document.

#### `pDF.createCustom(any width, any height) *pdf.PDF`

**Deprecated.** Deprecated: use pdf.new({width: width, height: height}) instead.

createCustom returns a new empty document width x height points.

#### `pDF.fontSize() int`

**Deprecated.** Deprecated: use pdf.new() and doc.fontSize() instead.

fontSize returns the font size in whole points.

#### `pDF.generateContent() string`

**Deprecated.** Deprecated: use doc.encode() on a pdf.new() document instead.

generateContent returns the document as PDF text.

#### `pDF.getInfo(any filePath) object`

**Deprecated.** Deprecated: use fs.stat(path) for file details, or pdf.decodeFile(path) to read the PDF.

getInfo returns the name, size, dates and extension of the file at path,
or {} when it doesn't exist.

#### `pDF.getPageCount() int`

**Deprecated.** Deprecated: use pdf.new() and doc.pageCount() instead.

getPageCount returns the number of pages, not counting an empty last page.

#### `pDF.getPageText(any pageNum) string`

**Deprecated.** Deprecated: use pdf.new() and doc.pageText(page) instead.

getPageText returns the text written to a page (1-based), or "" for a
page that doesn't exist.

#### `pDF.margin() int`

**Deprecated.** Deprecated: use pdf.new() and doc.margin() instead.

margin returns the margin in whole points.

#### `pDF.merge(array pdfFiles) *pdf.PDF`

**Deprecated.** Deprecated: use pdf.mergeFiles(paths) instead.

merge builds a document from the pages of PDF files this package wrote,
skipping files it can't read.

#### `pDF.newLine()`

**Deprecated.** Deprecated: use pdf.new() and doc.newLine() instead.

newLine moves the flowing text down one line.

#### `pDF.paragraph(any text) string`

**Deprecated.** Deprecated: use pdf.new() and doc.text(content, {wrap: true}) instead.

paragraph wraps text to the page width, estimating every character as
half the font size wide, writes it like text and returns it.

#### `pDF.save(any path) boolean`

**Deprecated.** Deprecated: use doc.save(path) on a pdf.new() document instead.

save writes the document to path, returning false on failure.

#### `pDF.setFontSize(any size)`

**Deprecated.** Deprecated: use pdf.new() and doc.setFontSize(size) instead.

setFontSize sets the font size, using 12 for a size under 1.

#### `pDF.setMargin(any margin)`

**Deprecated.** Deprecated: use pdf.new() and doc.setMargin(margin) instead.

setMargin sets the margin, using 0 for a negative margin.

#### `pDF.setMetadata(any title, any author, any subject)`

**Deprecated.** Deprecated: use pdf.new() and doc.setTitle, setAuthor and setSubject instead.

setMetadata sets the title, author and subject properties, leaving any
given as null unchanged.

#### `pDF.split(any pdfPath, any outputDir) boolean`

**Deprecated.** Deprecated: use pdf.splitFile(path, dir) instead.

split writes each page of a PDF this package wrote to outputDir/page_N.pdf,
returning false on failure.

#### `pDF.table(array headers, array rows) boolean`

**Deprecated.** Deprecated: use pdf.new() and doc.table(headers, rows) instead, which draws where the text has reached.

table draws headers and rows from the top margin of the current page,
returning false when headers is empty.

#### `pDF.text(any content) string`

**Deprecated.** Deprecated: use pdf.new() and doc.text(content) instead.

text writes content at the left margin below the previous text and
returns it.
