1use pdfrum_object::{Dict, Name, ObjRef, Object, Resolve};
11
12use crate::names;
13
14#[derive(Debug, Clone, PartialEq)]
20pub enum Kid {
21 Invalid,
24 Element {
32 dict: Dict,
34 reference: Option<ObjRef>,
36 slot: usize,
39 linked: Option<usize>,
41 },
42 PageContent {
44 content_id: i64,
46 },
47 StreamContent {
49 stream_obj_num: u32,
52 content_id: i64,
54 },
55 Object {
57 obj_num: u32,
60 },
61}
62
63#[derive(Debug, Clone, PartialEq)]
65pub struct StructElement {
66 pub dict: Dict,
68 pub reference: Option<ObjRef>,
70 pub kind: Vec<u8>,
73 pub kids: Vec<Kid>,
75 pub parent: Option<usize>,
78}
79
80impl StructElement {
81 pub fn load_kids<R: Resolve>(dict: &Dict, page_obj_num: u32, r: &R) -> Vec<Kid> {
88 let own_page = dict
90 .raw(names::PG)
91 .and_then(Object::as_ref_id)
92 .map_or(0, |reference| reference.num);
93
94 let Some(kids_obj) = dict.get(names::K, r).map(|k| k.get().clone()) else {
95 return Vec::new();
96 };
97 match &kids_obj {
98 Object::Array(array) => array
99 .iter()
100 .enumerate()
101 .map(|(slot, element)| {
102 let direct = element.resolve(r).ok().map(|d| d.get().clone());
103 load_kid(direct.as_ref(), element, own_page, page_obj_num, slot, r)
104 })
105 .collect(),
106 other => vec![load_kid(Some(other), other, own_page, page_obj_num, 0, r)],
107 }
108 }
109
110 pub fn link_kid(&mut self, target: &Dict, reference: Option<ObjRef>, element: usize) -> bool {
117 let mut matched = false;
118 for kid in &mut self.kids {
119 if let Kid::Element {
120 dict,
121 reference: kid_ref,
122 linked,
123 ..
124 } = kid
125 {
126 let same = match (*kid_ref, reference) {
127 (Some(a), Some(b)) => a == b,
128 _ => dict == target,
129 };
130 if same {
131 *linked = Some(element);
132 matched = true;
133 }
134 }
135 }
136 matched
137 }
138
139 #[must_use]
141 pub fn obj_type<R: Resolve>(&self, r: &R) -> Vec<u8> {
142 self.dict.byte_string(names::TYPE, r).unwrap_or_default()
143 }
144
145 #[must_use]
147 pub fn alt_text<R: Resolve>(&self, r: &R) -> String {
148 self.dict.text(names::ALT, r).unwrap_or_default()
149 }
150
151 #[must_use]
153 pub fn actual_text<R: Resolve>(&self, r: &R) -> String {
154 self.dict.text(names::ACTUAL_TEXT, r).unwrap_or_default()
155 }
156
157 #[must_use]
159 pub fn expansion<R: Resolve>(&self, r: &R) -> String {
160 self.dict.text(names::E, r).unwrap_or_default()
161 }
162
163 #[must_use]
165 pub fn title<R: Resolve>(&self, r: &R) -> String {
166 self.dict.text(names::T, r).unwrap_or_default()
167 }
168
169 #[must_use]
174 pub fn id(&self) -> Option<Vec<u8>> {
175 self.dict
176 .raw(names::ID)
177 .and_then(Object::as_string)
178 .map(|s| s.bytes.to_vec())
179 }
180
181 #[must_use]
186 pub fn lang(&self) -> Option<Vec<u8>> {
187 self.dict
188 .raw(names::LANG)
189 .and_then(Object::as_string)
190 .map(|s| s.bytes.to_vec())
191 }
192
193 #[must_use]
203 pub fn kid_content_id(&self, index: usize) -> Option<i64> {
204 match self.kids.get(index) {
205 Some(Kid::PageContent { content_id } | Kid::StreamContent { content_id, .. }) => {
206 Some(*content_id)
207 }
208 _ => None,
209 }
210 }
211}
212
213fn load_kid<R: Resolve>(
215 direct: Option<&Object>,
216 raw: &Object,
217 own_page: u32,
218 page_obj_num: u32,
219 slot: usize,
220 r: &R,
221) -> Kid {
222 let Some(direct) = direct else {
223 return Kid::Invalid;
224 };
225 match direct {
226 Object::Int(_) | Object::Real(_) => {
227 if own_page == page_obj_num {
228 Kid::PageContent {
229 content_id: direct.as_int().unwrap_or(0),
230 }
231 } else {
232 Kid::Invalid
233 }
234 }
235 Object::Dict(dict) => {
236 let page = dict
240 .raw(names::PG)
241 .and_then(Object::as_ref_id)
242 .map_or(own_page, |reference| reference.num);
243 let kind = dict.name(names::TYPE);
244 match kind {
245 Some(k) if k == names::MCR => {
246 if page != page_obj_num {
247 return Kid::Invalid;
248 }
249 Kid::StreamContent {
250 stream_obj_num: dict
251 .raw(names::STM)
252 .and_then(Object::as_ref_id)
253 .map_or(0, |reference| reference.num),
254 content_id: dict.int(names::MCID, r).unwrap_or(0),
255 }
256 }
257 Some(k) if k == names::OBJR => {
258 if page != page_obj_num {
259 return Kid::Invalid;
260 }
261 Kid::Object {
262 obj_num: dict
263 .raw(names::OBJ)
264 .and_then(Object::as_ref_id)
265 .map_or(0, |reference| reference.num),
266 }
267 }
268 _ => Kid::Element {
271 dict: dict.clone(),
272 reference: raw.as_ref_id(),
273 slot,
274 linked: None,
275 },
276 }
277 }
278 _ => Kid::Invalid,
279 }
280}
281
282#[must_use]
288pub(crate) fn marked_content_id_count<R: Resolve>(dict: &Dict, r: &R) -> Option<usize> {
289 match dict.get(names::K, r).map(|k| k.get().clone()) {
290 Some(Object::Int(_) | Object::Real(_) | Object::Dict(_)) => Some(1),
291 Some(Object::Array(array)) => Some(array.len()),
292 None | Some(_) => None,
294 }
295}
296
297#[must_use]
302pub(crate) fn marked_content_id_at<R: Resolve>(dict: &Dict, index: usize, r: &R) -> Option<i64> {
303 match dict.get(names::K, r).map(|k| k.get().clone()) {
304 Some(obj @ (Object::Int(_) | Object::Real(_))) => {
305 if index == 0 {
306 obj.as_int()
307 } else {
308 None
309 }
310 }
311 Some(Object::Dict(dict)) => mcid_from_dict(&dict, r),
313 Some(Object::Array(array)) => match array.get(index, r).map(|e| e.get().clone()) {
314 Some(obj @ (Object::Int(_) | Object::Real(_))) => obj.as_int(),
315 Some(Object::Dict(dict)) => mcid_from_dict(&dict, r),
316 _ => None,
317 },
318 _ => None,
319 }
320}
321
322fn mcid_from_dict<R: Resolve>(dict: &Dict, r: &R) -> Option<i64> {
325 if dict.name(names::TYPE) != Some(names::MCR) {
326 return None;
327 }
328 dict.get(names::MCID, r)
329 .and_then(|v| v.get().as_number().and_then(Object::as_int))
330}
331
332#[must_use]
334pub fn map_role(role_map: Option<&Dict>, kind: &[u8]) -> Vec<u8> {
335 role_map
336 .and_then(|map| map.name(&Name::new(kind.to_vec())))
337 .filter(|mapped| !mapped.as_bytes().is_empty())
338 .map_or_else(|| kind.to_vec(), |mapped| mapped.as_bytes().to_vec())
339}
340
341#[cfg(test)]
342mod tests {
343 use super::{Kid, StructElement, map_role, marked_content_id_at, marked_content_id_count};
344 use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
345
346 use crate::names;
347
348 fn dict(pairs: &[(&str, Object)]) -> Dict {
349 Dict::from_pairs(
350 pairs
351 .iter()
352 .map(|(k, v)| (Name::from(*k), v.clone()))
353 .collect::<Vec<_>>(),
354 )
355 }
356
357 #[test]
358 fn a_number_kid_is_page_content_only_for_its_own_page() {
359 let element = dict(&[("K", Object::Int(7))]);
360 assert_eq!(
362 StructElement::load_kids(&element, 0, &NoResolve),
363 vec![Kid::PageContent { content_id: 7 }]
364 );
365 assert_eq!(
366 StructElement::load_kids(&element, 4, &NoResolve),
367 vec![Kid::Invalid]
368 );
369 }
370
371 #[test]
372 fn a_dict_kid_with_no_recognized_type_is_a_nested_element() {
373 let inner = dict(&[("S", Object::Name(Name::from("P")))]);
374 let element = dict(&[("K", Object::Dict(inner.clone()))]);
375 let kids = StructElement::load_kids(&element, 0, &NoResolve);
376 assert_eq!(
377 kids,
378 vec![Kid::Element {
379 dict: inner,
380 reference: None,
381 slot: 0,
382 linked: None,
383 }]
384 );
385 }
386
387 #[test]
388 fn an_element_kid_is_never_page_tested() {
389 let inner = dict(&[("S", Object::Name(Name::from("P")))]);
390 let element = dict(&[("K", Object::Dict(inner))]);
391 assert!(matches!(
393 StructElement::load_kids(&element, 9, &NoResolve).as_slice(),
394 [Kid::Element { .. }]
395 ));
396 }
397
398 #[test]
399 fn a_marked_content_reference_needs_its_page_to_match() {
400 let mcr = dict(&[
401 ("Type", Object::Name(names::MCR.clone())),
402 ("MCID", Object::Int(3)),
403 ]);
404 let element = dict(&[("K", Object::Dict(mcr))]);
405 assert_eq!(
406 StructElement::load_kids(&element, 0, &NoResolve),
407 vec![Kid::StreamContent {
408 stream_obj_num: 0,
409 content_id: 3,
410 }]
411 );
412 assert_eq!(
413 StructElement::load_kids(&element, 1, &NoResolve),
414 vec![Kid::Invalid]
415 );
416 }
417
418 #[test]
419 fn the_unfiltered_count_ignores_the_page_and_answers_none_when_absent() {
420 assert_eq!(marked_content_id_count(&Dict::new(), &NoResolve), None);
421 assert_eq!(
422 marked_content_id_count(&dict(&[("K", Object::Int(0))]), &NoResolve),
423 Some(1)
424 );
425 assert_eq!(
426 marked_content_id_count(
427 &dict(&[(
428 "K",
429 Object::Array(Array::of([Object::Int(2), Object::Int(3)]))
430 )]),
431 &NoResolve
432 ),
433 Some(2)
434 );
435 assert_eq!(
436 marked_content_id_count(
437 &dict(&[("K", Object::Str(PdfString::literal(b"nope")))]),
438 &NoResolve
439 ),
440 None
441 );
442 }
443
444 #[test]
445 fn a_marked_content_reference_needs_the_name_type_and_a_number_mcid() {
446 let good = dict(&[(
447 "K",
448 Object::Dict(dict(&[
449 ("Type", Object::Name(names::MCR.clone())),
450 ("MCID", Object::Int(5)),
451 ])),
452 )]);
453 assert_eq!(marked_content_id_at(&good, 0, &NoResolve), Some(5));
454 assert_eq!(marked_content_id_at(&good, 9, &NoResolve), Some(5));
456
457 let wrong_type = dict(&[(
458 "K",
459 Object::Dict(dict(&[
460 ("Type", Object::Name(Name::from("Other"))),
461 ("MCID", Object::Int(5)),
462 ])),
463 )]);
464 assert_eq!(marked_content_id_at(&wrong_type, 0, &NoResolve), None);
465 }
466
467 #[test]
468 fn linking_a_kid_fills_its_element_index_and_leaves_its_slot_alone() {
469 let inner = dict(&[("S", Object::Name(Name::from("P")))]);
473 let mut element = StructElement {
474 dict: Dict::new(),
475 reference: None,
476 kind: b"Document".to_vec(),
477 kids: StructElement::load_kids(
478 &dict(&[("K", Object::Dict(inner.clone()))]),
479 0,
480 &NoResolve,
481 ),
482 parent: None,
483 };
484 assert!(element.link_kid(&inner, None, 7));
485 assert_eq!(
486 element.kids,
487 vec![Kid::Element {
488 dict: inner,
489 reference: None,
490 slot: 0,
492 linked: Some(7),
494 }]
495 );
496 }
497
498 #[test]
499 fn an_unmatched_kid_keeps_no_element_index_at_all() {
500 let inner = dict(&[("S", Object::Name(Name::from("P")))]);
501 let mut element = StructElement {
502 dict: Dict::new(),
503 reference: None,
504 kind: b"Document".to_vec(),
505 kids: StructElement::load_kids(&dict(&[("K", Object::Dict(inner))]), 0, &NoResolve),
506 parent: None,
507 };
508 let stranger = dict(&[("S", Object::Name(Name::from("Span")))]);
509 assert!(!element.link_kid(&stranger, None, 7));
510 assert!(matches!(
511 element.kids.first(),
512 Some(Kid::Element { linked: None, .. })
513 ));
514 }
515
516 #[test]
517 fn the_role_map_replaces_a_type_only_when_it_names_a_nonempty_one() {
518 let map = dict(&[("Quote", Object::Name(Name::from("BlockQuote")))]);
519 assert_eq!(map_role(Some(&map), b"Quote"), b"BlockQuote");
520 assert_eq!(map_role(Some(&map), b"P"), b"P");
521 assert_eq!(map_role(None, b"P"), b"P");
522 }
523}