diff --git a/crates/xfa/src/data/values.rs b/crates/xfa/src/data/values.rs index 1a476e1ae1..2fef49e85c 100644 --- a/crates/xfa/src/data/values.rs +++ b/crates/xfa/src/data/values.rs @@ -60,21 +60,32 @@ pub fn radio_dataset_value(doc: &Document, f: &FieldDatum, sel: &Option) .collect() }) .filter(|opt: &Vec>| opt.len() == kids.len()); - for (i, kid) in kids.iter().enumerate() { - let Some(widget) = doc.resolve(kid).as_dict().cloned() else { continue }; - let original = text_of(doc, &widget, b"PCOn"); - let state = widget - .get(b"AP") - .map(|ap| doc.resolve(ap)) - .and_then(|ap| ap.as_dict().and_then(|a| a.get(b"N").cloned())) - .and_then(|n| doc.resolve(&n).as_dict().cloned()) - .and_then(|n| n.iter().map(|(k, _)| String::from_utf8_lossy(k).into_owned()).find(|k| k != "Off")); - if state.as_deref() == Some(selected) || original.as_deref() == Some(selected) { + // Each widget's (`/PCOn` item, on-state name, `/Opt` entry). + let widgets: Vec<(Option, Option, Option)> = kids + .iter() + .enumerate() + .filter_map(|(i, kid)| { + let widget = doc.resolve(kid).as_dict().cloned()?; + let original = text_of(doc, &widget, b"PCOn"); + let state = widget + .get(b"AP") + .map(|ap| doc.resolve(ap)) + .and_then(|ap| ap.as_dict().and_then(|a| a.get(b"N").cloned())) + .and_then(|n| doc.resolve(&n).as_dict().cloned()) + .and_then(|n| n.iter().map(|(k, _)| String::from_utf8_lossy(k).into_owned()).find(|k| k != "Off")); let export = exports.as_ref().and_then(|e| e.get(i).cloned().flatten()); - return original.or(export).unwrap_or_else(|| selected.to_string()); - } + Some((original, state, export)) + }) + .collect(); + // `sel` is an on-state name, so every widget's state is compared before any `/PCOn`: with + // states `/0` `/1` and items `1` `2`, the state `1` is the second widget, not the first + // one's item. + let by_state = widgets.iter().find(|(_, state, _)| state.as_deref() == Some(selected)); + let by_item = || widgets.iter().find(|(original, _, _)| original.as_deref() == Some(selected)); + match by_state.or_else(by_item) { + Some((original, _, export)) => original.clone().or_else(|| export.clone()).unwrap_or_else(|| selected.to_string()), + None => selected.to_string(), } - selected.to_string() } /// The on and off item values of a check box (`/PCItems`), or the widget's on state. diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index e424be5441..829bb76853 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4145,3 +4145,21 @@ fn clear_password_values_clears_static_xfa_password_fields() { let packets = read_packets(&doc).unwrap().unwrap(); assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp); } + +/// The data a radio group writes for its selected on state compares every button's state before +/// any `/PCOn`: with states `/0` `/1` and `/PCOn` items `1` `2`, the state `1` is the second +/// button, whose item is `2` (it used to match the first button's `/PCOn` `1` and write `1`). +/// A selection that is only an item still finds its button. +#[test] +fn a_radio_state_is_matched_before_any_pcon_item() { + let doc = Document::open(Arc::new(crate::fixtures::static_radio_shell_with_pcon("", ["1", "2"]))).unwrap(); + let group = FieldDatum { obj: pdfcraft_cos::ObjRef::new(8, 0), name: "form1[0].page1[0].lang[0]".into(), data: FieldData::None }; + let write = |sel: &str| crate::data::radio_dataset_value(&doc, &group, &Some(sel.to_string())); + assert_eq!(write("0"), "1"); + assert_eq!(write("1"), "2"); + // `2` is no state: it is the second button's item. + assert_eq!(write("2"), "2"); + // Neither: written as given; nothing selected: empty. + assert_eq!(write("9"), "9"); + assert_eq!(crate::data::radio_dataset_value(&doc, &group, &None), ""); +}