mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Merge PR #4295: Check boxes and radio buttons draw their mark in the field's text colour
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)))?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user