Skip to main content

oxidize_pdf/forms/
button_widget.rs

1//! Button field widget integration for ISO 32000-1 compliance
2//!
3//! This module provides complete widget annotation support for button fields
4//! including checkboxes, radio buttons, and push buttons with proper appearance streams.
5
6use crate::annotations::{Annotation, AnnotationType};
7use crate::error::Result;
8use crate::forms::{CheckBox, PushButton, RadioButton};
9use crate::geometry::Rectangle;
10use crate::graphics::Color;
11use crate::objects::{Dictionary, Object, Stream};
12use std::io::Write;
13
14/// Button widget configuration
15#[derive(Debug, Clone)]
16pub struct ButtonWidget {
17    /// Widget rectangle on page
18    pub rect: Rectangle,
19    /// Border width
20    pub border_width: f64,
21    /// Border color
22    pub border_color: Color,
23    /// Background color
24    pub background_color: Option<Color>,
25    /// Text color for captions
26    pub text_color: Color,
27    /// Font size for captions
28    pub font_size: f64,
29}
30
31impl Default for ButtonWidget {
32    fn default() -> Self {
33        Self {
34            rect: Rectangle::new((0.0, 0.0).into(), (100.0, 20.0).into()),
35            border_width: 1.0,
36            border_color: Color::rgb(0.0, 0.0, 0.0),
37            background_color: Some(Color::rgb(1.0, 1.0, 1.0)),
38            text_color: Color::rgb(0.0, 0.0, 0.0),
39            font_size: 10.0,
40        }
41    }
42}
43
44impl ButtonWidget {
45    /// Create a new button widget
46    pub fn new(rect: Rectangle) -> Self {
47        Self {
48            rect,
49            ..Default::default()
50        }
51    }
52
53    /// Set border width
54    pub fn with_border_width(mut self, width: f64) -> Self {
55        self.border_width = width;
56        self
57    }
58
59    /// Set border color
60    pub fn with_border_color(mut self, color: Color) -> Self {
61        self.border_color = color;
62        self
63    }
64
65    /// Set background color
66    pub fn with_background_color(mut self, color: Option<Color>) -> Self {
67        self.background_color = color;
68        self
69    }
70
71    /// Set text color
72    pub fn with_text_color(mut self, color: Color) -> Self {
73        self.text_color = color;
74        self
75    }
76
77    /// Set font size
78    pub fn with_font_size(mut self, size: f64) -> Self {
79        self.font_size = size;
80        self
81    }
82}
83
84/// Create widget annotation for checkbox
85pub fn create_checkbox_widget(checkbox: &CheckBox, widget: &ButtonWidget) -> Result<Annotation> {
86    let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
87
88    // Set field reference
89    annotation
90        .properties
91        .set("FT", Object::Name("Btn".to_string()));
92    annotation
93        .properties
94        .set("T", Object::String(checkbox.name.clone()));
95
96    // Set current state
97    let state = if checkbox.checked {
98        &checkbox.export_value
99    } else {
100        "Off"
101    };
102    annotation
103        .properties
104        .set("AS", Object::Name(state.to_string()));
105    annotation
106        .properties
107        .set("V", Object::Name(state.to_string()));
108
109    // Create appearance dictionary
110    let mut ap_dict = Dictionary::new();
111
112    // Normal appearance states
113    let mut n_dict = Dictionary::new();
114
115    // Create checked appearance
116    let checked_stream = create_checkbox_appearance(widget, true)?;
117    n_dict.set(
118        &checkbox.export_value,
119        Object::Stream(
120            checked_stream.dictionary().clone(),
121            checked_stream.data().to_vec(),
122        ),
123    );
124
125    // Create unchecked appearance
126    let unchecked_stream = create_checkbox_appearance(widget, false)?;
127    n_dict.set(
128        "Off",
129        Object::Stream(
130            unchecked_stream.dictionary().clone(),
131            unchecked_stream.data().to_vec(),
132        ),
133    );
134
135    ap_dict.set("N", Object::Dictionary(n_dict));
136    annotation.properties.set("AP", Object::Dictionary(ap_dict));
137
138    // Set widget flags
139    let flags = 4; // Print flag
140    annotation.properties.set("F", Object::Integer(flags));
141
142    // Border style
143    let mut bs_dict = Dictionary::new();
144    bs_dict.set("W", Object::Real(widget.border_width));
145    bs_dict.set("S", Object::Name("S".to_string())); // Solid
146    annotation.properties.set("BS", Object::Dictionary(bs_dict));
147
148    // Appearance characteristics
149    let mut mk_dict = Dictionary::new();
150    if let Some(bg) = &widget.background_color {
151        mk_dict.set("BG", bg.to_pdf_array());
152    }
153    mk_dict.set("BC", widget.border_color.to_pdf_array());
154    mk_dict.set("CA", Object::String("✓".to_string())); // Check mark
155    annotation.properties.set("MK", Object::Dictionary(mk_dict));
156
157    Ok(annotation)
158}
159
160/// Create widget annotation for radio button
161pub fn create_radio_widget(
162    radio: &RadioButton,
163    widget: &ButtonWidget,
164    option_index: usize,
165) -> Result<Annotation> {
166    let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
167
168    // Set field reference
169    annotation
170        .properties
171        .set("FT", Object::Name("Btn".to_string()));
172    annotation
173        .properties
174        .set("T", Object::String(radio.name.clone()));
175
176    // Radio button flags
177    let flags = (1 << 15) | 4; // Radio + Print
178    annotation
179        .properties
180        .set("Ff", Object::Integer(flags as i64));
181
182    // Get option value
183    let (export_value, _label) = radio.options.get(option_index).ok_or_else(|| {
184        crate::error::PdfError::InvalidStructure("Invalid radio option index".to_string())
185    })?;
186
187    // Set current state
188    let state = if radio.selected == Some(option_index) {
189        export_value.as_str()
190    } else {
191        "Off"
192    };
193    annotation
194        .properties
195        .set("AS", Object::Name(state.to_string()));
196
197    // Create appearance dictionary
198    let mut ap_dict = Dictionary::new();
199    let mut n_dict = Dictionary::new();
200
201    // Create selected appearance
202    let selected_stream = create_radio_appearance(widget, true)?;
203    n_dict.set(
204        export_value,
205        Object::Stream(
206            selected_stream.dictionary().clone(),
207            selected_stream.data().to_vec(),
208        ),
209    );
210
211    // Create unselected appearance
212    let unselected_stream = create_radio_appearance(widget, false)?;
213    n_dict.set(
214        "Off",
215        Object::Stream(
216            unselected_stream.dictionary().clone(),
217            unselected_stream.data().to_vec(),
218        ),
219    );
220
221    ap_dict.set("N", Object::Dictionary(n_dict));
222    annotation.properties.set("AP", Object::Dictionary(ap_dict));
223
224    // Border and appearance characteristics
225    let mut bs_dict = Dictionary::new();
226    bs_dict.set("W", Object::Real(widget.border_width));
227    bs_dict.set("S", Object::Name("S".to_string()));
228    annotation.properties.set("BS", Object::Dictionary(bs_dict));
229
230    let mut mk_dict = Dictionary::new();
231    if let Some(bg) = &widget.background_color {
232        mk_dict.set("BG", bg.to_pdf_array());
233    }
234    mk_dict.set("BC", widget.border_color.to_pdf_array());
235    mk_dict.set("CA", Object::String("●".to_string())); // Radio dot
236    annotation.properties.set("MK", Object::Dictionary(mk_dict));
237
238    Ok(annotation)
239}
240
241/// Create widget annotation for push button
242pub fn create_pushbutton_widget(button: &PushButton, widget: &ButtonWidget) -> Result<Annotation> {
243    let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
244
245    // Set field reference
246    annotation
247        .properties
248        .set("FT", Object::Name("Btn".to_string()));
249    annotation
250        .properties
251        .set("T", Object::String(button.name.clone()));
252
253    // Push button flags
254    let flags = (1 << 16) | 4; // Pushbutton + Print
255    annotation
256        .properties
257        .set("Ff", Object::Integer(flags as i64));
258
259    // Create appearance
260    let mut ap_dict = Dictionary::new();
261    let appearance_stream = create_pushbutton_appearance(widget, button.caption.as_deref())?;
262    ap_dict.set(
263        "N",
264        Object::Stream(
265            appearance_stream.dictionary().clone(),
266            appearance_stream.data().to_vec(),
267        ),
268    );
269    annotation.properties.set("AP", Object::Dictionary(ap_dict));
270
271    // Border style
272    let mut bs_dict = Dictionary::new();
273    bs_dict.set("W", Object::Real(widget.border_width));
274    bs_dict.set("S", Object::Name("B".to_string())); // Beveled
275    annotation.properties.set("BS", Object::Dictionary(bs_dict));
276
277    // Appearance characteristics
278    let mut mk_dict = Dictionary::new();
279    if let Some(bg) = &widget.background_color {
280        mk_dict.set("BG", bg.to_pdf_array());
281    }
282    mk_dict.set("BC", widget.border_color.to_pdf_array());
283    if let Some(caption) = &button.caption {
284        mk_dict.set("CA", Object::String(caption.clone()));
285    }
286    annotation.properties.set("MK", Object::Dictionary(mk_dict));
287
288    // Highlight mode
289    annotation
290        .properties
291        .set("H", Object::Name("P".to_string())); // Push
292
293    Ok(annotation)
294}
295
296/// Create checkbox appearance stream
297fn create_checkbox_appearance(widget: &ButtonWidget, checked: bool) -> Result<Stream> {
298    let mut content = Vec::new();
299    let width = widget.rect.width();
300    let height = widget.rect.height();
301
302    // Draw background
303    if let Some(bg) = &widget.background_color {
304        crate::graphics::color::write_fill_color_bytes(&mut content, *bg);
305        writeln!(&mut content, "0 0 {} {} re f", width, height)?;
306    }
307
308    // Draw border
309    crate::graphics::color::write_stroke_color_bytes(&mut content, widget.border_color);
310    writeln!(&mut content, "{} w", widget.border_width)?;
311    writeln!(&mut content, "0 0 {} {} re S", width, height)?;
312
313    // Draw check mark if checked
314    if checked {
315        crate::graphics::color::write_stroke_color_bytes(&mut content, widget.text_color);
316        writeln!(&mut content, "2 w")?;
317        writeln!(&mut content, "1 J")?; // Round line cap
318
319        // Draw check mark path
320        let margin = width * 0.2;
321        let x1 = margin;
322        let y1 = height * 0.5;
323        let x2 = width * 0.4;
324        let y2 = margin;
325        let x3 = width - margin;
326        let y3 = height - margin;
327
328        writeln!(&mut content, "{} {} m", x1, y1)?;
329        writeln!(&mut content, "{} {} l", x2, y2)?;
330        writeln!(&mut content, "{} {} l S", x3, y3)?;
331    }
332
333    let mut resources = Dictionary::new();
334    resources.set(
335        "ProcSet",
336        Object::Array(vec![Object::Name("PDF".to_string())]),
337    );
338    let mut dict = Dictionary::new();
339    dict.set("Resources", Object::Dictionary(resources));
340
341    Ok(Stream::with_dictionary(dict, content))
342}
343
344/// Create radio button appearance stream
345fn create_radio_appearance(widget: &ButtonWidget, selected: bool) -> Result<Stream> {
346    let mut content = Vec::new();
347    let width = widget.rect.width();
348    let height = widget.rect.height();
349    let radius = width.min(height) / 2.0;
350    let center_x = width / 2.0;
351    let center_y = height / 2.0;
352
353    // Draw background circle
354    if let Some(bg) = &widget.background_color {
355        crate::graphics::color::write_fill_color_bytes(&mut content, *bg);
356        draw_circle(
357            &mut content,
358            center_x,
359            center_y,
360            radius - widget.border_width,
361        )?;
362        writeln!(&mut content, "f")?;
363    }
364
365    // Draw border circle
366    crate::graphics::color::write_stroke_color_bytes(&mut content, widget.border_color);
367    writeln!(&mut content, "{} w", widget.border_width)?;
368    draw_circle(
369        &mut content,
370        center_x,
371        center_y,
372        radius - widget.border_width / 2.0,
373    )?;
374    writeln!(&mut content, "S")?;
375
376    // Draw inner dot if selected
377    if selected {
378        crate::graphics::color::write_fill_color_bytes(&mut content, widget.text_color);
379        let dot_radius = radius * 0.4;
380        draw_circle(&mut content, center_x, center_y, dot_radius)?;
381        writeln!(&mut content, "f")?;
382    }
383
384    let mut resources = Dictionary::new();
385    resources.set(
386        "ProcSet",
387        Object::Array(vec![Object::Name("PDF".to_string())]),
388    );
389    let mut dict = Dictionary::new();
390    dict.set("Resources", Object::Dictionary(resources));
391
392    Ok(Stream::with_dictionary(dict, content))
393}
394
395/// Create push button appearance stream
396fn create_pushbutton_appearance(widget: &ButtonWidget, caption: Option<&str>) -> Result<Stream> {
397    let mut content = Vec::new();
398    let width = widget.rect.width();
399    let height = widget.rect.height();
400
401    // Draw background with beveled effect
402    if let Some(bg) = &widget.background_color {
403        // Main background
404        crate::graphics::color::write_fill_color_bytes(&mut content, *bg);
405        writeln!(&mut content, "0 0 {} {} re f", width, height)?;
406
407        // Top/left highlight (lighter)
408        crate::graphics::color::write_stroke_color_bytes(&mut content, Color::gray(0.9));
409        writeln!(&mut content, "2 w")?;
410        writeln!(&mut content, "1 {} m", height - 1.0)?;
411        writeln!(&mut content, "1 1 l")?;
412        writeln!(&mut content, "{} 1 l S", width - 1.0)?;
413
414        // Bottom/right shadow (darker)
415        crate::graphics::color::write_stroke_color_bytes(&mut content, Color::gray(0.5));
416        writeln!(&mut content, "{} 1 m", width - 1.0)?;
417        writeln!(&mut content, "{} {} l", width - 1.0, height - 1.0)?;
418        writeln!(&mut content, "1 {} l S", height - 1.0)?;
419    }
420
421    // Draw border
422    crate::graphics::color::write_stroke_color_bytes(&mut content, widget.border_color);
423    writeln!(&mut content, "{} w", widget.border_width)?;
424    writeln!(&mut content, "0 0 {} {} re S", width, height)?;
425
426    // Draw caption text
427    if let Some(text) = caption {
428        writeln!(&mut content, "BT")?;
429        writeln!(&mut content, "/Helvetica {} Tf", widget.font_size)?;
430        crate::graphics::color::write_fill_color_bytes(&mut content, widget.text_color);
431
432        // Center text
433        let text_width = text.len() as f64 * widget.font_size * 0.5;
434        let x = (width - text_width) / 2.0;
435        let y = (height - widget.font_size) / 2.0;
436
437        writeln!(&mut content, "{} {} Td", x, y)?;
438        writeln!(&mut content, "({}) Tj", escape_pdf_string(text))?;
439        writeln!(&mut content, "ET")?;
440    }
441
442    let mut resources = Dictionary::new();
443
444    // Add font resources
445    let mut fonts = Dictionary::new();
446    let mut font_dict = Dictionary::new();
447    font_dict.set("Type", Object::Name("Font".to_string()));
448    font_dict.set("Subtype", Object::Name("Type1".to_string()));
449    font_dict.set("BaseFont", Object::Name("Helvetica".to_string()));
450    fonts.set("Helvetica", Object::Dictionary(font_dict));
451    resources.set("Font", Object::Dictionary(fonts));
452
453    resources.set(
454        "ProcSet",
455        Object::Array(vec![
456            Object::Name("PDF".to_string()),
457            Object::Name("Text".to_string()),
458        ]),
459    );
460
461    let mut dict = Dictionary::new();
462    dict.set("Resources", Object::Dictionary(resources));
463
464    Ok(Stream::with_dictionary(dict, content))
465}
466
467/// Helper to draw a circle using Bézier curves
468fn draw_circle<W: Write>(writer: &mut W, cx: f64, cy: f64, r: f64) -> Result<()> {
469    let k = 0.5522847498; // Magic constant for circle approximation
470    let dx = r * k;
471    let dy = r * k;
472
473    writeln!(writer, "{} {} m", cx + r, cy)?;
474    writeln!(
475        writer,
476        "{} {} {} {} {} {} c",
477        cx + r,
478        cy + dy,
479        cx + dx,
480        cy + r,
481        cx,
482        cy + r
483    )?;
484    writeln!(
485        writer,
486        "{} {} {} {} {} {} c",
487        cx - dx,
488        cy + r,
489        cx - r,
490        cy + dy,
491        cx - r,
492        cy
493    )?;
494    writeln!(
495        writer,
496        "{} {} {} {} {} {} c",
497        cx - r,
498        cy - dy,
499        cx - dx,
500        cy - r,
501        cx,
502        cy - r
503    )?;
504    writeln!(
505        writer,
506        "{} {} {} {} {} {} c",
507        cx + dx,
508        cy - r,
509        cx + r,
510        cy - dy,
511        cx + r,
512        cy
513    )?;
514
515    Ok(())
516}
517
518/// Escape special characters in PDF strings
519fn escape_pdf_string(s: &str) -> String {
520    s.chars()
521        .flat_map(|c| match c {
522            '(' => vec!['\\', '('],
523            ')' => vec!['\\', ')'],
524            '\\' => vec!['\\', '\\'],
525            _ => vec![c],
526        })
527        .collect()
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    #[test]
535    fn test_checkbox_widget() {
536        let checkbox = CheckBox::new("agree").checked().with_export_value("Yes");
537
538        let widget = ButtonWidget::new(Rectangle::new((0.0, 0.0).into(), (20.0, 20.0).into()));
539
540        let annotation = create_checkbox_widget(&checkbox, &widget).unwrap();
541
542        // Verify widget annotation properties
543        assert_eq!(annotation.annotation_type, AnnotationType::Widget);
544        assert!(annotation.properties.get("AP").is_some());
545        assert!(annotation.properties.get("AS").is_some());
546        assert_eq!(
547            annotation.properties.get("AS"),
548            Some(&Object::Name("Yes".to_string()))
549        );
550    }
551
552    #[test]
553    fn test_radio_widget() {
554        let radio = RadioButton::new("size")
555            .add_option("S", "Small")
556            .add_option("M", "Medium")
557            .add_option("L", "Large")
558            .with_selected(1);
559
560        let widget = ButtonWidget::new(Rectangle::new((0.0, 0.0).into(), (20.0, 20.0).into()));
561
562        let annotation = create_radio_widget(&radio, &widget, 1).unwrap();
563
564        // Verify radio button widget properties
565        assert_eq!(annotation.annotation_type, AnnotationType::Widget);
566        assert!(annotation.properties.get("AP").is_some());
567        assert_eq!(
568            annotation.properties.get("AS"),
569            Some(&Object::Name("M".to_string()))
570        );
571    }
572
573    #[test]
574    fn test_pushbutton_widget() {
575        let button = PushButton::new("submit").with_caption("Submit Form");
576
577        let widget = ButtonWidget::new(Rectangle::new((0.0, 0.0).into(), (100.0, 30.0).into()));
578
579        let annotation = create_pushbutton_widget(&button, &widget).unwrap();
580
581        // Verify push button widget properties
582        assert_eq!(annotation.annotation_type, AnnotationType::Widget);
583        assert!(annotation.properties.get("AP").is_some());
584        assert!(annotation.properties.get("MK").is_some());
585    }
586
587    #[test]
588    fn test_widget_customization() {
589        let widget = ButtonWidget::new(Rectangle::new((0.0, 0.0).into(), (50.0, 50.0).into()))
590            .with_border_width(2.0)
591            .with_border_color(Color::rgb(1.0, 0.0, 0.0))
592            .with_background_color(Some(Color::rgb(0.9, 0.9, 1.0)))
593            .with_text_color(Color::rgb(0.0, 0.0, 1.0))
594            .with_font_size(12.0);
595
596        assert_eq!(widget.border_width, 2.0);
597        assert_eq!(widget.border_color, Color::rgb(1.0, 0.0, 0.0));
598        assert_eq!(widget.font_size, 12.0);
599    }
600}