use crate::ops::MarkProperties;
use pdfrum_object::{Dict, Name};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub struct Mark {
pub tag: Name,
pub properties: Option<Arc<Dict>>,
pub from_resources: bool,
}
impl Mark {
#[must_use]
pub fn content_id(&self) -> Option<i64> {
self.properties
.as_ref()
.and_then(|d| d.direct_int(&Name::from("MCID")))
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ContentMarks {
marks: Vec<Mark>,
}
impl ContentMarks {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn marks(&self) -> &[Mark] {
&self.marks
}
#[must_use]
pub fn len(&self) -> usize {
self.marks.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.marks.is_empty()
}
pub fn push(&mut self, tag: Name) {
self.marks.push(Mark {
tag,
properties: None,
from_resources: false,
});
}
pub fn push_with_properties(
&mut self,
tag: Name,
properties: &MarkProperties,
resolve: impl FnOnce(&Name) -> Option<Dict>,
) -> bool {
let (dict, from_resources) = match properties {
MarkProperties::Named(name) => match resolve(name) {
Some(d) => (d, true),
None => return false,
},
MarkProperties::Inline(d) => ((**d).clone(), false),
};
self.marks.push(Mark {
tag,
properties: Some(Arc::new(dict)),
from_resources,
});
true
}
pub fn pop(&mut self) -> bool {
self.marks.pop().is_some()
}
#[must_use]
pub fn content_id(&self) -> Option<i64> {
self.marks.iter().find_map(Mark::content_id)
}
#[must_use]
pub fn optional_content(&self) -> Option<&Dict> {
self.optional_content_all().into_iter().next()
}
#[must_use]
pub fn optional_content_all(&self) -> Vec<&Dict> {
self.marks
.iter()
.filter(|m| m.tag.as_bytes() == b"OC" && m.from_resources)
.filter_map(|m| m.properties.as_deref())
.collect()
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::ContentMarks;
use crate::ops::MarkProperties;
use pdfrum_object::{Dict, Name, Object};
fn mcid(n: i64) -> Dict {
Dict::from_pairs([(Name::from("MCID"), Object::Int(n))])
}
#[test]
fn emc_never_pops_past_the_bottom() {
let mut marks = ContentMarks::new();
assert!(!marks.pop());
assert!(!marks.pop());
assert!(marks.is_empty());
marks.push(Name::from("Span"));
assert!(marks.pop());
assert!(!marks.pop());
}
#[test]
fn an_unresolvable_named_property_list_pushes_nothing() {
let mut marks = ContentMarks::new();
let pushed = marks.push_with_properties(
Name::from("OC"),
&MarkProperties::Named(Name::from("MC0")),
|_| None,
);
assert!(!pushed);
assert!(marks.is_empty());
}
#[test]
fn the_first_mark_with_an_mcid_wins() {
let mut marks = ContentMarks::new();
marks.push(Name::from("Span"));
marks.push_with_properties(
Name::from("P"),
&MarkProperties::Inline(Box::new(mcid(7))),
|_| None,
);
marks.push_with_properties(
Name::from("Span"),
&MarkProperties::Inline(Box::new(mcid(9))),
|_| None,
);
assert_eq!(marks.content_id(), Some(7));
}
#[test]
fn an_inline_oc_dictionary_is_ignored_for_visibility() {
let mut marks = ContentMarks::new();
marks.push_with_properties(
Name::from("OC"),
&MarkProperties::Inline(Box::new(Dict::new())),
|_| None,
);
assert!(marks.optional_content().is_none());
let mut marks = ContentMarks::new();
marks.push_with_properties(
Name::from("OC"),
&MarkProperties::Named(Name::from("MC0")),
|_| Some(Dict::new()),
);
assert!(marks.optional_content().is_some());
}
#[test]
fn only_the_exact_oc_tag_counts() {
let mut marks = ContentMarks::new();
marks.push_with_properties(
Name::from("OCX"),
&MarkProperties::Named(Name::from("MC0")),
|_| Some(Dict::new()),
);
assert!(marks.optional_content().is_none());
}
}