Forms: fields that share a name take the same value (fixes #2214) (#2217)

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Marco Arnulfo
2026-10-15 02:49:17 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent dfdb31b25d
commit c4e95d3af3
3 changed files with 86 additions and 0 deletions
+41
View File
@@ -854,10 +854,51 @@ pub fn set_value_with(doc: &mut Document, name: &str, value: &FieldValue, script
return Err(FormError::ReadOnly(name.into()));
}
write_value(doc, f, value, scripts)?;
// Fields with the same fully qualified name are one field (§12.7.3.1). Writers that repeat a
// name as separate field dictionaries, instead of widgets under one parent, get the value
// the first one took after its scripts, without running them again.
let twins: Vec<&Field> = all.iter().filter(|t| t.name == name && t.obj != f.obj && t.kind == f.kind).collect();
if !twins.is_empty() {
let taken = fields(doc).into_iter().find(|x| x.obj == f.obj).map(|x| x.value).unwrap_or_default();
for t in twins {
share_value(doc, f.obj, t, &taken, scripts)?;
}
}
recalculate_with(doc, scripts)?;
Ok(())
}
/// Give `twin` the value `source` holds: its `/V` (and a choice's `/I`), drawn in the twin's own
/// widgets.
fn share_value(doc: &mut Document, source: ObjRef, twin: &Field, value: &[String], scripts: &mut dyn Scripts) -> Result<(), FormError> {
match twin.kind {
FieldKind::CheckBox => {
let state = twin.widgets.iter().find_map(|w| w.on_state.clone()).unwrap_or_else(|| "Yes".into());
set_states(doc, twin, (!value.is_empty()).then_some(state.as_str()))
}
FieldKind::Radio => {
let choice = value.first().filter(|c| twin.widgets.iter().any(|w| w.on_state.as_deref() == Some(c.as_str())));
set_states(doc, twin, choice.map(String::as_str))
}
FieldKind::Text | FieldKind::Combo | FieldKind::List => {
let src = doc.get(source).as_dict().cloned().unwrap_or_default();
doc.update_dict(twin.obj, |d| {
for key in [&b"V"[..], b"I"] {
match src.get(key) {
Some(v) => d.set(key.to_vec(), v.clone()),
None => {
d.remove(key);
}
}
}
d.remove(b"RV");
})?;
redraw(doc, twin, value, scripts)
}
FieldKind::PushButton | FieldKind::Signature => Ok(()),
}
}
/// Run every field's Calculate script in the form's calculation order (`/CO`, then the other
/// calculated fields), as Acrobat does after any value changes. Returns how many changed.
pub fn recalculate(doc: &mut Document) -> Result<usize, FormError> {
+43
View File
@@ -1173,6 +1173,49 @@ fn duplicating_a_field_across_pages_shares_its_value() {
assert!(duplicate_field(&mut doc.clone(), &name, &[7]).is_err());
}
/// Fields with the same fully qualified name are one field (§12.7.3.1): a parent whose widgets are
/// its kids, as Duplicate Across Pages writes it, and separate field dictionaries that repeat the
/// same `/T`, as other writers do. Filling the name fills every one of them.
#[test]
fn fields_sharing_a_name_take_the_same_value() {
let objs: Vec<String> = vec![
"<< /Type /Catalog /Pages 2 0 R /AcroForm 5 0 R >>".into(), // 1
"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 600 800] >>".into(), // 2
"<< /Type /Page /Parent 2 0 R /Annots [7 0 R 11 0 R] >>".into(), // 3
"<< /Type /Page /Parent 2 0 R /Annots [8 0 R 9 0 R 12 0 R] >>".into(), // 4
"<< /Fields [6 0 R 9 0 R 11 0 R 12 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 10 0 R >> >> >>".into(), // 5
"<< /FT /Tx /T (ClientName) /Kids [7 0 R 8 0 R] >>".into(), // 6 parent
"<< /Type /Annot /Subtype /Widget /Parent 6 0 R /Rect [50 700 250 720] /P 3 0 R >>".into(), // 7 its widget, page 1
"<< /Type /Annot /Subtype /Widget /Parent 6 0 R /Rect [50 700 250 720] /P 4 0 R >>".into(), // 8 its widget, page 2
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (ClientName) /Rect [50 600 250 620] /P 4 0 R >>".into(), // 9 same name, own field
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(), // 10
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (Agree) /V /Off /AS /Off /Rect [300 700 315 715] /P 3 0 R /AP << /N << /Yes 13 0 R /Off 14 0 R >> >> >>".into(), // 11
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (Agree) /V /Off /AS /Off /Rect [300 700 315 715] /P 4 0 R /AP << /N << /Yes 13 0 R /Off 14 0 R >> >> >>".into(), // 12
"<< /Length 0 >>\nstream\n\nendstream".into(), // 13
"<< /Length 0 >>\nstream\n\nendstream".into(), // 14
];
let mut doc = document(&objs);
set_value(&mut doc, "ClientName", &FieldValue::Text("Ada Lovelace".into())).unwrap();
set_value(&mut doc, "Agree", &FieldValue::Check(true)).unwrap();
let doc = reopen(&doc);
let v = |n: u32| doc.get(ObjRef { num: n, generation: 0 }).as_dict().unwrap().get(b"V").cloned();
let typed = Some(Object::String(PdfString::text("Ada Lovelace")));
assert_eq!((v(6), v(9)), (typed.clone(), typed), "the value sits on the parent and on the field that repeats its name");
assert_eq!((v(7), v(8)), (None, None), "the parent's widgets carry no value of their own");
let all = fields(&doc);
let named: Vec<&Field> = all.iter().filter(|f| f.name == "ClientName").collect();
let widgets: Vec<&Widget> = named.iter().flat_map(|f| &f.widgets).collect();
assert_eq!(widgets.iter().map(|w| w.obj.num).collect::<Vec<_>>(), [7, 8, 9]);
for w in widgets {
assert!(ap(&doc, w).contains("Ada Lovelace"), "widget {} shows the value", w.obj.num);
}
let agree: Vec<&Field> = all.iter().filter(|f| f.name == "Agree").collect();
assert_eq!(agree.len(), 2);
for f in agree {
assert_eq!((f.value.as_slice(), f.widgets[0].state.as_deref()), (&["Yes".to_string()][..], Some("Yes")), "check box {}", f.obj.num);
}
}
#[test]
fn locked_fields_only_take_unlocking() {
let mut doc = one_page();