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 crate::graphics::color::write_fill_color_bytes(&mut content, *bg);
305 writeln!(&mut content, "0 0 {} {} re f", width, height)?;
306 }
307
308 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 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")?; 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
344fn 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 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 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 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
395fn 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 if let Some(bg) = &widget.background_color {
403 crate::graphics::color::write_fill_color_bytes(&mut content, *bg);
405 writeln!(&mut content, "0 0 {} {} re f", width, height)?;
406
407 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 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 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 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 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 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
467fn draw_circle<W: Write>(writer: &mut W, cx: f64, cy: f64, r: f64) -> Result<()> {
469 let k = 0.5522847498; 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
518fn 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 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 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 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}