use std::collections::BTreeMap;
use pdfrum_object::{Array, Dict, Name, ObjRef, Object, Resolve, names};
use crate::doc::EditDoc;
const MAX_DEPTH: u32 = 128;
const SKIPPED_KEYS: [&Name; 3] = [names::PARENT, names::PREV, names::FIRST];
#[derive(Debug, Default)]
pub(crate) struct ObjectMap {
map: BTreeMap<u32, u32>,
}
impl ObjectMap {
#[must_use]
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn record(&mut self, from: u32, to: u32) {
self.map.insert(from, to);
}
#[must_use]
pub(crate) fn get(&self, from: u32) -> Option<u32> {
self.map.get(&from).copied()
}
}
pub(crate) fn copy_object(
dest: &mut EditDoc<'_>,
src: &impl Resolve,
reference: ObjRef,
map: &mut ObjectMap,
pages_node: u32,
) -> Option<u32> {
if let Some(existing) = map.get(reference.num) {
return Some(existing);
}
let resolved = src.fetch(reference).ok()?;
if resolved.is_null() {
return None;
}
let mut clone = Object::clone(&resolved);
if let Some(dict) = clone.as_dict()
&& let Some(kind) = dict.name(names::TYPE)
{
if eq_ignore_case(kind.as_bytes(), b"Pages") {
return Some(pages_node);
}
if eq_ignore_case(kind.as_bytes(), b"Page") {
return None;
}
}
let new = dest.add(Object::Null);
map.record(reference.num, new.num);
if !rewrite(dest, src, &mut clone, map, pages_node, 0) {
return None;
}
dest.replace(new, clone);
Some(new.num)
}
pub(crate) fn rewrite_in_place(
dest: &mut EditDoc<'_>,
src: &impl Resolve,
value: &mut Object,
map: &mut ObjectMap,
pages_node: u32,
) -> bool {
rewrite(dest, src, value, map, pages_node, 0)
}
fn rewrite(
dest: &mut EditDoc<'_>,
src: &impl Resolve,
obj: &mut Object,
map: &mut ObjectMap,
pages_node: u32,
depth: u32,
) -> bool {
if depth > MAX_DEPTH {
return false;
}
match obj {
Object::Ref(r) => match copy_object(dest, src, *r, map, pages_node) {
Some(num) => {
*obj = Object::Ref(ObjRef::new(num, 0));
true
}
None => false,
},
Object::Dict(dict) => {
*dict = rewrite_dict(dest, src, dict, map, pages_node, depth);
true
}
Object::Array(array) => {
let mut out = Array::new();
for value in array.iter() {
let mut value = value.clone();
if !rewrite(
dest,
src,
&mut value,
map,
pages_node,
depth.saturating_add(1),
) {
return false;
}
out.push(value);
}
*array = out;
true
}
Object::Stream(stream) => {
stream.dict = rewrite_dict(dest, src, &stream.dict, map, pages_node, depth);
true
}
_ => true,
}
}
fn rewrite_dict(
dest: &mut EditDoc<'_>,
src: &impl Resolve,
dict: &Dict,
map: &mut ObjectMap,
pages_node: u32,
depth: u32,
) -> Dict {
let mut out = Dict::new();
for (key, value) in dict.iter() {
if SKIPPED_KEYS.contains(&key) {
continue;
}
let mut value = value.clone();
if rewrite(
dest,
src,
&mut value,
map,
pages_node,
depth.saturating_add(1),
) {
out.push(key.clone(), value);
}
}
out
}
fn eq_ignore_case(a: &[u8], b: &[u8]) -> bool {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.eq_ignore_ascii_case(y))
}
#[cfg(test)]
mod tests {
use super::{ObjectMap, copy_object};
use crate::doc::EditDoc;
use pdfrum_object::{Array, ByteSpan, Dict, Name, ObjRef, Object, Resolve, Stream, names};
use pdfrum_parser::{Document, LoadOptions, load};
use std::collections::BTreeMap;
use std::sync::Arc;
struct Src(BTreeMap<u32, Object>);
impl Resolve for Src {
fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, pdfrum_object::Error> {
Ok(Arc::new(
self.0.get(&r.num).cloned().unwrap_or(Object::Null),
))
}
}
fn dest_doc() -> Document {
let file = b"%PDF-1.7\n\
1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n\
2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n\
trailer\n<< /Root 1 0 R /Size 4 >>\n";
load(Arc::from(&file[..]), &LoadOptions::default()).expect("opens")
}
fn dict(pairs: impl IntoIterator<Item = (&'static str, Object)>) -> Object {
Object::Dict(Dict::from_pairs(
pairs.into_iter().map(|(k, v)| (Name::from(k), v)),
))
}
fn r(num: u32) -> Object {
Object::Ref(ObjRef::new(num, 0))
}
#[test]
fn a_simple_object_is_copied_and_renumbered() {
let src = Src(BTreeMap::from([
(7, dict([("Leaf", r(8))])),
(8, Object::Int(42)),
]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
let num = copy_object(&mut dest, &src, ObjRef::new(7, 0), &mut map, 2).expect("copies");
let copied = dest.fetch(ObjRef::new(num, 0)).expect("fetch");
let leaf = copied
.as_dict()
.and_then(|d| d.reference(&Name::from("Leaf")));
assert!(leaf.is_some_and(|l| l.num != 8));
assert_eq!(
*dest.fetch(leaf.expect("leaf")).expect("leaf"),
Object::Int(42)
);
}
#[test]
fn an_object_reached_twice_is_copied_once() {
let src = Src(BTreeMap::from([
(1, dict([("A", r(3))])),
(2, dict([("B", r(3))])),
(3, Object::Int(9)),
]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
let a = copy_object(&mut dest, &src, ObjRef::new(1, 0), &mut map, 2).expect("a");
let b = copy_object(&mut dest, &src, ObjRef::new(2, 0), &mut map, 2).expect("b");
let leaf_of = |num: u32, key: &str| {
dest.fetch(ObjRef::new(num, 0))
.ok()
.and_then(|o| o.as_dict().and_then(|d| d.reference(&Name::from(key))))
};
assert_eq!(leaf_of(a, "A"), leaf_of(b, "B"), "one copy, two pointers");
}
#[test]
fn a_cycle_terminates() {
let src = Src(BTreeMap::from([
(1, dict([("Next", r(2))])),
(2, dict([("Back", r(1))])),
]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
assert!(copy_object(&mut dest, &src, ObjRef::new(1, 0), &mut map, 2).is_some());
assert!(map.get(1).is_some());
assert!(map.get(2).is_some());
}
#[test]
fn parent_prev_and_first_are_never_followed() {
let src = Src(BTreeMap::from([
(
1,
dict([
("Parent", r(50)),
("Prev", r(51)),
("First", r(52)),
("Keep", r(53)),
]),
),
(50, Object::Int(50)),
(51, Object::Int(51)),
(52, Object::Int(52)),
(53, Object::Int(53)),
]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
let num = copy_object(&mut dest, &src, ObjRef::new(1, 0), &mut map, 2).expect("copies");
let copied = dest.fetch(ObjRef::new(num, 0)).expect("fetch");
let d = copied.as_dict().expect("a dict");
assert!(!d.contains_key(names::PARENT));
assert!(!d.contains_key(names::PREV));
assert!(!d.contains_key(names::FIRST));
assert!(d.contains_key(&Name::from("Keep")));
assert!(map.get(1).is_some());
assert!(map.get(53).is_some());
assert!(map.get(50).is_none());
assert!(map.get(51).is_none());
assert!(map.get(52).is_none());
}
#[test]
fn a_pages_node_resolves_to_the_destinations_own() {
let src = Src(BTreeMap::from([(
9,
dict([("Type", Object::Name(names::PAGES.clone()))]),
)]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
assert_eq!(
copy_object(&mut dest, &src, ObjRef::new(9, 0), &mut map, 2),
Some(2)
);
}
#[test]
fn type_matching_ignores_case() {
for spelling in ["PAGES", "pages", "PaGeS"] {
let src = Src(BTreeMap::from([(
9,
dict([("Type", Object::Name(Name::from(spelling)))]),
)]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
assert_eq!(
copy_object(&mut dest, &src, ObjRef::new(9, 0), &mut map, 2),
Some(2),
"{spelling}"
);
}
}
#[test]
fn a_reference_to_another_page_is_refused() {
let src = Src(BTreeMap::from([(
9,
dict([("Type", Object::Name(names::PAGE.clone()))]),
)]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
assert_eq!(
copy_object(&mut dest, &src, ObjRef::new(9, 0), &mut map, 2),
None
);
}
#[test]
fn a_dangling_reference_drops_its_dict_key() {
let src = Src(BTreeMap::from([(
1,
dict([("Good", Object::Int(1)), ("Bad", r(99))]),
)]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
let num = copy_object(&mut dest, &src, ObjRef::new(1, 0), &mut map, 2).expect("copies");
let copied = dest.fetch(ObjRef::new(num, 0)).expect("fetch");
let d = copied.as_dict().expect("a dict");
assert!(d.contains_key(&Name::from("Good")));
assert!(!d.contains_key(&Name::from("Bad")), "the bad key is gone");
}
#[test]
fn a_dangling_reference_kills_its_whole_array() {
let src = Src(BTreeMap::from([(
1,
dict([(
"List",
Object::Array(Array::of([Object::Int(1), r(99), Object::Int(3)])),
)]),
)]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
let num = copy_object(&mut dest, &src, ObjRef::new(1, 0), &mut map, 2).expect("copies");
let copied = dest.fetch(ObjRef::new(num, 0)).expect("fetch");
assert!(
!copied
.as_dict()
.expect("a dict")
.contains_key(&Name::from("List"))
);
}
#[test]
fn a_stream_keeps_its_bytes_and_its_filter() {
let payload: Arc<[u8]> = Arc::from(&b"\x01\x02\x03compressed"[..]);
let stream = Object::Stream(Box::new(Stream::new(
Dict::from_pairs([
(
names::FILTER.clone(),
Object::Name(names::FLATE_DECODE.clone()),
),
(names::LENGTH.clone(), Object::Int(14)),
]),
ByteSpan::whole(payload),
)));
let src = Src(BTreeMap::from([(1, dict([("S", r(2))])), (2, stream)]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
let num = copy_object(&mut dest, &src, ObjRef::new(1, 0), &mut map, 2).expect("copies");
let copied = dest.fetch(ObjRef::new(num, 0)).expect("fetch");
let s_ref = copied
.as_dict()
.and_then(|d| d.reference(&Name::from("S")))
.expect("the stream reference");
let s = dest.fetch(s_ref).expect("the stream");
let s = s.as_stream().expect("a stream");
assert_eq!(s.data.as_bytes(), b"\x01\x02\x03compressed");
assert_eq!(s.dict.name(names::FILTER), Some(names::FLATE_DECODE));
}
#[test]
fn a_pre_registered_self_reference_survives() {
let src = Src(BTreeMap::from([(
5,
dict([("Type", Object::Name(names::PAGE.clone())), ("Me", r(5))]),
)]));
let base = dest_doc();
let mut dest = EditDoc::new(&base);
let mut map = ObjectMap::new();
let placeholder = dest.add(Object::Null);
map.record(5, placeholder.num);
assert_eq!(
copy_object(&mut dest, &src, ObjRef::new(5, 0), &mut map, 2),
Some(placeholder.num),
"the map wins over the /Type /Page refusal"
);
}
}