Packages

pdf

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

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. 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:

main.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()
Click the code to edit it

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:

main.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()
Click the code to edit it

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:

main.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()
Click the code to edit it

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:

main.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)
Click the code to edit it

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:

main.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
Click the code to edit it

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, a qr code included, goes in as it is. Translucent pixels are blended onto white:

main.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
Click the code to edit it

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:

main.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() == ""
Click the code to edit it

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:

main.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
Click the code to edit it

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

Quick check

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

Your turn

A two-page document

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

main.osl
Ctrl Enter runs
Expected output
2

API reference#

60 members, generated from the std:pdf source of OSL 9.3.0.

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.