mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 11:08:48 -06:00
fix(optimize): keep a destination referenced only through an indirect /Dest (#4212)
Reduce File Size removes named destinations that nothing refers to. It collected the referenced names by reading /Dest and a GoTo's /D directly, so when a link's /Dest was an indirect reference to the name string (valid PDF) rather than the string itself, the name was not counted. The destination it used was then treated as unreferenced and deleted, silently breaking the link. referenced_names now resolves /Dest and /D before reading the name, so an indirectly referenced destination is kept. Test: a link whose /Dest is a reference to the name string keeps its destination through optimize(); an unused destination is still removed. Reported-by: BS.ehlas.cz
This commit is contained in:
@@ -150,11 +150,14 @@ fn referenced_names(doc: &Document) -> HashSet<Vec<u8>> {
|
||||
while let Some(o) = stack.pop() {
|
||||
match o {
|
||||
Object::Dict(d) => {
|
||||
if let Some(n) = d.get(b"Dest").and_then(dest_name) {
|
||||
// Resolve first: /Dest and a GoTo's /D may be an indirect reference to the name
|
||||
// string, and an unresolved ref has no name, so the destination it uses would
|
||||
// look unreferenced and be removed.
|
||||
if let Some(n) = d.get(b"Dest").and_then(|x| dest_name(doc.resolve(x).as_ref())) {
|
||||
out.insert(n);
|
||||
}
|
||||
if matches!(d.name(b"S"), Some(b"GoTo" | b"GoToR" | b"GoToE"))
|
||||
&& let Some(n) = d.get(b"D").and_then(dest_name)
|
||||
&& let Some(n) = d.get(b"D").and_then(|x| dest_name(doc.resolve(x).as_ref()))
|
||||
{
|
||||
out.insert(n);
|
||||
}
|
||||
|
||||
@@ -1097,3 +1097,45 @@ fn a_hostile_soft_mask_leaves_its_image_unchanged() {
|
||||
assert_eq!(stream(&doc, photo), before);
|
||||
assert_eq!(stream(&doc, mask), hostile);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indirectly_referenced_destination_is_kept() {
|
||||
// A link's /Dest can be an indirect reference to the name string, not the string itself. Reduce
|
||||
// File Size read the name only from a direct string, so it deleted a destination the link still
|
||||
// used, silently breaking the link.
|
||||
let mut doc = Document::new_empty();
|
||||
let p = page(&mut doc, &[], "BT ET");
|
||||
let s = |t: &str| Object::String(pdfcraft_cos::PdfString::literal(t.as_bytes().to_vec()));
|
||||
// A link whose /Dest is `ref -> "target"`, and an unused destination for contrast.
|
||||
let name_ref = Object::Ref(doc.add(s("target")));
|
||||
let mut link = Dict::new();
|
||||
link.set(b"Type".to_vec(), Object::name("Annot"));
|
||||
link.set(b"Subtype".to_vec(), Object::name("Link"));
|
||||
link.set(b"Rect".to_vec(), Object::Array(vec![0.into(), 0.into(), 10.into(), 10.into()]));
|
||||
link.set(b"Dest".to_vec(), name_ref);
|
||||
let link = Object::Ref(doc.add(Object::Dict(link)));
|
||||
doc.update_dict(p, |d| d.set(b"Annots".to_vec(), Object::Array(vec![link]))).unwrap();
|
||||
let fit = Object::Array(vec![Object::Ref(p), Object::name("Fit")]);
|
||||
let mut tree = Dict::new();
|
||||
tree.set(b"Names".to_vec(), Object::Array(vec![s("target"), fit.clone(), s("unused"), fit]));
|
||||
let tree = doc.add(Object::Dict(tree));
|
||||
let mut names = Dict::new();
|
||||
names.set(b"Dests".to_vec(), Object::Ref(tree));
|
||||
let root = doc.root().unwrap();
|
||||
doc.update_dict(root, |c| c.set(b"Names".to_vec(), Object::Dict(names))).unwrap();
|
||||
let report = optimize(&mut doc, &Settings::default()).unwrap();
|
||||
// Only "unused" is removed; the indirectly-referenced "target" is kept.
|
||||
assert_eq!(report.unreferenced_dests, 1);
|
||||
let left = named_dest_keys(&doc);
|
||||
assert!(left.contains(&b"target".to_vec()), "indirectly referenced destination was kept: {left:?}");
|
||||
assert!(!left.contains(&b"unused".to_vec()));
|
||||
}
|
||||
|
||||
/// The keys in the catalog's /Names /Dests name tree (one level, as the optimizer writes it).
|
||||
fn named_dest_keys(doc: &Document) -> Vec<Vec<u8>> {
|
||||
let cat = doc.get(doc.root().unwrap()).as_dict().cloned().unwrap();
|
||||
let names = doc.resolve(cat.get(b"Names").unwrap()).as_dict().cloned().unwrap();
|
||||
let tree = doc.resolve(names.get(b"Dests").unwrap()).as_dict().cloned().unwrap();
|
||||
let arr = doc.resolve(tree.get(b"Names").unwrap()).as_array().cloned().unwrap();
|
||||
arr.chunks(2).filter_map(|c| doc.resolve(&c[0]).as_string().map(|s| s.bytes.clone())).collect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user