use pdfrum_object::Name;
use pdfrum_page::{ContentMarks, Mark};
use crate::write::object::{write_dict, write_name};
pub(crate) type PropertyNamer<'a> = &'a dyn Fn(&Mark) -> Option<Name>;
pub(crate) fn emit_mark_diff(
out: &mut String,
previous: &ContentMarks,
current: &ContentMarks,
namer: PropertyNamer<'_>,
) -> usize {
let shared = common_prefix(previous, current);
for _ in shared..previous.len() {
out.push_str("EMC\n");
}
for mark in current.marks().get(shared..).unwrap_or_default() {
emit_open(out, mark, namer);
}
current.len()
}
pub(crate) fn finish_marks(out: &mut String, open: usize) {
for _ in 0..open {
out.push_str("EMC\n");
}
}
fn common_prefix(a: &ContentMarks, b: &ContentMarks) -> usize {
a.marks()
.iter()
.zip(b.marks())
.take_while(|(x, y)| same(x, y))
.count()
}
fn same(a: &Mark, b: &Mark) -> bool {
a.tag == b.tag
&& a.from_resources == b.from_resources
&& match (&a.properties, &b.properties) {
(None, None) => true,
(Some(x), Some(y)) => x == y,
_ => false,
}
}
fn emit_open(out: &mut String, mark: &Mark, namer: PropertyNamer<'_>) {
let mut bytes = Vec::new();
write_name(&mut bytes, &mark.tag);
out.push_str(&String::from_utf8_lossy(&bytes));
out.push(' ');
let Some(dict) = mark.properties.as_ref() else {
out.push_str("BMC\n");
return;
};
if mark.from_resources
&& let Some(name) = namer(mark)
{
let mut bytes = Vec::new();
write_name(&mut bytes, &name);
out.push_str(&String::from_utf8_lossy(&bytes));
out.push_str(" BDC\n");
return;
}
let mut bytes = Vec::new();
write_dict(&mut bytes, dict, None);
out.push_str(&String::from_utf8_lossy(&bytes));
out.push_str(" BDC\n");
}
#[cfg(test)]
mod tests {
use super::{emit_mark_diff, finish_marks};
use pdfrum_object::{Dict, Name, Object};
use pdfrum_page::ContentMarks;
use pdfrum_page::MarkProperties;
fn marks(specs: &[(&str, Option<Dict>)]) -> ContentMarks {
let mut out = ContentMarks::new();
for (tag, props) in specs {
match props {
None => out.push(Name::from(*tag)),
Some(d) => {
out.push_with_properties(
Name::from(*tag),
&MarkProperties::Inline(Box::new(d.clone())),
|_| None,
);
}
}
}
out
}
fn named_marks(specs: &[(&str, &str, Dict)]) -> ContentMarks {
let mut out = ContentMarks::new();
for (tag, resource, dict) in specs {
let dict = dict.clone();
out.push_with_properties(
Name::from(*tag),
&MarkProperties::Named(Name::from(*resource)),
move |_| Some(dict),
);
}
out
}
fn diff(previous: &ContentMarks, current: &ContentMarks) -> String {
let mut out = String::new();
emit_mark_diff(&mut out, previous, current, &|_| None);
out
}
#[test]
fn a_mark_with_no_parameters_opens_with_bmc() {
assert_eq!(
diff(&ContentMarks::new(), &marks(&[("Span", None)])),
"/Span BMC\n"
);
}
#[test]
fn a_parameter_dictionary_opens_with_bdc_and_encodes_its_tag() {
let props = Dict::from_pairs([(
Name::from("Name"),
Object::Name(Name::from("Property Name With Space")),
)]);
let out = diff(&ContentMarks::new(), &marks(&[("M1", Some(props))]));
assert!(
out.contains("/M1 <</Name/Property#20Name#20With#20Space>> BDC"),
"got {out}"
);
}
#[test]
fn a_shared_prefix_is_left_open() {
let a = marks(&[("Outer", None), ("Inner", None)]);
let b = marks(&[("Outer", None), ("Other", None)]);
assert_eq!(diff(&a, &b), "EMC\n/Other BMC\n");
}
#[test]
fn identical_marks_emit_nothing() {
let a = marks(&[("Outer", None), ("Inner", None)]);
assert_eq!(diff(&a, &a), "");
}
#[test]
fn closing_everything_writes_one_emc_per_open_mark() {
let a = marks(&[("A", None), ("B", None), ("C", None)]);
assert_eq!(diff(&a, &ContentMarks::new()), "EMC\nEMC\nEMC\n");
}
#[test]
fn opening_from_nothing_writes_no_emc() {
let b = marks(&[("A", None), ("B", None)]);
assert_eq!(diff(&ContentMarks::new(), &b), "/A BMC\n/B BMC\n");
}
#[test]
fn the_same_tag_with_different_parameters_is_a_different_mark() {
let one = Dict::from_pairs([(Name::from("K"), Object::Int(1))]);
let two = Dict::from_pairs([(Name::from("K"), Object::Int(2))]);
let a = marks(&[("Span", Some(one))]);
let b = marks(&[("Span", Some(two))]);
assert!(diff(&a, &b).starts_with("EMC\n"));
}
#[test]
fn finishing_closes_exactly_what_is_open() {
let mut out = String::new();
finish_marks(&mut out, 2);
assert_eq!(out, "EMC\nEMC\n");
let mut none = String::new();
finish_marks(&mut none, 0);
assert_eq!(none, "");
}
#[test]
fn the_diff_reports_how_many_stay_open() {
let mut out = String::new();
let open = emit_mark_diff(
&mut out,
&ContentMarks::new(),
&marks(&[("A", None), ("B", None)]),
&|_| None,
);
assert_eq!(open, 2);
}
#[test]
fn a_resource_property_list_is_written_as_a_name() {
let dict = Dict::from_pairs([(Name::from("MCID"), Object::Int(3))]);
let current = named_marks(&[("OC", "MC0", dict)]);
let mut out = String::new();
emit_mark_diff(&mut out, &ContentMarks::new(), ¤t, &|_| {
Some(Name::from("MC0"))
});
assert_eq!(out, "/OC /MC0 BDC\n");
}
#[test]
fn an_unrealizable_resource_property_list_falls_back_to_inline() {
let dict = Dict::from_pairs([(Name::from("MCID"), Object::Int(3))]);
let current = named_marks(&[("OC", "MC0", dict)]);
let mut out = String::new();
emit_mark_diff(&mut out, &ContentMarks::new(), ¤t, &|_| None);
assert_eq!(out, "/OC <</MCID 3>> BDC\n");
}
}