Skip to main content

oxidize_pdf/forms/
signature_widget.rs

1//! Enhanced signature widget implementation with full annotation support
2//!
3//! This module provides widget annotation support for signature fields
4//! according to ISO 32000-1 Section 12.5.6.19 (Widget Annotations) and
5//! Section 12.7.4.5 (Signature Fields).
6
7use 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/// Enhanced signature widget with full annotation support
16#[derive(Debug, Clone)]
17pub struct SignatureWidget {
18    /// Base widget properties
19    pub widget: Widget,
20    /// Signature field reference
21    pub field_ref: Option<ObjectReference>,
22    /// Visual representation type
23    pub visual_type: SignatureVisualType,
24    /// Signature handler reference
25    pub handler_ref: Option<String>,
26}
27
28/// Visual representation types for signatures
29#[derive(Debug, Clone)]
30pub enum SignatureVisualType {
31    /// Text-only signature
32    Text {
33        /// Show signer name
34        show_name: bool,
35        /// Show signing date
36        show_date: bool,
37        /// Show reason for signing
38        show_reason: bool,
39        /// Show location
40        show_location: bool,
41    },
42    /// Graphical signature (e.g., handwritten)
43    Graphic {
44        /// Image data (PNG/JPEG)
45        image_data: Vec<u8>,
46        /// Image format
47        format: ImageFormat,
48        /// Maintain aspect ratio
49        maintain_aspect: bool,
50    },
51    /// Mixed text and graphics
52    Mixed {
53        /// Image data for signature
54        image_data: Vec<u8>,
55        /// Image format
56        format: ImageFormat,
57        /// Text position relative to image
58        text_position: TextPosition,
59        /// Include text details
60        show_details: bool,
61    },
62    /// Handwritten ink signature
63    InkSignature {
64        /// Ink paths (strokes)
65        strokes: Vec<InkStroke>,
66        /// Stroke color
67        color: Color,
68        /// Stroke width
69        width: f64,
70    },
71}
72
73/// Image formats supported for signature graphics
74#[derive(Debug, Clone, Copy)]
75pub enum ImageFormat {
76    PNG,
77    JPEG,
78}
79
80/// Text position relative to signature image
81#[derive(Debug, Clone, Copy)]
82pub enum TextPosition {
83    Above,
84    Below,
85    Left,
86    Right,
87    Overlay,
88}
89
90/// Ink stroke for handwritten signatures
91#[derive(Debug, Clone)]
92pub struct InkStroke {
93    /// Points in the stroke
94    pub points: Vec<(f64, f64)>,
95    /// Pressure values (optional)
96    pub pressures: Option<Vec<f64>>,
97}
98
99impl SignatureWidget {
100    /// Create a new signature widget
101    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    /// Set the field reference
111    pub fn with_field_ref(mut self, field_ref: ObjectReference) -> Self {
112        self.field_ref = Some(field_ref);
113        self
114    }
115
116    /// Set the handler reference
117    pub fn with_handler(mut self, handler: impl Into<String>) -> Self {
118        self.handler_ref = Some(handler.into());
119        self
120    }
121
122    /// Generate appearance stream for the signature widget
123    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        // Save graphics state
137        stream.extend(b"q\n");
138
139        // Draw background if specified
140        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        // Draw border
146        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        // Generate content based on visual type
155        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        // Restore graphics state
215        stream.extend(b"Q\n");
216
217        Ok(stream)
218    }
219
220    /// Generate text-only appearance
221    #[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        // Begin text object
240        stream.extend(b"BT\n");
241
242        // Set font (using Helvetica as default)
243        stream.extend(b"/Helv 10 Tf\n");
244
245        // Set text color (black)
246        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                // Track y_offset for future use
259                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                // Track y_offset for future use
269                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                // Track y_offset for future use
279                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            // Unsigned placeholder
290            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        // End text object
295        stream.extend(b"ET\n");
296
297        Ok(())
298    }
299
300    /// Generate graphic appearance (image-based signature)
301    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        // For now, create a placeholder for image
313        // In production, this would decode and embed the actual image
314        stream.extend(b"q\n");
315
316        if maintain_aspect {
317            // Calculate aspect-preserving transform
318            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        // Placeholder for image XObject reference
324        stream.extend(b"/Img1 Do\n");
325        stream.extend(b"Q\n");
326
327        Ok(())
328    }
329
330    /// Generate mixed text and graphic appearance
331    #[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        // Calculate regions for image and text
349        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        // Draw image in its region
382        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        // Draw text in its region if showing details
394        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                // Track y_pos for future use
406                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    /// Generate ink signature appearance (handwritten)
421    fn generate_ink_appearance(
422        &self,
423        stream: &mut Vec<u8>,
424        strokes: &[InkStroke],
425        color: &Color,
426        width: f64,
427    ) -> Result<(), PdfError> {
428        // Set stroke color and width
429        Self::set_stroke_color(stream, color);
430        stream.extend(format!("{} w\n", width).as_bytes());
431        stream.extend(b"1 J\n"); // Round line cap
432        stream.extend(b"1 j\n"); // Round line join
433
434        // Draw each stroke
435        for stroke in strokes {
436            if stroke.points.len() < 2 {
437                continue;
438            }
439
440            // Move to first point
441            let first = &stroke.points[0];
442            stream.extend(format!("{} {} m\n", first.0, first.1).as_bytes());
443
444            // Draw lines to subsequent points
445            for point in &stroke.points[1..] {
446                stream.extend(format!("{} {} l\n", point.0, point.1).as_bytes());
447            }
448
449            // Stroke the path
450            stream.extend(b"S\n");
451        }
452
453        Ok(())
454    }
455
456    /// Helper to set fill color
457    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    /// Helper to set stroke color
472    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    /// Convert to PDF widget annotation dictionary
487    pub fn to_widget_dict(&self) -> Dictionary {
488        let mut dict = Dictionary::new();
489
490        // Annotation type
491        dict.set("Type", Object::Name("Annot".to_string()));
492        dict.set("Subtype", Object::Name("Widget".to_string()));
493
494        // Rectangle
495        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        // Field reference
507        if let Some(ref field_ref) = self.field_ref {
508            dict.set("Parent", Object::Reference(*field_ref));
509        }
510
511        // Border appearance
512        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        // Appearance characteristics
522        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        // Flags
532        dict.set("F", Object::Integer(4)); // Print flag
533
534        dict
535    }
536
537    /// Convert color to PDF array
538    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        // Check that the stream contains expected content
597        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        // Check that paths are created
629        assert!(stream_str.contains("m")); // moveto
630        assert!(stream_str.contains("l")); // lineto
631        assert!(stream_str.contains("S")); // stroke
632    }
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        // Verify dictionary structure
651        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]; // JPEG magic bytes
698        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]; // PNG magic bytes
721        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        // Test gray color
790        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        // Test RGB color
795        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        // Test CMYK color
807        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        // Test RGB fill
825        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        // Test gray fill
831        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        // Test CMYK fill
838        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        // Test RGB stroke
850        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        // Test gray stroke
856        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        // Test CMYK stroke
863        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        // Should still have basic structure
888        assert!(stream_str.contains("q")); // Save state
889        assert!(stream_str.contains("Q")); // Restore state
890    }
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        // Check all text elements are present
916        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        // Check border style dictionary
939        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}