mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(edit): prune a field again when a shorter path reaches it past the depth cutoff
This commit is contained in:
+34
-17
@@ -309,42 +309,59 @@ fn prune_fields(doc: &mut Document, widgets: &HashSet<ObjRef>, on_page: Option<&
|
||||
let af_ref = af.as_ref();
|
||||
let mut af_dict = doc.resolve(&af).as_dict().cloned().unwrap_or_default();
|
||||
let fields = af_dict.get(b"Fields").map(|f| doc.resolve(f)).and_then(|f| f.as_array().cloned()).unwrap_or_default();
|
||||
// Returns whether the node survives. `known` holds each node's answer once it is visited, so
|
||||
// a node listed twice is pruned once, and one still being visited (a cycle) survives.
|
||||
// Returns whether the node survives, and whether that answer depends on the depth cutoff.
|
||||
// `known` holds each node's answer once it is visited, so a node listed twice is pruned once,
|
||||
// and one still being visited (a cycle) survives. An answer the cutoff decided records the depth
|
||||
// it was reached at, and a shallower path visits the node again: at most once per depth.
|
||||
fn keep(
|
||||
doc: &mut Document,
|
||||
r: ObjRef,
|
||||
widgets: &HashSet<ObjRef>,
|
||||
on_page: Option<&HashSet<ObjRef>>,
|
||||
known: &mut HashMap<ObjRef, bool>,
|
||||
known: &mut HashMap<ObjRef, (bool, Option<usize>)>,
|
||||
depth: usize,
|
||||
) -> bool {
|
||||
) -> (bool, bool) {
|
||||
#[cfg(test)]
|
||||
KEEP_CALLS.with(|c| c.set(c.get().saturating_add(1)));
|
||||
if widgets.contains(&r) || depth > 64 {
|
||||
return depth > 64;
|
||||
if widgets.contains(&r) {
|
||||
return (false, false);
|
||||
}
|
||||
if let Some(&survives) = known.get(&r) {
|
||||
return survives;
|
||||
if depth > 64 {
|
||||
return (true, true);
|
||||
}
|
||||
known.insert(r, true);
|
||||
let Some(d) = doc.get(r).as_dict().cloned() else { return true };
|
||||
match known.get(&r) {
|
||||
Some(&(survives, None)) => return (survives, false),
|
||||
Some(&(survives, Some(at))) if depth >= at => return (survives, true),
|
||||
_ => {}
|
||||
}
|
||||
known.insert(r, (true, None));
|
||||
let Some(d) = doc.get(r).as_dict().cloned() else { return (true, false) };
|
||||
let Some(kids) = d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()) else {
|
||||
let survives = on_page.is_none_or(|p| p.contains(&r));
|
||||
known.insert(r, survives);
|
||||
return survives;
|
||||
known.insert(r, (survives, None));
|
||||
return (survives, false);
|
||||
};
|
||||
let survivors: Vec<Object> =
|
||||
kids.iter().filter(|k| k.as_ref().is_none_or(|kr| keep(doc, kr, widgets, on_page, known, depth + 1))).cloned().collect();
|
||||
let mut cut = false;
|
||||
let survivors: Vec<Object> = kids
|
||||
.iter()
|
||||
.filter(|k| {
|
||||
k.as_ref().is_none_or(|kr| {
|
||||
let (survives, kid_cut) = keep(doc, kr, widgets, on_page, known, depth + 1);
|
||||
cut |= kid_cut;
|
||||
survives
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
if survivors.len() != kids.len() {
|
||||
let _ = doc.update_dict(r, |d| d.set(b"Kids".to_vec(), Object::Array(survivors.clone())));
|
||||
}
|
||||
known.insert(r, !survivors.is_empty());
|
||||
!survivors.is_empty()
|
||||
known.insert(r, (!survivors.is_empty(), cut.then_some(depth)));
|
||||
(!survivors.is_empty(), cut)
|
||||
}
|
||||
let mut known = HashMap::new();
|
||||
let survivors: Vec<Object> =
|
||||
fields.iter().filter(|f| f.as_ref().is_none_or(|r| keep(doc, r, widgets, on_page, &mut known, 0))).cloned().collect();
|
||||
fields.iter().filter(|f| f.as_ref().is_none_or(|r| keep(doc, r, widgets, on_page, &mut known, 0).0)).cloned().collect();
|
||||
if survivors.len() == fields.len() && widgets.is_empty() {
|
||||
return Ok(()); // only looked for fields on no page, and there were none
|
||||
}
|
||||
|
||||
@@ -6403,6 +6403,38 @@ fn flattening_prunes_a_field_tree_with_repeated_kids_once_per_node() {
|
||||
assert_eq!(left, 2, "the chain to the hidden widget and the self-listing node stay");
|
||||
}
|
||||
|
||||
/// A field reached first at the end of a long chain has its kids cut off by the depth limit, so
|
||||
/// they survive there. A shorter path to the same field still looks at its kids: an off-page widget
|
||||
/// under it is taken out when every field is flattened.
|
||||
#[test]
|
||||
fn a_field_reached_again_by_a_shorter_path_is_pruned_there() {
|
||||
let chain = 64;
|
||||
let page = stream("", " ");
|
||||
// 7…: the chain; then the shared field, its off-page widget, and the short path to it.
|
||||
let shared = 7 + chain;
|
||||
let fields = format!("<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R 7 0 R {} 0 R] >> >>", shared + 2);
|
||||
let mut objs: Vec<String> = vec![
|
||||
fields,
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R /Annots [5 0 R] >>".into(),
|
||||
page,
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (shown) /Rect [10 10 150 40] /F 4 /AP << /N 6 0 R >> >>".into(),
|
||||
"<< /Type /XObject /Subtype /Form /BBox [0 0 140 30] /Length 0 >>\nstream\n\nendstream".into(),
|
||||
];
|
||||
for i in 0..chain {
|
||||
objs.push(format!("<< /T (c{i}) /Kids [{} 0 R] >>", 8 + i));
|
||||
}
|
||||
objs.push(format!("<< /T (shared) /Kids [{} 0 R] >>", shared + 1));
|
||||
objs.push("<< /Type /Annot /Subtype /Widget /FT /Tx /T (orphan) /Rect [10 50 150 80] /F 4 >>".into());
|
||||
objs.push(format!("<< /T (short) /Kids [{shared} 0 R] >>"));
|
||||
let objs: Vec<&str> = objs.iter().map(String::as_str).collect();
|
||||
let mut doc = build(&objs);
|
||||
assert_eq!(flatten(&mut doc, &[0], false, true).unwrap(), 1);
|
||||
let shared_ref = pdfcraft_cos::ObjRef::new(u32::try_from(shared).unwrap(), 0);
|
||||
let kids = doc.resolve(doc.get(shared_ref).as_dict().unwrap().get(b"Kids").unwrap()).as_array().unwrap().len();
|
||||
assert_eq!(kids, 0, "the off-page widget is pruned along the short path");
|
||||
}
|
||||
|
||||
/// A ToUnicode destination is repeated for every glyph that shows its code: a page whose text
|
||||
/// would run past the extraction budget is refused, not built in memory.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user