1use crate::error::PdfError;
8use crate::forms::Widget;
9#[cfg(test)]
10use crate::geometry::Point;
11use crate::geometry::Rectangle;
12use crate::graphics::Color;
13use crate::objects::{Dictionary, Object, ObjectReference};
14
15#[derive(Debug, Clone)]
17pub struct SignatureWidget {
18 pub widget: Widget,
20 pub field_ref: Option<ObjectReference>,
22 pub visual_type: SignatureVisualType,
24 pub handler_ref: Option<String>,
26}
27
28#[derive(Debug, Clone)]
30pub enum SignatureVisualType {
31 Text {
33 show_name: bool,
35 show_date: bool,
37 show_reason: bool,
39 show_location: bool,
41 },
42 Graphic {
44 image_data: Vec<u8>,
46 format: ImageFormat,
48 maintain_aspect: bool,
50 },
51 Mixed {
53 image_data: Vec<u8>,
55 format: ImageFormat,
57 text_position: TextPosition,
59 show_details: bool,
61 },
62 InkSignature {
64 strokes: Vec<InkStroke>,
66 color: Color,
68 width: f64,
70 },
71}
72
73#[derive(Debug, Clone, Copy)]
75pub enum ImageFormat {
76 PNG,
77 JPEG,
78}
79
80#[derive(Debug, Clone, Copy)]
82pub enum TextPosition {
83 Above,
84 Below,
85 Left,
86 Right,
87 Overlay,
88}
89
90#[derive(Debug, Clone)]
92pub struct InkStroke {
93 pub points: Vec<(f64, f64)>,
95 pub pressures: Option<Vec<f64>>,
97}
98
99impl SignatureWidget {
100 pub fn new(rect: Rectangle, visual_type: SignatureVisualType) -> Self {
102 Self {
103 widget: Widget::new(rect),
104 field_ref: None,
105 visual_type,
106 handler_ref: None,
107 }
108 }
109
110 pub fn with_field_ref(mut self, field_ref: ObjectReference) -> Self {
112 self.field_ref = Some(field_ref);
113 self
114 }
115
116 pub fn with_handler(mut self, handler: impl Into<String>) -> Self {
118 self.handler_ref = Some(handler.into());
119 self
120 }
121
122 pub fn generate_appearance_stream(
124 &self,
125 signed: bool,
126 signer_name: Option<&str>,
127 reason: Option<&str>,
128 location: Option<&str>,
129 date: Option<&str>,
130 ) -> Result<Vec<u8>, PdfError> {
131 let mut stream = Vec::new();
132 let rect = &self.widget.rect;
133 let width = rect.width();
134 let height = rect.height();
135
136 stream.extend(b"q\n");
138
139 if let Some(bg_color) = &self.widget.appearance.background_color {
141 Self::set_fill_color(&mut stream, bg_color);
142 stream.extend(format!("0 0 {} {} re f\n", width, height).as_bytes());
143 }
144
145 if self.widget.appearance.border_width > 0.0 {
147 if let Some(border_color) = &self.widget.appearance.border_color {
148 Self::set_stroke_color(&mut stream, border_color);
149 stream.extend(format!("{} w\n", self.widget.appearance.border_width).as_bytes());
150 stream.extend(format!("0 0 {} {} re S\n", width, height).as_bytes());
151 }
152 }
153
154 match &self.visual_type {
156 SignatureVisualType::Text {
157 show_name,
158 show_date,
159 show_reason,
160 show_location,
161 } => {
162 self.generate_text_appearance(
163 &mut stream,
164 signed,
165 signer_name,
166 reason,
167 location,
168 date,
169 *show_name,
170 *show_date,
171 *show_reason,
172 *show_location,
173 )?;
174 }
175 SignatureVisualType::Graphic {
176 image_data,
177 format,
178 maintain_aspect,
179 } => {
180 self.generate_graphic_appearance(
181 &mut stream,
182 image_data,
183 *format,
184 *maintain_aspect,
185 )?;
186 }
187 SignatureVisualType::Mixed {
188 image_data,
189 format,
190 text_position,
191 show_details,
192 } => {
193 self.generate_mixed_appearance(
194 &mut stream,
195 image_data,
196 *format,
197 *text_position,
198 *show_details,
199 signed,
200 signer_name,
201 reason,
202 date,
203 )?;
204 }
205 SignatureVisualType::InkSignature {
206 strokes,
207 color,
208 width,
209 } => {
210 self.generate_ink_appearance(&mut stream, strokes, color, *width)?;
211 }
212 }
213
214 stream.extend(b"Q\n");
216
217 Ok(stream)
218 }
219
220 #[allow(clippy::too_many_arguments)]
222 fn generate_text_appearance(
223 &self,
224 stream: &mut Vec<u8>,
225 signed: bool,
226 signer_name: Option<&str>,
227 reason: Option<&str>,
228 location: Option<&str>,
229 date: Option<&str>,
230 show_name: bool,
231 show_date: bool,
232 show_reason: bool,
233 show_location: bool,
234 ) -> Result<(), PdfError> {
235 let rect = &self.widget.rect;
236 let width = rect.width();
237 let height = rect.height();
238
239 stream.extend(b"BT\n");
241
242 stream.extend(b"/Helv 10 Tf\n");
244
245 stream.extend(b"0 g\n");
247
248 let mut y_offset = height - 15.0;
249 let x_offset = 5.0;
250
251 if signed {
252 if show_name && signer_name.is_some() {
253 stream.extend(format!("{} {} Td\n", x_offset, y_offset).as_bytes());
254 if let Some(name) = signer_name {
255 stream.extend(format!("(Digitally signed by: {}) Tj\n", name).as_bytes());
256 }
257 y_offset -= 12.0;
258 let _ = y_offset;
260 }
261
262 if show_date && date.is_some() {
263 stream.extend(b"0 -12 Td\n");
264 if let Some(d) = date {
265 stream.extend(format!("(Date: {}) Tj\n", d).as_bytes());
266 }
267 y_offset -= 12.0;
268 let _ = y_offset;
270 }
271
272 if show_reason && reason.is_some() {
273 stream.extend(b"0 -12 Td\n");
274 if let Some(r) = reason {
275 stream.extend(format!("(Reason: {}) Tj\n", r).as_bytes());
276 }
277 y_offset -= 12.0;
278 let _ = y_offset;
280 }
281
282 if show_location && location.is_some() {
283 stream.extend(b"0 -12 Td\n");
284 if let Some(l) = location {
285 stream.extend(format!("(Location: {}) Tj\n", l).as_bytes());
286 }
287 }
288 } else {
289 stream.extend(format!("{} {} Td\n", width / 2.0 - 30.0, height / 2.0).as_bytes());
291 stream.extend(b"(Click to sign) Tj\n");
292 }
293
294 stream.extend(b"ET\n");
296
297 Ok(())
298 }
299
300 fn generate_graphic_appearance(
302 &self,
303 stream: &mut Vec<u8>,
304 _image_data: &[u8],
305 _format: ImageFormat,
306 maintain_aspect: bool,
307 ) -> Result<(), PdfError> {
308 let rect = &self.widget.rect;
309 let width = rect.width();
310 let height = rect.height();
311
312 stream.extend(b"q\n");
315
316 if maintain_aspect {
317 stream.extend(format!("{} 0 0 {} 0 0 cm\n", width * 0.8, height * 0.8).as_bytes());
319 } else {
320 stream.extend(format!("{} 0 0 {} 0 0 cm\n", width, height).as_bytes());
321 }
322
323 stream.extend(b"/Img1 Do\n");
325 stream.extend(b"Q\n");
326
327 Ok(())
328 }
329
330 #[allow(clippy::too_many_arguments)]
332 fn generate_mixed_appearance(
333 &self,
334 stream: &mut Vec<u8>,
335 _image_data: &[u8],
336 _format: ImageFormat,
337 text_position: TextPosition,
338 show_details: bool,
339 signed: bool,
340 signer_name: Option<&str>,
341 _reason: Option<&str>,
342 date: Option<&str>,
343 ) -> Result<(), PdfError> {
344 let rect = &self.widget.rect;
345 let width = rect.width();
346 let height = rect.height();
347
348 let (img_rect, text_rect) = match text_position {
350 TextPosition::Above => {
351 let text_height = height * 0.3;
352 (
353 (0.0, 0.0, width, height - text_height),
354 (0.0, height - text_height, width, text_height),
355 )
356 }
357 TextPosition::Below => {
358 let text_height = height * 0.3;
359 (
360 (0.0, text_height, width, height - text_height),
361 (0.0, 0.0, width, text_height),
362 )
363 }
364 TextPosition::Left => {
365 let text_width = width * 0.4;
366 (
367 (text_width, 0.0, width - text_width, height),
368 (0.0, 0.0, text_width, height),
369 )
370 }
371 TextPosition::Right => {
372 let text_width = width * 0.4;
373 (
374 (0.0, 0.0, width - text_width, height),
375 (width - text_width, 0.0, text_width, height),
376 )
377 }
378 TextPosition::Overlay => ((0.0, 0.0, width, height), (0.0, 0.0, width, height * 0.3)),
379 };
380
381 stream.extend(b"q\n");
383 stream.extend(
384 format!(
385 "{} 0 0 {} {} {} cm\n",
386 img_rect.2, img_rect.3, img_rect.0, img_rect.1
387 )
388 .as_bytes(),
389 );
390 stream.extend(b"/Img1 Do\n");
391 stream.extend(b"Q\n");
392
393 if show_details && signed {
395 stream.extend(b"BT\n");
396 stream.extend(b"/Helv 8 Tf\n");
397 stream.extend(b"0 g\n");
398
399 let mut y_pos = text_rect.1 + text_rect.3 - 10.0;
400
401 if let Some(name) = signer_name {
402 stream.extend(format!("{} {} Td\n", text_rect.0 + 2.0, y_pos).as_bytes());
403 stream.extend(format!("({}) Tj\n", name).as_bytes());
404 y_pos -= 10.0;
405 let _ = y_pos;
407 }
408
409 if let Some(d) = date {
410 stream.extend(b"0 -10 Td\n");
411 stream.extend(format!("({}) Tj\n", d).as_bytes());
412 }
413
414 stream.extend(b"ET\n");
415 }
416
417 Ok(())
418 }
419
420 fn generate_ink_appearance(
422 &self,
423 stream: &mut Vec<u8>,
424 strokes: &[InkStroke],
425 color: &Color,
426 width: f64,
427 ) -> Result<(), PdfError> {
428 Self::set_stroke_color(stream, color);
430 stream.extend(format!("{} w\n", width).as_bytes());
431 stream.extend(b"1 J\n"); stream.extend(b"1 j\n"); for stroke in strokes {
436 if stroke.points.len() < 2 {
437 continue;
438 }
439
440 let first = &stroke.points[0];
442 stream.extend(format!("{} {} m\n", first.0, first.1).as_bytes());
443
444 for point in &stroke.points[1..] {
446 stream.extend(format!("{} {} l\n", point.0, point.1).as_bytes());
447 }
448
449 stream.extend(b"S\n");
451 }
452
453 Ok(())
454 }
455
456 fn set_fill_color(stream: &mut Vec<u8>, color: &Color) {
459 crate::graphics::color::write_fill_color_bytes(stream, *color);
460 }
461
462 fn set_stroke_color(stream: &mut Vec<u8>, color: &Color) {
464 crate::graphics::color::write_stroke_color_bytes(stream, *color);
465 }
466
467 pub fn to_widget_dict(&self) -> Dictionary {
469 let mut dict = Dictionary::new();
470
471 dict.set("Type", Object::Name("Annot".to_string()));
473 dict.set("Subtype", Object::Name("Widget".to_string()));
474
475 let rect = &self.widget.rect;
477 dict.set(
478 "Rect",
479 Object::Array(vec![
480 Object::Real(rect.lower_left.x),
481 Object::Real(rect.lower_left.y),
482 Object::Real(rect.upper_right.x),
483 Object::Real(rect.upper_right.y),
484 ]),
485 );
486
487 if let Some(ref field_ref) = self.field_ref {
489 dict.set("Parent", Object::Reference(*field_ref));
490 }
491
492 let mut bs_dict = Dictionary::new();
494 bs_dict.set("Type", Object::Name("Border".to_string()));
495 bs_dict.set("W", Object::Real(self.widget.appearance.border_width));
496 bs_dict.set(
497 "S",
498 Object::Name(self.widget.appearance.border_style.pdf_name().to_string()),
499 );
500 dict.set("BS", Object::Dictionary(bs_dict));
501
502 let mut mk_dict = Dictionary::new();
504 if let Some(ref bg_color) = self.widget.appearance.background_color {
505 mk_dict.set("BG", Self::color_to_array(bg_color));
506 }
507 if let Some(ref border_color) = self.widget.appearance.border_color {
508 mk_dict.set("BC", Self::color_to_array(border_color));
509 }
510 dict.set("MK", Object::Dictionary(mk_dict));
511
512 dict.set("F", Object::Integer(4)); dict
516 }
517
518 fn color_to_array(color: &Color) -> Object {
520 match color {
521 Color::Gray(v) => Object::Array(vec![Object::Real(*v)]),
522 Color::Rgb(r, g, b) => {
523 Object::Array(vec![Object::Real(*r), Object::Real(*g), Object::Real(*b)])
524 }
525 Color::Cmyk(c, m, y, k) => Object::Array(vec![
526 Object::Real(*c),
527 Object::Real(*m),
528 Object::Real(*y),
529 Object::Real(*k),
530 ]),
531 }
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
540 fn test_signature_widget_creation() {
541 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 150.0));
542 let visual = SignatureVisualType::Text {
543 show_name: true,
544 show_date: true,
545 show_reason: false,
546 show_location: false,
547 };
548
549 let widget = SignatureWidget::new(rect, visual);
550 assert!(widget.field_ref.is_none());
551 assert!(widget.handler_ref.is_none());
552 }
553
554 #[test]
555 fn test_text_appearance_generation() {
556 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 50.0));
557 let visual = SignatureVisualType::Text {
558 show_name: true,
559 show_date: true,
560 show_reason: true,
561 show_location: false,
562 };
563
564 let widget = SignatureWidget::new(rect, visual);
565 let appearance = widget.generate_appearance_stream(
566 true,
567 Some("John Doe"),
568 Some("Approval"),
569 None,
570 Some("2025-08-13"),
571 );
572
573 assert!(appearance.is_ok());
574 let stream = appearance.unwrap();
575 assert!(!stream.is_empty());
576
577 let stream_str = String::from_utf8_lossy(&stream);
579 assert!(stream_str.contains("John Doe"));
580 assert!(stream_str.contains("2025-08-13"));
581 assert!(stream_str.contains("Approval"));
582 }
583
584 #[test]
585 fn test_ink_signature_appearance() {
586 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(150.0, 50.0));
587 let stroke1 = InkStroke {
588 points: vec![(10.0, 10.0), (20.0, 20.0), (30.0, 15.0)],
589 pressures: None,
590 };
591 let stroke2 = InkStroke {
592 points: vec![(40.0, 25.0), (50.0, 30.0), (60.0, 25.0)],
593 pressures: None,
594 };
595
596 let visual = SignatureVisualType::InkSignature {
597 strokes: vec![stroke1, stroke2],
598 color: Color::black(),
599 width: 2.0,
600 };
601
602 let widget = SignatureWidget::new(rect, visual);
603 let appearance = widget.generate_appearance_stream(true, None, None, None, None);
604
605 assert!(appearance.is_ok());
606 let stream = appearance.unwrap();
607 let stream_str = String::from_utf8_lossy(&stream);
608
609 assert!(stream_str.contains("m")); assert!(stream_str.contains("l")); assert!(stream_str.contains("S")); }
614
615 #[test]
616 fn test_widget_dict_generation() {
617 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 150.0));
618 let visual = SignatureVisualType::Text {
619 show_name: true,
620 show_date: false,
621 show_reason: false,
622 show_location: false,
623 };
624
625 let mut widget = SignatureWidget::new(rect, visual);
626 widget.widget.appearance.background_color = Some(Color::gray(0.9));
627 widget.widget.appearance.border_color = Some(Color::black());
628
629 let dict = widget.to_widget_dict();
630
631 assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
633 assert_eq!(
634 dict.get("Subtype"),
635 Some(&Object::Name("Widget".to_string()))
636 );
637 assert!(dict.get("Rect").is_some());
638 assert!(dict.get("BS").is_some());
639 assert!(dict.get("MK").is_some());
640 }
641
642 #[test]
643 fn test_signature_widget_with_field_ref() {
644 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0));
645 let visual = SignatureVisualType::Text {
646 show_name: true,
647 show_date: true,
648 show_reason: false,
649 show_location: false,
650 };
651
652 let field_ref = ObjectReference::new(10, 0);
653 let widget = SignatureWidget::new(rect, visual).with_field_ref(field_ref);
654
655 assert_eq!(widget.field_ref, Some(field_ref));
656
657 let dict = widget.to_widget_dict();
658 assert_eq!(dict.get("Parent"), Some(&Object::Reference(field_ref)));
659 }
660
661 #[test]
662 fn test_signature_widget_with_handler() {
663 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0));
664 let visual = SignatureVisualType::Text {
665 show_name: true,
666 show_date: false,
667 show_reason: false,
668 show_location: false,
669 };
670
671 let widget = SignatureWidget::new(rect, visual).with_handler("Adobe.PPKLite");
672
673 assert_eq!(widget.handler_ref, Some("Adobe.PPKLite".to_string()));
674 }
675
676 #[test]
677 fn test_graphic_signature_visual_type() {
678 let image_data = vec![0xFF, 0xD8, 0xFF, 0xE0]; let visual = SignatureVisualType::Graphic {
680 image_data: image_data.clone(),
681 format: ImageFormat::JPEG,
682 maintain_aspect: true,
683 };
684
685 match visual {
686 SignatureVisualType::Graphic {
687 image_data: data,
688 format,
689 maintain_aspect,
690 } => {
691 assert_eq!(data, image_data);
692 matches!(format, ImageFormat::JPEG);
693 assert!(maintain_aspect);
694 }
695 _ => panic!("Expected Graphic visual type"),
696 }
697 }
698
699 #[test]
700 fn test_mixed_signature_visual_type() {
701 let image_data = vec![0x89, 0x50, 0x4E, 0x47]; let visual = SignatureVisualType::Mixed {
703 image_data: image_data.clone(),
704 format: ImageFormat::PNG,
705 text_position: TextPosition::Below,
706 show_details: true,
707 };
708
709 match visual {
710 SignatureVisualType::Mixed {
711 image_data: data,
712 format,
713 text_position,
714 show_details,
715 } => {
716 assert_eq!(data, image_data);
717 matches!(format, ImageFormat::PNG);
718 matches!(text_position, TextPosition::Below);
719 assert!(show_details);
720 }
721 _ => panic!("Expected Mixed visual type"),
722 }
723 }
724
725 #[test]
726 fn test_ink_stroke_with_pressure() {
727 let stroke = InkStroke {
728 points: vec![(10.0, 10.0), (20.0, 20.0), (30.0, 15.0)],
729 pressures: Some(vec![0.5, 0.7, 0.6]),
730 };
731
732 assert_eq!(stroke.points.len(), 3);
733 assert_eq!(stroke.pressures.as_ref().unwrap().len(), 3);
734 assert_eq!(stroke.points[0], (10.0, 10.0));
735 assert_eq!(stroke.pressures.as_ref().unwrap()[1], 0.7);
736 }
737
738 #[test]
739 fn test_text_position_variants() {
740 let positions = vec![
741 TextPosition::Above,
742 TextPosition::Below,
743 TextPosition::Left,
744 TextPosition::Right,
745 TextPosition::Overlay,
746 ];
747
748 for pos in positions {
749 match pos {
750 TextPosition::Above => assert!(true),
751 TextPosition::Below => assert!(true),
752 TextPosition::Left => assert!(true),
753 TextPosition::Right => assert!(true),
754 TextPosition::Overlay => assert!(true),
755 }
756 }
757 }
758
759 #[test]
760 fn test_image_format_variants() {
761 let png = ImageFormat::PNG;
762 let jpeg = ImageFormat::JPEG;
763
764 matches!(png, ImageFormat::PNG);
765 matches!(jpeg, ImageFormat::JPEG);
766 }
767
768 #[test]
769 fn test_color_to_array() {
770 let gray = Color::gray(0.5);
772 let gray_array = SignatureWidget::color_to_array(&gray);
773 assert_eq!(gray_array, Object::Array(vec![Object::Real(0.5)]));
774
775 let rgb = Color::rgb(1.0, 0.0, 0.0);
777 let rgb_array = SignatureWidget::color_to_array(&rgb);
778 assert_eq!(
779 rgb_array,
780 Object::Array(vec![
781 Object::Real(1.0),
782 Object::Real(0.0),
783 Object::Real(0.0),
784 ])
785 );
786
787 let cmyk = Color::cmyk(0.0, 1.0, 1.0, 0.0);
789 let cmyk_array = SignatureWidget::color_to_array(&cmyk);
790 assert_eq!(
791 cmyk_array,
792 Object::Array(vec![
793 Object::Real(0.0),
794 Object::Real(1.0),
795 Object::Real(1.0),
796 Object::Real(0.0),
797 ])
798 );
799 }
800
801 #[test]
802 fn test_set_fill_color() {
803 let mut stream = Vec::new();
804
805 let rgb = Color::rgb(1.0, 0.5, 0.0);
809 SignatureWidget::set_fill_color(&mut stream, &rgb);
810 let result = String::from_utf8_lossy(&stream);
811 assert!(result.contains("1.000 0.500 0.000 rg"));
812
813 stream.clear();
815 let gray = Color::gray(0.7);
816 SignatureWidget::set_fill_color(&mut stream, &gray);
817 let result = String::from_utf8_lossy(&stream);
818 assert!(result.contains("0.700 g"));
819
820 stream.clear();
822 let cmyk = Color::cmyk(0.2, 0.3, 0.4, 0.1);
823 SignatureWidget::set_fill_color(&mut stream, &cmyk);
824 let result = String::from_utf8_lossy(&stream);
825 assert!(result.contains("0.200 0.300 0.400 0.100 k"));
826 }
827
828 #[test]
829 fn test_set_stroke_color() {
830 let mut stream = Vec::new();
831
832 let rgb = Color::rgb(0.0, 0.0, 1.0);
834 SignatureWidget::set_stroke_color(&mut stream, &rgb);
835 let result = String::from_utf8_lossy(&stream);
836 assert!(result.contains("0.000 0.000 1.000 RG"));
837
838 stream.clear();
840 let gray = Color::gray(0.3);
841 SignatureWidget::set_stroke_color(&mut stream, &gray);
842 let result = String::from_utf8_lossy(&stream);
843 assert!(result.contains("0.300 G"));
844
845 stream.clear();
847 let cmyk = Color::cmyk(1.0, 0.0, 0.0, 0.0);
848 SignatureWidget::set_stroke_color(&mut stream, &cmyk);
849 let result = String::from_utf8_lossy(&stream);
850 assert!(result.contains("1.000 0.000 0.000 0.000 K"));
851 }
852
853 #[test]
854 fn test_empty_text_signature() {
855 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 50.0));
856 let visual = SignatureVisualType::Text {
857 show_name: false,
858 show_date: false,
859 show_reason: false,
860 show_location: false,
861 };
862
863 let widget = SignatureWidget::new(rect, visual);
864 let appearance = widget.generate_appearance_stream(false, None, None, None, None);
865
866 assert!(appearance.is_ok());
867 let stream = appearance.unwrap();
868 let stream_str = String::from_utf8_lossy(&stream);
869
870 assert!(stream_str.contains("q")); assert!(stream_str.contains("Q")); }
874
875 #[test]
876 fn test_full_text_signature() {
877 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(300.0, 100.0));
878 let visual = SignatureVisualType::Text {
879 show_name: true,
880 show_date: true,
881 show_reason: true,
882 show_location: true,
883 };
884
885 let widget = SignatureWidget::new(rect, visual);
886 let appearance = widget.generate_appearance_stream(
887 true,
888 Some("Jane Smith"),
889 Some("Document Review"),
890 Some("New York"),
891 Some("2025-08-14"),
892 );
893
894 assert!(appearance.is_ok());
895 let stream = appearance.unwrap();
896 let stream_str = String::from_utf8_lossy(&stream);
897
898 assert!(stream_str.contains("Jane Smith"));
900 assert!(stream_str.contains("Document Review"));
901 assert!(stream_str.contains("New York"));
902 assert!(stream_str.contains("2025-08-14"));
903 }
904
905 #[test]
906 fn test_widget_with_border_styles() {
907 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0));
908 let visual = SignatureVisualType::Text {
909 show_name: true,
910 show_date: false,
911 show_reason: false,
912 show_location: false,
913 };
914
915 let mut widget = SignatureWidget::new(rect, visual);
916 widget.widget.appearance.border_width = 2.0;
917 widget.widget.appearance.border_color = Some(Color::rgb(0.0, 0.0, 1.0));
918
919 let dict = widget.to_widget_dict();
920
921 if let Some(Object::Dictionary(bs_dict)) = dict.get("BS") {
923 assert_eq!(bs_dict.get("W"), Some(&Object::Real(2.0)));
924 assert!(bs_dict.get("S").is_some());
925 } else {
926 panic!("Expected BS dictionary");
927 }
928 }
929
930 #[test]
931 fn test_multiple_ink_strokes() {
932 let _rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 100.0));
933 let strokes = vec![
934 InkStroke {
935 points: vec![(10.0, 10.0), (20.0, 20.0)],
936 pressures: None,
937 },
938 InkStroke {
939 points: vec![(30.0, 30.0), (40.0, 40.0), (50.0, 35.0)],
940 pressures: Some(vec![0.3, 0.5, 0.4]),
941 },
942 InkStroke {
943 points: vec![(60.0, 20.0), (70.0, 25.0)],
944 pressures: None,
945 },
946 ];
947
948 let visual = SignatureVisualType::InkSignature {
949 strokes: strokes,
950 color: Color::rgb(0.0, 0.0, 0.5),
951 width: 1.5,
952 };
953
954 match visual {
955 SignatureVisualType::InkSignature {
956 strokes: s,
957 color: _,
958 width,
959 } => {
960 assert_eq!(s.len(), 3);
961 assert_eq!(width, 1.5);
962 assert_eq!(s[1].points.len(), 3);
963 assert!(s[1].pressures.is_some());
964 }
965 _ => panic!("Expected InkSignature"),
966 }
967 }
968}