Merge PR #4295: Check boxes and radio buttons draw their mark in the field's text colour

This commit is contained in:
aiongg
2026-10-24 06:21:42 +00:00
4 changed files with 59 additions and 12 deletions
+13 -9
View File
@@ -663,8 +663,10 @@ fn field_appearance_fit(doc: &Document, f: &Field, w: &Widget, values: &[String]
/// On/Off appearances for a check box or radio button that has none (a check mark or a dot,
/// drawn as paths, so no symbol font is needed).
/// The mark is drawn in the colour of the widget's `/DA`, else of `f_da` (the field's): the
/// Appearance tab's Text Color (#4261).
/// The streams are added to `doc` as indirect objects (streams can't be direct objects).
pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name: &str) -> Dict {
pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name: &str, f_da: &str) -> Dict {
let wobj = doc.get(w.obj);
let wd = wobj.as_dict().cloned().unwrap_or_default();
let (page_w, page_h) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0));
@@ -672,6 +674,7 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name
let (width, height) = (place.layout_w, place.layout_h);
let (frame_c, _) = frame(doc, &wd, width, height);
let style = crate::author::CheckStyle::of_widget(doc, &wd, kind);
let da = parse_da(wd.get(b"DA").and_then(|o| doc.resolve(o).as_string().map(|s| s.to_text())).as_deref().unwrap_or(f_da));
let mut form = |content: String| -> Object {
let mut d = Dict::new();
d.set(b"Type".to_vec(), Object::name("XObject"));
@@ -680,7 +683,7 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name
Object::Ref(doc.add(Object::Stream(Stream::flate(d, content.as_bytes()))))
};
let s = width.min(height);
let mark = check_mark(style, width / 2.0, height / 2.0, s);
let mark = check_mark(style, width / 2.0, height / 2.0, s, &da.color);
let mut nd = Dict::new();
nd.set(crate::name_bytes(on_name), form(format!("{frame_c}{mark}")));
nd.set(b"Off".to_vec(), form(frame_c));
@@ -690,12 +693,13 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name
}
/// The on-state mark for `style` (#94), centred on (`cx`, `cy`) in a box whose smaller side is
/// `s`, drawn in black.
fn check_mark(style: crate::author::CheckStyle, cx: f64, cy: f64, s: f64) -> String {
/// `s`, drawn in the fill colour operation `color` (`0 g`, `0 0 1 rg`).
fn check_mark(style: crate::author::CheckStyle, cx: f64, cy: f64, s: f64, color: &str) -> String {
use crate::author::CheckStyle;
let (fill, stroke) = (color, stroke_op(color));
// A filled polygon through `pts`.
let polygon = |pts: &[(f64, f64)]| {
let mut c = String::from("0 g\n");
let mut c = format!("{fill}\n");
for (i, (x, y)) in pts.iter().enumerate() {
c.push_str(&format!("{} {} {} ", n(*x), n(*y), if i == 0 { "m" } else { "l" }));
}
@@ -705,7 +709,7 @@ fn check_mark(style: crate::author::CheckStyle, cx: f64, cy: f64, s: f64) -> Str
let line = n((s * 0.1).max(1.0));
match style {
CheckStyle::Check => format!(
"0 G\n{line} w 1 J 1 j\n{} {} m {} {} l {} {} l S\n",
"{stroke}\n{line} w 1 J 1 j\n{} {} m {} {} l {} {} l S\n",
n(cx - s * 0.28),
n(cy),
n(cx - s * 0.08),
@@ -716,7 +720,7 @@ fn check_mark(style: crate::author::CheckStyle, cx: f64, cy: f64, s: f64) -> Str
CheckStyle::Cross => {
let d = s * 0.25;
format!(
"0 G\n{line} w 1 J\n{} {} m {} {} l S\n{} {} m {} {} l S\n",
"{stroke}\n{line} w 1 J\n{} {} m {} {} l S\n{} {} m {} {} l S\n",
n(cx - d),
n(cy - d),
n(cx + d),
@@ -729,7 +733,7 @@ fn check_mark(style: crate::author::CheckStyle, cx: f64, cy: f64, s: f64) -> Str
}
CheckStyle::Square => {
let d = s * 0.22;
format!("0 g\n{} {} {} {} re f\n", n(cx - d), n(cy - d), n(2.0 * d), n(2.0 * d))
format!("{fill}\n{} {} {} {} re f\n", n(cx - d), n(cy - d), n(2.0 * d), n(2.0 * d))
}
CheckStyle::Diamond => {
let d = s * 0.3;
@@ -751,7 +755,7 @@ fn check_mark(style: crate::author::CheckStyle, cx: f64, cy: f64, s: f64) -> Str
let r = s * 0.25;
let k = 0.5523 * r;
format!(
"0 g\n{} {} m {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c f\n",
"{fill}\n{} {} m {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c f\n",
n(cx + r),
n(cy),
n(cx + r),
+2 -2
View File
@@ -964,7 +964,7 @@ pub fn add_field(doc: &mut Document, page: usize, rect: [f64; 4], kind: &NewFiel
caption: None,
list: None,
};
let ap = appearance::check_box_states(doc, &w, FieldKind::Radio, export);
let ap = appearance::check_box_states(doc, &w, FieldKind::Radio, export, "/ZaDb 0 Tf 0 g");
doc.update_dict(widget, |d| d.set(b"AP".to_vec(), Object::Dict(ap)))?;
}
_ => attach(doc, af, widget, &parts)?,
@@ -1004,7 +1004,7 @@ fn redraw_widgets(doc: &mut Document, name: &str, only: Option<usize>) -> Result
}
FieldKind::CheckBox | FieldKind::Radio => {
let on = w.on_state.clone().unwrap_or_else(|| "Yes".into());
appearance::check_box_states(doc, w, f.kind, &on)
appearance::check_box_states(doc, w, f.kind, &on, &f.da)
}
FieldKind::PushButton => {
let (s, m) = button_appearance(doc, w, &f.da);
+1 -1
View File
@@ -1192,7 +1192,7 @@ fn set_states(doc: &mut Document, f: &Field, on: Option<&str>) -> Result<(), For
let has_ap = doc.get(w.obj).as_dict().and_then(|d| d.get(b"AP").cloned()).is_some();
if !has_ap {
let on_name = w.on_state.clone().unwrap_or_else(|| on.unwrap_or("Yes").to_string());
let ap = appearance::check_box_states(doc, w, f.kind, &on_name);
let ap = appearance::check_box_states(doc, w, f.kind, &on_name, &f.da);
doc.update_dict(w.obj, |d| d.set(b"AP".to_vec(), Object::Dict(ap)))?;
}
doc.update_dict(w.obj, |d| d.set(b"AS".to_vec(), name_obj(state)))?;
+43
View File
@@ -1752,6 +1752,49 @@ fn check_box_styles_change_the_mark() {
);
}
#[test]
fn check_mark_takes_the_fields_text_colour() {
// #4261: Appearance ▸ Text Color is written to /DA; the mark of a check box or radio button
// is drawn in it (Acrobat does the same), black when the /DA has no colour.
let mut doc = one_page();
let check = add_field(&mut doc, 0, [50.0, 700.0, 70.0, 720.0], &NewField::CheckBox, None).unwrap();
let radio = add_field(&mut doc, 0, [50.0, 600.0, 70.0, 620.0], &NewField::Radio { group: None, export: "A".into() }, None).unwrap();
let on_ap = |doc: &Document, name: &str| {
let all = fields(doc);
let w = &field(&all, name).widgets[0];
let on = w.on_state.clone().unwrap();
let n = doc
.get(w.obj)
.as_dict()
.unwrap()
.get(b"AP")
.unwrap()
.as_dict()
.unwrap()
.get(b"N")
.unwrap()
.as_dict()
.unwrap()
.reference(on.as_bytes())
.unwrap();
let Object::Stream(s) = &*doc.get(n) else { panic!() };
String::from_utf8_lossy(&s.decoded().unwrap()).into_owned()
};
assert!(on_ap(&doc, &check).contains("0 G"), "black by default");
let blue = FieldProps { appearance: Some(LookPatch { text: Some([0.0, 0.0, 1.0]), ..Default::default() }), ..Default::default() };
for name in [&check, &radio] {
set_props(&mut doc, name, &blue).unwrap();
let ap = on_ap(&reopen(&doc), name);
// The check strokes, the radio's default circle fills.
let op = if name == &check { "0 0 1 RG" } else { "0 0 1 rg" };
assert!(ap.contains(op), "{name}: the mark is blue: {ap}");
assert!(!ap.contains("0 G\n") && !ap.contains("0 g\n"), "{name}: no black left: {ap}");
}
// A style change redraws too, and keeps the colour.
set_props(&mut doc, &check, &FieldProps { check_style: Some(CheckStyle::Square), ..FieldProps::default() }).unwrap();
assert!(on_ap(&doc, &check).contains("0 0 1 rg"), "a square is blue as well");
}
#[test]
fn ordering_tabs_manually() {
let mut doc = one_page();