mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 01:41:18 +00:00
M6: a radio group's XFA data compares every button's on state before any /PCOn
radio_dataset_value returned the first widget whose on state or /PCOn equalled the selected state. With states /0 /1 and /PCOn items 1 2, selecting /1 matched the first widget's /PCOn "1" and wrote 1 instead of 2, and strip_xfa's comparison, which uses the same function, could then miss a lost value. The selection is an on-state name, so every widget's state is now compared first and /PCOn only after. Test (fails with the one-pass match restored, writing "1" for /1): the static radio fixture with /PCOn 1 2 writes 1 for /0, 2 for /1, 2 for the item 2, the value itself when nothing matches, and nothing with no selection. This changes the XFA data saved for such groups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
1 parent
c42851da1e
commit
dbea0793e8
2 files changed
+42
-13
No files matched your search
@@ -60,21 +60,32 @@ pub fn radio_dataset_value(doc: &Document, f: &FieldDatum, sel: &Option<String>)
|
||||
.collect()
|
||||
})
|
||||
.filter(|opt: &Vec<Option<String>>| 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<String>, Option<String>, Option<String>)> = 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.
|
||||
|
||||
@@ -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("<form1/>", ["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), "");
|
||||
}
|
||||
Reference in new issue
Block a user