mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 05:28:09 -06:00
* M6: flattening removes empty fields it cannot draw (#4017) Flattening kept every widget without an appearance so that its value was not lost with nothing drawn in its place (#1794). A field without a value has nothing to lose, yet it stayed in /AcroForm /Fields and on the page, so a flattened file could still be filled in. A widget without an appearance now stays only when its field has a value: its /V, or the nearest one inherited through /Parent. A missing value, an empty string or an empty array is no value. Comments and valued widgets are unchanged. Fixes #4017 * M6: flattening keeps a widget whose value is past the parent limit (#4017) has_value walks at most 64 levels up /Parent. When it ran out it said "no value", so a widget whose /V sat on the 64th ancestor was removed. Running out now counts as a value: one that wasn't checked must not be removed.
This commit is contained in:
@@ -37,6 +37,28 @@ fn appearance(doc: &Document, d: &Dict) -> Option<ObjRef> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a widget's field has a value: its `/V`, or the nearest one inherited through `/Parent`
|
||||
/// (ISO 32000-2 §12.7.4.1). An empty string or array is no value. Past the depth limit the answer
|
||||
/// is yes: a value that wasn't checked must not be removed.
|
||||
fn has_value(doc: &Document, widget: &Dict) -> bool {
|
||||
let mut d = widget.clone();
|
||||
let mut seen = HashSet::new();
|
||||
for _ in 0..64 {
|
||||
if let Some(v) = d.get(b"V") {
|
||||
return match &*doc.resolve(v) {
|
||||
Object::Null => false,
|
||||
Object::String(s) => !s.to_text().is_empty(),
|
||||
Object::Array(a) => !a.is_empty(),
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
let Some(parent) = d.reference(b"Parent").filter(|p| seen.insert(*p)) else { return false };
|
||||
let Some(next) = doc.get(parent).as_dict().cloned() else { return false };
|
||||
d = next;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Algorithm 8.1: the matrix that maps the form's bounding box onto the annotation rectangle.
|
||||
fn placement(doc: &Document, form: &Dict, rect: [f64; 4]) -> Option<[f64; 6]> {
|
||||
let bbox = nums(doc, form.get(b"BBox")).filter(|b| b.len() == 4)?;
|
||||
@@ -220,13 +242,14 @@ fn flatten_in(doc: &mut Document, pages: &[usize], which: Which) -> Result<usize
|
||||
// left on the page. A widget stays when it has no appearance to draw at all (none
|
||||
// stored, or none for its selected state): removing it would lose the field's value
|
||||
// with nothing in its place (#1794). One whose appearance draws nothing (an empty
|
||||
// stream) shows nothing anywhere, so flattening still removes it.
|
||||
// stream) shows nothing anywhere, so flattening still removes it, and so does one whose
|
||||
// field has no value: there is nothing to lose (#4017).
|
||||
if !drew {
|
||||
let no_appearance = appearance(doc, d).is_none();
|
||||
if no_appearance && let Some(r) = entry.as_ref() {
|
||||
let keep = appearance(doc, d).is_none() && (!widget || has_value(doc, d));
|
||||
if keep && let Some(r) = entry.as_ref() {
|
||||
undrawn.insert(r);
|
||||
}
|
||||
if !widget || no_appearance {
|
||||
if !widget || keep {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,6 +611,55 @@ fn flattening_keeps_a_valued_widget_it_cannot_draw() {
|
||||
assert_eq!(value(6), Some(Object::name("Yes")));
|
||||
}
|
||||
|
||||
/// #4017: a widget with no appearance whose field has no value (none, or an empty string) has
|
||||
/// nothing to lose, so flattening removes it instead of leaving a fillable field behind. One whose
|
||||
/// value it inherits from its parent field still stays (#1794).
|
||||
#[test]
|
||||
fn flattening_removes_an_empty_widget_it_cannot_draw() {
|
||||
let page = stream("", " ");
|
||||
let mut doc = build(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R 6 0 R 7 0 R 8 0 R] >> >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R /Annots [5 0 R 6 0 R 7 0 R 9 0 R] >>",
|
||||
&page,
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (no_value) /Rect [10 10 150 40] /F 4 >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (empty) /V () /Rect [10 50 150 80] /F 4 >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (drawn) /Rect [10 90 150 120] /F 4 /AP << /N 10 0 R >> >>",
|
||||
"<< /FT /Tx /T (parent) /V (Ada) /Kids [9 0 R] >>",
|
||||
"<< /Type /Annot /Subtype /Widget /Parent 8 0 R /Rect [10 130 150 160] /F 4 >>",
|
||||
"<< /Type /XObject /Subtype /Form /BBox [0 0 140 30] /Length 0 >>\nstream\n\nendstream",
|
||||
]);
|
||||
assert_eq!(flatten(&mut doc, &[0], false, true).unwrap(), 1, "only the field with an appearance is drawn");
|
||||
let doc = reopen(&doc);
|
||||
let refs = |o: &Object| -> Vec<u32> { doc.resolve(o).as_array().unwrap().iter().filter_map(|f| f.as_ref()).map(|r| r.num).collect() };
|
||||
let p = &pdfcraft_model::pages(&doc)[0];
|
||||
assert_eq!(refs(p.dict.get(b"Annots").unwrap()), [9], "only the widget with an inherited value stays on the page");
|
||||
let root = doc.root().unwrap();
|
||||
let af = doc.resolve(doc.get(root).as_dict().unwrap().get(b"AcroForm").unwrap());
|
||||
assert_eq!(refs(af.as_dict().unwrap().get(b"Fields").unwrap()), [8], "and its field in the form");
|
||||
}
|
||||
|
||||
/// #4017: when the walk up the parent fields reaches its limit before finding a value, the widget
|
||||
/// stays: an unverified value must not be removed. Here `/V` sits on the 64th ancestor.
|
||||
#[test]
|
||||
fn flattening_keeps_a_widget_whose_value_is_past_the_parent_limit() {
|
||||
let page = stream("", " ");
|
||||
let mut objs: Vec<String> = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [69 0 R] >> >>".into(),
|
||||
"<< /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 /Parent 6 0 R /Rect [10 10 150 40] /F 4 >>".into(),
|
||||
];
|
||||
objs.extend((6..69).map(|n| format!("<< /Parent {} 0 R /Kids [{} 0 R] >>", n + 1, n - 1)));
|
||||
objs.push("<< /FT /Tx /T (deep) /V (Ada) /Kids [68 0 R] >>".into());
|
||||
let mut doc = build(&objs.iter().map(String::as_str).collect::<Vec<_>>());
|
||||
flatten(&mut doc, &[0], false, true).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
let annots = pdfcraft_model::pages(&doc)[0].dict.get(b"Annots").map(|a| doc.resolve(a).as_array().map_or(0, Vec::len));
|
||||
assert_eq!(annots, Some(1), "the widget stays");
|
||||
}
|
||||
|
||||
/// #1794: a reply this builder cannot draw stays on the page even when the comment it replies to
|
||||
/// is flattened; the comment's pop-up still goes with it.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user