mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
fix(edit): flattening form fields also drops the ones whose widgets are on no page (fixes #1470) (#1480)
This commit is contained in:
1 parent
0d300c6a41
commit
68f8b2d5f7
2 files changed
+52
-8
No files matched your search
@@ -249,33 +249,49 @@ fn flatten_which(doc: &mut Document, pages: &[usize], which: Which) -> Result<us
|
||||
})?,
|
||||
}
|
||||
}
|
||||
if !removed_widgets.is_empty() {
|
||||
prune_fields(doc, &removed_widgets)?;
|
||||
// A field whose widgets are on no page has nothing to draw, but flattening every field of the
|
||||
// document must still take it out of the form, or other software keeps seeing one (#1470).
|
||||
let orphans = matches!(which, Which::Comments { fields: true, .. }) && (0..all.len()).all(|i| pages.contains(&i));
|
||||
if !removed_widgets.is_empty() || orphans {
|
||||
let on_page: HashSet<ObjRef> = page_list(doc)
|
||||
.iter()
|
||||
.filter_map(|p| p.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()))
|
||||
.flatten()
|
||||
.filter_map(|e| e.as_ref())
|
||||
.collect();
|
||||
prune_fields(doc, &removed_widgets, orphans.then_some(&on_page))?;
|
||||
}
|
||||
Ok(drawn)
|
||||
}
|
||||
|
||||
/// Remove flattened widgets from the AcroForm field tree, and fields left without widgets.
|
||||
fn prune_fields(doc: &mut Document, widgets: &HashSet<ObjRef>) -> Result<(), EditError> {
|
||||
/// Remove flattened widgets from the AcroForm field tree, and fields left without widgets. With
|
||||
/// `on_page`, also the widgets (fields without kids) that are on no page.
|
||||
fn prune_fields(doc: &mut Document, widgets: &HashSet<ObjRef>, on_page: Option<&HashSet<ObjRef>>) -> Result<(), EditError> {
|
||||
let Some(root) = doc.root() else { return Ok(()) };
|
||||
let Some(af) = doc.get(root).as_dict().and_then(|d| d.get(b"AcroForm").cloned()) else { return Ok(()) };
|
||||
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.
|
||||
fn keep(doc: &mut Document, r: ObjRef, widgets: &HashSet<ObjRef>, depth: usize) -> bool {
|
||||
fn keep(doc: &mut Document, r: ObjRef, widgets: &HashSet<ObjRef>, on_page: Option<&HashSet<ObjRef>>, depth: usize) -> bool {
|
||||
if widgets.contains(&r) || depth > 64 {
|
||||
return depth > 64;
|
||||
}
|
||||
let Some(d) = doc.get(r).as_dict().cloned() else { return true };
|
||||
let Some(kids) = d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()) else { return true };
|
||||
let survivors: Vec<Object> = kids.iter().filter(|k| k.as_ref().is_none_or(|kr| keep(doc, kr, widgets, depth + 1))).cloned().collect();
|
||||
let Some(kids) = d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()) else {
|
||||
return on_page.is_none_or(|p| p.contains(&r));
|
||||
};
|
||||
let survivors: Vec<Object> =
|
||||
kids.iter().filter(|k| k.as_ref().is_none_or(|kr| keep(doc, kr, widgets, on_page, depth + 1))).cloned().collect();
|
||||
if survivors.len() != kids.len() {
|
||||
let _ = doc.update_dict(r, |d| d.set(b"Kids".to_vec(), Object::Array(survivors.clone())));
|
||||
}
|
||||
!survivors.is_empty()
|
||||
}
|
||||
let survivors: Vec<Object> = fields.iter().filter(|f| f.as_ref().is_none_or(|r| keep(doc, r, widgets, 0))).cloned().collect();
|
||||
let survivors: Vec<Object> = fields.iter().filter(|f| f.as_ref().is_none_or(|r| keep(doc, r, widgets, on_page, 0))).cloned().collect();
|
||||
if survivors.len() == fields.len() && widgets.is_empty() {
|
||||
return Ok(()); // only looked for fields on no page, and there were none
|
||||
}
|
||||
af_dict.set(b"Fields".to_vec(), Object::Array(survivors));
|
||||
match af_ref {
|
||||
Some(r) => doc.set(r, Object::Dict(af_dict)),
|
||||
|
||||
@@ -234,6 +234,34 @@ fn flattening_keeps_a_note_whose_appearance_is_missing() {
|
||||
assert!(flat.contains("q 1 0 0 1 10 10 cm /PCFl0 Do Q"), "{flat}");
|
||||
}
|
||||
|
||||
/// #1470: a form field whose widget is on no page (left behind by page extraction or combining)
|
||||
/// has nothing to draw, but flattening every form field still takes it out of `/AcroForm /Fields`,
|
||||
/// or other software keeps treating the flattened file as a form. A hidden widget on a page is
|
||||
/// left alone, as before.
|
||||
#[test]
|
||||
fn flattening_fields_drops_the_ones_on_no_page() {
|
||||
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 7 0 R] >>",
|
||||
&page,
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (on_page) /Rect [10 10 150 40] /F 4 /AP << /N 10 0 R >> >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (orphan) /Rect [10 50 150 80] /F 4 /AP << /N 10 0 R >> >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (hidden) /Rect [10 90 150 120] /F 2 /AP << /N 10 0 R >> >>",
|
||||
"<< /FT /Tx /T (parent) /Kids [9 0 R] >>",
|
||||
"<< /Type /Annot /Subtype /Widget /Parent 8 0 R /Rect [10 130 150 160] /F 4 /AP << /N 10 0 R >> >>",
|
||||
"<< /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 on the page is drawn");
|
||||
let doc = reopen(&doc);
|
||||
let root = doc.root().unwrap();
|
||||
let af = doc.resolve(doc.get(root).as_dict().unwrap().get(b"AcroForm").unwrap());
|
||||
let fields = doc.resolve(af.as_dict().unwrap().get(b"Fields").unwrap());
|
||||
let left: Vec<pdfcraft_cos::ObjRef> = fields.as_array().unwrap().iter().filter_map(|f| f.as_ref()).collect();
|
||||
assert_eq!(left, [pdfcraft_cos::ObjRef::new(7, 0)], "only the hidden field on the page stays");
|
||||
}
|
||||
|
||||
/// Flattening never makes a `?` permanent: it refuses, changing nothing, a text box without an
|
||||
/// appearance whose text Helvetica can't draw (another program's), and a comment or form field
|
||||
/// whose appearance PdfCraft drew with `?` (marked `PCUndrawn`). A comment another program drew
|
||||
|
||||
Reference in new issue
Block a user