1use folio_core::{FolioError, Result};
4use folio_cos::{CosDoc, ObjectId, PdfObject};
5
6#[derive(Debug, Clone)]
8pub struct Bookmark {
9 id: ObjectId,
11 title: String,
13 dest: Option<PdfObject>,
15 action: Option<PdfObject>,
16 first_child: Option<ObjectId>,
18 last_child: Option<ObjectId>,
19 next: Option<ObjectId>,
20 prev: Option<ObjectId>,
21 parent: Option<ObjectId>,
22 count: i64,
24 flags: i32,
26 color: Option<[f64; 3]>,
28}
29
30impl Bookmark {
31 pub fn load(obj_num: u32, doc: &mut CosDoc) -> Result<Self> {
33 let obj = doc
34 .get_object(obj_num)?
35 .ok_or_else(|| FolioError::InvalidObject(format!("Bookmark {} not found", obj_num)))?
36 .clone();
37
38 let dict = obj
39 .as_dict()
40 .ok_or_else(|| FolioError::InvalidObject("Bookmark is not a dict".into()))?;
41
42 let title = dict
43 .get(b"Title".as_slice())
44 .and_then(|o| o.as_str())
45 .map(decode_text)
46 .unwrap_or_default();
47
48 Ok(Self {
49 id: ObjectId::new(obj_num, 0),
50 title,
51 dest: dict.get(b"Dest".as_slice()).cloned(),
52 action: dict.get(b"A".as_slice()).cloned(),
53 first_child: dict.get(b"First".as_slice()).and_then(|o| o.as_reference()),
54 last_child: dict.get(b"Last".as_slice()).and_then(|o| o.as_reference()),
55 next: dict.get(b"Next".as_slice()).and_then(|o| o.as_reference()),
56 prev: dict.get(b"Prev".as_slice()).and_then(|o| o.as_reference()),
57 parent: dict
58 .get(b"Parent".as_slice())
59 .and_then(|o| o.as_reference()),
60 count: dict
61 .get(b"Count".as_slice())
62 .and_then(|o| o.as_i64())
63 .unwrap_or(0),
64 flags: dict
65 .get(b"F".as_slice())
66 .and_then(|o| o.as_i64())
67 .unwrap_or(0) as i32,
68 color: dict.get(b"C".as_slice()).and_then(|o| {
69 let arr = o.as_array()?;
70 if arr.len() >= 3 {
71 Some([arr[0].as_f64()?, arr[1].as_f64()?, arr[2].as_f64()?])
72 } else {
73 None
74 }
75 }),
76 })
77 }
78
79 pub fn title(&self) -> &str {
81 &self.title
82 }
83
84 pub fn id(&self) -> ObjectId {
86 self.id
87 }
88
89 pub fn is_open(&self) -> bool {
91 self.count > 0
92 }
93
94 pub fn is_italic(&self) -> bool {
96 self.flags & 1 != 0
97 }
98
99 pub fn is_bold(&self) -> bool {
101 self.flags & 2 != 0
102 }
103
104 pub fn color(&self) -> Option<[f64; 3]> {
106 self.color
107 }
108
109 pub fn destination(&self) -> Option<&PdfObject> {
111 self.dest.as_ref()
112 }
113
114 pub fn action(&self) -> Option<&PdfObject> {
116 self.action.as_ref()
117 }
118
119 pub fn first_child(&self) -> Option<ObjectId> {
121 self.first_child
122 }
123
124 pub fn next(&self) -> Option<ObjectId> {
126 self.next
127 }
128
129 pub fn prev(&self) -> Option<ObjectId> {
131 self.prev
132 }
133
134 pub fn parent(&self) -> Option<ObjectId> {
136 self.parent
137 }
138
139 pub fn has_children(&self) -> bool {
141 self.first_child.is_some()
142 }
143
144 pub fn get_all(doc: &mut CosDoc) -> Result<Vec<(Bookmark, u32)>> {
146 let catalog_ref = doc
147 .trailer()
148 .get(b"Root".as_slice())
149 .and_then(|o| o.as_reference())
150 .ok_or_else(|| FolioError::InvalidObject("No /Root".into()))?;
151
152 let catalog = doc
153 .get_object(catalog_ref.num)?
154 .ok_or_else(|| FolioError::InvalidObject("Catalog not found".into()))?
155 .clone();
156
157 let outlines_ref = match catalog.dict_get(b"Outlines") {
158 Some(PdfObject::Reference(id)) => *id,
159 _ => return Ok(Vec::new()),
160 };
161
162 let outlines = doc
163 .get_object(outlines_ref.num)?
164 .ok_or_else(|| FolioError::InvalidObject("Outlines not found".into()))?
165 .clone();
166
167 let first = match outlines.dict_get(b"First") {
168 Some(PdfObject::Reference(id)) => *id,
169 _ => return Ok(Vec::new()),
170 };
171
172 let mut result = Vec::new();
173 Self::collect_bookmarks(first, doc, 0, &mut result)?;
174 Ok(result)
175 }
176
177 fn collect_bookmarks(
178 id: ObjectId,
179 doc: &mut CosDoc,
180 depth: u32,
181 result: &mut Vec<(Bookmark, u32)>,
182 ) -> Result<()> {
183 let bm = Bookmark::load(id.num, doc)?;
184 let next = bm.next;
185 let first_child = bm.first_child;
186 result.push((bm, depth));
187
188 if let Some(child_id) = first_child {
190 Self::collect_bookmarks(child_id, doc, depth + 1, result)?;
191 }
192
193 if let Some(next_id) = next {
195 Self::collect_bookmarks(next_id, doc, depth, result)?;
196 }
197
198 Ok(())
199 }
200}
201
202fn decode_text(data: &[u8]) -> String {
203 if data.len() >= 2 && data[0] == 0xFE && data[1] == 0xFF {
204 let mut chars = Vec::new();
205 let mut i = 2;
206 while i + 1 < data.len() {
207 chars.push(((data[i] as u16) << 8) | (data[i + 1] as u16));
208 i += 2;
209 }
210 String::from_utf16_lossy(&chars)
211 } else {
212 String::from_utf8_lossy(data).into_owned()
213 }
214}