pdfrum_page/state/
marks.rs1use crate::ops::MarkProperties;
13use pdfrum_object::{Dict, Name};
14use std::sync::Arc;
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct Mark {
19 pub tag: Name,
21 pub properties: Option<Arc<Dict>>,
23 pub from_resources: bool,
27}
28
29impl Mark {
30 #[must_use]
32 pub fn content_id(&self) -> Option<i64> {
33 self.properties
34 .as_ref()
35 .and_then(|d| d.direct_int(&Name::from("MCID")))
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Default)]
43pub struct ContentMarks {
44 marks: Vec<Mark>,
45}
46
47impl ContentMarks {
48 #[must_use]
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 #[must_use]
56 pub fn marks(&self) -> &[Mark] {
57 &self.marks
58 }
59
60 #[must_use]
62 pub fn len(&self) -> usize {
63 self.marks.len()
64 }
65
66 #[must_use]
68 pub fn is_empty(&self) -> bool {
69 self.marks.is_empty()
70 }
71
72 pub fn push(&mut self, tag: Name) {
74 self.marks.push(Mark {
75 tag,
76 properties: None,
77 from_resources: false,
78 });
79 }
80
81 pub fn push_with_properties(
86 &mut self,
87 tag: Name,
88 properties: &MarkProperties,
89 resolve: impl FnOnce(&Name) -> Option<Dict>,
90 ) -> bool {
91 let (dict, from_resources) = match properties {
92 MarkProperties::Named(name) => match resolve(name) {
93 Some(d) => (d, true),
94 None => return false,
96 },
97 MarkProperties::Inline(d) => ((**d).clone(), false),
99 };
100 self.marks.push(Mark {
101 tag,
102 properties: Some(Arc::new(dict)),
103 from_resources,
104 });
105 true
106 }
107
108 pub fn pop(&mut self) -> bool {
113 self.marks.pop().is_some()
114 }
115
116 #[must_use]
118 pub fn content_id(&self) -> Option<i64> {
119 self.marks.iter().find_map(Mark::content_id)
120 }
121
122 #[must_use]
128 pub fn optional_content(&self) -> Option<&Dict> {
129 self.optional_content_all().into_iter().next()
130 }
131
132 #[must_use]
142 pub fn optional_content_all(&self) -> Vec<&Dict> {
143 self.marks
144 .iter()
145 .filter(|m| m.tag.as_bytes() == b"OC" && m.from_resources)
146 .filter_map(|m| m.properties.as_deref())
147 .collect()
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 #![allow(
157 clippy::unreadable_literal,
158 clippy::float_cmp,
159 clippy::indexing_slicing,
160 clippy::cast_precision_loss,
161 clippy::cast_possible_truncation,
162 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
163 )]
164
165 use super::ContentMarks;
166 use crate::ops::MarkProperties;
167 use pdfrum_object::{Dict, Name, Object};
168
169 fn mcid(n: i64) -> Dict {
170 Dict::from_pairs([(Name::from("MCID"), Object::Int(n))])
171 }
172
173 #[test]
174 fn emc_never_pops_past_the_bottom() {
175 let mut marks = ContentMarks::new();
176 assert!(!marks.pop());
178 assert!(!marks.pop());
179 assert!(marks.is_empty());
180 marks.push(Name::from("Span"));
181 assert!(marks.pop());
182 assert!(!marks.pop());
183 }
184
185 #[test]
186 fn an_unresolvable_named_property_list_pushes_nothing() {
187 let mut marks = ContentMarks::new();
188 let pushed = marks.push_with_properties(
189 Name::from("OC"),
190 &MarkProperties::Named(Name::from("MC0")),
191 |_| None,
192 );
193 assert!(!pushed);
194 assert!(marks.is_empty());
195 }
196
197 #[test]
198 fn the_first_mark_with_an_mcid_wins() {
199 let mut marks = ContentMarks::new();
200 marks.push(Name::from("Span"));
201 marks.push_with_properties(
202 Name::from("P"),
203 &MarkProperties::Inline(Box::new(mcid(7))),
204 |_| None,
205 );
206 marks.push_with_properties(
207 Name::from("Span"),
208 &MarkProperties::Inline(Box::new(mcid(9))),
209 |_| None,
210 );
211 assert_eq!(marks.content_id(), Some(7));
212 }
213
214 #[test]
215 fn an_inline_oc_dictionary_is_ignored_for_visibility() {
216 let mut marks = ContentMarks::new();
217 marks.push_with_properties(
219 Name::from("OC"),
220 &MarkProperties::Inline(Box::new(Dict::new())),
221 |_| None,
222 );
223 assert!(marks.optional_content().is_none());
224
225 let mut marks = ContentMarks::new();
227 marks.push_with_properties(
228 Name::from("OC"),
229 &MarkProperties::Named(Name::from("MC0")),
230 |_| Some(Dict::new()),
231 );
232 assert!(marks.optional_content().is_some());
233 }
234
235 #[test]
236 fn only_the_exact_oc_tag_counts() {
237 let mut marks = ContentMarks::new();
238 marks.push_with_properties(
239 Name::from("OCX"),
240 &MarkProperties::Named(Name::from("MC0")),
241 |_| Some(Dict::new()),
242 );
243 assert!(marks.optional_content().is_none());
244 }
245}