M5: a push button's caption uses the font, size and color of its /DA

Button appearances always drew the caption in regular Helvetica, sized
from the button's height, whatever the field's /DA asked for: DA Form
31's 'Attachments Menu' (9 pt bold) came out at 4 pt. The caption now
takes the /DA's font from the form's /DR, its size (an automatic size
still fits the button) and its color, as text fields already do.
This commit is contained in:
Eric Kaden
2026-10-19 15:34:27 -04:00
parent ec0a965074
commit 6b4e396af3
3 changed files with 56 additions and 17 deletions
+20
View File
@@ -220,6 +220,26 @@ fn dr_font(doc: &Document, name: &str) -> Option<(Object, FontText)> {
Some((entry, FontText::Simple(Box::new(Metrics::from_dict(doc, fd)))))
}
/// A push button's caption, centered in a `width` × `height` box, in the font, size and color its
/// `/DA` asks for: the font from the form's `/DR`, else Helvetica, and the size `auto` when the
/// `/DA` leaves it to the viewer (0). Returns the content operators, the font resource (name and
/// object), and the first character drawn as `?` because the font can't show it.
pub(crate) fn button_caption(doc: &Document, da: &str, caption: &str, width: f64, height: f64, auto: f64) -> (Vec<u8>, String, Object, Option<char>) {
let da = parse_da(da);
let (name, font, enc) = match dr_font(doc, &da.font).filter(|(_, enc)| enc.can_encode(caption)) {
Some((o, enc)) => (da.font.clone(), o, enc),
None => ("Helv".to_string(), helvetica(), FontText::WinAnsi),
};
let missing = if enc == FontText::WinAnsi { pdfcraft_fonts::first_undrawable(caption) } else { None };
let size = if da.size > 0.0 { da.size } else { auto };
let tw = enc.width(caption, size);
let mut out =
format!("BT /{name} {size:.2} Tf {} 1 0 0 1 {:.3} {:.3} Tm ", da.color, (width - tw) / 2.0, (height - size * 0.7) / 2.0).into_bytes();
out.extend(literal(&enc.encode(caption)));
out.extend_from_slice(b" Tj ET\n");
(out, name, font, missing)
}
fn helvetica() -> Object {
let mut f = Dict::new();
f.set(b"Type".to_vec(), Object::name("Font"));
+12 -17
View File
@@ -7,7 +7,6 @@
//! immediately so every viewer shows the empty field.
use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream};
use pdfcraft_fonts::{helvetica_width, literal, win_ansi};
use crate::{Field, FieldKind, FormError, Widget, appearance, fields, flags, page_refs};
@@ -706,7 +705,7 @@ fn redraw_widgets(doc: &mut Document, name: &str, only: Option<usize>) -> Result
appearance::check_box_states(doc, w, f.kind, &on)
}
FieldKind::PushButton => {
let (s, m) = button_appearance(doc, w);
let (s, m) = button_appearance(doc, w, &f.da);
missing = m;
let r = doc.add(Object::Stream(s));
let mut d = Dict::new();
@@ -767,7 +766,9 @@ fn form_stream(place: crate::appearance::Placement, content: Vec<u8>, resources:
/// A push button: background, border, its icon (`/MK /I`, scaled to fit and centred) and its
/// centred caption (unless the layout is icon only, `/TP 1`). The caption is drawn in Helvetica,
/// with the first character it can't draw (drawn as `?`), for the caller to mark and report.
fn button_appearance(doc: &Document, w: &Widget) -> (Stream, Option<char>) {
/// A push button's appearance: its icon and caption, the caption in the font its `/DA` asks for
/// (the widget's, else `field_da`, the field's own or inherited one).
fn button_appearance(doc: &Document, w: &Widget, field_da: &str) -> (Stream, Option<char>) {
let (mut c, place) = frame_only(doc, w);
let (width, height) = (place.layout_w, place.layout_h);
let wobj = doc.get(w.obj);
@@ -792,21 +793,15 @@ fn button_appearance(doc: &Document, w: &Widget) -> (Stream, Option<char>) {
}
}
let mut missing = None;
if !icon_only && !caption.is_empty() {
missing = pdfcraft_fonts::first_undrawable(&caption);
let size = ((height - 4.0) * 0.6).clamp(4.0, 14.0);
let tw = helvetica_width(&caption, size);
content.extend(format!("BT /Helv {size:.2} Tf 0 g 1 0 0 1 {:.3} {:.3} Tm ", (width - tw) / 2.0, (height - size * 0.7) / 2.0).bytes());
content.extend(literal(&win_ansi(&caption)));
content.extend_from_slice(b" Tj ET\n");
}
let mut font = Dict::new();
font.set(b"Type".to_vec(), Object::name("Font"));
font.set(b"Subtype".to_vec(), Object::name("Type1"));
font.set(b"BaseFont".to_vec(), Object::name("Helvetica"));
font.set(b"Encoding".to_vec(), Object::name("WinAnsiEncoding"));
let mut fonts = Dict::new();
fonts.set(b"Helv".to_vec(), Object::Dict(font));
if !icon_only && !caption.is_empty() {
let da = wobj.as_dict().and_then(|d| d.get(b"DA")).and_then(|o| doc.resolve(o).as_string().map(|s| s.to_text()));
let auto = ((height - 4.0) * 0.6).clamp(4.0, 14.0);
let (ops, name, font, m) = appearance::button_caption(doc, da.as_deref().unwrap_or(field_da), &caption, width, height, auto);
content.extend(ops);
fonts.set(name.into_bytes(), font);
missing = m;
}
let mut res = Dict::new();
res.set(b"Font".to_vec(), Object::Dict(fonts));
if !xobjects.is_empty() {
+24
View File
@@ -87,6 +87,30 @@ fn ap(doc: &Document, w: &Widget) -> String {
String::from_utf8_lossy(&s.decoded().unwrap()).into_owned()
}
#[test]
fn a_button_caption_is_drawn_in_its_fields_font_size_and_color() {
let objs: Vec<String> = vec![
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
"<< /Type /Page /Parent 2 0 R /Annots [7 0 R 8 0 R] >>".into(),
"<< /Fields [7 0 R 8 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 5 0 R /HeBo 6 0 R >> >> >>".into(),
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>".into(),
// Its /DA asks for 9pt Helvetica Bold in blue.
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (menu) /Ff 65536 /DA (/HeBo 9 Tf 0 0 1 rg) /MK << /CA (Menu) >> /Rect [50 700 140 711] /P 3 0 R >>".into(),
// An automatic size (0): sized to the button, as before.
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (auto) /Ff 65536 /MK << /CA (Auto) >> /Rect [50 600 140 630] /P 3 0 R >>".into(),
];
let mut doc = document(&objs);
redraw_field(&mut doc, "menu").unwrap();
redraw_field(&mut doc, "auto").unwrap();
let all = fields(&doc);
let menu = ap(&doc, &field(&all, "menu").widgets[0]);
assert!(menu.contains("/HeBo 9.00 Tf") && menu.contains("0 0 1 rg"), "{menu}");
let auto = ap(&doc, &field(&all, "auto").widgets[0]);
assert!(auto.contains("/Helv 14.00 Tf") && auto.contains(" 0 g"), "{auto}");
}
#[test]
fn button_caption_preserves_other_widgets_and_shared_dictionaries() {
for indirect_caption in [false, true] {