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        match bg {
305            Color::Rgb(r, g, b) => writeln!(&mut content, "{} {} {} rg", r, g, b)?,
306            Color::Gray(g) => writeln!(&mut content, "{} g", g)?,
307            Color::Cmyk(c, m, y, k) => writeln!(&mut content, "{} {} {} {} k", c, m, y, k)?,
308        }
309        writeln!(&mut content, "0 0 {} {} re f", width, height)?;
310    }
311
312    // Draw border
313    match &widget.border_color {
314        Color::Rgb(r, g, b) => writeln!(&mut content, "{} {} {} RG", r, g, b)?,
315        Color::Gray(gray) => writeln!(&mut content, "{} G", gray)?,
316        Color::Cmyk(c, m, y, k) => writeln!(&mut content, "{} {} {} {} K", c, m, y, k)?,
317    }
318    writeln!(&mut content, "{} w", widget.border_width)?;
319    writeln!(&mut content, "0 0 {} {} re S", width, height)?;
320
321    // Draw check mark if checked
322    if checked {
323        match &widget.text_color {
324            Color::Rgb(r, g, b) => writeln!(&mut content, "{} {} {} RG", r, g, b)?,
325            Color::Gray(gray) => writeln!(&mut content, "{} G", gray)?,
326            Color::Cmyk(c, m, y, k) => writeln!(&mut content, "{} {} {} {} K", c, m, y, k)?,
327        }
328        writeln!(&mut content, "2 w")?;
329        writeln!(&mut content, "1 J")?; // Round line cap
330
331        // Draw check mark path
332        let margin = width * 0.2;
333        let x1 = margin;
334        let y1 = height * 0.5;
335        let x2 = width * 0.4;
336        let y2 = margin;
337        let x3 = width - margin;
338        let y3 = height - margin;
339
340        writeln!(&mut content, "{} {} m", x1, y1)?;
341        writeln!(&mut content, "{} {} l", x2, y2)?;
342        writeln!(&mut content, "{} {} l S", x3, y3)?;
343    }
344
345    let mut resources = Dictionary::new();
346    resources.set(
347        "ProcSet",
348        Object::Array(vec![Object::Name("PDF".to_string())]),
349    );
350    let mut dict = Dictionary::new();
351    dict.set("Resources", Object::Dictionary(resources));
352
353    Ok(Stream::with_dictionary(dict, content))
354}
355
356/// Create radio button appearance stream
357fn create_radio_appearance(widget: &ButtonWidget, selected: bool) -> Result<Stream> {
358    let mut content = Vec::new();
359    let width = widget.rect.width();
360    let height = widget.rect.height();
361    let radius = width.min(height) / 2.0;
362    let center_x = width / 2.0;
363    let center_y = height / 2.0;
364
365    // Draw background circle
366    if let Some(bg) = &widget.background_color {
367        match bg {
368            Color::Rgb(r, g, b) => writeln!(&mut content, "{} {} {} rg", r, g, b)?,
369            Color::Gray(g) => writeln!(&mut content, "{} g", g)?,
370            Color::Cmyk(c, m, y, k) => writeln!(&mut content, "{} {} {} {} k", c, m, y, k)?,
371        }
372        draw_circle(
373            &mut content,
374            center_x,
375            center_y,
376            radius - widget.border_width,
377        )?;
378        writeln!(&mut content, "f")?;
379    }
380
381    // Draw border circle
382    match &widget.border_color {
383        Color::Rgb(r, g, b) => writeln!(&mut content, "{} {} {} RG", r, g, b)?,
384        Color::Gray(gray) => writeln!(&mut content, "{} G", gray)?,
385        Color::Cmyk(c, m, y, k) => writeln!(&mut content, "{} {} {} {} K", c, m, y, k)?,
386    }
387    writeln!(&mut content, "{} w", widget.border_width)?;
388    draw_circle(
389        &mut content,
390        center_x,
391        center_y,
392        radius - widget.border_width / 2.0,
393    )?;
394    writeln!(&mut content, "S")?;
395
396    // Draw inner dot if selected
397    if selected {
398        match &widget.text_color {
399            Color::Rgb(r, g, b) => writeln!(&mut content, "{} {} {} rg", r, g, b)?,
400            Color::Gray(gray) => writeln!(&mut content, "{} g", gray)?,
401            Color::Cmyk(c, m, y, k) => writeln!(&mut content, "{} {} {} {} k", c, m, y, k)?,
402        }
403        let dot_radius = radius * 0.4;
404        draw_circle(&mut content, center_x, center_y, dot_radius)?;
405        writeln!(&mut content, "f")?;
406    }
407
408    let mut resources = Dictionary::new();
409    resources.set(
410        "ProcSet",
411        Object::Array(vec![Object::Name("PDF".to_string())]),
412    );
413    let mut dict = Dictionary::new();
414    dict.set("Resources", Object::Dictionary(resources));
415
416    Ok(Stream::with_dictionary(dict, content))
417}
418
419/// Create push button appearance stream
420fn create_pushbutton_appearance(widget: &ButtonWidget, caption: Option<&str>) -> Result<Stream> {
421    let mut content = Vec::new();
422    let width = widget.rect.width();
423    let height = widget.rect.height();
424
425    // Draw background with beveled effect
426    if let Some(bg) = &widget.background_color {
427        // Main background
428        match bg {
429            Color::Rgb(r, g, b) => writeln!(&mut content, "{} {} {} rg", r, g, b)?,
430            Color::Gray(g) => writeln!(&mut content, "{} g", g)?,
431            Color::Cmyk(c, m, y, k) => writeln!(&mut content, "{} {} {} {} k", c, m, y, k)?,
432        }
433        writeln!(&mut content, "0 0 {} {} re f", width, height)?;
434
435        // Top/left highlight (lighter)
436        writeln!(&mut content, "0.9 0.9 0.9 RG")?;
437        writeln!(&mut content, "2 w")?;
438        writeln!(&mut content, "1 {} m", height - 1.0)?;
439        writeln!(&mut content, "1 1 l")?;
440        writeln!(&mut content, "{} 1 l S", width - 1.0)?;
441
442        // Bottom/right shadow (darker)
443        writeln!(&mut content, "0.5 0.5 0.5 RG")?;
444        writeln!(&mut content, "{} 1 m", width - 1.0)?;
445        writeln!(&mut content, "{} {} l", width - 1.0, height - 1.0)?;
446        writeln!(&mut content, "1 {} l S", height - 1.0)?;
447    }
448
449    // Draw border
450    match &widget.border_color {
451        Color::Rgb(r, g, b) => writeln!(&mut content, "{} {} {} RG", r, g, b)?,
452        Color::Gray(gray) => writeln!(&mut content, "{} G", gray)?,
453        Color::Cmyk(c, m, y, k) => writeln!(&mut content, "{} {} {} {} K", c, m, y, k)?,
454    }
455    writeln!(&mut content, "{} w", widget.border_width)?;
456    writeln!(&mut content, "0 0 {} {} re S", width, height)?;
457
458    // Draw caption text
459    if let Some(text) = caption {
460        writeln!(&mut content, "BT")?;
461        writeln!(&mut content, "/Helvetica {} Tf", widget.font_size)?;
462        match &widget.text_color {
463            Color::Rgb(r, g, b) => writeln!(&mut content, "{} {} {} rg", r, g, b)?,
464            Color::Gray(gray) => writeln!(&mut content, "{} g", gray)?,
465            Color::Cmyk(c, m, y, k) => writeln!(&mut content, "{} {} {} {} k", c, m, y, k)?,
466        }
467
468        // Center text
469        let text_width = text.len() as f64 * widget.font_size * 0.5;
470        let x = (width - text_width) / 2.0;
471        let y = (height - widget.font_size) / 2.0;
472
473        writeln!(&mut content, "{} {} Td", x, y)?;
474        writeln!(&mut content, "({}) Tj", escape_pdf_string(text))?;
475        writeln!(&mut content, "ET")?;
476    }
477
478    let mut resources = Dictionary::new();
479
480    // Add font resources
481    let mut fonts = Dictionary::new();
482    let mut font_dict = Dictionary::new();
483    font_dict.set("Type", Object::Name("Font".to_string()));
484    font_dict.set("Subtype", Object::Name("Type1".to_string()));
485    font_dict.set("BaseFont", Object::Name("Helvetica".to_string()));
486    fonts.set("Helvetica", Object::Dictionary(font_dict));
487    resources.set("Font", Object::Dictionary(fonts));
488
489    resources.set(
490        "ProcSet",
491        Object::Array(vec![
492            Object::Name("PDF".to_string()),
493            Object::Name("Text".to_string()),
494        ]),
495    );
496
497    let mut dict = Dictionary::new();
498    dict.set("Resources", Object::Dictionary(resources));
499
500    Ok(Stream::with_dictionary(dict, content))
501}
502
503/// Helper to draw a circle using Bézier curves
504fn draw_circle<W: Write>(writer: &mut W, cx: f64, cy: f64, r: f64) -> Result<()> {
505    let k = 0.5522847498; // Magic constant for circle approximation
506    let dx = r * k;
507    let dy = r * k;
508
509    writeln!(writer, "{} {} m", cx + r, cy)?;
510    writeln!(
511        writer,
512        "{} {} {} {} {} {} c",
513        cx + r,
514        cy + dy,
515        cx + dx,
516        cy + r,
517        cx,
518        cy + r
519    )?;
520    writeln!(
521        writer,
522        "{} {} {} {} {} {} c",
523        cx - dx,
524        cy + r,
525        cx - r,
526        cy + dy,
527        cx - r,
528        cy
529    )?;
530    writeln!(
531        writer,
532        "{} {} {} {} {} {} c",
533        cx - r,
534        cy - dy,
535        cx - dx,
536        cy - r,
537        cx,
538        cy - r
539    )?;
540    writeln!(
541        writer,
542        "{} {} {} {} {} {} c",
543        cx + dx,
544        cy - r,
545        cx + r,
546        cy - dy,
547        cx + r,
548        cy
549    )?;
550
551    Ok(())
552}
553
554/// Escape special characters in PDF strings
555fn escape_pdf_string(s: &str) -> String {
556    s.chars()
557        .flat_map(|c| match c {
558            '(' => vec!['\\', '('],
559            ')' => vec!['\\', ')'],
560            '\\' => vec!['\\', '\\'],
561            _ => vec![c],
562        })
563        .collect()
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569
570    #[test]
571    fn test_checkbox_widget() {
572        let checkbox = CheckBox::new("agree").checked().with_export_value("Yes");
573
574        let widget = ButtonWidget::new(Rectangle::new((0.0, 0.0).into(), (20.0, 20.0).into()));
575
576        let annotation = create_checkbox_widget(&checkbox, &widget).unwrap();
577
578        // Verify widget annotation properties
579        assert_eq!(annotation.annotation_type, AnnotationType::Widget);
580        assert!(annotation.properties.get("AP").is_some());
581        assert!(annotation.properties.get("AS").is_some());
582        assert_eq!(
583            annotation.properties.get("AS"),
584            Some(&Object::Name("Yes".to_string()))
585        );
586    }
587
588    #[test]
589    fn test_radio_widget() {
590        let radio = RadioButton::new("size")
591            .add_option("S", "Small")
592            .add_option("M", "Medium")
593            .add_option("L", "Large")
594            .with_selected(1);
595
596        let widget = ButtonWidget::new(Rectangle::new((0.0, 0.0).into(), (20.0, 20.0).into()));
597
598        let annotation = create_radio_widget(&radio, &widget, 1).unwrap();
599
600        // Verify radio button widget properties
601        assert_eq!(annotation.annotation_type, AnnotationType::Widget);
602        assert!(annotation.properties.get("AP").is_some());
603        assert_eq!(
604            annotation.properties.get("AS"),
605            Some(&Object::Name("M".to_string()))
606        );
607    }
608
609    #[test]
610    fn test_pushbutton_widget() {
611        let button = PushButton::new("submit").with_caption("Submit Form");
612
613        let widget = ButtonWidget::new(Rectangle::new((0.0, 0.0).into(), (100.0, 30.0).into()));
614
615        let annotation = create_pushbutton_widget(&button, &widget).unwrap();
616
617        // Verify push button widget properties
618        assert_eq!(annotation.annotation_type, AnnotationType::Widget);
619        assert!(annotation.properties.get("AP").is_some());
620        assert!(annotation.properties.get("MK").is_some());
621    }
622
623    #[test]
624    fn test_widget_customization() {
625        let widget = ButtonWidget::new(Rectangle::new((0.0, 0.0).into(), (50.0, 50.0).into()))
626            .with_border_width(2.0)
627            .with_border_color(Color::rgb(1.0, 0.0, 0.0))
628            .with_background_color(Some(Color::rgb(0.9, 0.9, 1.0)))
629            .with_text_color(Color::rgb(0.0, 0.0, 1.0))
630            .with_font_size(12.0);
631
632        assert_eq!(widget.border_width, 2.0);
633        assert_eq!(widget.border_color, Color::rgb(1.0, 0.0, 0.0));
634        assert_eq!(widget.font_size, 12.0);
635    }
636}