Skip to main content

fop_render/pdf/document/
outline.rs

1//! PDF outline (bookmark) serialization
2//!
3//! Functions to write PDF outline (bookmark) objects into a PDF byte stream.
4
5use super::types::{PdfOutline, PdfOutlineItem};
6
7/// Count total number of outline objects needed (root + all items)
8pub(super) fn count_outline_objects(outline: &PdfOutline) -> usize {
9    1 + count_outline_items(&outline.items)
10}
11
12/// Count outline items recursively
13pub(super) fn count_outline_items(items: &[PdfOutlineItem]) -> usize {
14    let mut count = items.len();
15    for item in items {
16        count += count_outline_items(&item.children);
17    }
18    count
19}
20
21/// Write outline objects to PDF bytes
22pub(super) fn write_outline_objects(
23    outline: &PdfOutline,
24    bytes: &mut Vec<u8>,
25    xref_offsets: &mut Vec<usize>,
26    first_obj_id: usize,
27    page_obj_ids: &[usize],
28) {
29    let mut next_obj_id = first_obj_id;
30
31    // Object 4: Outlines root
32    let outlines_root_id = next_obj_id;
33    next_obj_id += 1;
34
35    let total_count = count_outline_items(&outline.items);
36
37    // Write outlines root
38    xref_offsets.push(bytes.len());
39    bytes.extend_from_slice(format!("{} 0 obj\n", outlines_root_id).as_bytes());
40    bytes.extend_from_slice(b"<<\n");
41    bytes.extend_from_slice(b"/Type /Outlines\n");
42
43    if !outline.items.is_empty() {
44        let first_child_id = next_obj_id;
45        let last_child_id = next_obj_id + outline.items.len() - 1;
46        bytes.extend_from_slice(format!("/First {} 0 R\n", first_child_id).as_bytes());
47        bytes.extend_from_slice(format!("/Last {} 0 R\n", last_child_id).as_bytes());
48    }
49
50    bytes.extend_from_slice(format!("/Count {}\n", total_count).as_bytes());
51    bytes.extend_from_slice(b">>\n");
52    bytes.extend_from_slice(b"endobj\n");
53
54    // Write outline items
55    if !outline.items.is_empty() {
56        write_outline_items(
57            &outline.items,
58            bytes,
59            xref_offsets,
60            &mut next_obj_id,
61            outlines_root_id,
62            page_obj_ids,
63        );
64    }
65}
66
67/// Write outline items recursively
68#[allow(clippy::too_many_arguments)]
69pub(super) fn write_outline_items(
70    items: &[PdfOutlineItem],
71    bytes: &mut Vec<u8>,
72    xref_offsets: &mut Vec<usize>,
73    next_obj_id: &mut usize,
74    parent_id: usize,
75    page_obj_ids: &[usize],
76) {
77    let start_obj_id = *next_obj_id;
78    let item_count = items.len();
79
80    // Reserve object IDs for all items at this level
81    let end_obj_id = start_obj_id + item_count;
82
83    for (idx, item) in items.iter().enumerate() {
84        let obj_id = start_obj_id + idx;
85        *next_obj_id = obj_id + 1;
86
87        xref_offsets.push(bytes.len());
88        bytes.extend_from_slice(format!("{} 0 obj\n", obj_id).as_bytes());
89        bytes.extend_from_slice(b"<<\n");
90
91        // Escape title for PDF string
92        let escaped_title = escape_pdf_string(&item.title);
93        bytes.extend_from_slice(format!("/Title ({})\n", escaped_title).as_bytes());
94        bytes.extend_from_slice(format!("/Parent {} 0 R\n", parent_id).as_bytes());
95
96        // Add prev/next pointers
97        if idx > 0 {
98            bytes.extend_from_slice(format!("/Prev {} 0 R\n", obj_id - 1).as_bytes());
99        }
100        if idx < item_count - 1 {
101            bytes.extend_from_slice(format!("/Next {} 0 R\n", obj_id + 1).as_bytes());
102        }
103
104        // Add destination
105        if let Some(page_idx) = item.page_index {
106            if page_idx < page_obj_ids.len() {
107                let page_obj_id = page_obj_ids[page_idx];
108                bytes.extend_from_slice(
109                    format!("/Dest [{} 0 R /XYZ 0 792 0]\n", page_obj_id).as_bytes(),
110                );
111            }
112        }
113
114        // Add children if present
115        if !item.children.is_empty() {
116            let first_child_id = end_obj_id + count_child_offset(items, idx);
117            let child_count = item.children.len();
118            let last_child_id = first_child_id + child_count - 1;
119
120            bytes.extend_from_slice(format!("/First {} 0 R\n", first_child_id).as_bytes());
121            bytes.extend_from_slice(format!("/Last {} 0 R\n", last_child_id).as_bytes());
122            bytes.extend_from_slice(format!("/Count {}\n", child_count).as_bytes());
123        }
124
125        bytes.extend_from_slice(b">>\n");
126        bytes.extend_from_slice(b"endobj\n");
127    }
128
129    // Now write all children
130    *next_obj_id = end_obj_id;
131    for (idx, item) in items.iter().enumerate() {
132        if !item.children.is_empty() {
133            let parent = start_obj_id + idx;
134            write_outline_items(
135                &item.children,
136                bytes,
137                xref_offsets,
138                next_obj_id,
139                parent,
140                page_obj_ids,
141            );
142        }
143    }
144}
145
146/// Calculate offset to first child of an item
147fn count_child_offset(items: &[PdfOutlineItem], current_idx: usize) -> usize {
148    let mut offset = 0;
149    for item in items.iter().take(current_idx) {
150        offset += count_outline_items(&item.children);
151    }
152    offset
153}
154
155/// Escape special characters in PDF strings
156pub(super) fn escape_pdf_string(s: &str) -> String {
157    s.replace('\\', "\\\\")
158        .replace('(', "\\(")
159        .replace(')', "\\)")
160        .replace('\r', "\\r")
161        .replace('\n', "\\n")
162}