From 24ccb5f3ae73fa076cea60d4e6eab04988313bae Mon Sep 17 00:00:00 2001 From: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:20:15 +0200 Subject: [PATCH] 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. --- crates/forms/src/lib.rs | 10 ++++++---- crates/forms/src/tests.rs | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 337ae765..514563e5 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -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 { +fn values_of(doc: &Document, o: Option<&Object>, kind: FieldKind) -> Vec { 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, diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 975f607c..bee36653 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -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()); +}