Skip to main content

rust_pdf_box/stamp/
stamper.rs

1use crate::content::color::Color;
2use crate::content::stream::{AppendMode, PDPageContentStream};
3use crate::error::PdfResult;
4use crate::font::type1::PDType1Font;
5use crate::image::PDImageXObject;
6use crate::pd::document::PDDocument;
7use crate::stamp::options::{PageRange, StampLayer, StampPosition};
8
9#[derive(Debug, Clone)]
10enum StampContent {
11    Watermark {
12        text: String,
13        font: PDType1Font,
14        font_size: f64,
15        color: Color,
16        rotation: f64,
17        opacity: f64,
18    },
19    RubberStamp {
20        title: String,
21        subtitle: Option<String>,
22        border_color: Color,
23        fill_color: Option<Color>,
24        text_color: Color,
25        rotation: f64,
26        opacity: f64,
27    },
28    HeaderFooter {
29        text: String,
30        position: StampPosition,
31        font: PDType1Font,
32        font_size: f64,
33        color: Color,
34        opacity: f64,
35    },
36    Image {
37        image: PDImageXObject,
38        width: f64,
39        height: f64,
40        rotation: f64,
41        opacity: f64,
42    },
43}
44
45/// Context-oriented Stamper builder for applying watermarks, rubber stamps, headers/footers, and images.
46#[derive(Debug, Clone)]
47pub struct Stamper {
48    pages: PageRange,
49    layer: StampLayer,
50    position: StampPosition,
51    content: Option<StampContent>,
52}
53
54impl Default for Stamper {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl Stamper {
61    pub fn new() -> Self {
62        Self {
63            pages: PageRange::All,
64            layer: StampLayer::UnderContent,
65            position: StampPosition::Center,
66            content: None,
67        }
68    }
69
70    /// Selects pages to apply the stamp on.
71    pub fn pages(mut self, range: PageRange) -> Self {
72        self.pages = range;
73        self
74    }
75
76    /// Sets whether stamp is placed under or over page content.
77    pub fn layer(mut self, layer: StampLayer) -> Self {
78        self.layer = layer;
79        self
80    }
81
82    /// Sets the position on the page.
83    pub fn position(mut self, pos: StampPosition) -> Self {
84        self.position = pos;
85        self
86    }
87
88    /// Configures a semi-transparent text watermark.
89    pub fn watermark(
90        mut self,
91        text: impl Into<String>,
92        font_size: f64,
93        color: Color,
94        rotation: f64,
95        opacity: f64,
96    ) -> Self {
97        self.layer = StampLayer::UnderContent;
98        self.position = StampPosition::Center;
99        self.content = Some(StampContent::Watermark {
100            text: text.into(),
101            font: PDType1Font::HelveticaBold,
102            font_size,
103            color,
104            rotation,
105            opacity,
106        });
107        self
108    }
109
110    /// Configures a simple centered diagonal watermark with default styling.
111    pub fn simple_watermark(self, text: impl Into<String>) -> Self {
112        self.watermark(text, 54.0, Color::rgb(0.85, 0.1, 0.1), 45.0, 0.25)
113    }
114
115    /// Configures a boxed rubber stamp (e.g. "APPROVED", "DRAFT", "CONFIDENTIAL").
116    pub fn rubber_stamp(
117        mut self,
118        title: impl Into<String>,
119        subtitle: Option<String>,
120        color: Color,
121        rotation: f64,
122    ) -> Self {
123        self.layer = StampLayer::OverContent;
124        self.content = Some(StampContent::RubberStamp {
125            title: title.into(),
126            subtitle,
127            border_color: color,
128            fill_color: Some(Color::rgb(1.0, 1.0, 1.0)),
129            text_color: color,
130            rotation,
131            opacity: 0.9,
132        });
133        self
134    }
135
136    /// Configures a header stamp with placeholder support: `{page}`, `{total}`.
137    pub fn header(
138        mut self,
139        text: impl Into<String>,
140        position: StampPosition,
141        font_size: f64,
142        color: Color,
143    ) -> Self {
144        self.layer = StampLayer::OverContent;
145        self.position = position;
146        self.content = Some(StampContent::HeaderFooter {
147            text: text.into(),
148            position,
149            font: PDType1Font::Helvetica,
150            font_size,
151            color,
152            opacity: 1.0,
153        });
154        self
155    }
156
157    /// Configures a footer stamp with placeholder support: `{page}`, `{total}`.
158    pub fn footer(
159        mut self,
160        text: impl Into<String>,
161        position: StampPosition,
162        font_size: f64,
163        color: Color,
164    ) -> Self {
165        self.layer = StampLayer::OverContent;
166        self.position = position;
167        self.content = Some(StampContent::HeaderFooter {
168            text: text.into(),
169            position,
170            font: PDType1Font::Helvetica,
171            font_size,
172            color,
173            opacity: 1.0,
174        });
175        self
176    }
177
178    /// Configures an image stamp.
179    pub fn image(mut self, img: PDImageXObject, width: f64, height: f64, opacity: f64) -> Self {
180        self.layer = StampLayer::OverContent;
181        self.content = Some(StampContent::Image {
182            image: img,
183            width,
184            height,
185            rotation: 0.0,
186            opacity,
187        });
188        self
189    }
190
191    /// Applies the configured stamp across all matching pages in the document.
192    pub fn apply(&self, doc: &mut PDDocument) -> PdfResult<()> {
193        let Some(content) = &self.content else {
194            return Ok(());
195        };
196
197        let total_pages = doc.page_count();
198        let append_mode = match self.layer {
199            StampLayer::UnderContent => AppendMode::Prepend,
200            StampLayer::OverContent => AppendMode::Append,
201        };
202
203        for page_idx in 0..total_pages {
204            if !self.pages.matches(page_idx, total_pages) {
205                continue;
206            }
207
208            let (page_w, page_h) = {
209                let page = doc.get_page(page_idx)?;
210                let mb = page.media_box();
211                (mb.width(), mb.height())
212            };
213
214            let mut stream = PDPageContentStream::new(append_mode);
215            stream.save_graphics_state();
216
217            match content {
218                StampContent::Watermark {
219                    text,
220                    font,
221                    font_size,
222                    color,
223                    rotation,
224                    opacity,
225                } => {
226                    let page_ref = doc.get_page_mut(page_idx)?;
227                    stream.set_opacity_on_page(page_ref, *opacity, *opacity);
228
229                    let (cx, cy) = (page_w / 2.0, page_h / 2.0);
230                    let text_w = font.get_string_width(text, *font_size);
231
232                    stream.save_graphics_state();
233                    stream.rotate(*rotation, cx, cy);
234
235                    stream.begin_text();
236                    stream.set_font_type1(doc, page_idx, *font, *font_size);
237                    stream.set_fill_color(*color);
238                    stream.show_text_at(text, cx - (text_w / 2.0), cy - (*font_size / 3.0));
239                    stream.end_text();
240
241                    stream.restore_graphics_state();
242                }
243
244                StampContent::RubberStamp {
245                    title,
246                    subtitle,
247                    border_color,
248                    fill_color,
249                    text_color,
250                    rotation,
251                    opacity,
252                } => {
253                    let page_ref = doc.get_page_mut(page_idx)?;
254                    stream.set_opacity_on_page(page_ref, *opacity, *opacity);
255
256                    let font = PDType1Font::HelveticaBold;
257                    let title_font_size = 20.0;
258                    let title_w = font.get_string_width(title, title_font_size);
259
260                    let sub_font_size = 9.0;
261                    let sub_w = subtitle
262                        .as_ref()
263                        .map(|s| font.get_string_width(s, sub_font_size))
264                        .unwrap_or(0.0);
265
266                    let stamp_w = title_w.max(sub_w) + 36.0;
267                    let stamp_h = if subtitle.is_some() { 56.0 } else { 38.0 };
268
269                    let (x, y) = self.calculate_xy(self.position, page_w, page_h, stamp_w, stamp_h);
270                    let (cx, cy) = (x + stamp_w / 2.0, y + stamp_h / 2.0);
271
272                    stream.save_graphics_state();
273                    if *rotation != 0.0 {
274                        stream.rotate(*rotation, cx, cy);
275                    }
276
277                    // Background fill
278                    if let Some(fc) = fill_color {
279                        stream.set_fill_color(*fc);
280                        stream.round_rect(x, y, stamp_w, stamp_h, 6.0);
281                        stream.fill();
282                    }
283
284                    // Border
285                    stream.set_stroke_color(*border_color);
286                    stream.set_line_width(2.5);
287                    stream.round_rect(x, y, stamp_w, stamp_h, 6.0);
288                    stream.stroke();
289
290                    // Title
291                    stream.begin_text();
292                    stream.set_font_type1(doc, page_idx, font, title_font_size);
293                    stream.set_fill_color(*text_color);
294                    let title_y = if subtitle.is_some() {
295                        y + 26.0
296                    } else {
297                        y + 12.0
298                    };
299                    stream.show_text_at(title, x + (stamp_w - title_w) / 2.0, title_y);
300                    stream.end_text();
301
302                    // Subtitle
303                    if let Some(sub) = subtitle {
304                        stream.begin_text();
305                        stream.set_font_type1(doc, page_idx, PDType1Font::Helvetica, sub_font_size);
306                        stream.set_fill_color(*text_color);
307                        stream.show_text_at(sub, x + (stamp_w - sub_w) / 2.0, y + 10.0);
308                        stream.end_text();
309                    }
310
311                    stream.restore_graphics_state();
312                }
313
314                StampContent::HeaderFooter {
315                    text,
316                    position,
317                    font,
318                    font_size,
319                    color,
320                    opacity,
321                } => {
322                    let page_ref = doc.get_page_mut(page_idx)?;
323                    stream.set_opacity_on_page(page_ref, *opacity, *opacity);
324
325                    let formatted_text = text
326                        .replace("{page}", &(page_idx + 1).to_string())
327                        .replace("{total}", &total_pages.to_string());
328
329                    let text_w = font.get_string_width(&formatted_text, *font_size);
330                    let (x, y) = self.calculate_xy(*position, page_w, page_h, text_w, *font_size);
331
332                    stream.begin_text();
333                    stream.set_font_type1(doc, page_idx, *font, *font_size);
334                    stream.set_fill_color(*color);
335                    stream.show_text_at(&formatted_text, x, y);
336                    stream.end_text();
337                }
338
339                StampContent::Image {
340                    image,
341                    width,
342                    height,
343                    rotation,
344                    opacity,
345                } => {
346                    let res_name = {
347                        let page_ref = doc.get_page_mut(page_idx)?;
348                        stream.set_opacity_on_page(page_ref, *opacity, *opacity);
349                        image.register_with_page(page_ref)
350                    };
351
352                    let (x, y) = self.calculate_xy(self.position, page_w, page_h, *width, *height);
353                    let (cx, cy) = (x + width / 2.0, y + height / 2.0);
354
355                    stream.save_graphics_state();
356                    if *rotation != 0.0 {
357                        stream.rotate(*rotation, cx, cy);
358                    }
359                    stream.draw_image(&res_name, x, y, *width, *height);
360                    stream.restore_graphics_state();
361                }
362            }
363
364            stream.restore_graphics_state();
365            stream.finish(doc, page_idx)?;
366        }
367
368        Ok(())
369    }
370
371    fn calculate_xy(
372        &self,
373        pos: StampPosition,
374        page_w: f64,
375        page_h: f64,
376        element_w: f64,
377        element_h: f64,
378    ) -> (f64, f64) {
379        match pos {
380            StampPosition::Center => ((page_w - element_w) / 2.0, (page_h - element_h) / 2.0),
381            StampPosition::TopLeft { margin_x, margin_y } => {
382                (margin_x, page_h - margin_y - element_h)
383            }
384            StampPosition::TopCenter { margin_y } => {
385                ((page_w - element_w) / 2.0, page_h - margin_y - element_h)
386            }
387            StampPosition::TopRight { margin_x, margin_y } => {
388                (page_w - margin_x - element_w, page_h - margin_y - element_h)
389            }
390            StampPosition::BottomLeft { margin_x, margin_y } => (margin_x, margin_y),
391            StampPosition::BottomCenter { margin_y } => ((page_w - element_w) / 2.0, margin_y),
392            StampPosition::BottomRight { margin_x, margin_y } => {
393                (page_w - margin_x - element_w, margin_y)
394            }
395            StampPosition::Custom { x, y } => (x, y),
396        }
397    }
398}