Files
pdf-odin-llm/object.odin
T

475 lines
15 KiB
Odin
Raw Normal View History

2026-09-25 23:32:29 +02:00
package pdf
import "core:strconv"
import "core:strings"
// Ref is an indirect reference: "12 0 R".
//
// Object number 0 is reserved by the format for the head of the free list, so
// a Ref with num == 0 is the library's "no reference" value.
Ref :: struct {
num: u32,
gen: u16,
}
// Name is a PDF name object with the leading '/' stripped and any #xx escapes
// already decoded, so `Name("Adobe Green")` is what a file wrote as
// `/Adobe#20Green`.
Name :: distinct string
// String is a PDF string object as raw bytes: escapes resolved, hex pairs
// packed, and -- in an encrypted document -- already decrypted. It is *not*
// text. A string that is meant to be read by a human still has to go through
// `decode_text_string`, which handles the UTF-16BE and UTF-8 byte-order marks
// and the PDFDocEncoding fallback.
String :: distinct []byte
// Array is a PDF array. Elements may be indirect references; use `array_get`
// rather than indexing directly when the element could be one.
//
// The struct wrapper is not decoration: `Object` holds a `^Array` and an array
// holds `Object`s, and Odin will not close that cycle through a bare
// `[dynamic]Object`. Going through `items` also keeps every read site honest
// about the fact that an element may be a reference.
Array :: struct {
items: [dynamic]Object,
}
// Dict is a PDF dictionary, keyed by the decoded name without its '/'.
//
// A key that is present with a null value and a key that is absent mean the
// same thing in almost every part of the format, and this library does not
// distinguish them: nulls are dropped at parse time.
Dict :: map[string]Object
// Stream is a PDF stream: a dictionary plus a run of bytes.
//
// `raw` is the encoded payload exactly as stored (but decrypted, if the
// document was encrypted). The decoded bytes are produced on demand by
// `stream_data`, which caches them here.
Stream :: struct {
dict: ^Dict,
raw: []byte,
// Cache filled in by `stream_data`. `state` distinguishes "not decoded
// yet" from "decoded to nothing", which a length-0 slice cannot.
decoded: []byte,
state: Stream_Decode_State,
// The object this stream was parsed from, needed to derive its per-object
// decryption key. {0,0} for streams that came from an object stream (those
// are decrypted as part of their container) or were synthesised.
ref: Ref,
}
Stream_Decode_State :: enum u8 {
Pending,
Decoded,
Failed,
}
// Object is any PDF object. The zero value -- an empty union -- is the null
// object, which is also what a missing dictionary entry reads as.
Object :: union {
bool,
i64, // integer object
f64, // real object
Name,
String,
Ref,
^Array,
^Dict,
^Stream,
}
// ---------------------------------------------------------------------------
// Construction
//
// Everything allocated here is meant to live in a Document's arena, so nothing
// has a destructor: `close` reclaims the lot. Passing a non-arena allocator is
// supported (the parser is usable standalone) but then the caller owns the
// teardown.
// ---------------------------------------------------------------------------
new_dict :: proc(allocator := context.allocator, cap := 8) -> ^Dict {
d := new(Dict, allocator)
d^ = make(Dict, cap, allocator)
return d
}
new_array :: proc(allocator := context.allocator, cap := 8) -> ^Array {
a := new(Array, allocator)
a.items = make([dynamic]Object, 0, cap, allocator)
return a
}
new_stream :: proc(dict: ^Dict, raw: []byte, allocator := context.allocator) -> ^Stream {
s := new(Stream, allocator)
s.dict = dict
s.raw = raw
s.state = .Pending
return s
}
// ---------------------------------------------------------------------------
// Type tests and coercions
//
// Every `to_*` returns (value, ok) and never resolves references, so a caller
// that might be holding a Ref must go through `resolve` (or one of the
// resolving accessors below) first.
// ---------------------------------------------------------------------------
is_null :: proc(o: Object) -> bool {
return o == nil
}
to_bool :: proc(o: Object) -> (val: bool, ok: bool) {
val, ok = o.(bool)
return
}
to_int :: proc(o: Object) -> (val: i64, ok: bool) {
#partial switch v in o {
case i64: return v, true
// Files write "1.0" where an integer is required often enough that
// refusing it would reject working documents.
case f64:
if v >= -9.22e18 && v <= 9.22e18 {
return i64(v), true
}
}
return 0, false
}
to_real :: proc(o: Object) -> (val: f64, ok: bool) {
val, ok = o.(f64)
return
}
// to_number accepts either numeric object type. This is what almost every
// caller wants: the format's distinction between 1 and 1.0 carries no meaning
// outside of a handful of places (array indices, object numbers, /Length).
to_number :: proc(o: Object) -> (val: f64, ok: bool) {
#partial switch v in o {
case i64: return f64(v), true
case f64: return v, true
}
return 0, false
}
to_name :: proc(o: Object) -> (val: string, ok: bool) {
n, is_name := o.(Name)
return string(n), is_name
}
to_bytes :: proc(o: Object) -> (val: []byte, ok: bool) {
s, is_str := o.(String)
return transmute([]byte)s, is_str
}
to_array :: proc(o: Object) -> (val: ^Array, ok: bool) {
val, ok = o.(^Array)
return
}
// to_dict accepts a stream where a dictionary is wanted and hands back the
// stream's dictionary. Several parts of the format (/Resources, /Group,
// annotation appearances) are reached through entries that are sometimes one
// and sometimes the other.
to_dict :: proc(o: Object) -> (val: ^Dict, ok: bool) {
#partial switch v in o {
case ^Dict: return v, true
case ^Stream: return v.dict, v.dict != nil
}
return nil, false
}
to_stream :: proc(o: Object) -> (val: ^Stream, ok: bool) {
val, ok = o.(^Stream)
return
}
to_ref :: proc(o: Object) -> (val: Ref, ok: bool) {
val, ok = o.(Ref)
return
}
// name_is reports whether o is exactly this name. Saves a cast at every
// /Type and /Subtype check in the library.
name_is :: proc(o: Object, want: string) -> bool {
n, ok := o.(Name)
return ok && string(n) == want
}
// object_kind names an object's type for diagnostics.
object_kind :: proc(o: Object) -> string {
switch _ in o {
case bool: return "boolean"
case i64: return "integer"
case f64: return "real"
case Name: return "name"
case String: return "string"
case Ref: return "reference"
case ^Array: return "array"
case ^Dict: return "dictionary"
case ^Stream: return "stream"
}
return "null"
}
// object_identity is a stable cache key for an object: the address of the
// container it parsed to. Document.cache guarantees one address per object
// number, so the same indirect reference always keys the same slot, and a
// dictionary written inline in a resource is keyed by its own address.
// Scalars (a name, a number) have no identity and key nothing -- they cost
// nothing to rebuild. This is what `colorspace_load` and `function_load` cache
// on, which is why it lives here rather than in either of them.
@(private)
object_identity :: proc(o: Object) -> uintptr {
#partial switch v in o {
case ^Dict: return uintptr(rawptr(v))
case ^Array: return uintptr(rawptr(v))
case ^Stream: return uintptr(rawptr(v))
}
return 0
}
// ---------------------------------------------------------------------------
// Dictionary access
//
// The `*_raw` forms never touch the Document and never follow a reference.
// Everything else takes a Document (which may be nil, degrading to the raw
// behaviour) and resolves indirect references before answering, because at
// almost every read site in a PDF the value is allowed to be one.
// ---------------------------------------------------------------------------
dict_has :: proc(d: ^Dict, key: string) -> bool {
if d == nil { return false }
return key in d^
}
dict_get_raw :: proc(d: ^Dict, key: string) -> Object {
if d == nil { return nil }
return d[key] or_else nil
}
dict_get :: proc(doc: ^Document, d: ^Dict, key: string) -> Object {
if d == nil { return nil }
v := d[key] or_else nil
return resolve(doc, v)
}
// dict_get_alt reads `key`, falling back to `alt`. Inline image dictionaries
// abbreviate their keys (/W for /Width, /BPC for /BitsPerComponent), and the
// abbreviation is legal in both spellings for some of them.
dict_get_alt :: proc(doc: ^Document, d: ^Dict, key, alt: string) -> Object {
v := dict_get(doc, d, key)
if v == nil {
v = dict_get(doc, d, alt)
}
return v
}
dict_set :: proc(d: ^Dict, key: string, value: Object) {
if d == nil { return }
d[key] = value
}
dict_bool :: proc(doc: ^Document, d: ^Dict, key: string, default := false) -> bool {
v, ok := to_bool(dict_get(doc, d, key))
return ok ? v : default
}
dict_int :: proc(doc: ^Document, d: ^Dict, key: string, default: i64 = 0) -> i64 {
v, ok := to_int(dict_get(doc, d, key))
return ok ? v : default
}
dict_num :: proc(doc: ^Document, d: ^Dict, key: string, default: f64 = 0) -> f64 {
v, ok := to_number(dict_get(doc, d, key))
return ok ? v : default
}
dict_name :: proc(doc: ^Document, d: ^Dict, key: string, default := "") -> string {
v, ok := to_name(dict_get(doc, d, key))
return ok ? v : default
}
dict_bytes :: proc(doc: ^Document, d: ^Dict, key: string) -> []byte {
v, _ := to_bytes(dict_get(doc, d, key))
return v
}
dict_dict :: proc(doc: ^Document, d: ^Dict, key: string) -> ^Dict {
v, _ := to_dict(dict_get(doc, d, key))
return v
}
dict_array :: proc(doc: ^Document, d: ^Dict, key: string) -> ^Array {
v, _ := to_array(dict_get(doc, d, key))
return v
}
dict_stream :: proc(doc: ^Document, d: ^Dict, key: string) -> ^Stream {
v, _ := to_stream(dict_get(doc, d, key))
return v
}
// dict_ref returns the entry *without* resolving it, for the places that need
// the identity of the referenced object rather than its value -- structural
// keys like /Parent, where following the reference is exactly what causes a
// cyclic walk.
dict_ref :: proc(d: ^Dict, key: string) -> (Ref, bool) {
return to_ref(dict_get_raw(d, key))
}
// dict_type reads /Type. Missing /Type is extremely common in the wild
// (it is optional on most dictionaries), so callers should treat "" as
// "unknown", never as "wrong".
dict_type :: proc(doc: ^Document, d: ^Dict) -> string {
return dict_name(doc, d, "Type")
}
dict_subtype :: proc(doc: ^Document, d: ^Dict) -> string {
return dict_name(doc, d, "Subtype")
}
// ---------------------------------------------------------------------------
// Array access
// ---------------------------------------------------------------------------
array_len :: proc(a: ^Array) -> int {
if a == nil { return 0 }
return len(a.items)
}
array_get_raw :: proc(a: ^Array, i: int) -> Object {
if a == nil || i < 0 || i >= len(a.items) { return nil }
return a.items[i]
}
array_get :: proc(doc: ^Document, a: ^Array, i: int) -> Object {
return resolve(doc, array_get_raw(a, i))
}
array_num :: proc(doc: ^Document, a: ^Array, i: int, default: f64 = 0) -> f64 {
v, ok := to_number(array_get(doc, a, i))
return ok ? v : default
}
array_int :: proc(doc: ^Document, a: ^Array, i: int, default: i64 = 0) -> i64 {
v, ok := to_int(array_get(doc, a, i))
return ok ? v : default
}
array_name :: proc(doc: ^Document, a: ^Array, i: int, default := "") -> string {
v, ok := to_name(array_get(doc, a, i))
return ok ? v : default
}
array_dict :: proc(doc: ^Document, a: ^Array, i: int) -> ^Dict {
v, _ := to_dict(array_get(doc, a, i))
return v
}
array_push :: proc(a: ^Array, o: Object) {
if a == nil { return }
append(&a.items, o)
}
// array_numbers copies an array of numbers into a freshly allocated slice.
// Non-numeric entries read as 0 rather than failing, matching how viewers
// treat a stray null in a /Widths or /Decode array.
array_numbers :: proc(doc: ^Document, a: ^Array, allocator := context.allocator) -> []f64 {
n := array_len(a)
if n == 0 { return nil }
out := make([]f64, n, allocator)
for i in 0 ..< n {
out[i] = array_num(doc, a, i, 0)
}
return out
}
// ---------------------------------------------------------------------------
// Names
// ---------------------------------------------------------------------------
// decode_name_escapes resolves the #xx escapes in a raw name token.
// Returns the input unchanged (and allocates nothing) when there are none,
// which is the overwhelmingly common case.
decode_name_escapes :: proc(raw: string, allocator := context.allocator) -> string {
if strings.index_byte(raw, '#') < 0 {
return raw
}
b := strings.builder_make(0, len(raw), allocator)
for i := 0; i < len(raw); {
if raw[i] == '#' && i + 2 < len(raw) {
hi, hi_ok := hex_value(raw[i + 1])
lo, lo_ok := hex_value(raw[i + 2])
// "#" followed by anything that isn't two hex digits is kept
// verbatim: it is a broken name, not a reason to drop bytes.
if hi_ok && lo_ok {
strings.write_byte(&b, hi << 4 | lo)
i += 3
continue
}
}
strings.write_byte(&b, raw[i])
i += 1
}
return strings.to_string(b)
}
@(private)
hex_value :: proc "contextless" (c: byte) -> (val: byte, ok: bool) {
switch c {
case '0' ..= '9': return c - '0', true
case 'a' ..= 'f': return c - 'a' + 10, true
case 'A' ..= 'F': return c - 'A' + 10, true
}
return 0, false
}
// ---------------------------------------------------------------------------
// Numbers
// ---------------------------------------------------------------------------
// parse_number reads the number syntax PDF actually uses, which is looser than
// the grammar in the spec: a leading '+', repeated signs, a bare '.', a
// trailing '.', and exponent notation all turn up in files produced by real
// software. Anything unparsable reads as 0, the value a viewer substitutes.
parse_number :: proc(s: string) -> (value: Object, ok: bool) {
if len(s) == 0 { return i64(0), false }
is_real := false
for c in transmute([]byte)s {
if c == '.' || c == 'e' || c == 'E' {
is_real = true
break
}
}
if !is_real {
if v, iok := strconv.parse_i64_of_base(s, 10); iok {
return v, true
}
// Integers too large for i64 (seen in damaged files) clamp rather than
// failing, so a bad /Length cannot abort a parse on its own.
if v, fok := strconv.parse_f64(s); fok {
return v, true
}
return i64(0), false
}
if v, fok := strconv.parse_f64(s); fok {
return v, true
}
// ".5" and "4." are both legal PDF and both rejected by strict parsers.
cleaned := s
if len(cleaned) > 0 && cleaned[len(cleaned) - 1] == '.' {
cleaned = cleaned[:len(cleaned) - 1]
if v, fok := strconv.parse_f64(cleaned); fok {
return v, true
}
}
return f64(0), false
}