Files
2026-09-25 23:32:29 +02:00

472 lines
18 KiB
Odin

package pdf
import "core:math"
// ---------------------------------------------------------------------------
// Page tree -- ISO 32000-2, 7.7.3
//
// The catalog's /Pages is the root of a tree whose interior nodes (/Type
// /Pages) carry /Kids and whose leaves (/Type /Page) are the pages. Four
// attributes -- /Resources, /MediaBox, /CropBox and /Rotate -- are inherited
// from the nearest ancestor that supplies them (table 30).
//
// The tree is flattened once, at open, into `doc.pages`. Inheritance is
// resolved on the way *down*, by carrying the ancestors' values along the path
// and writing whichever ones the leaf lacks into the leaf's own dictionary.
// Doing it downwards rather than by following /Parent upwards from each page is
// what makes a wrong or cyclic /Parent chain harmless: the attributes come from
// the path actually taken to reach the page, which is the path the file's own
// structure defines. It also means every later reader of a page dictionary --
// the content interpreter, the annotation code -- sees a complete dictionary
// and never has to know inheritance exists.
// ---------------------------------------------------------------------------
// Page is one page of the document with its inherited attributes resolved.
//
// A Page borrows from the Document: `dict` and `resources` live in the
// Document's arena and become invalid when `close` is called. It is a value,
// cheap to copy, and holds no state of its own -- fetching the same index twice
// yields two equal Pages.
Page :: struct {
doc: ^Document,
// Position in the flattened page list, 0-based.
index: int,
// The page's object number, or {0,0} when /Kids held the page as a direct
// dictionary rather than a reference.
ref: Ref,
dict: ^Dict,
// /Resources after inheritance. nil for a page that names none anywhere in
// its ancestry, which is legal and means "no resources"; the content
// interpreter treats every name lookup in it as a miss.
resources: ^Dict,
// Always finite and non-empty: a page whose /MediaBox is missing, damaged
// or degenerate gets DEFAULT_PAGE_RECT instead.
media_box: Rect,
// /CropBox intersected with /MediaBox, falling back to /MediaBox. This is
// the rectangle a viewer displays.
crop_box: Rect,
// Clockwise, normalised to one of 0, 90, 180, 270.
rotate: int,
// /UserUnit: the size in points of one unit of page space, default 1. A
// large-format drawing uses it to stay inside the format's 14400-point
// limit, so ignoring it renders such a page at the wrong scale.
user_unit: f64,
}
// ---------------------------------------------------------------------------
// Building the flattened list
// ---------------------------------------------------------------------------
// Inherited carries the four inheritable attributes down the walk as
// unresolved objects. They are copied verbatim -- a Ref stays a Ref -- so that
// writing one into a leaf costs nothing and shares rather than duplicates.
@(private)
Inherited :: struct {
resources: Object,
media_box: Object,
crop_box: Object,
rotate: Object,
}
// pagetree_build flattens the page tree into doc.pages and doc.page_refs.
//
// It returns .No_Pages only when both the tree walk and the fallback scan came
// up empty, because a document with no page objects anywhere in it is the one
// case where there is genuinely nothing to hand back.
@(private)
pagetree_build :: proc(doc: ^Document) -> Error {
if doc == nil { return .Invalid_Argument }
clear(&doc.pages)
clear(&doc.page_refs)
if root := dict_dict(doc, doc.catalog, "Pages"); root != nil {
// Only interior nodes go in the visited set. A leaf cannot close a
// cycle -- it has no kids to recurse through -- and files that
// reference one page object from two places are relying on it
// appearing twice, so marking leaves visited would silently drop
// pages.
// Deliberately *not* the Document arena: this is scratch for the walk,
// and the arena is never reclaimed before `close`. Document-arena
// memory is also never freed, so a set allocated there could not be
// released here without breaking that rule.
visited := make(map[u32]bool, 64, context.allocator)
defer delete(visited)
// Fuel bounds the total node visits, not just the depth. A tree can be
// shallow, acyclic and still enormous if /Kids arrays are shared
// between parents, which is how a small file expands into a very long
// walk.
fuel := MAX_PAGES
root_ref, _ := dict_ref(doc.catalog, "Pages")
pagetree_walk(doc, root, root_ref, Inherited{}, 0, &visited, &fuel)
}
// No catalog, an unusable /Pages, or a tree whose leaves were all
// unreadable. Objects are still objects: look for pages directly.
if len(doc.pages) == 0 {
pagetree_scan(doc)
}
if len(doc.pages) == 0 {
return .No_Pages
}
return .None
}
// pagetree_walk visits one node, appending any leaves beneath it.
@(private)
pagetree_walk :: proc(
doc: ^Document,
node: ^Dict,
node_ref: Ref,
inherited: Inherited,
depth: int,
visited: ^map[u32]bool,
fuel: ^int,
) {
if node == nil { return }
if depth >= MAX_DEPTH {
add_repair(doc, .Missing_Page_Node, 0, node_ref)
return
}
if fuel^ <= 0 { return }
fuel^ -= 1
if len(doc.pages) >= MAX_PAGES { return }
type := dict_type(doc, node)
kids := dict_array(doc, node, "Kids")
// /Type is checked before /Kids because a leaf that also carries a stray
// /Kids -- a page edited by a tool that did not clean up after itself --
// is a page, not an interior node.
if type != "Page" && array_len(kids) > 0 {
if node_ref.num != 0 {
if visited[node_ref.num] or_else false {
add_repair(doc, .Cyclic_Page_Tree, 0, node_ref)
return
}
visited[node_ref.num] = true
}
down := pagetree_inherit(node, inherited)
for i in 0 ..< array_len(kids) {
if fuel^ <= 0 { break }
raw := array_get_raw(kids, i)
kid_ref, _ := to_ref(raw)
kid, ok := to_dict(resolve(doc, raw))
if !ok {
add_repair(doc, .Missing_Page_Node, 0, kid_ref)
continue
}
pagetree_walk(doc, kid, kid_ref, down, depth + 1, visited, fuel)
}
// Unmarked on the way out, so that a node legitimately reachable by two
// different paths is walked for each of them. Only a node reachable
// from *itself* is a cycle. This cannot be a `defer` at the point the
// mark is set: that scope closes before the kids are walked.
if node_ref.num != 0 {
delete_key(visited, node_ref.num)
}
return
}
// An interior node with no kids contributes nothing. Anything else is a
// leaf: /Type /Page, and also the untyped dictionaries that turn up in
// /Kids from producers that omit it, which are pages by position.
if type == "Pages" { return }
pagetree_add_leaf(doc, node, node_ref, inherited)
}
// pagetree_inherit returns the inherited set as seen by this node's children:
// the parent's values, overridden by whichever ones this node supplies.
@(private)
pagetree_inherit :: proc(node: ^Dict, parent: Inherited) -> (out: Inherited) {
out = parent
if dict_has(node, "Resources") { out.resources = dict_get_raw(node, "Resources") }
if dict_has(node, "MediaBox") { out.media_box = dict_get_raw(node, "MediaBox") }
if dict_has(node, "CropBox") { out.crop_box = dict_get_raw(node, "CropBox") }
if dict_has(node, "Rotate") { out.rotate = dict_get_raw(node, "Rotate") }
return
}
// pagetree_add_leaf records one page, first filling in the inheritable keys it
// does not define for itself.
@(private)
pagetree_add_leaf :: proc(doc: ^Document, leaf: ^Dict, ref: Ref, inherited: Inherited) {
if len(doc.pages) >= MAX_PAGES { return }
if !dict_has(leaf, "Resources") && inherited.resources != nil {
dict_set(leaf, "Resources", inherited.resources)
}
if !dict_has(leaf, "MediaBox") && inherited.media_box != nil {
dict_set(leaf, "MediaBox", inherited.media_box)
}
if !dict_has(leaf, "CropBox") && inherited.crop_box != nil {
dict_set(leaf, "CropBox", inherited.crop_box)
}
if !dict_has(leaf, "Rotate") && inherited.rotate != nil {
dict_set(leaf, "Rotate", inherited.rotate)
}
append(&doc.pages, leaf)
append(&doc.page_refs, ref)
}
// pagetree_scan finds pages by looking at every object in the file, for
// documents whose catalog or page tree is unusable.
//
// The order is object number order, which is not document order. It is the only
// ordering available once the tree is gone, and it is right far more often than
// it is wrong: nearly every producer writes pages in order.
@(private)
pagetree_scan :: proc(doc: ^Document) {
for num in 1 ..< len(doc.xref) {
if len(doc.pages) >= MAX_PAGES { break }
ref := Ref{u32(num), doc.xref[num].gen}
d, ok := to_dict(get_object(doc, ref))
if !ok { continue }
// Without a tree to place them, only an explicit /Type /Page is
// trustworthy: a resource dictionary or an annotation would otherwise
// be swept up as a page.
if dict_type(doc, d) != "Page" { continue }
pagetree_inherit_upwards(doc, d)
append(&doc.pages, d)
append(&doc.page_refs, ref)
}
if len(doc.pages) > 0 {
add_repair(doc, .Missing_Page_Node, 0, {})
}
}
// pagetree_inherit_upwards resolves inheritance by following /Parent, for pages
// that were found by scanning and so have no known path from the root.
//
// The visited set is what makes this safe: a /Parent chain that loops back on
// itself is a real thing in damaged files, and is the whole reason the normal
// path walks downwards instead.
@(private)
pagetree_inherit_upwards :: proc(doc: ^Document, leaf: ^Dict) {
visited: map[u32]bool
defer delete(visited)
node := leaf
for _ in 0 ..< MAX_DEPTH {
parent_ref, has_ref := dict_ref(node, "Parent")
if has_ref {
if visited[parent_ref.num] or_else false {
add_repair(doc, .Cyclic_Page_Tree, 0, parent_ref)
return
}
visited[parent_ref.num] = true
}
parent, ok := to_dict(dict_get(doc, node, "Parent"))
if !ok || parent == leaf { return }
if !dict_has(leaf, "Resources") && dict_has(parent, "Resources") {
dict_set(leaf, "Resources", dict_get_raw(parent, "Resources"))
}
if !dict_has(leaf, "MediaBox") && dict_has(parent, "MediaBox") {
dict_set(leaf, "MediaBox", dict_get_raw(parent, "MediaBox"))
}
if !dict_has(leaf, "CropBox") && dict_has(parent, "CropBox") {
dict_set(leaf, "CropBox", dict_get_raw(parent, "CropBox"))
}
if !dict_has(leaf, "Rotate") && dict_has(parent, "Rotate") {
dict_set(leaf, "Rotate", dict_get_raw(parent, "Rotate"))
}
node = parent
}
}
// ---------------------------------------------------------------------------
// Public page access
// ---------------------------------------------------------------------------
// page_count is the number of pages the document has.
//
// This is the flattened count, not the catalog's /Count, which is a hint that
// damaged files get wrong and which this library never trusts.
page_count :: proc(doc: ^Document) -> int {
if doc == nil { return 0 }
return len(doc.pages)
}
// page_get returns page `index`, 0-based, with inherited attributes resolved
// and every geometric value guaranteed finite.
page_get :: proc(doc: ^Document, index: int) -> (page: Page, err: Error) {
if doc == nil { return {}, .Invalid_Argument }
if index < 0 || index >= len(doc.pages) { return {}, .Page_Out_Of_Range }
d := doc.pages[index]
page.doc = doc
page.index = index
page.dict = d
if index < len(doc.page_refs) {
page.ref = doc.page_refs[index]
}
page.resources = dict_dict(doc, d, "Resources")
// A missing, unparsable or zero-area /MediaBox is common enough -- and
// fatal enough to a renderer that divides by the page height -- that a
// substitute is the only sane answer. Viewers all pick one; this one picks
// A4, which is right more often than Letter outside North America and
// wrong by a survivable margin either way.
mb := dict_rect(doc, d, "MediaBox", RECT_EMPTY)
if rect_is_empty(mb) || !rect_is_finite(mb) {
mb = DEFAULT_PAGE_RECT
}
page.media_box = mb
// /CropBox is required to lie inside /MediaBox; when it does not, the
// intersection is what the format says to display (ISO 32000-2, 14.11.2).
cb := dict_rect(doc, d, "CropBox", RECT_EMPTY)
if rect_is_empty(cb) || !rect_is_finite(cb) {
cb = mb
} else {
cb = rect_intersect(cb, mb)
if rect_is_empty(cb) {
cb = mb
}
}
page.crop_box = cb
page.rotate = page_normalize_rotate(dict_int(doc, d, "Rotate", 0))
// /UserUnit is inheritable in no version of the format, so it is read from
// the page alone. A non-positive or non-finite value means the producer
// wrote nonsense; 1 is the default and the only safe substitute.
page.user_unit = dict_num(doc, d, "UserUnit", 1)
if !is_finite(page.user_unit) || page.user_unit <= 0 {
page.user_unit = 1
}
return page, .None
}
// page_normalize_rotate folds /Rotate into 0, 90, 180 or 270.
//
// The format requires a multiple of 90 and files write 45, 360 and -90 anyway.
// Negatives are legal and mean anticlockwise; the rest are rounded down to the
// quarter turn below them, which is what viewers do.
@(private)
page_normalize_rotate :: proc(raw: i64) -> int {
// Reduced in i64 first: /Rotate is file-controlled and can arrive as a
// value whose magnitude would overflow a narrower type.
r := raw % 360
if r < 0 { r += 360 }
return int(r / 90 * 90)
}
// page_index_of returns the position of a page object in the flattened list, or
// -1. Destinations and outline entries name a page by reference, and this is
// how they become a page number.
page_index_of :: proc(doc: ^Document, ref: Ref) -> int {
if doc == nil || ref.num == 0 { return -1 }
for r, i in doc.page_refs {
if r.num == ref.num { return i }
}
return -1
}
// ---------------------------------------------------------------------------
// Page geometry and content
//
// These three are what a consumer of the library reaches for first: how big is
// the page, how big is the bitmap, and what does it draw. They are deliberately
// not methods on the Document -- a Page is a value, cheap to copy, and holds
// everything they need.
// ---------------------------------------------------------------------------
// page_bounds is the rectangle a viewer displays: the crop box, already
// intersected with the media box by page_get, and guaranteed finite and
// non-empty.
page_bounds :: proc(page: ^Page) -> Rect {
if page == nil { return DEFAULT_PAGE_RECT }
return page.crop_box
}
// page_size is the pixel size of the page at `scale`, with /Rotate applied, so
// a page rotated 90 degrees comes back landscape. /UserUnit is included:
// ignoring it renders a large-format drawing at the wrong size.
//
// The result is clamped to at least 1x1 -- a zero-sized bitmap is not something
// a caller can do anything useful with, and every path that produces one here
// comes from a damaged page.
page_size :: proc(page: ^Page, scale: f64 = 1) -> (width, height: int) {
if page == nil { return 1, 1 }
r := page_bounds(page)
unit := page.user_unit
if !is_finite(unit) || unit <= 0 { unit = 1 }
s := scale * unit
if !is_finite(s) || s <= 0 { s = 1 }
w := rect_width(r) * s
h := rect_height(r) * s
if page.rotate == 90 || page.rotate == 270 {
w, h = h, w
}
if !is_finite(w) || !is_finite(h) { return 1, 1 }
// Round outward, not to nearest: a page 100.2 pixels wide needs 101
// columns, or the last fraction of content falls off the right edge. The
// epsilon keeps an exact 200.0 -- which floating-point scaling routinely
// renders as 200.00000000000003 -- from claiming a 201st column.
width = max(1, int(math.ceil(w - 1e-9)))
height = max(1, int(math.ceil(h - 1e-9)))
return
}
// page_content concatenates the page's content streams into one buffer,
// allocated with `allocator` and owned by the caller.
//
// /Contents is one stream or an array of them, and the format explicitly allows
// a single operator -- even a single token -- to be split across the boundary
// between two of them (ISO 32000-2, 7.8.2). They are therefore joined with a
// newline between, never concatenated bare, or "1 0 0 1 50" + "50 cm" would
// lex as "501" and "cm". A stream that fails to decode contributes nothing and
// leaves the rest intact, because one broken stream should not blank the page.
page_content :: proc(page: ^Page, allocator := context.allocator) -> (data: []byte, err: Error) {
if page == nil || page.doc == nil { return nil, .Invalid_Argument }
doc := page.doc
out := make([dynamic]byte, 0, 4096, allocator)
contents := dict_get(doc, page.dict, "Contents")
add :: proc(doc: ^Document, out: ^[dynamic]byte, s: ^Stream) {
if s == nil { return }
bytes, derr := stream_data(doc, s)
if derr != .None || len(bytes) == 0 { return }
if len(out) > 0 {
append(out, byte('\n'))
}
append(out, ..bytes)
}
#partial switch v in contents {
case ^Stream:
add(doc, &out, v)
case ^Array:
// Cap the number of parts: /Contents is attacker-controlled, and an
// array of a million references would otherwise be walked in full.
n := min(array_len(v), MAX_CONTENT_PARTS)
for i in 0 ..< n {
s, ok := to_stream(array_get(doc, v, i))
if ok {
add(doc, &out, s)
}
}
}
return out[:], .None
}
// MAX_CONTENT_PARTS bounds the /Contents array. Real documents use one stream,
// or a handful when a producer emits one per layer; a thousand is already
// pathological.
MAX_CONTENT_PARTS :: 4096