fix(forms): a literal Off value reads as Off outside buttons (#1320) (#1393)

The field reader dropped the value "Off" for every field kind, so a
text field, combo box or single-select list set to "Off" read as empty
and exported an empty value to CSV, text, XML, XFDF and FDF, although
the document stored it. "Off" is now treated as no value only for
check boxes and radio buttons, where it is the off state.

Adds tests::a_literal_off_value_reads_as_off_outside_buttons, which
fails before this change.
This commit is contained in:
Marco Arnulfo 2026-10-12 13:20:15 +02:00 committed by GitHub
parent 3e2877cbbb
commit 24ccb5f3ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 26 additions and 4 deletions

View File

@ -430,12 +430,14 @@ fn actions_of(doc: &Document, d: &Dict, widgets: &[ObjRef]) -> af::Actions {
out
}
fn values_of(doc: &Document, o: Option<&Object>) -> Vec<String> {
fn values_of(doc: &Document, o: Option<&Object>, kind: FieldKind) -> Vec<String> {
let Some(o) = o else { return Vec::new() };
// "Off" is a button's off state; in a text or choice field it is the value.
let button = matches!(kind, FieldKind::CheckBox | FieldKind::Radio);
match &*doc.resolve(o) {
Object::Array(a) => a.iter().filter_map(|x| text_of(&doc.resolve(x))).collect(),
Object::Stream(s) => s.decoded().ok().map(|b| PdfString::literal(b).to_text()).into_iter().collect(),
other => text_of(other).filter(|s| s != "Off" && !s.is_empty()).into_iter().collect(),
other => text_of(other).filter(|s| !(button && s == "Off") && !s.is_empty()).into_iter().collect(),
}
}
@ -818,8 +820,8 @@ fn walk(
name: inh.name.clone(),
obj: r,
kind,
value: values_of(doc, inh.v.as_ref()),
default: values_of(doc, inh.dv.as_ref()),
value: values_of(doc, inh.v.as_ref(), kind),
default: values_of(doc, inh.dv.as_ref(), kind),
flags: ff,
max_len: inh.max_len,
options,

View File

@ -1850,3 +1850,23 @@ fn invalid_appearance_patches_are_refused_before_anything_is_written() {
assert!(set_props(&mut doc, "name", &props).is_err());
assert_eq!(write_incremental(&doc, &SaveOptions::default()).unwrap(), before);
}
/// "Off" is a button's off state, not a value; a text or choice field that holds "Off" reads
/// it (#1320), while an unchecked check box still reads as no value.
#[test]
fn a_literal_off_value_reads_as_off_outside_buttons() {
for (kind, value) in [
(NewField::Text { multiline: false }, FieldValue::Text("Off".into())),
(NewField::Combo { options: vec!["On".into(), "Off".into()], editable: false }, FieldValue::Choice(vec!["Off".into()])),
(NewField::List { options: vec!["On".into(), "Off".into()], multi: false }, FieldValue::Choice(vec!["Off".into()])),
] {
let mut doc = one_page();
add_field(&mut doc, 0, [50.0, 600.0, 250.0, 620.0], &kind, Some("power")).unwrap();
set_value(&mut doc, "power", &value).unwrap();
assert_eq!(field(&fields(&reopen(&doc)), "power").value, ["Off"], "{kind:?}");
}
let mut doc = one_page();
add_field(&mut doc, 0, [50.0, 600.0, 70.0, 620.0], &NewField::CheckBox, Some("power")).unwrap();
set_value(&mut doc, "power", &FieldValue::Check(false)).unwrap();
assert!(field(&fields(&doc), "power").value.is_empty());
}