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 — routed through the shared NaN-sanitising
457    /// helper (issues #220 + #221).
458    fn set_fill_color(stream: &mut Vec<u8>, color: &Color) {
459        crate::graphics::color::write_fill_color_bytes(stream, *color);
460    }
461
462    /// Helper to set stroke color — routed through the shared sanitising helper.
463    fn set_stroke_color(stream: &mut Vec<u8>, color: &Color) {
464        crate::graphics::color::write_stroke_color_bytes(stream, *color);
465    }
466
467    /// Convert to PDF widget annotation dictionary
468    pub fn to_widget_dict(&self) -> Dictionary {
469        let mut dict = Dictionary::new();
470
471        // Annotation type
472        dict.set("Type", Object::Name("Annot".to_string()));
473        dict.set("Subtype", Object::Name("Widget".to_string()));
474
475        // Rectangle
476        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        // Field reference
488        if let Some(ref field_ref) = self.field_ref {
489            dict.set("Parent", Object::Reference(*field_ref));
490        }
491
492        // Border appearance
493        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        // Appearance characteristics
503        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        // Flags
513        dict.set("F", Object::Integer(4)); // Print flag
514
515        dict
516    }
517
518    /// Convert color to PDF array
519    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        // Check that the stream contains expected content
578        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        // Check that paths are created
610        assert!(stream_str.contains("m")); // moveto
611        assert!(stream_str.contains("l")); // lineto
612        assert!(stream_str.contains("S")); // stroke
613    }
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        // Verify dictionary structure
632        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]; // JPEG magic bytes
679        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]; // PNG magic bytes
702        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        // Test gray color
771        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        // Test RGB color
776        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        // Test CMYK color
788        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        // After the issue #220/#221 helper migration these emitters now
806        // share the `.3`-precision format used elsewhere in the pipeline,
807        // so the wire form is `1.000 0.500 0.000 rg`, not `1 0.5 0 rg`.
808        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        // Test gray fill
814        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        // Test CMYK fill
821        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        // `.3`-precision format (issue #220/#221 helper migration).
833        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        // Test gray stroke
839        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        // Test CMYK stroke
846        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        // Should still have basic structure
871        assert!(stream_str.contains("q")); // Save state
872        assert!(stream_str.contains("Q")); // Restore state
873    }
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        // Check all text elements are present
899        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        // Check border style dictionary
922        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}