1use super::{OperationError, OperationResult, PageRange};
13use crate::geometry::{Point, Rectangle};
14use crate::graphics::{ExtGState, FormXObject};
15use crate::parser::{PdfDocument, PdfReader};
16use crate::{Document, Page};
17use std::io::{Read, Seek};
18use std::path::Path;
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum OverlayPosition {
23 Center,
25 TopLeft,
27 TopRight,
29 BottomLeft,
31 BottomRight,
33 Custom(f64, f64),
35}
36
37impl Default for OverlayPosition {
38 fn default() -> Self {
39 Self::Center
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct OverlayOptions {
46 pub pages: PageRange,
48 pub position: OverlayPosition,
50 pub opacity: f64,
52 pub scale: f64,
54 pub repeat: bool,
56}
57
58impl Default for OverlayOptions {
59 fn default() -> Self {
60 Self {
61 pages: PageRange::All,
62 position: OverlayPosition::Center,
63 opacity: 1.0,
64 scale: 1.0,
65 repeat: false,
66 }
67 }
68}
69
70impl OverlayOptions {
71 pub fn validate(&self) -> OperationResult<()> {
73 if self.scale <= 0.0 {
74 return Err(OperationError::ProcessingError(
75 "Overlay scale must be greater than 0".to_string(),
76 ));
77 }
78 Ok(())
79 }
80
81 fn clamped_opacity(&self) -> f64 {
83 self.opacity.clamp(0.0, 1.0)
84 }
85}
86
87pub(crate) fn compute_ctm(
93 base_w: f64,
94 base_h: f64,
95 overlay_w: f64,
96 overlay_h: f64,
97 scale: f64,
98 position: &OverlayPosition,
99) -> [f64; 6] {
100 let scaled_w = overlay_w * scale;
101 let scaled_h = overlay_h * scale;
102
103 let (tx, ty) = match position {
104 OverlayPosition::Center => ((base_w - scaled_w) / 2.0, (base_h - scaled_h) / 2.0),
105 OverlayPosition::TopLeft => (0.0, base_h - scaled_h),
106 OverlayPosition::TopRight => (base_w - scaled_w, base_h - scaled_h),
107 OverlayPosition::BottomLeft => (0.0, 0.0),
108 OverlayPosition::BottomRight => (base_w - scaled_w, 0.0),
109 OverlayPosition::Custom(x, y) => (*x, *y),
110 };
111
112 [scale, 0.0, 0.0, scale, tx, ty]
113}
114
115fn convert_parser_dict_to_objects_dict(
119 parser_dict: &crate::parser::objects::PdfDictionary,
120) -> crate::objects::Dictionary {
121 let mut result = crate::objects::Dictionary::new();
122 for (key, value) in &parser_dict.0 {
123 let converted = convert_parser_obj_to_objects_obj(value);
124 result.set(key.as_str(), converted);
125 }
126 result
127}
128
129fn convert_parser_obj_to_objects_obj(
131 obj: &crate::parser::objects::PdfObject,
132) -> crate::objects::Object {
133 use crate::objects::Object as WObj;
134 use crate::parser::objects::PdfObject as PObj;
135
136 match obj {
137 PObj::Null => WObj::Null,
138 PObj::Boolean(b) => WObj::Boolean(*b),
139 PObj::Integer(i) => WObj::Integer(*i),
140 PObj::Real(r) => WObj::Real(*r),
141 PObj::String(s) => WObj::String(String::from_utf8_lossy(s.as_bytes()).to_string()),
142 PObj::Name(n) => WObj::Name(n.as_str().to_string()),
143 PObj::Array(arr) => {
144 let items: Vec<WObj> = arr
145 .0
146 .iter()
147 .map(convert_parser_obj_to_objects_obj)
148 .collect();
149 WObj::Array(items)
150 }
151 PObj::Dictionary(dict) => WObj::Dictionary(convert_parser_dict_to_objects_dict(dict)),
152 PObj::Stream(stream) => {
153 let dict = convert_parser_dict_to_objects_dict(&stream.dict);
154 WObj::Stream(dict, stream.data.clone())
155 }
156 PObj::Reference(num, gen) => WObj::Reference(crate::objects::ObjectId::new(*num, *gen)),
157 }
158}
159
160pub struct PdfOverlay<R: Read + Seek> {
162 base_doc: PdfDocument<R>,
163 overlay_doc: PdfDocument<R>,
164}
165
166impl<R: Read + Seek> PdfOverlay<R> {
167 pub fn new(base_doc: PdfDocument<R>, overlay_doc: PdfDocument<R>) -> Self {
169 Self {
170 base_doc,
171 overlay_doc,
172 }
173 }
174
175 pub fn apply(&self, options: &OverlayOptions) -> OperationResult<Document> {
177 options.validate()?;
178
179 let base_count =
180 self.base_doc
181 .page_count()
182 .map_err(|e| OperationError::ParseError(e.to_string()))? as usize;
183
184 if base_count == 0 {
185 return Err(OperationError::NoPagesToProcess);
186 }
187
188 let overlay_count =
189 self.overlay_doc
190 .page_count()
191 .map_err(|e| OperationError::ParseError(e.to_string()))? as usize;
192
193 if overlay_count == 0 {
194 return Err(OperationError::ProcessingError(
195 "Overlay PDF has no pages".to_string(),
196 ));
197 }
198
199 let target_indices = options.pages.get_indices(base_count)?;
200 let clamped_opacity = options.clamped_opacity();
201
202 let mut output_doc = Document::new();
203
204 for page_idx in 0..base_count {
205 let parsed_base = self
206 .base_doc
207 .get_page(page_idx as u32)
208 .map_err(|e| OperationError::ParseError(e.to_string()))?;
209
210 let mut page = Page::from_parsed_with_content(&parsed_base, &self.base_doc)
211 .map_err(OperationError::PdfError)?;
212
213 if target_indices.contains(&page_idx) {
214 let target_pos = target_indices
216 .iter()
217 .position(|&i| i == page_idx)
218 .unwrap_or(0);
219
220 let overlay_page_idx = if options.repeat || overlay_count == 1 {
221 target_pos % overlay_count
222 } else if target_pos < overlay_count {
223 target_pos
224 } else {
225 output_doc.add_page(page);
227 continue;
228 };
229
230 self.apply_overlay_to_page(
231 &mut page,
232 overlay_page_idx,
233 &parsed_base,
234 clamped_opacity,
235 options.scale,
236 &options.position,
237 )?;
238 }
239
240 output_doc.add_page(page);
241 }
242
243 Ok(output_doc)
244 }
245
246 fn apply_overlay_to_page(
248 &self,
249 page: &mut Page,
250 overlay_page_idx: usize,
251 parsed_base: &crate::parser::page_tree::ParsedPage,
252 opacity: f64,
253 scale: f64,
254 position: &OverlayPosition,
255 ) -> OperationResult<()> {
256 let parsed_overlay = self
257 .overlay_doc
258 .get_page(overlay_page_idx as u32)
259 .map_err(|e| OperationError::ParseError(e.to_string()))?;
260
261 let overlay_streams = self
263 .overlay_doc
264 .get_page_content_streams(&parsed_overlay)
265 .map_err(|e| OperationError::ParseError(e.to_string()))?;
266
267 let mut overlay_content = Vec::new();
268 for stream in &overlay_streams {
269 overlay_content.extend_from_slice(stream);
270 overlay_content.push(b'\n');
271 }
272
273 let ov_w = parsed_overlay.width();
275 let ov_h = parsed_overlay.height();
276 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(ov_w, ov_h));
277
278 let mut form = FormXObject::new(bbox).with_content(overlay_content);
279
280 if let Some(resources) = parsed_overlay.get_resources() {
282 let writer_dict = convert_parser_dict_to_objects_dict(resources);
283 form = form.with_resources(writer_dict);
284 }
285
286 let xobj_name = format!("Overlay{}", overlay_page_idx);
287 page.add_form_xobject(&xobj_name, form);
288
289 let base_w = parsed_base.width();
291 let base_h = parsed_base.height();
292 let ctm = compute_ctm(base_w, base_h, ov_w, ov_h, scale, position);
293
294 let mut ops = String::new();
296 ops.push_str("q\n");
297
298 if (opacity - 1.0).abs() > f64::EPSILON {
300 let mut state = ExtGState::new();
301 state.alpha_fill = Some(opacity);
302 state.alpha_stroke = Some(opacity);
303
304 let registered_name = page
305 .graphics()
306 .extgstate_manager_mut()
307 .add_state(state)
308 .map_err(|e| OperationError::ProcessingError(format!("ExtGState error: {e}")))?;
309
310 ops.push_str(&format!("/{} gs\n", registered_name));
311 }
312
313 ops.push_str(&format!(
315 "{} {} {} {} {} {} cm\n",
316 ctm[0], ctm[1], ctm[2], ctm[3], ctm[4], ctm[5]
317 ));
318
319 ops.push_str(&format!("/{} Do\n", xobj_name));
321 ops.push_str("Q\n");
322
323 page.append_raw_content(ops.as_bytes());
325
326 Ok(())
327 }
328}
329
330pub fn overlay_pdf<P, Q, R>(
360 base_path: P,
361 overlay_path: Q,
362 output_path: R,
363 options: OverlayOptions,
364) -> OperationResult<()>
365where
366 P: AsRef<Path>,
367 Q: AsRef<Path>,
368 R: AsRef<Path>,
369{
370 let base_reader = PdfReader::open(base_path.as_ref())
371 .map_err(|e| OperationError::ParseError(format!("Failed to open base PDF: {e}")))?;
372 let base_doc = PdfDocument::new(base_reader);
373
374 let overlay_reader = PdfReader::open(overlay_path.as_ref())
375 .map_err(|e| OperationError::ParseError(format!("Failed to open overlay PDF: {e}")))?;
376 let overlay_doc = PdfDocument::new(overlay_reader);
377
378 let overlay_applicator = PdfOverlay::new(base_doc, overlay_doc);
379 let mut doc = overlay_applicator.apply(&options)?;
380 doc.save(output_path)?;
381 Ok(())
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn test_overlay_options_default() {
390 let opts = OverlayOptions::default();
391 assert_eq!(opts.opacity, 1.0);
392 assert_eq!(opts.scale, 1.0);
393 assert!(!opts.repeat);
394 assert!(matches!(opts.position, OverlayPosition::Center));
395 assert!(matches!(opts.pages, PageRange::All));
396 }
397
398 #[test]
399 fn test_overlay_options_validate_ok() {
400 let opts = OverlayOptions::default();
401 assert!(opts.validate().is_ok());
402 }
403
404 #[test]
405 fn test_overlay_options_validate_zero_scale() {
406 let opts = OverlayOptions {
407 scale: 0.0,
408 ..Default::default()
409 };
410 assert!(opts.validate().is_err());
411 }
412
413 #[test]
414 fn test_overlay_options_validate_negative_scale() {
415 let opts = OverlayOptions {
416 scale: -1.0,
417 ..Default::default()
418 };
419 assert!(opts.validate().is_err());
420 }
421
422 #[test]
423 fn test_overlay_options_validate_high_opacity_ok() {
424 let opts = OverlayOptions {
425 opacity: 2.5,
426 ..Default::default()
427 };
428 assert!(opts.validate().is_ok());
430 assert_eq!(opts.clamped_opacity(), 1.0);
431 }
432
433 #[test]
434 fn test_overlay_options_clamped_opacity() {
435 assert_eq!(
436 OverlayOptions {
437 opacity: -0.5,
438 ..Default::default()
439 }
440 .clamped_opacity(),
441 0.0
442 );
443 assert_eq!(
444 OverlayOptions {
445 opacity: 0.5,
446 ..Default::default()
447 }
448 .clamped_opacity(),
449 0.5
450 );
451 assert_eq!(
452 OverlayOptions {
453 opacity: 3.0,
454 ..Default::default()
455 }
456 .clamped_opacity(),
457 1.0
458 );
459 }
460
461 #[test]
462 fn test_compute_ctm_center_same_size() {
463 let ctm = compute_ctm(595.0, 842.0, 595.0, 842.0, 1.0, &OverlayPosition::Center);
464 assert_eq!(ctm[0], 1.0);
465 assert_eq!(ctm[3], 1.0);
466 assert!((ctm[4] - 0.0).abs() < 0.001);
467 assert!((ctm[5] - 0.0).abs() < 0.001);
468 }
469
470 #[test]
471 fn test_compute_ctm_center_different_sizes() {
472 let ctm = compute_ctm(595.0, 842.0, 200.0, 200.0, 1.0, &OverlayPosition::Center);
473 assert!((ctm[4] - 197.5).abs() < 0.001);
474 assert!((ctm[5] - 321.0).abs() < 0.001);
475 }
476
477 #[test]
478 fn test_compute_ctm_with_scale() {
479 let ctm = compute_ctm(595.0, 842.0, 595.0, 842.0, 0.5, &OverlayPosition::Center);
480 assert!((ctm[0] - 0.5).abs() < 0.001);
481 assert!((ctm[3] - 0.5).abs() < 0.001);
482 assert!((ctm[4] - 148.75).abs() < 0.001);
484 assert!((ctm[5] - 210.5).abs() < 0.001);
485 }
486
487 #[test]
488 fn test_compute_ctm_bottom_left() {
489 let ctm = compute_ctm(
490 595.0,
491 842.0,
492 200.0,
493 200.0,
494 1.0,
495 &OverlayPosition::BottomLeft,
496 );
497 assert!((ctm[4]).abs() < 0.001);
498 assert!((ctm[5]).abs() < 0.001);
499 }
500
501 #[test]
502 fn test_compute_ctm_bottom_right() {
503 let ctm = compute_ctm(
504 595.0,
505 842.0,
506 200.0,
507 200.0,
508 1.0,
509 &OverlayPosition::BottomRight,
510 );
511 assert!((ctm[4] - 395.0).abs() < 0.001);
512 assert!((ctm[5]).abs() < 0.001);
513 }
514
515 #[test]
516 fn test_compute_ctm_top_left() {
517 let ctm = compute_ctm(595.0, 842.0, 200.0, 200.0, 1.0, &OverlayPosition::TopLeft);
518 assert!((ctm[4]).abs() < 0.001);
519 assert!((ctm[5] - 642.0).abs() < 0.001);
520 }
521
522 #[test]
523 fn test_compute_ctm_top_right() {
524 let ctm = compute_ctm(595.0, 842.0, 200.0, 200.0, 1.0, &OverlayPosition::TopRight);
525 assert!((ctm[4] - 395.0).abs() < 0.001);
526 assert!((ctm[5] - 642.0).abs() < 0.001);
527 }
528
529 #[test]
530 fn test_compute_ctm_custom_position() {
531 let ctm = compute_ctm(
532 595.0,
533 842.0,
534 200.0,
535 200.0,
536 1.0,
537 &OverlayPosition::Custom(100.0, 150.0),
538 );
539 assert!((ctm[4] - 100.0).abs() < 0.001);
540 assert!((ctm[5] - 150.0).abs() < 0.001);
541 }
542
543 #[test]
544 fn test_overlay_position_default() {
545 assert_eq!(OverlayPosition::default(), OverlayPosition::Center);
546 }
547
548 #[test]
549 fn test_overlay_position_equality() {
550 assert_eq!(OverlayPosition::Center, OverlayPosition::Center);
551 assert_eq!(
552 OverlayPosition::Custom(1.0, 2.0),
553 OverlayPosition::Custom(1.0, 2.0)
554 );
555 assert_ne!(OverlayPosition::Center, OverlayPosition::TopLeft);
556 }
557}