README

#pdf

Core types and operations for in-memory PDF document representation.

#Overview

The pdf package provides the fundamental data structures for representing PDF documents in memory. It defines the PdfObject enum for all PDF value types, the Pdf struct for complete documents, and operations for manipulating objects, dictionaries, streams, and object graphs.

#Core Types

#PdfObject

The central enum representing all PDF value types:

///|
pub(all) enum PdfObject {
Null
Boolean(Bool)
Integer(Int)
Real(Double)
String(String)
Name(String)
Array(Array[PdfObject])
Dictionary(Array[(String, PdfObject)])
Stream(Ref[(PdfObject, Stream)])
Indirect(Int)
}

  • Null: PDF null value
  • Boolean: true or false
  • Integer/Real: Numeric values
  • String: Literal or hexadecimal strings
  • Name: PDF names like /Type, /Page
  • Array: Ordered collection of objects
  • Dictionary: Key-value pairs (keys are names). Stored as an Array[(String, PdfObject)] so malformed PDFs with duplicate keys can be represented; use lookup_immediate (first match) or lookup_immediate_all (all matches).
  • Stream: Dictionary plus binary data
  • Indirect: Reference to another object by number

#Pdf

The in-memory document representation:

///|
pub(all) struct Pdf {
major : Int // PDF major version
minor : Int // PDF minor version
root : Int // Object number of document catalog
objects : PdfObjects // All objects in the document
mut trailerdict : PdfObject
was_linearized : Bool
mut saved_encryption : SavedEncryption?
}

#Stream

Stream data can be loaded or deferred:

///|
pub(all) enum Stream {
Got(@pdfio.MutableBytes) // Data in memory
ToGet(ToGet) // Data still on disk
}

#Creating Documents

#Empty Document

///|
let pdf = @pdf.Pdf::empty()
// Creates PDF 2.0 with no objects

#Adding Objects

let pdf = @pdf.Pdf::empty()

// Add an object and get its number
let objnum = pdf.addobj(@pdf.PdfObject::Dictionary([
("/Type", @pdf.PdfObject::Name("/Page")),
]))

// Add with a specific object number
pdf.addobj_given_num((42, @pdf.PdfObject::Integer(100)))

#Object Lookup

#Basic Lookup

///|
test "lookup_obj returns Null for missing objects" {
let pdf = @pdf.Pdf::empty()
assert_true(pdf.lookup_obj(999) is Null)
}

#Following Indirect References

///|
test "direct follows indirects" {
let pdf = @pdf.Pdf::empty()
let objnum = pdf.addobj(@pdf.PdfObject::Integer(42))
let indirect = @pdf.PdfObject::Indirect(objnum)
guard pdf.direct(indirect) is Integer(n) else { fail("expected Integer") }
inspect(n, content="42")
}

#Dictionary Key Lookup

///|
test "lookup_direct finds keys" {
let pdf = @pdf.Pdf::empty()
let dict = @pdf.PdfObject::Dictionary([
("/Type", @pdf.PdfObject::Name(@pdf.PdfName::of_string("/Page"))),
("/Count", @pdf.PdfObject::Integer(5)),
])
guard pdf.lookup_direct("/Type", dict) is Some(Name(name)) else {
fail("expected Name")
}
inspect(name.to_string_bytes(), content="/Page")
assert_true(pdf.lookup_direct("/Missing", dict) is None)
}

#Nested Chain Lookup

For deeply nested dictionaries, use lookup_chain:

///|
test "lookup_chain navigates nested dicts" {
let pdf = @pdf.Pdf::empty()
let inner = @pdf.PdfObject::Dictionary([
("/Value", @pdf.PdfObject::Integer(100)),
])
let outer = @pdf.PdfObject::Dictionary([("/Inner", inner)])
guard pdf.lookup_chain(outer, ["/Inner", "/Value"][:]) is Some(Integer(n)) else {
fail("expected Integer")
}
inspect(n, content="100")
}

#Dictionary Manipulation

#Adding Entries

///|
test "add_dict_entry" {
let dict = @pdf.PdfObject::Dictionary([
("/Type", @pdf.PdfObject::Name(@pdf.PdfName::of_string("/Page"))),
])
let updated = dict.add_entry("/Count", @pdf.PdfObject::Integer(1))
match updated {
Dictionary(entries) => inspect(entries.length(), content="2")
_ => fail("expected dictionary")
}
}

#Traits

#ToPdfNumber

@pdf.ToPdfNumber is a small helper trait for converting primitive numeric types into PdfObject numeric nodes.

///|
test "ToPdfNumber converts Int/Double to PdfObject numbers" {
guard @pdf.ToPdfNumber::to_pdf_number(42) is Integer(42) else {
fail("expected Integer(42)")
}
guard @pdf.ToPdfNumber::to_pdf_number(1.5) is Real(r) && r == 1.5 else {
fail("expected Real(1.5)")
}
}

#Replacing Entries

///|
test "replace_dict_entry" {
let dict = @pdf.PdfObject::Dictionary([("/Count", @pdf.PdfObject::Integer(1))])
let updated = dict.replace_entry("/Count", @pdf.PdfObject::Integer(5))
guard updated.lookup_immediate("/Count") is Some(Integer(n)) else {
fail("expected Integer")
}
inspect(n, content="5")
}

#Removing Entries

///|
test "remove_dict_entry" {
let dict = @pdf.PdfObject::Dictionary([
("/Type", @pdf.PdfObject::Name(@pdf.PdfName::of_string("/Page"))),
("/Count", @pdf.PdfObject::Integer(1)),
])
let updated = dict.remove_entry("/Count")
match updated {
Dictionary(entries) => inspect(entries.length(), content="1")
_ => fail("expected dictionary")
}
}

#Object Iteration

#Iterating All Objects

obj.objiter(fn(objnum) {
println("Object \{objnum}: \{obj}")
},
pdf,
)

#Selecting Objects by Predicate

// Find all page objects

///|
let page_nums = obj.objselect(
fn(obj) {
match pdf.lookup_direct("/Type") {
Some(Name("/Page")) => true
_ => false
}
},
pdf,
)

#Transforming All Objects

// Apply a transformation to every object
pdf,
.objselfmap(fn(obj) {
// Return transformed object
obj
})

#Stream Operations

#Getting Stream Data

match obj {
Stream(_) => {
obj.getstream!() // Loads data if deferred
let bytes = obj.bigarray_of_stream!()
// Use bytes...
}
_ => ()
}

#Geometry Operations

#Parsing Rectangles

///|
test "parse_rectangle" {
let pdf = @pdf.Pdf::empty()
let rect = @pdf.PdfObject::Array([
@pdf.PdfObject::Real(0.0),
@pdf.PdfObject::Real(0.0),
@pdf.PdfObject::Real(612.0),
@pdf.PdfObject::Real(792.0),
])
let (minx, miny, maxx, maxy) = pdf.parse_rectangle(rect)
inspect((minx, miny, maxx, maxy), content="(0, 0, 612, 792)")
}

#Matrices

// Parse a matrix from a dictionary

///|
let matrix = pdf.parse_matrix("/Matrix", dict)

// Create a matrix object

///|
let matrix_obj = @pdf.make_matrix(@pdftransform.TransformMatrix::identity())

#Reference Management

#Finding Referenced Objects

// Find all objects reachable from a starting object

///|
let refs = pdf.objects_referenced([], [], start_obj)

#Removing Unreferenced Objects

// Garbage collect unreferenced objects
pdf.remove_unreferenced!()

#Document Operations

#Renumbering Objects

// Calculate changes to renumber 1..n

///|
let change_table = pdf.changes()

// Apply renumbering

///|
let renumbered = pdf.renumber(change_table)

#Deep Copy

// Create an independent copy

///|
let copy = pdf.deep_copy()

#Renumbering Multiple PDFs

// Make object numbers mutually exclusive across documents

///|
let renumbered = @pdf.renumber_pdfs([pdf1, pdf2, pdf3])

#Name Trees

PDF name trees are hierarchical structures for mapping names to values:

// Lookup in a name tree

///|
let value = pdf.nametree_lookup(@pdf.PdfObject::String("key"), tree)

// Get all entries

///|
let entries = pdf.contents_of_nametree(tree)

#Character Classification

///|
test "is_delimiter" {
inspect(is_delimiter('('), content="true") // internal function
inspect(is_delimiter('/'), content="true")
inspect(is_delimiter('a'), content="false")
}

///|
test "is_whitespace" {
inspect(@pdf.is_whitespace(' '), content="true")
inspect(@pdf.is_whitespace('\n'), content="true")
inspect(@pdf.is_whitespace('a'), content="false")
}

#Error Handling

The package uses PdfError for error conditions:

///|
pub(all) suberror PdfError {
Msg(String)
}

Functions that can fail use the raise keyword and should be called with ! or within error handling contexts.

#
EncryptionValues

type EncryptionValues = (
Encryption
, String, String, Int, String, String?, String?)

Snapshot of encryption settings used for re-encryption.

#
PdfObjMap

type PdfObjMap = Map[Int, (Ref[ObjectData], Int)]

Mutable linked hash map that maintains the order of insertion, not thread safe.

Example

test {
let map = { 3: "three", 8: "eight", 1: "one" }
assert_eq(map.get(2), None)
assert_eq(map.get(3), Some("three"))
map.set(3, "updated")
assert_eq(map.get(3), Some("updated"))
}

#
ToPdfNumber

pub trait ToPdfNumber {
to_pdf_number(Self) -> PdfObject
}

Convert primitive numeric types into PDF numeric objects.

This is intentionally narrow (only Int/Double) so callers don't have to choose between PDF Strings vs Names, etc.
impl ToPdfNumber for Int

#
PdfError

pub(all) suberror PdfError {
Msg(String)
}

Core error used across the PDF surface.

#
DeferredEncryption

pub(all) struct DeferredEncryption {
crypt_type :
Encryption

file_encryption_key : String?
obj : Int
gen : Int
key : Array[Int]
keylength : Int
r : Int
}

Deferred crypt settings for stream materialization.

#
ObjectData

pub(all) enum ObjectData {
Parsed(PdfObject)
ParsedAlreadyDecrypted(PdfObject)
ToParse
ToParseFromObjectStream(Map[Int, Array[Int]], Int, Int, (Int, Array[Int]) -> Array[(Int, (Ref[ObjectData], Int))])
}

Object data state during parsing or decryption.

#
Pdf

pub(all) struct Pdf {
major : Int
minor : Int
root : Int
objects : PdfObjects
trailerdict : PdfObject
was_linearized : Bool
saved_encryption : SavedEncryption?
}

In-memory PDF document.

#
Pdf::addobj

fn Pdf::addobj(self : Pdf, obj : PdfObject) -> Int

Add an object. Returns the number chosen.

#
Pdf::addobj_given_num

fn Pdf::addobj_given_num(self : Pdf, pair : (Int, PdfObject)) -> Unit

Same as addobj, but pick a number ourselves.

#
Pdf::catalog_of_pdf

fn Pdf::catalog_of_pdf(self : Pdf) -> PdfObject raise

Return the document catalog.

#
Pdf::change_id

fn Pdf::change_id(self : Pdf, path : String) -> Unit raise

Replace the /ID entry in the trailer dictionary.

#
Pdf::changes

fn Pdf::changes(self : Pdf) -> Map[Int, Int]

Calculate the change table required to renumber objects 1..n.

#
Pdf::contents_of_nametree

fn Pdf::contents_of_nametree(self : Pdf, tree : PdfObject) -> Array[(PdfObject, PdfObject)] raise

Return an ordered list of all (k, v) pairs in a name tree.

#
Pdf::deep_copy

fn Pdf::deep_copy(self : Pdf) -> Pdf

Create a deep copy of a PDF document.

#
Pdf::direct

fn Pdf::direct(self : Pdf, obj : PdfObject) -> PdfObject

Make a PDF object direct -- that is, follow any indirect links.

#
Pdf::empty

fn Pdf::empty() -> Pdf

The empty document (PDF 2.0, no objects, no root, empty trailer dictionary).

#
Pdf::generate_id

fn Pdf::generate_id(self : Pdf, path : String, gettime : () -> Double) -> PdfObject

Generate an /ID entry for a document.

#
Pdf::getnum

fn Pdf::getnum(self : Pdf, obj : PdfObject) -> Double raise

Return a float from a PDF number.

#
Pdf::indirect_number

fn Pdf::indirect_number(self : Pdf, key : String, dict : PdfObject) -> Int?

Return the object number of an indirect dictionary object, if it is indirect.

#
Pdf::lookup_chain

fn Pdf::lookup_chain(self : Pdf, start : PdfObject, keys : ArrayView[String]) -> PdfObject?

Lookup a key in a nested dictionary chain.

#
Pdf::lookup_direct

fn Pdf::lookup_direct(self : Pdf, key : String, dict : PdfObject) -> PdfObject?

Lookup the key, resolving indirections at source and destination.

#
Pdf::lookup_direct_orelse

fn Pdf::lookup_direct_orelse(self : Pdf, key : String, alt : String, dict : PdfObject) -> PdfObject?

Same as lookup_direct, but allow a second alternative key.

#
Pdf::lookup_fail

fn Pdf::lookup_fail(self : Pdf, errtext : String, key : String, dict : PdfObject) -> PdfObject raise

Lookup an object, failing if not found.

#
Pdf::lookup_obj

fn Pdf::lookup_obj(self : Pdf, objnum : Int) -> PdfObject

Lookup an object in a document, parsing it if required.

#
Pdf::nametree_lookup

fn Pdf::nametree_lookup(self : Pdf, key : PdfObject, dict : PdfObject) -> PdfObject? raise

Look something up in a name tree.

#
Pdf::objcard

fn Pdf::objcard(self : Pdf) -> Int

Return the size of the object map.

#
Pdf::objects_referenced

fn Pdf::objects_referenced(self : Pdf, no_follow_entries : Array[String], no_follow_contains : Array[(String, PdfObject)], pdfobject : PdfObject) -> Array[Int]

Find the objects reachable from the given object.

#
Pdf::objiter

fn Pdf::objiter(self : Pdf, f : (Int, PdfObject) -> Unit raise?) -> Unit raise?

Iterate over all objects in a document.

#
Pdf::objiter_gen

fn Pdf::objiter_gen(self : Pdf, f : (Int, Int, PdfObject) -> Unit raise?) -> Unit raise?

Iterate over all objects in a document with generation numbers.

#
Pdf::objiter_inorder

fn Pdf::objiter_inorder(self : Pdf, f : (Int, PdfObject) -> Unit raise?) -> Unit raise?

Iterate over all objects in a document, ordered by object number.

#
Pdf::objnumbers

fn Pdf::objnumbers(self : Pdf) -> Array[Int]

Return the object numbers in ascending order.

#
Pdf::objselect

fn Pdf::objselect(self : Pdf, f : (PdfObject) -> Bool raise?) -> Array[Int] raise?

Select objects matching a predicate and return their numbers.

#
Pdf::objselfmap

fn Pdf::objselfmap(self : Pdf, f : (PdfObject) -> PdfObject raise?) -> Unit raise?

Map a function over all objects in a document.

#
Pdf::page_reference_numbers

fn Pdf::page_reference_numbers(self : Pdf) -> Array[Int] raise

Return the page reference numbers in order.

#
Pdf::parse_matrix

fn Pdf::parse_matrix(self : Pdf, key : String, dict : PdfObject) ->
TransformMatrix
raise

Parse a transform matrix, or return the identity if missing.

#
Pdf::parse_rectangle

fn Pdf::parse_rectangle(self : Pdf, obj : PdfObject) -> (Double, Double, Double, Double) raise

Parse a PDF rectangle structure into min x, min y, max x, max y.

#
Pdf::remove_unreferenced

fn Pdf::remove_unreferenced(self : Pdf) -> Unit raise

Remove any unreferenced objects.

#
Pdf::removeobj

fn Pdf::removeobj(self : Pdf, objnum : Int) -> Unit

Remove the given object.

#
Pdf::renumber

fn Pdf::renumber(self : Pdf, change_table : Map[Int, Int], preserve_order? : Bool) -> Pdf

Renumber a PDF's objects using a change table.

#
Pdf::renumber_object_parsed

fn Pdf::renumber_object_parsed(self : Pdf, changes : Map[Int, Int], obj : PdfObject, preserve_order? : Bool) -> PdfObject

Renumber indirect references using a change table.

#
Pdf::replace_chain

fn Pdf::replace_chain(self : Pdf, chain : ArrayView[String], obj : PdfObject) -> Unit raise

Replace or insert a chain from the trailer dictionary.

#
Pdf::transform_quadpoints

fn Pdf::transform_quadpoints(self : Pdf, transform :
TransformMatrix
, qp : PdfObject) -> PdfObject raise

Transform quadpoints by a matrix.

#
Pdf::transform_rect

fn Pdf::transform_rect(self : Pdf, transform :
TransformMatrix
, rect : PdfObject) -> PdfObject raise

Transform a rectangle by a matrix, returning the bounding rectangle.

#
PdfName

pub(all) struct PdfName {
bytes : Bytes
}

PDF Name object stored as raw bytes.

PDF names are byte sequences (not Unicode text). In PDF syntax they are written like /Type and may use #xx hex escapes to represent arbitrary bytes. Our lexer decodes #xx escapes into the underlying bytes.

This type keeps the bytes (including the leading / byte) so that:
  • parsing/serialization is byte-faithful,
  • we avoid accidentally applying Unicode/text semantics,
  • comparisons can be made lexicographically by byte.
impl Compare for PdfName
impl Eq for PdfName
impl Hash for PdfName
impl Show for PdfName

#
PdfName::equal_string_bytes

fn PdfName::equal_string_bytes(self : PdfName, key : String) -> Bool

Compare a PdfName with a name key written as a String (byte-string).

This expects key to contain the leading / and to be in the same "bytes-in-String" encoding as PdfName::to_string_bytes.

#
PdfName::lexical_compare

fn PdfName::lexical_compare(self : PdfName, other : PdfName) -> Int

Compare by raw bytes, lexicographically (prefix order).

Note: MoonBit's default compare for sequences/strings is shortlex (length-first, then element-wise). PDF name ordering (e.g. in name trees) needs byte-wise lexicographic order, so we implement it explicitly.

#
PdfName::of_bytes

fn PdfName::of_bytes(bytes : Bytes) -> PdfName

Build a PdfName from raw bytes.

bytes should include the leading / byte.

#
PdfName::of_string

fn PdfName::of_string(value : String) -> PdfName

Build a PdfName from a "bytes-in-String" representation.

This is mainly for internal construction sites and tests. Each UTF-16 code unit is truncated to its low 8 bits (i.e. treated as a byte).

#
PdfName::to_bytes

fn PdfName::to_bytes(self : PdfName) -> Bytes

Raw bytes (including the leading /).

#
PdfName::to_string_bytes

fn PdfName::to_string_bytes(self : PdfName) -> String

Convert to the historical "bytes-in-String" representation.

This maps each byte to a single UTF-16 code unit with the same numeric value (0..255). It is not a Unicode decoding.

#
PdfObject

pub(all) enum PdfObject {
Null
Boolean(Bool)
Integer(Int)
Real(Double)
String(String)
Name(PdfName)
Array(Array[PdfObject])
Dictionary(Array[(String, PdfObject)])
Stream(Ref[(PdfObject, Stream)])
Indirect(Int)
}

Core PDF object tree.

#
PdfObject::add_entry

fn PdfObject::add_entry(self : PdfObject, key : String, value : PdfObject) -> PdfObject raise

Add a dictionary entry, replacing if already present (also works for streams).

#
PdfObject::bigarray_of_stream

fn PdfObject::bigarray_of_stream(self : PdfObject) -> Array[Byte] raise

Find the contents of a stream as bytes.

#
PdfObject::find_indirect

fn PdfObject::find_indirect(self : PdfObject, key : String) -> Int? raise

Find the indirect reference given by the value associated with a key.

#
PdfObject::getstream

fn PdfObject::getstream(self : PdfObject) -> Unit raise

Get a stream from disk if it hasn't already been got.

#
PdfObject::lookup_immediate

fn PdfObject::lookup_immediate(self : PdfObject, key : String) -> PdfObject?

Lookup the key without following indirects at either source or destination.

#
PdfObject::lookup_immediate_all

fn PdfObject::lookup_immediate_all(self : PdfObject, key : String) -> Array[PdfObject]

Lookup all values for a key without following indirects.

This is useful for handling malformed PDFs where dictionaries may contain duplicate keys.

#
PdfObject::remove_entry

fn PdfObject::remove_entry(self : PdfObject, key : String) -> PdfObject raise

Remove a dictionary entry (also works for streams).

#
PdfObject::replace_entry

fn PdfObject::replace_entry(self : PdfObject, key : String, value : PdfObject) -> PdfObject raise

Replace a dictionary entry, raising if it's not present (also works for streams).

#
PdfObject::to_string

fn PdfObject::to_string(self : PdfObject) -> String

Convert a PDF object to its string representation.

#
PdfObject::to_string_including_data

fn PdfObject::to_string_including_data(self : PdfObject) -> String

Convert a PDF object to string including stream data.

#
PdfObjects

pub(all) struct PdfObjects {
max_obj_num : Int
parse : (Int) -> PdfObject?
objects : Map[Int, (Ref[ObjectData], Int)]
object_stream_ids : Map[Int, Int]
}

Container for document objects.

#
SavedEncryption

pub(all) struct SavedEncryption {
from_get_encryption_values : (
Encryption
, String, String, Int, String, String?, String?)
encrypt_metadata : Bool
perms : String
}

Saved encryption metadata used by recrypt helpers.

#
Stream

pub(all) enum Stream {
Got(Array[Byte])
ToGet(ToGet)
}

Stream payload, either in memory or deferred.

#
ToGet

pub(all) struct ToGet {
input :
Input

position : Int
length : Int
crypt : ToGetCrypt
}

Stream descriptor when bytes are not yet materialized.

#
ToGet::length

fn ToGet::length(self : ToGet) -> Int

#
ToGet::new

fn ToGet::new(input :
Input
, position : Int, length : Int, crypt? : ToGetCrypt) -> ToGet

#
ToGetCrypt

pub(all) enum ToGetCrypt {
NoChange
ToDecrypt(DeferredEncryption)
}

Indicates whether a stream needs decryption when materialized.

#
input_pdferror

fn input_pdferror(input :
Input
, message : String) -> String

Build an error string which includes the input source and position.

#
is_whitespace

fn is_whitespace(c : Char) -> Bool

PDF whitespace predicate.

#
make_matrix

Build a matrix object.

#
pdfobjmap_bindings

fn pdfobjmap_bindings(map : Map[Int, (Ref[ObjectData], Int)]) -> Array[(Int, (Ref[ObjectData], Int))]

#
pdfobjmap_empty

fn pdfobjmap_empty() -> Map[Int, (Ref[ObjectData], Int)]

#
pdfobjmap_find

fn pdfobjmap_find(key : Int, map : Map[Int, (Ref[ObjectData], Int)]) -> (Ref[ObjectData], Int) raise

#
recurse_array

fn recurse_array(f : (PdfObject) -> PdfObject, elts : Array[PdfObject]) -> PdfObject

Recursively rebuild an array.

#
renumber_pdfs

fn renumber_pdfs(pdfs : Array[Pdf]) -> Array[Pdf]

Renumber a list of PDFs so their object numbers are mutually exclusive.