///|
/// Core PDF object tree.
///
/// This is a low-level representation close to PDF syntax. In particular:
/// - `PdfObject::PdfString` is treated as a byte string by the parser/serializer.
/// - `PdfObject::Dictionary` keys are also stored as "bytes-in-String" so we can
/// round-trip weird/malformed PDFs without losing information.
pub(all) enum PdfObject {
/// PDF `null`.
Null
/// PDF boolean (`true` or `false`).
Boolean(Bool)
/// PDF integer number.
Integer(Int)
/// PDF real number.
Real(Double)
/// PDF string object (either literal `(...)` or hexadecimal `<...>`).
///
/// Note: this is stored in a MoonBit `String` but treated as raw bytes by the
/// parser/serializer (each UTF-16 code unit stores one byte).
PdfString(String)
/// PDF name object (written like `/Type`).
///
/// Names are byte sequences, not Unicode text; see `PdfName`.
Name(PdfName)
/// PDF array object (ordered list).
PdfArray(Array[PdfObject])
/// PDF dictionary object.
///
/// Stored as an array of `(key, value)` pairs instead of a map:
/// - Allows representing malformed PDFs with duplicate keys.
/// - Preserves "first match wins" semantics used by `lookup_*` (search_by).
/// - Keeps insertion order stable for serialization.
/// - Key encoding: `String` is treated as a "bytes-in-String" byte string,
/// matching the parser/serializer (each UTF-16 code unit stores one byte).
Dictionary(Array[(String, PdfObject)])
/// PDF stream object.
///
/// Streams are represented as a `(stream_dict, stream_data)` pair stored in a
/// mutable reference so code can "materialize" deferred bytes in-place.
Stream(Ref[(PdfObject, Stream)])
/// PDF indirect reference (`n 0 R`) to an object number.
Indirect(Int)
} derive(Debug)
///|
/// 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.
pub trait ToPdfNumber {
fn to_pdf_number(Self) -> PdfObject
}
///|
pub impl ToPdfNumber for Int with fn to_pdf_number(self : Int) -> PdfObject {
Integer(self)
}
///|
pub impl ToPdfNumber for Double with fn to_pdf_number(self : Double) -> PdfObject {
Real(self)
}
///|
/// Remove a dictionary entry (also works for streams).
pub fn PdfObject::remove_entry(
self : PdfObject,
key : String,
) -> PdfObject raise {
match self {
Dictionary(entries) => {
let out = entries.filter(pair => pair.0 != key)
Dictionary(out)
}
Stream(r) => {
let (inner, stream) = r.val
r.val = (inner.remove_entry(key), stream)
Stream(r)
}
_ => raise PdfError::Msg("remove_entry: not a dictionary")
}
}
///|
/// Replace a dictionary entry, raising if it's not present (also works for streams).
pub fn PdfObject::replace_entry(
self : PdfObject,
key : String,
value : PdfObject,
) -> PdfObject raise {
match self {
Null => Dictionary([(key, value)])
Dictionary(entries) => {
let mut replaced = false
let out = Array(capacity=entries.length())
for entry in entries {
let (k, v) = entry
if k == key {
out.push((k, value))
replaced = true
} else {
out.push((k, v))
}
}
if !replaced {
raise PdfError::Msg("replace_entry: key not found")
}
Dictionary(out)
}
Stream(r) => {
let (inner, stream) = r.val
r.val = (inner.replace_entry(key, value), stream)
Stream(r)
}
_ => raise PdfError::Msg("replace_entry: not a dictionary")
}
}
///|
/// Add a dictionary entry, replacing if already present (also works for streams).
pub fn PdfObject::add_entry(
self : PdfObject,
key : String,
value : PdfObject,
) -> PdfObject raise {
match self {
Null => Dictionary([(key, value)])
Dictionary(entries) => {
let mut replaced = false
let out = Array(capacity=entries.length() + 1)
for entry in entries {
let (k, v) = entry
if k == key {
out.push((k, value))
replaced = true
} else {
out.push((k, v))
}
}
if !replaced {
out.push((key, value))
}
Dictionary(out)
}
Stream(r) => {
let (inner, stream) = r.val
r.val = (inner.add_entry(key, value), stream)
Stream(r)
}
_ => raise PdfError::Msg("add_entry: not a dictionary")
}
}
///|
/// Lookup the key without following indirects at either source or destination.
pub fn PdfObject::lookup_immediate(
self : PdfObject,
key : String,
) -> PdfObject? {
fn lookup_entries(entries : Array[(String, PdfObject)]) -> PdfObject? {
match entries.search_by(pair => pair.0 == key) {
None => None
Some(i) => Some(entries[i].1)
}
}
match self {
Dictionary(entries) => lookup_entries(entries)
Stream(r) => {
let (inner, _) = r.val
match inner {
Dictionary(entries) => lookup_entries(entries)
_ => None
}
}
_ => None
}
}
///|
/// Lookup all values for a key without following indirects.
///
/// This is useful for handling malformed PDFs where dictionaries may contain
/// duplicate keys.
pub fn PdfObject::lookup_immediate_all(
self : PdfObject,
key : String,
) -> Array[PdfObject] {
fn collect(entries : Array[(String, PdfObject)]) -> Array[PdfObject] {
let out = Array(capacity=entries.length())
for entry in entries {
let (k, v) = entry
if k == key {
out.push(v)
}
}
out
}
match self {
Dictionary(entries) => collect(entries)
Stream(r) => {
let (inner, _) = r.val
match inner {
Dictionary(entries) => collect(entries)
_ => []
}
}
_ => []
}
}
///|
/// Find the indirect reference given by the value associated with a key.
pub fn PdfObject::find_indirect(self : PdfObject, key : String) -> Int? raise {
match self {
Dictionary(_) =>
match self.lookup_immediate(key) {
Some(Indirect(i)) => Some(i)
_ => None
}
_ => raise PdfError::Msg("find_indirect: not a dictionary")
}
}
// Explicit method promotion for derived/implemented traits.
// (Previously implicit; see warning [0079] implicit_impl_as_method.)
///|
pub extend PdfObject with Debug::{to_repr}