Skip to main content

oxidize_pdf/forms/
choice_widget.rs

1//! Choice field widgets (ComboBox and ListBox) for PDF forms
2//!
3//! This module implements ISO 32000-1 Section 12.7.4.4 (Choice Fields)
4//! including combo boxes (dropdowns) and list boxes with single or multi-select.
5
6use crate::annotations::{Annotation, AnnotationType};
7use crate::error::Result;
8use crate::forms::{ComboBox, ListBox};
9use crate::geometry::Rectangle;
10use crate::graphics::Color;
11use crate::objects::{Dictionary, Object, Stream};
12use crate::text::Font;
13use std::fmt::Write;
14
15/// Widget annotation for choice fields (ComboBox and ListBox)
16#[derive(Debug, Clone)]
17pub struct ChoiceWidget {
18    /// Widget rectangle
19    pub rect: Rectangle,
20    /// Border color
21    pub border_color: Color,
22    /// Border width
23    pub border_width: f64,
24    /// Background color
25    pub background_color: Option<Color>,
26    /// Text color
27    pub text_color: Color,
28    /// Font
29    pub font: Font,
30    /// Font size
31    pub font_size: f64,
32    /// Highlight color for selected items
33    pub highlight_color: Option<Color>,
34}
35
36impl Default for ChoiceWidget {
37    fn default() -> Self {
38        Self {
39            rect: Rectangle::from_position_and_size(0.0, 0.0, 100.0, 20.0),
40            border_color: Color::rgb(0.0, 0.0, 0.0),
41            border_width: 1.0,
42            background_color: Some(Color::rgb(1.0, 1.0, 1.0)),
43            text_color: Color::rgb(0.0, 0.0, 0.0),
44            font: Font::Helvetica,
45            font_size: 10.0,
46            highlight_color: Some(Color::rgb(0.8, 0.8, 1.0)),
47        }
48    }
49}
50
51impl ChoiceWidget {
52    /// Create a new choice widget
53    pub fn new(rect: Rectangle) -> Self {
54        Self {
55            rect,
56            ..Default::default()
57        }
58    }
59
60    /// Set border color
61    pub fn with_border_color(mut self, color: Color) -> Self {
62        self.border_color = color;
63        self
64    }
65
66    /// Set border width
67    pub fn with_border_width(mut self, width: f64) -> Self {
68        self.border_width = width;
69        self
70    }
71
72    /// Set background color
73    pub fn with_background_color(mut self, color: Option<Color>) -> Self {
74        self.background_color = color;
75        self
76    }
77
78    /// Set text color
79    pub fn with_text_color(mut self, color: Color) -> Self {
80        self.text_color = color;
81        self
82    }
83
84    /// Set font
85    pub fn with_font(mut self, font: Font) -> Self {
86        self.font = font;
87        self
88    }
89
90    /// Set font size
91    pub fn with_font_size(mut self, size: f64) -> Self {
92        self.font_size = size;
93        self
94    }
95
96    /// Set highlight color for selected items
97    pub fn with_highlight_color(mut self, color: Option<Color>) -> Self {
98        self.highlight_color = color;
99        self
100    }
101
102    /// Create appearance stream for a combo box
103    fn create_combobox_appearance(&self, combo: &ComboBox) -> String {
104        let mut stream = String::new();
105
106        // Save graphics state
107        writeln!(&mut stream, "q").expect("Writing to string should never fail");
108
109        // Draw background if specified
110        if let Some(bg_color) = &self.background_color {
111            writeln!(
112                &mut stream,
113                "{:.3} {:.3} {:.3} rg",
114                bg_color.r(),
115                bg_color.g(),
116                bg_color.b()
117            )
118            .expect("Writing to string should never fail");
119            writeln!(
120                &mut stream,
121                "0 0 {} {} re",
122                self.rect.width(),
123                self.rect.height()
124            )
125            .expect("Writing to string should never fail");
126            writeln!(&mut stream, "f").expect("Writing to string should never fail");
127        }
128
129        // Draw border
130        writeln!(
131            &mut stream,
132            "{:.3} {:.3} {:.3} RG",
133            self.border_color.r(),
134            self.border_color.g(),
135            self.border_color.b()
136        )
137        .expect("Writing to string should never fail");
138        writeln!(&mut stream, "{} w", self.border_width)
139            .expect("Writing to string should never fail");
140        writeln!(
141            &mut stream,
142            "0 0 {} {} re",
143            self.rect.width(),
144            self.rect.height()
145        )
146        .expect("Writing to string should never fail");
147        writeln!(&mut stream, "S").expect("Writing to string should never fail");
148
149        // Draw dropdown arrow on the right
150        let arrow_x = self.rect.width() - 15.0;
151        let arrow_y = self.rect.height() / 2.0;
152        writeln!(&mut stream, "{:.3} {:.3} {:.3} rg", 0.3, 0.3, 0.3)
153            .expect("Writing to string should never fail");
154        writeln!(&mut stream, "{} {} m", arrow_x, arrow_y + 3.0)
155            .expect("Writing to string should never fail");
156        writeln!(&mut stream, "{} {} l", arrow_x + 8.0, arrow_y + 3.0)
157            .expect("Writing to string should never fail");
158        writeln!(&mut stream, "{} {} l", arrow_x + 4.0, arrow_y - 3.0)
159            .expect("Writing to string should never fail");
160        writeln!(&mut stream, "f").expect("Writing to string should never fail");
161
162        // Draw selected text if any
163        if let Some(selected_idx) = combo.selected {
164            if let Some((_, display_text)) = combo.options.get(selected_idx) {
165                writeln!(&mut stream, "BT").expect("Writing to string should never fail");
166                writeln!(
167                    &mut stream,
168                    "/{} {} Tf",
169                    self.font.pdf_name(),
170                    self.font_size
171                )
172                .expect("Writing to string should never fail");
173                writeln!(
174                    &mut stream,
175                    "{:.3} {:.3} {:.3} rg",
176                    self.text_color.r(),
177                    self.text_color.g(),
178                    self.text_color.b()
179                )
180                .expect("Writing to string should never fail");
181                writeln!(
182                    &mut stream,
183                    "2 {} Td",
184                    (self.rect.height() - self.font_size) / 2.0
185                )
186                .expect("Writing to string should never fail");
187                writeln!(&mut stream, "({}) Tj", escape_pdf_string(display_text))
188                    .expect("Writing to string should never fail");
189                writeln!(&mut stream, "ET").expect("Writing to string should never fail");
190            }
191        }
192
193        // Restore graphics state
194        writeln!(&mut stream, "Q").expect("Writing to string should never fail");
195
196        stream
197    }
198
199    /// Create appearance stream for a list box
200    fn create_listbox_appearance(&self, listbox: &ListBox) -> String {
201        let mut stream = String::new();
202
203        // Save graphics state
204        writeln!(&mut stream, "q").expect("Writing to string should never fail");
205
206        // Draw background
207        if let Some(bg_color) = &self.background_color {
208            writeln!(
209                &mut stream,
210                "{:.3} {:.3} {:.3} rg",
211                bg_color.r(),
212                bg_color.g(),
213                bg_color.b()
214            )
215            .expect("Writing to string should never fail");
216            writeln!(
217                &mut stream,
218                "0 0 {} {} re",
219                self.rect.width(),
220                self.rect.height()
221            )
222            .expect("Writing to string should never fail");
223            writeln!(&mut stream, "f").expect("Writing to string should never fail");
224        }
225
226        // Draw border
227        writeln!(
228            &mut stream,
229            "{:.3} {:.3} {:.3} RG",
230            self.border_color.r(),
231            self.border_color.g(),
232            self.border_color.b()
233        )
234        .expect("Writing to string should never fail");
235        writeln!(&mut stream, "{} w", self.border_width)
236            .expect("Writing to string should never fail");
237        writeln!(
238            &mut stream,
239            "0 0 {} {} re",
240            self.rect.width(),
241            self.rect.height()
242        )
243        .expect("Writing to string should never fail");
244        writeln!(&mut stream, "S").expect("Writing to string should never fail");
245
246        // Calculate visible items
247        let item_height = self.font_size + 4.0;
248        let visible_items = (self.rect.height() / item_height) as usize;
249
250        // Draw items
251
252        for (idx, (_, display_text)) in listbox.options.iter().enumerate().take(visible_items) {
253            let y_pos = self.rect.height() - ((idx + 1) as f64 * item_height);
254
255            // Draw highlight for selected items
256            if listbox.selected.contains(&idx) {
257                if let Some(highlight) = &self.highlight_color {
258                    writeln!(
259                        &mut stream,
260                        "{:.3} {:.3} {:.3} rg",
261                        highlight.r(),
262                        highlight.g(),
263                        highlight.b()
264                    )
265                    .expect("Writing to string should never fail");
266                    writeln!(
267                        &mut stream,
268                        "0 {} {} {} re",
269                        y_pos,
270                        self.rect.width(),
271                        item_height
272                    )
273                    .expect("Writing to string should never fail");
274                    writeln!(&mut stream, "f").expect("Writing to string should never fail");
275                }
276            }
277
278            // Draw text
279            writeln!(&mut stream, "BT").expect("Writing to string should never fail");
280            writeln!(
281                &mut stream,
282                "/{} {} Tf",
283                self.font.pdf_name(),
284                self.font_size
285            )
286            .expect("Writing to string should never fail");
287            writeln!(
288                &mut stream,
289                "{:.3} {:.3} {:.3} rg",
290                self.text_color.r(),
291                self.text_color.g(),
292                self.text_color.b()
293            )
294            .expect("Writing to string should never fail");
295            writeln!(&mut stream, "2 {} Td", y_pos + 2.0)
296                .expect("Writing to string should never fail");
297            writeln!(&mut stream, "({}) Tj", escape_pdf_string(display_text))
298                .expect("Writing to string should never fail");
299            writeln!(&mut stream, "ET").expect("Writing to string should never fail");
300        }
301
302        // Draw scrollbar if needed
303        if listbox.options.len() > visible_items {
304            writeln!(&mut stream, "0.7 0.7 0.7 rg").expect("Writing to string should never fail");
305            let scrollbar_x = self.rect.width() - 10.0;
306            writeln!(&mut stream, "{} 0 8 {} re", scrollbar_x, self.rect.height())
307                .expect("Writing to string should never fail");
308            writeln!(&mut stream, "f").expect("Writing to string should never fail");
309
310            // Draw scroll thumb
311            writeln!(&mut stream, "0.4 0.4 0.4 rg").expect("Writing to string should never fail");
312            let thumb_height =
313                (visible_items as f64 / listbox.options.len() as f64) * self.rect.height();
314            writeln!(
315                &mut stream,
316                "{} {} 8 {} re",
317                scrollbar_x,
318                self.rect.height() - thumb_height,
319                thumb_height
320            )
321            .expect("Writing to string should never fail");
322            writeln!(&mut stream, "f").expect("Writing to string should never fail");
323        }
324
325        // Restore graphics state
326        writeln!(&mut stream, "Q").expect("Writing to string should never fail");
327
328        stream
329    }
330}
331
332/// Create a widget annotation for a combo box
333pub fn create_combobox_widget(combo: &ComboBox, widget: &ChoiceWidget) -> Result<Annotation> {
334    let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
335
336    // Set field reference
337    let mut field_dict = combo.to_dict();
338
339    // Add widget-specific entries
340    field_dict.set(
341        "Rect",
342        Object::Array(vec![
343            Object::Real(widget.rect.lower_left.x),
344            Object::Real(widget.rect.lower_left.y),
345            Object::Real(widget.rect.upper_right.x),
346            Object::Real(widget.rect.upper_right.y),
347        ]),
348    );
349
350    // Create appearance stream
351    let appearance_content = widget.create_combobox_appearance(combo);
352    let appearance_stream = create_appearance_stream(
353        appearance_content.as_bytes(),
354        widget.rect.width(),
355        widget.rect.height(),
356    );
357
358    // Create appearance dictionary
359    let mut ap_dict = Dictionary::new();
360    let mut n_dict = Dictionary::new();
361    n_dict.set(
362        "default",
363        Object::Stream(
364            appearance_stream.dictionary().clone(),
365            appearance_stream.data().to_vec(),
366        ),
367    );
368    ap_dict.set("N", Object::Dictionary(n_dict));
369    field_dict.set("AP", Object::Dictionary(ap_dict));
370
371    // Set default appearance string
372    let da = format!(
373        "/{} {} Tf {} {} {} rg",
374        widget.font.pdf_name(),
375        widget.font_size,
376        widget.text_color.r(),
377        widget.text_color.g(),
378        widget.text_color.b(),
379    );
380    field_dict.set("DA", Object::String(da));
381
382    // Set the field dictionary as the annotation's dictionary
383    annotation.set_field_dict(field_dict);
384
385    Ok(annotation)
386}
387
388/// Create a widget annotation for a list box
389pub fn create_listbox_widget(listbox: &ListBox, widget: &ChoiceWidget) -> Result<Annotation> {
390    let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
391
392    // Set field reference
393    let mut field_dict = listbox.to_dict();
394
395    // Add widget-specific entries
396    field_dict.set(
397        "Rect",
398        Object::Array(vec![
399            Object::Real(widget.rect.lower_left.x),
400            Object::Real(widget.rect.lower_left.y),
401            Object::Real(widget.rect.upper_right.x),
402            Object::Real(widget.rect.upper_right.y),
403        ]),
404    );
405
406    // Create appearance stream
407    let appearance_content = widget.create_listbox_appearance(listbox);
408    let appearance_stream = create_appearance_stream(
409        appearance_content.as_bytes(),
410        widget.rect.width(),
411        widget.rect.height(),
412    );
413
414    // Create appearance dictionary
415    let mut ap_dict = Dictionary::new();
416    let mut n_dict = Dictionary::new();
417    n_dict.set(
418        "default",
419        Object::Stream(
420            appearance_stream.dictionary().clone(),
421            appearance_stream.data().to_vec(),
422        ),
423    );
424    ap_dict.set("N", Object::Dictionary(n_dict));
425    field_dict.set("AP", Object::Dictionary(ap_dict));
426
427    // Set default appearance string
428    let da = format!(
429        "/{} {} Tf {} {} {} rg",
430        widget.font.pdf_name(),
431        widget.font_size,
432        widget.text_color.r(),
433        widget.text_color.g(),
434        widget.text_color.b(),
435    );
436    field_dict.set("DA", Object::String(da));
437
438    // Set the field dictionary as the annotation's dictionary
439    annotation.set_field_dict(field_dict);
440
441    Ok(annotation)
442}
443
444/// Helper function to escape PDF strings
445fn escape_pdf_string(s: &str) -> String {
446    s.chars()
447        .map(|c| match c {
448            '(' => "\\(".to_string(),
449            ')' => "\\)".to_string(),
450            '\\' => "\\\\".to_string(),
451            _ => c.to_string(),
452        })
453        .collect()
454}
455
456/// Create an appearance stream
457fn create_appearance_stream(content: &[u8], width: f64, height: f64) -> Stream {
458    let mut dict = Dictionary::new();
459    dict.set("Type", Object::Name("XObject".to_string()));
460    dict.set("Subtype", Object::Name("Form".to_string()));
461    dict.set(
462        "BBox",
463        Object::Array(vec![
464            Object::Integer(0),
465            Object::Integer(0),
466            Object::Real(width),
467            Object::Real(height),
468        ]),
469    );
470
471    Stream::with_dictionary(dict, content.to_vec())
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use crate::geometry::Point;
478
479    #[test]
480    fn test_choice_widget_creation() {
481        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
482        let widget = ChoiceWidget::new(rect.clone());
483
484        assert_eq!(widget.rect, rect);
485        assert_eq!(widget.font_size, 10.0);
486    }
487
488    #[test]
489    fn test_choice_widget_builder() {
490        let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 30.0));
491        let widget = ChoiceWidget::new(rect)
492            .with_border_color(Color::rgb(1.0, 0.0, 0.0))
493            .with_font_size(12.0)
494            .with_font(Font::HelveticaBold);
495
496        assert_eq!(widget.border_color, Color::rgb(1.0, 0.0, 0.0));
497        assert_eq!(widget.font_size, 12.0);
498        assert_eq!(widget.font, Font::HelveticaBold);
499    }
500
501    #[test]
502    fn test_combobox_widget_creation() {
503        let combo = ComboBox::new("country")
504            .add_option("US", "United States")
505            .add_option("CA", "Canada")
506            .with_selected(0);
507
508        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(250.0, 125.0));
509        let widget = ChoiceWidget::new(rect);
510
511        let annotation = create_combobox_widget(&combo, &widget);
512        assert!(annotation.is_ok());
513    }
514
515    #[test]
516    fn test_listbox_widget_creation() {
517        let listbox = ListBox::new("languages")
518            .add_option("en", "English")
519            .add_option("es", "Spanish")
520            .add_option("fr", "French")
521            .with_selected(vec![0, 2]);
522
523        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 200.0));
524        let widget = ChoiceWidget::new(rect);
525
526        let annotation = create_listbox_widget(&listbox, &widget);
527        assert!(annotation.is_ok());
528    }
529
530    #[test]
531    fn test_escape_pdf_string() {
532        assert_eq!(escape_pdf_string("Hello"), "Hello");
533        assert_eq!(escape_pdf_string("Hello (World)"), "Hello \\(World\\)");
534        assert_eq!(escape_pdf_string("Path\\to\\file"), "Path\\\\to\\\\file");
535    }
536}