Skip to main content

rust_pdf_box/sign/
visual.rs

1use crate::content::color::Color;
2use crate::content::stream::{AppendMode, PDPageContentStream};
3use crate::cos::object::{COSDictionary, COSName, COSStream, ObjectId};
4use crate::error::PdfResult;
5use crate::font::type1::PDType1Font;
6use crate::image::PDImageXObject;
7use crate::pd::rectangle::PDRectangle;
8
9/// Configuration for visual signature representation on a page.
10#[derive(Debug, Clone)]
11pub struct VisualSignature {
12    pub page_number: usize, // 1-based page number
13    pub rect: PDRectangle,
14    pub title: String,
15    pub details: String,
16    pub seal_image: Option<PDImageXObject>,
17    pub border_color: Color,
18    pub background_color: Option<Color>,
19    pub text_color: Color,
20}
21
22impl VisualSignature {
23    /// Initializes a visual signature at a given 1-based page number.
24    pub fn at_page(page_number: usize) -> Self {
25        Self {
26            page_number,
27            rect: PDRectangle::new(72.0, 72.0, 272.0, 152.0),
28            title: "Digitally Signed".to_string(),
29            details: "Verified Digital Signature".to_string(),
30            seal_image: None,
31            border_color: Color::rgb(0.0, 0.4, 0.8),
32            background_color: Some(Color::rgb(0.96, 0.98, 1.0)),
33            text_color: Color::rgb(0.1, 0.1, 0.2),
34        }
35    }
36
37    pub fn rect(mut self, llx: f64, lly: f64, urx: f64, ury: f64) -> Self {
38        self.rect = PDRectangle::new(llx, lly, urx, ury);
39        self
40    }
41
42    pub fn title(mut self, title: impl Into<String>) -> Self {
43        self.title = title.into();
44        self
45    }
46
47    pub fn detail(mut self, details: impl Into<String>) -> Self {
48        self.details = details.into();
49        self
50    }
51
52    pub fn seal(mut self, image: PDImageXObject) -> Self {
53        self.seal_image = Some(image);
54        self
55    }
56
57    pub fn border_color(mut self, color: Color) -> Self {
58        self.border_color = color;
59        self
60    }
61
62    pub fn background_color(mut self, color: Color) -> Self {
63        self.background_color = Some(color);
64        self
65    }
66
67    /// Creates an appearance stream (Form XObject) representing this visual signature.
68    pub fn build_appearance_stream(
69        &self,
70        cos_doc: &mut crate::cos::document::COSDocument,
71    ) -> PdfResult<ObjectId> {
72        let w = self.rect.width();
73        let h = self.rect.height();
74
75        let mut stream_content = PDPageContentStream::new(AppendMode::Overwrite);
76        stream_content.save_graphics_state();
77
78        // 1. Background
79        if let Some(bg) = self.background_color {
80            stream_content.set_fill_color(bg);
81            stream_content.round_rect(1.0, 1.0, w - 2.0, h - 2.0, 4.0);
82            stream_content.fill();
83        }
84
85        // 2. Border
86        stream_content.set_stroke_color(self.border_color);
87        stream_content.set_line_width(1.5);
88        stream_content.round_rect(1.0, 1.0, w - 2.0, h - 2.0, 4.0);
89        stream_content.stroke();
90
91        // 3. Optional Seal / Logo Image
92        let text_start_x = if let Some(ref img) = self.seal_image {
93            let img_size = (h - 16.0).min(50.0);
94            let img_name = "ImSig";
95            let mut resources = COSDictionary::new();
96            let mut xobj_dict = COSDictionary::new();
97            xobj_dict.set(COSName::new(img_name), img.image_id);
98            resources.set("XObject", xobj_dict);
99
100            stream_content.draw_image(img_name, 8.0, (h - img_size) / 2.0, img_size, img_size);
101            img_size + 16.0
102        } else {
103            10.0
104        };
105
106        // 4. Text (Title + Details)
107        let font_title = PDType1Font::HelveticaBold;
108        let font_body = PDType1Font::Helvetica;
109
110        // Register fonts in Form XObject resources
111        let font_title_id = cos_doc.add_object(font_title.to_dict());
112        let font_body_id = cos_doc.add_object(font_body.to_dict());
113
114        let mut font_dict = COSDictionary::new();
115        font_dict.set(COSName::new("FTitle"), font_title_id);
116        font_dict.set(COSName::new("FBody"), font_body_id);
117
118        let mut res_dict = COSDictionary::new();
119        res_dict.set("Font", font_dict);
120
121        if let Some(ref img) = self.seal_image {
122            let mut xobj_dict = COSDictionary::new();
123            xobj_dict.set(COSName::new("ImSig"), img.image_id);
124            res_dict.set("XObject", xobj_dict);
125        }
126
127        // Draw title
128        stream_content.begin_text();
129        stream_content.set_font("FTitle", 11.0);
130        stream_content.set_fill_color(self.text_color);
131        stream_content.show_text_at(&self.title, text_start_x, h - 18.0);
132        stream_content.end_text();
133
134        // Draw details
135        stream_content.begin_text();
136        stream_content.set_font("FBody", 8.0);
137        stream_content.set_fill_color(self.text_color);
138
139        let lines: Vec<&str> = self.details.lines().collect();
140        stream_content.show_text_multiline(&lines, text_start_x, h - 30.0, 10.0);
141        stream_content.end_text();
142
143        stream_content.restore_graphics_state();
144
145        // Build Form XObject stream dictionary
146        let mut form_dict = COSDictionary::new();
147        form_dict.set("Type", COSName::new("XObject"));
148        form_dict.set("Subtype", COSName::new("Form"));
149        form_dict.set("BBox", PDRectangle::new(0.0, 0.0, w, h));
150        form_dict.set("Resources", res_dict);
151
152        let raw_bytes = stream_content.finish_to_bytes();
153        let mut cos_stream = COSStream::new(form_dict, raw_bytes);
154        let _ = cos_stream.encode_flate();
155
156        let stream_id = cos_doc.add_object(cos_stream);
157        Ok(stream_id)
158    }
159}