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#[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 pub fn pages(mut self, range: PageRange) -> Self {
72 self.pages = range;
73 self
74 }
75
76 pub fn layer(mut self, layer: StampLayer) -> Self {
78 self.layer = layer;
79 self
80 }
81
82 pub fn position(mut self, pos: StampPosition) -> Self {
84 self.position = pos;
85 self
86 }
87
88 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 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 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 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 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 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 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 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 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 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 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}