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) {
458 match color {
459 Color::Rgb(r, g, b) => {
460 stream.extend(format!("{} {} {} rg\n", r, g, b).as_bytes());
461 }
462 Color::Gray(v) => {
463 stream.extend(format!("{} g\n", v).as_bytes());
464 }
465 Color::Cmyk(c, m, y, k) => {
466 stream.extend(format!("{} {} {} {} k\n", c, m, y, k).as_bytes());
467 }
468 }
469 }
470
471 fn set_stroke_color(stream: &mut Vec<u8>, color: &Color) {
473 match color {
474 Color::Rgb(r, g, b) => {
475 stream.extend(format!("{} {} {} RG\n", r, g, b).as_bytes());
476 }
477 Color::Gray(v) => {
478 stream.extend(format!("{} G\n", v).as_bytes());
479 }
480 Color::Cmyk(c, m, y, k) => {
481 stream.extend(format!("{} {} {} {} K\n", c, m, y, k).as_bytes());
482 }
483 }
484 }
485
486 pub fn to_widget_dict(&self) -> Dictionary {
488 let mut dict = Dictionary::new();
489
490 dict.set("Type", Object::Name("Annot".to_string()));
492 dict.set("Subtype", Object::Name("Widget".to_string()));
493
494 let rect = &self.widget.rect;
496 dict.set(
497 "Rect",
498 Object::Array(vec![
499 Object::Real(rect.lower_left.x),
500 Object::Real(rect.lower_left.y),
501 Object::Real(rect.upper_right.x),
502 Object::Real(rect.upper_right.y),
503 ]),
504 );
505
506 if let Some(ref field_ref) = self.field_ref {
508 dict.set("Parent", Object::Reference(*field_ref));
509 }
510
511 let mut bs_dict = Dictionary::new();
513 bs_dict.set("Type", Object::Name("Border".to_string()));
514 bs_dict.set("W", Object::Real(self.widget.appearance.border_width));
515 bs_dict.set(
516 "S",
517 Object::Name(self.widget.appearance.border_style.pdf_name().to_string()),
518 );
519 dict.set("BS", Object::Dictionary(bs_dict));
520
521 let mut mk_dict = Dictionary::new();
523 if let Some(ref bg_color) = self.widget.appearance.background_color {
524 mk_dict.set("BG", Self::color_to_array(bg_color));
525 }
526 if let Some(ref border_color) = self.widget.appearance.border_color {
527 mk_dict.set("BC", Self::color_to_array(border_color));
528 }
529 dict.set("MK", Object::Dictionary(mk_dict));
530
531 dict.set("F", Object::Integer(4)); dict
535 }
536
537 fn color_to_array(color: &Color) -> Object {
539 match color {
540 Color::Gray(v) => Object::Array(vec![Object::Real(*v)]),
541 Color::Rgb(r, g, b) => {
542 Object::Array(vec![Object::Real(*r), Object::Real(*g), Object::Real(*b)])
543 }
544 Color::Cmyk(c, m, y, k) => Object::Array(vec![
545 Object::Real(*c),
546 Object::Real(*m),
547 Object::Real(*y),
548 Object::Real(*k),
549 ]),
550 }
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn test_signature_widget_creation() {
560 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 150.0));
561 let visual = SignatureVisualType::Text {
562 show_name: true,
563 show_date: true,
564 show_reason: false,
565 show_location: false,
566 };
567
568 let widget = SignatureWidget::new(rect, visual);
569 assert!(widget.field_ref.is_none());
570 assert!(widget.handler_ref.is_none());
571 }
572
573 #[test]
574 fn test_text_appearance_generation() {
575 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 50.0));
576 let visual = SignatureVisualType::Text {
577 show_name: true,
578 show_date: true,
579 show_reason: true,
580 show_location: false,
581 };
582
583 let widget = SignatureWidget::new(rect, visual);
584 let appearance = widget.generate_appearance_stream(
585 true,
586 Some("John Doe"),
587 Some("Approval"),
588 None,
589 Some("2025-08-13"),
590 );
591
592 assert!(appearance.is_ok());
593 let stream = appearance.unwrap();
594 assert!(!stream.is_empty());
595
596 let stream_str = String::from_utf8_lossy(&stream);
598 assert!(stream_str.contains("John Doe"));
599 assert!(stream_str.contains("2025-08-13"));
600 assert!(stream_str.contains("Approval"));
601 }
602
603 #[test]
604 fn test_ink_signature_appearance() {
605 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(150.0, 50.0));
606 let stroke1 = InkStroke {
607 points: vec![(10.0, 10.0), (20.0, 20.0), (30.0, 15.0)],
608 pressures: None,
609 };
610 let stroke2 = InkStroke {
611 points: vec![(40.0, 25.0), (50.0, 30.0), (60.0, 25.0)],
612 pressures: None,
613 };
614
615 let visual = SignatureVisualType::InkSignature {
616 strokes: vec![stroke1, stroke2],
617 color: Color::black(),
618 width: 2.0,
619 };
620
621 let widget = SignatureWidget::new(rect, visual);
622 let appearance = widget.generate_appearance_stream(true, None, None, None, None);
623
624 assert!(appearance.is_ok());
625 let stream = appearance.unwrap();
626 let stream_str = String::from_utf8_lossy(&stream);
627
628 assert!(stream_str.contains("m")); assert!(stream_str.contains("l")); assert!(stream_str.contains("S")); }
633
634 #[test]
635 fn test_widget_dict_generation() {
636 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 150.0));
637 let visual = SignatureVisualType::Text {
638 show_name: true,
639 show_date: false,
640 show_reason: false,
641 show_location: false,
642 };
643
644 let mut widget = SignatureWidget::new(rect, visual);
645 widget.widget.appearance.background_color = Some(Color::gray(0.9));
646 widget.widget.appearance.border_color = Some(Color::black());
647
648 let dict = widget.to_widget_dict();
649
650 assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
652 assert_eq!(
653 dict.get("Subtype"),
654 Some(&Object::Name("Widget".to_string()))
655 );
656 assert!(dict.get("Rect").is_some());
657 assert!(dict.get("BS").is_some());
658 assert!(dict.get("MK").is_some());
659 }
660
661 #[test]
662 fn test_signature_widget_with_field_ref() {
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: true,
667 show_reason: false,
668 show_location: false,
669 };
670
671 let field_ref = ObjectReference::new(10, 0);
672 let widget = SignatureWidget::new(rect, visual).with_field_ref(field_ref);
673
674 assert_eq!(widget.field_ref, Some(field_ref));
675
676 let dict = widget.to_widget_dict();
677 assert_eq!(dict.get("Parent"), Some(&Object::Reference(field_ref)));
678 }
679
680 #[test]
681 fn test_signature_widget_with_handler() {
682 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0));
683 let visual = SignatureVisualType::Text {
684 show_name: true,
685 show_date: false,
686 show_reason: false,
687 show_location: false,
688 };
689
690 let widget = SignatureWidget::new(rect, visual).with_handler("Adobe.PPKLite");
691
692 assert_eq!(widget.handler_ref, Some("Adobe.PPKLite".to_string()));
693 }
694
695 #[test]
696 fn test_graphic_signature_visual_type() {
697 let image_data = vec![0xFF, 0xD8, 0xFF, 0xE0]; let visual = SignatureVisualType::Graphic {
699 image_data: image_data.clone(),
700 format: ImageFormat::JPEG,
701 maintain_aspect: true,
702 };
703
704 match visual {
705 SignatureVisualType::Graphic {
706 image_data: data,
707 format,
708 maintain_aspect,
709 } => {
710 assert_eq!(data, image_data);
711 matches!(format, ImageFormat::JPEG);
712 assert!(maintain_aspect);
713 }
714 _ => panic!("Expected Graphic visual type"),
715 }
716 }
717
718 #[test]
719 fn test_mixed_signature_visual_type() {
720 let image_data = vec![0x89, 0x50, 0x4E, 0x47]; let visual = SignatureVisualType::Mixed {
722 image_data: image_data.clone(),
723 format: ImageFormat::PNG,
724 text_position: TextPosition::Below,
725 show_details: true,
726 };
727
728 match visual {
729 SignatureVisualType::Mixed {
730 image_data: data,
731 format,
732 text_position,
733 show_details,
734 } => {
735 assert_eq!(data, image_data);
736 matches!(format, ImageFormat::PNG);
737 matches!(text_position, TextPosition::Below);
738 assert!(show_details);
739 }
740 _ => panic!("Expected Mixed visual type"),
741 }
742 }
743
744 #[test]
745 fn test_ink_stroke_with_pressure() {
746 let stroke = InkStroke {
747 points: vec![(10.0, 10.0), (20.0, 20.0), (30.0, 15.0)],
748 pressures: Some(vec![0.5, 0.7, 0.6]),
749 };
750
751 assert_eq!(stroke.points.len(), 3);
752 assert_eq!(stroke.pressures.as_ref().unwrap().len(), 3);
753 assert_eq!(stroke.points[0], (10.0, 10.0));
754 assert_eq!(stroke.pressures.as_ref().unwrap()[1], 0.7);
755 }
756
757 #[test]
758 fn test_text_position_variants() {
759 let positions = vec![
760 TextPosition::Above,
761 TextPosition::Below,
762 TextPosition::Left,
763 TextPosition::Right,
764 TextPosition::Overlay,
765 ];
766
767 for pos in positions {
768 match pos {
769 TextPosition::Above => assert!(true),
770 TextPosition::Below => assert!(true),
771 TextPosition::Left => assert!(true),
772 TextPosition::Right => assert!(true),
773 TextPosition::Overlay => assert!(true),
774 }
775 }
776 }
777
778 #[test]
779 fn test_image_format_variants() {
780 let png = ImageFormat::PNG;
781 let jpeg = ImageFormat::JPEG;
782
783 matches!(png, ImageFormat::PNG);
784 matches!(jpeg, ImageFormat::JPEG);
785 }
786
787 #[test]
788 fn test_color_to_array() {
789 let gray = Color::gray(0.5);
791 let gray_array = SignatureWidget::color_to_array(&gray);
792 assert_eq!(gray_array, Object::Array(vec![Object::Real(0.5)]));
793
794 let rgb = Color::rgb(1.0, 0.0, 0.0);
796 let rgb_array = SignatureWidget::color_to_array(&rgb);
797 assert_eq!(
798 rgb_array,
799 Object::Array(vec![
800 Object::Real(1.0),
801 Object::Real(0.0),
802 Object::Real(0.0),
803 ])
804 );
805
806 let cmyk = Color::cmyk(0.0, 1.0, 1.0, 0.0);
808 let cmyk_array = SignatureWidget::color_to_array(&cmyk);
809 assert_eq!(
810 cmyk_array,
811 Object::Array(vec![
812 Object::Real(0.0),
813 Object::Real(1.0),
814 Object::Real(1.0),
815 Object::Real(0.0),
816 ])
817 );
818 }
819
820 #[test]
821 fn test_set_fill_color() {
822 let mut stream = Vec::new();
823
824 let rgb = Color::rgb(1.0, 0.5, 0.0);
826 SignatureWidget::set_fill_color(&mut stream, &rgb);
827 let result = String::from_utf8_lossy(&stream);
828 assert!(result.contains("1 0.5 0 rg"));
829
830 stream.clear();
832 let gray = Color::gray(0.7);
833 SignatureWidget::set_fill_color(&mut stream, &gray);
834 let result = String::from_utf8_lossy(&stream);
835 assert!(result.contains("0.7 g"));
836
837 stream.clear();
839 let cmyk = Color::cmyk(0.2, 0.3, 0.4, 0.1);
840 SignatureWidget::set_fill_color(&mut stream, &cmyk);
841 let result = String::from_utf8_lossy(&stream);
842 assert!(result.contains("0.2 0.3 0.4 0.1 k"));
843 }
844
845 #[test]
846 fn test_set_stroke_color() {
847 let mut stream = Vec::new();
848
849 let rgb = Color::rgb(0.0, 0.0, 1.0);
851 SignatureWidget::set_stroke_color(&mut stream, &rgb);
852 let result = String::from_utf8_lossy(&stream);
853 assert!(result.contains("0 0 1 RG"));
854
855 stream.clear();
857 let gray = Color::gray(0.3);
858 SignatureWidget::set_stroke_color(&mut stream, &gray);
859 let result = String::from_utf8_lossy(&stream);
860 assert!(result.contains("0.3 G"));
861
862 stream.clear();
864 let cmyk = Color::cmyk(1.0, 0.0, 0.0, 0.0);
865 SignatureWidget::set_stroke_color(&mut stream, &cmyk);
866 let result = String::from_utf8_lossy(&stream);
867 assert!(result.contains("1 0 0 0 K"));
868 }
869
870 #[test]
871 fn test_empty_text_signature() {
872 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 50.0));
873 let visual = SignatureVisualType::Text {
874 show_name: false,
875 show_date: false,
876 show_reason: false,
877 show_location: false,
878 };
879
880 let widget = SignatureWidget::new(rect, visual);
881 let appearance = widget.generate_appearance_stream(false, None, None, None, None);
882
883 assert!(appearance.is_ok());
884 let stream = appearance.unwrap();
885 let stream_str = String::from_utf8_lossy(&stream);
886
887 assert!(stream_str.contains("q")); assert!(stream_str.contains("Q")); }
891
892 #[test]
893 fn test_full_text_signature() {
894 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(300.0, 100.0));
895 let visual = SignatureVisualType::Text {
896 show_name: true,
897 show_date: true,
898 show_reason: true,
899 show_location: true,
900 };
901
902 let widget = SignatureWidget::new(rect, visual);
903 let appearance = widget.generate_appearance_stream(
904 true,
905 Some("Jane Smith"),
906 Some("Document Review"),
907 Some("New York"),
908 Some("2025-08-14"),
909 );
910
911 assert!(appearance.is_ok());
912 let stream = appearance.unwrap();
913 let stream_str = String::from_utf8_lossy(&stream);
914
915 assert!(stream_str.contains("Jane Smith"));
917 assert!(stream_str.contains("Document Review"));
918 assert!(stream_str.contains("New York"));
919 assert!(stream_str.contains("2025-08-14"));
920 }
921
922 #[test]
923 fn test_widget_with_border_styles() {
924 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0));
925 let visual = SignatureVisualType::Text {
926 show_name: true,
927 show_date: false,
928 show_reason: false,
929 show_location: false,
930 };
931
932 let mut widget = SignatureWidget::new(rect, visual);
933 widget.widget.appearance.border_width = 2.0;
934 widget.widget.appearance.border_color = Some(Color::rgb(0.0, 0.0, 1.0));
935
936 let dict = widget.to_widget_dict();
937
938 if let Some(Object::Dictionary(bs_dict)) = dict.get("BS") {
940 assert_eq!(bs_dict.get("W"), Some(&Object::Real(2.0)));
941 assert!(bs_dict.get("S").is_some());
942 } else {
943 panic!("Expected BS dictionary");
944 }
945 }
946
947 #[test]
948 fn test_multiple_ink_strokes() {
949 let _rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(200.0, 100.0));
950 let strokes = vec![
951 InkStroke {
952 points: vec![(10.0, 10.0), (20.0, 20.0)],
953 pressures: None,
954 },
955 InkStroke {
956 points: vec![(30.0, 30.0), (40.0, 40.0), (50.0, 35.0)],
957 pressures: Some(vec![0.3, 0.5, 0.4]),
958 },
959 InkStroke {
960 points: vec![(60.0, 20.0), (70.0, 25.0)],
961 pressures: None,
962 },
963 ];
964
965 let visual = SignatureVisualType::InkSignature {
966 strokes: strokes,
967 color: Color::rgb(0.0, 0.0, 0.5),
968 width: 1.5,
969 };
970
971 match visual {
972 SignatureVisualType::InkSignature {
973 strokes: s,
974 color: _,
975 width,
976 } => {
977 assert_eq!(s.len(), 3);
978 assert_eq!(width, 1.5);
979 assert_eq!(s[1].points.len(), 3);
980 assert!(s[1].pressures.is_some());
981 }
982 _ => panic!("Expected InkSignature"),
983 }
984 }
985}