initial commit
This commit is contained in:
+474
@@ -0,0 +1,474 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user