1use 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#[derive(Debug, Clone)]
16pub struct ButtonWidget {
17 pub rect: Rectangle,
19 pub border_width: f64,
21 pub border_color: Color,
23 pub background_color: Option<Color>,
25 pub text_color: Color,
27 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 pub fn new(rect: Rectangle) -> Self {
47 Self {
48 rect,
49 ..Default::default()
50 }
51 }
52
53 pub fn with_border_width(mut self, width: f64) -> Self {
55 self.border_width = width;
56 self
57 }
58
59 pub fn with_border_color(mut self, color: Color) -> Self {
61 self.border_color = color;
62 self
63 }
64
65 pub fn with_background_color(mut self, color: Option<Color>) -> Self {
67 self.background_color = color;
68 self
69 }
70
71 pub fn with_text_color(mut self, color: Color) -> Self {
73 self.text_color = color;
74 self
75 }
76
77 pub fn with_font_size(mut self, size: f64) -> Self {
79 self.font_size = size;
80 self
81 }
82}
83
84pub fn create_checkbox_widget(checkbox: &CheckBox, widget: &ButtonWidget) -> Result<Annotation> {
86 let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
87
88 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 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 let mut ap_dict = Dictionary::new();
111
112 let mut n_dict = Dictionary::new();
114
115 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 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 let flags = 4; annotation.properties.set("F", Object::Integer(flags));
141
142 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())); annotation.properties.set("BS", Object::Dictionary(bs_dict));
147
148 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())); annotation.properties.set("MK", Object::Dictionary(mk_dict));
156
157 Ok(annotation)
158}
159
160pub 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 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 let flags = (1 << 15) | 4; annotation
179 .properties
180 .set("Ff", Object::Integer(flags as i64));
181
182 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 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 let mut ap_dict = Dictionary::new();
199 let mut n_dict = Dictionary::new();
200
201 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 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 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())); annotation.properties.set("MK", Object::Dictionary(mk_dict));
237
238 Ok(annotation)
239}
240
241pub fn create_pushbutton_widget(button: &PushButton, widget: &ButtonWidget) -> Result<Annotation> {
243 let mut annotation = Annotation::new(AnnotationType::Widget, widget.rect);
244
245 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 let flags = (1 << 16) | 4; annotation
256 .properties
257 .set("Ff", Object::Integer(flags as i64));
258
259 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 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())); annotation.properties.set("BS", Object::Dictionary(bs_dict));
276
277 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 annotation
290 .properties
291 .set("H", Object::Name("P".to_string())); Ok(annotation)
294}
295
296fn 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 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 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 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")?; 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
356fn 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 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 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 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
419fn 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 if let Some(bg) = &widget.background_color {
427 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 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 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 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 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 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 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
503fn draw_circle<W: Write>(writer: &mut W, cx: f64, cy: f64, r: f64) -> Result<()> {
505 let k = 0.5522847498; 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
554fn 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 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 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 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}