M4: copied pages keep the resources they list only up the page tree (part of #1628) (#1635)

This commit is contained in:
Marco Arnulfo authored and GitHub committed 2026-10-13 00:16:19 -04:00
1 parent 6a948fcc22
commit da9cefa33c
2 files changed
+90 -1

No files matched your search

+56 -1
View File
@@ -300,9 +300,10 @@ fn import_pages_mapped(
ocgs: Vec::new(),
});
let src_dict = src.get(page).as_dict().cloned().unwrap_or_default();
let resources = resources_with_ancestors(src, &src_dict);
let mut d = Dict::new();
for (k, v) in src_dict.iter() {
if SKIP_ON_PAGE.contains(&k.as_slice()) {
if SKIP_ON_PAGE.contains(&k.as_slice()) || (k.as_slice() == b"Resources" && resources.is_some()) {
continue;
}
if k.as_slice() == b"Annots" {
@@ -364,6 +365,10 @@ fn import_pages_mapped(
}
d.set(k.clone(), copier.copy_value(dst, v, None));
}
if let Some(r) = resources {
let r = copier.copy_value(dst, &r, None);
d.set(b"Resources".to_vec(), r);
}
for k in INHERITABLE {
if !d.contains(k)
&& let Some(v) = inherited.get(k)
@@ -398,6 +403,56 @@ fn import_pages_mapped(
Ok(Imported { pages: new_pages, page_map: copier.pages, objects: copier.map, renamed_fields })
}
/// A page's `/Resources` with the names that only its page-tree ancestors list, or `None` when
/// they add nothing to the dictionary it would have anyway (its own, or else its nearest
/// ancestor's). Readers (ours included) look a resource name up the page and then up the tree,
/// so a page that lists its font but inherits its images still shows them. A copy hangs from
/// another tree, so it must list them itself or it draws nothing of them (#1628). The page's
/// entry wins over an ancestor's, and a nearer ancestor's over a farther one's.
fn resources_with_ancestors(src: &Document, page: &Dict) -> Option<Object> {
let mut merged = page.get(b"Resources").map(|r| src.dict(r).unwrap_or_default());
let mut added = false;
let mut seen = std::collections::HashSet::new();
let mut parent = page.reference(b"Parent");
// Bounded: a damaged tree's `/Parent` links can loop or run deep.
while let Some(r) = parent
&& seen.insert(r)
&& seen.len() <= 256
{
let node = src.get(r);
let Some(node) = node.as_dict() else { break };
parent = node.reference(b"Parent");
let Some(theirs) = node.get(b"Resources").and_then(|r| src.dict(r)) else { continue };
let Some(mine) = merged.as_mut() else {
merged = Some(theirs);
continue;
};
for (category, entries) in theirs.iter() {
match (mine.get(category).map(|c| src.dict(c)), src.dict(entries)) {
(None, _) => {
mine.set(category.clone(), entries.clone());
added = true;
}
(Some(Some(mut names)), Some(entries)) => {
let before = names.len();
for (name, value) in entries.iter() {
if !names.contains(name) {
names.set(name.clone(), value.clone());
}
}
if names.len() > before {
mine.set(category.clone(), Object::Dict(names));
added = true;
}
}
// `/ProcSet` and the like: the page's own value stands.
_ => {}
}
}
}
merged.filter(|_| added).map(Object::Dict)
}
/// Look up a named destination in the source (`/Dests` dictionary or `/Names /Dests` tree).
fn named_dest(src: &Document, name: &[u8]) -> Option<Object> {
let catalog = src.get(src.root()?).as_dict().cloned()?;
+34
View File
@@ -476,6 +476,40 @@ fn inherited_attributes_travel_with_copied_pages() {
assert_eq!(hay.pages().first().unwrap().render_dimensions(), (250.0, 500.0), "rotated 90°");
}
#[test]
fn resources_split_between_page_and_tree_travel_together() {
// The page names its own font but draws a form that only the page-tree node lists. Readers
// (ours included) look a name up the whole chain, so the page shows; a copy that kept only
// the page's own dictionary drew nothing (#1628).
let b: Vec<String> = vec![
"<< /Type /Catalog /Pages 2 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] /Resources << /XObject << /X0 5 0 R >> /Font << /F1 6 0 R /F2 6 0 R >> >> >>"
.into(),
"<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /Font << /F2 7 0 R >> >> >>".into(),
{
let s = "q 100 0 0 100 50 50 cm /X0 Do Q BT /F2 12 Tf (x) Tj ET";
format!("<< /Length {} >>\nstream\n{s}\nendstream", s.len())
},
{
let s = "0 0 1 rg 0 0 1 1 re f";
format!("<< /Type /XObject /Subtype /Form /BBox [0 0 1 1] /Length {} >>\nstream\n{s}\nendstream", s.len())
},
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".into(),
"<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>".into(),
];
let src = open(build(&b, "/Root 1 0 R"));
let out = full_roundtrip(&combine(&[("S", &src), ("S again", &src)]).unwrap());
for i in 0..2 {
let res = out.dict(page_dict(&out, i).get(b"Resources").unwrap()).unwrap();
let entry = |cat: &[u8], name: &[u8]| res.get(cat).and_then(|c| out.dict(c)).and_then(|d| d.get(name).map(|v| out.resolve(v)));
let form = entry(b"XObject", b"X0").expect("the tree's form comes along");
assert_eq!(form.as_dict().and_then(|d| d.name(b"Subtype")), Some(&b"Form"[..]));
let font = |name: &[u8]| entry(b"Font", name).and_then(|f| f.as_dict().and_then(|d| d.name(b"BaseFont").map(<[u8]>::to_vec)));
assert_eq!(font(b"F1").as_deref(), Some(&b"Helvetica"[..]), "a name only the tree has is added");
assert_eq!(font(b"F2").as_deref(), Some(&b"Courier"[..]), "the page's own entry wins over the tree's");
}
}
#[test]
fn links_are_rewired_to_copied_pages_or_dropped() {
let a = doc_a();