fix(redact): remove AcroForm on Hidden::FormFields even when field count is zero (#4571)

* fix(redact): remove AcroForm on Hidden::FormFields even when field count is zero

* fix(redact): report AcroForm presence in scan() and format
This commit is contained in:
Dylan M. Taylor
2026-10-24 21:07:16 -04:00
committed by GitHub
parent 4c69b8cf88
commit aa9a9105fc
2 changed files with 37 additions and 3 deletions
+16 -3
View File
@@ -474,7 +474,16 @@ pub fn scan(doc: &Document) -> Vec<(Hidden, usize)> {
+ owned(Hidden::Attachments)
}
Hidden::Comments => annots.iter().filter(|a| is_comment(a.name(b"Subtype").unwrap_or(b""))).count(),
Hidden::FormFields => pdfcraft_forms::fields(doc).len(),
Hidden::FormFields => {
let n = pdfcraft_forms::fields(doc).len();
if n > 0 {
n
} else if cat.contains(b"AcroForm") {
1
} else {
0
}
}
Hidden::HiddenText => content_pass(&mut doc2, true, false, false).map(|(n, _)| n).unwrap_or(0),
Hidden::HiddenLayers => {
let off = hidden_layers(doc).len();
@@ -510,7 +519,8 @@ pub fn remove_hidden(doc: &mut Document, which: &[Hidden]) -> Result<Vec<(Hidden
// Every annotation the pages list now: whatever is no longer listed at the end (flattened
// widgets included) leaves the structure tree too.
let listed_before = listed_annotations(doc);
let fields = which.contains(&Hidden::FormFields) && count(Hidden::FormFields) > 0;
let has_acroform = doc.get(root).as_dict().is_some_and(|d| d.contains(b"AcroForm"));
let fields = which.contains(&Hidden::FormFields) && (count(Hidden::FormFields) > 0 || has_acroform);
if fields {
// Fields without appearances get one first, so their values stay visible.
for f in pdfcraft_forms::fields(doc) {
@@ -631,9 +641,12 @@ pub fn remove_hidden(doc: &mut Document, which: &[Hidden]) -> Result<Vec<(Hidden
cat.set(b"OCProperties".to_vec(), Object::Dict(oc));
}
}
Hidden::FormFields => {
cat.remove(b"AcroForm");
}
_ => {}
}
let n = count(h);
let n = if h == Hidden::FormFields && fields { count(h).max(1) } else { count(h) };
if n > 0 {
done.push((h, n));
}
+21
View File
@@ -2708,6 +2708,27 @@ fn removing_form_fields_takes_hidden_widgets_and_their_values_out_of_the_file()
assert!(annots_of(&doc, &pdfcraft_model::pages(&doc).swap_remove(0).dict).is_empty());
}
/// F-021: removing form fields on a document with an AcroForm dictionary (even one with no
/// enumerable terminal fields) removes the AcroForm dictionary, processes widgets, and requires full save.
#[test]
fn removing_form_fields_removes_acroform_even_without_terminal_fields() {
use crate::sanitize::{Hidden, remove_hidden, scan};
let mut doc =
one_page(b"BT /F1 10 Tf 10 100 Td (Visible) Tj ET", "", vec![b"<< /Type /Annot /Subtype /Widget /Rect [10 200 150 220] >>".to_vec()]);
doc.update_dict(doc.root().unwrap(), |d| d.set(b"AcroForm".to_vec(), fields_dict(&[]))).unwrap();
doc.update_dict(ObjRef::new(3, 0), |d| d.set(b"Annots".to_vec(), ref_array(&[7]))).unwrap();
let counts = scan(&doc);
assert_eq!(counts.iter().find(|c| c.0 == Hidden::FormFields).map(|c| c.1), Some(1));
assert!(doc.get(doc.root().unwrap()).as_dict().is_some_and(|d| d.contains(b"AcroForm")));
assert!(!doc.full_save_required());
let done = remove_hidden(&mut doc, &[Hidden::FormFields]).unwrap();
assert_eq!(done, [(Hidden::FormFields, 1)]);
assert!(!doc.get(doc.root().unwrap()).as_dict().is_some_and(|d| d.contains(b"AcroForm")));
assert!(annots_of(&doc, &pdfcraft_model::pages(&doc).swap_remove(0).dict).is_empty());
assert!(doc.full_save_required());
}
/// Removing hidden text cleans the structure tree as redaction does: the elements describing the
/// removed text lose their descriptions, and a reference into a rewritten Form points at the copy.
#[test]