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<R: Read + Seek>(
122 parser_dict: &crate::parser::objects::PdfDictionary,
123 doc: &PdfDocument<R>,
124) -> crate::objects::Dictionary {
125 let mut result = crate::objects::Dictionary::new();
126 for (key, value) in &parser_dict.0 {
127 let converted = convert_parser_obj_to_objects_obj(value, doc);
128 result.set(key.as_str(), converted);
129 }
130 result
131}
132
133fn convert_parser_obj_to_objects_obj<R: Read + Seek>(
140 obj: &crate::parser::objects::PdfObject,
141 doc: &PdfDocument<R>,
142) -> crate::objects::Object {
143 use crate::objects::Object as WObj;
144 use crate::parser::objects::PdfObject as PObj;
145
146 match obj {
147 PObj::Null => WObj::Null,
148 PObj::Boolean(b) => WObj::Boolean(*b),
149 PObj::Integer(i) => WObj::Integer(*i),
150 PObj::Real(r) => WObj::Real(*r),
151 PObj::String(s) => WObj::String(String::from_utf8_lossy(s.as_bytes()).to_string()),
152 PObj::Name(n) => WObj::Name(n.as_str().to_string()),
153 PObj::Array(arr) => {
154 let items: Vec<WObj> = arr
155 .0
156 .iter()
157 .map(|item| convert_parser_obj_to_objects_obj(item, doc))
158 .collect();
159 WObj::Array(items)
160 }
161 PObj::Dictionary(dict) => WObj::Dictionary(convert_parser_dict_to_objects_dict(dict, doc)),
162 PObj::Stream(stream) => {
163 let dict = convert_parser_dict_to_objects_dict(&stream.dict, doc);
164 WObj::Stream(dict, stream.data.clone())
165 }
166 PObj::Reference(num, gen) => {
167 match doc.get_object(*num, *gen as u16) {
172 Ok(resolved) => convert_parser_obj_to_objects_obj(&resolved, doc),
173 Err(_) => {
174 tracing::warn!(
175 "Could not resolve reference {} {} R from overlay; replacing with Null",
176 num,
177 gen
178 );
179 WObj::Null
180 }
181 }
182 }
183 }
184}
185
186pub struct PdfOverlay<R: Read + Seek> {
188 base_doc: PdfDocument<R>,
189 overlay_doc: PdfDocument<R>,
190}
191
192impl<R: Read + Seek> PdfOverlay<R> {
193 pub fn new(base_doc: PdfDocument<R>, overlay_doc: PdfDocument<R>) -> Self {
195 Self {
196 base_doc,
197 overlay_doc,
198 }
199 }
200
201 pub fn apply(&self, options: &OverlayOptions) -> OperationResult<Document> {
203 options.validate()?;
204
205 let base_count =
206 self.base_doc
207 .page_count()
208 .map_err(|e| OperationError::ParseError(e.to_string()))? as usize;
209
210 if base_count == 0 {
211 return Err(OperationError::NoPagesToProcess);
212 }
213
214 let overlay_count =
215 self.overlay_doc
216 .page_count()
217 .map_err(|e| OperationError::ParseError(e.to_string()))? as usize;
218
219 if overlay_count == 0 {
220 return Err(OperationError::ProcessingError(
221 "Overlay PDF has no pages".to_string(),
222 ));
223 }
224
225 let target_indices = options.pages.get_indices(base_count)?;
226 let clamped_opacity = options.clamped_opacity();
227
228 let mut output_doc = Document::new();
229
230 for page_idx in 0..base_count {
231 let parsed_base = self
232 .base_doc
233 .get_page(page_idx as u32)
234 .map_err(|e| OperationError::ParseError(e.to_string()))?;
235
236 let mut page = Page::from_parsed_with_content(&parsed_base, &self.base_doc)
237 .map_err(OperationError::PdfError)?;
238
239 if target_indices.contains(&page_idx) {
240 let target_pos = target_indices
242 .iter()
243 .position(|&i| i == page_idx)
244 .unwrap_or(0);
245
246 let overlay_page_idx = if options.repeat || overlay_count == 1 {
247 target_pos % overlay_count
248 } else if target_pos < overlay_count {
249 target_pos
250 } else {
251 output_doc.add_page(page);
253 continue;
254 };
255
256 self.apply_overlay_to_page(
257 &mut page,
258 overlay_page_idx,
259 &parsed_base,
260 clamped_opacity,
261 options.scale,
262 &options.position,
263 )?;
264 }
265
266 output_doc.add_page(page);
267 }
268
269 Ok(output_doc)
270 }
271
272 fn apply_overlay_to_page(
274 &self,
275 page: &mut Page,
276 overlay_page_idx: usize,
277 parsed_base: &crate::parser::page_tree::ParsedPage,
278 opacity: f64,
279 scale: f64,
280 position: &OverlayPosition,
281 ) -> OperationResult<()> {
282 let parsed_overlay = self
283 .overlay_doc
284 .get_page(overlay_page_idx as u32)
285 .map_err(|e| OperationError::ParseError(e.to_string()))?;
286
287 let overlay_streams = self
289 .overlay_doc
290 .get_page_content_streams(&parsed_overlay)
291 .map_err(|e| OperationError::ParseError(e.to_string()))?;
292
293 let mut overlay_content = Vec::new();
294 for stream in &overlay_streams {
295 overlay_content.extend_from_slice(stream);
296 overlay_content.push(b'\n');
297 }
298
299 let ov_w = parsed_overlay.width();
301 let ov_h = parsed_overlay.height();
302 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(ov_w, ov_h));
303
304 let mut form = FormXObject::new(bbox).with_content(overlay_content);
305
306 if let Some(resources) = parsed_overlay.get_resources() {
308 let writer_dict = convert_parser_dict_to_objects_dict(resources, &self.overlay_doc);
309 form = form.with_resources(writer_dict);
310 }
311
312 let xobj_name = format!("Overlay{}", overlay_page_idx);
313 page.add_form_xobject(&xobj_name, form);
314
315 let base_w = parsed_base.width();
317 let base_h = parsed_base.height();
318 let ctm = compute_ctm(base_w, base_h, ov_w, ov_h, scale, position);
319
320 let mut ops = String::new();
322 ops.push_str("q\n");
323
324 if (opacity - 1.0).abs() > f64::EPSILON {
326 let mut state = ExtGState::new();
327 state.alpha_fill = Some(opacity);
328 state.alpha_stroke = Some(opacity);
329
330 let registered_name = page
331 .graphics()
332 .extgstate_manager_mut()
333 .add_state(state)
334 .map_err(|e| OperationError::ProcessingError(format!("ExtGState error: {e}")))?;
335
336 ops.push_str(&format!("/{} gs\n", registered_name));
337 }
338
339 ops.push_str(&format!(
341 "{} {} {} {} {} {} cm\n",
342 ctm[0], ctm[1], ctm[2], ctm[3], ctm[4], ctm[5]
343 ));
344
345 ops.push_str(&format!("/{} Do\n", xobj_name));
347 ops.push_str("Q\n");
348
349 page.append_raw_content(ops.as_bytes());
351
352 Ok(())
353 }
354}
355
356pub fn overlay_pdf<P, Q, R>(
386 base_path: P,
387 overlay_path: Q,
388 output_path: R,
389 options: OverlayOptions,
390) -> OperationResult<()>
391where
392 P: AsRef<Path>,
393 Q: AsRef<Path>,
394 R: AsRef<Path>,
395{
396 let base_reader = PdfReader::open(base_path.as_ref())
397 .map_err(|e| OperationError::ParseError(format!("Failed to open base PDF: {e}")))?;
398 let base_doc = PdfDocument::new(base_reader);
399
400 let overlay_reader = PdfReader::open(overlay_path.as_ref())
401 .map_err(|e| OperationError::ParseError(format!("Failed to open overlay PDF: {e}")))?;
402 let overlay_doc = PdfDocument::new(overlay_reader);
403
404 let overlay_applicator = PdfOverlay::new(base_doc, overlay_doc);
405 let mut doc = overlay_applicator.apply(&options)?;
406 doc.save(output_path)?;
407 Ok(())
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn test_overlay_options_default() {
416 let opts = OverlayOptions::default();
417 assert_eq!(opts.opacity, 1.0);
418 assert_eq!(opts.scale, 1.0);
419 assert!(!opts.repeat);
420 assert!(matches!(opts.position, OverlayPosition::Center));
421 assert!(matches!(opts.pages, PageRange::All));
422 }
423
424 #[test]
425 fn test_overlay_options_validate_ok() {
426 let opts = OverlayOptions::default();
427 assert!(opts.validate().is_ok());
428 }
429
430 #[test]
431 fn test_overlay_options_validate_zero_scale() {
432 let opts = OverlayOptions {
433 scale: 0.0,
434 ..Default::default()
435 };
436 assert!(opts.validate().is_err());
437 }
438
439 #[test]
440 fn test_overlay_options_validate_negative_scale() {
441 let opts = OverlayOptions {
442 scale: -1.0,
443 ..Default::default()
444 };
445 assert!(opts.validate().is_err());
446 }
447
448 #[test]
449 fn test_overlay_options_validate_high_opacity_ok() {
450 let opts = OverlayOptions {
451 opacity: 2.5,
452 ..Default::default()
453 };
454 assert!(opts.validate().is_ok());
456 assert_eq!(opts.clamped_opacity(), 1.0);
457 }
458
459 #[test]
460 fn test_overlay_options_clamped_opacity() {
461 assert_eq!(
462 OverlayOptions {
463 opacity: -0.5,
464 ..Default::default()
465 }
466 .clamped_opacity(),
467 0.0
468 );
469 assert_eq!(
470 OverlayOptions {
471 opacity: 0.5,
472 ..Default::default()
473 }
474 .clamped_opacity(),
475 0.5
476 );
477 assert_eq!(
478 OverlayOptions {
479 opacity: 3.0,
480 ..Default::default()
481 }
482 .clamped_opacity(),
483 1.0
484 );
485 }
486
487 #[test]
488 fn test_compute_ctm_center_same_size() {
489 let ctm = compute_ctm(595.0, 842.0, 595.0, 842.0, 1.0, &OverlayPosition::Center);
490 assert_eq!(ctm[0], 1.0);
491 assert_eq!(ctm[3], 1.0);
492 assert!((ctm[4] - 0.0).abs() < 0.001);
493 assert!((ctm[5] - 0.0).abs() < 0.001);
494 }
495
496 #[test]
497 fn test_compute_ctm_center_different_sizes() {
498 let ctm = compute_ctm(595.0, 842.0, 200.0, 200.0, 1.0, &OverlayPosition::Center);
499 assert!((ctm[4] - 197.5).abs() < 0.001);
500 assert!((ctm[5] - 321.0).abs() < 0.001);
501 }
502
503 #[test]
504 fn test_compute_ctm_with_scale() {
505 let ctm = compute_ctm(595.0, 842.0, 595.0, 842.0, 0.5, &OverlayPosition::Center);
506 assert!((ctm[0] - 0.5).abs() < 0.001);
507 assert!((ctm[3] - 0.5).abs() < 0.001);
508 assert!((ctm[4] - 148.75).abs() < 0.001);
510 assert!((ctm[5] - 210.5).abs() < 0.001);
511 }
512
513 #[test]
514 fn test_compute_ctm_bottom_left() {
515 let ctm = compute_ctm(
516 595.0,
517 842.0,
518 200.0,
519 200.0,
520 1.0,
521 &OverlayPosition::BottomLeft,
522 );
523 assert!((ctm[4]).abs() < 0.001);
524 assert!((ctm[5]).abs() < 0.001);
525 }
526
527 #[test]
528 fn test_compute_ctm_bottom_right() {
529 let ctm = compute_ctm(
530 595.0,
531 842.0,
532 200.0,
533 200.0,
534 1.0,
535 &OverlayPosition::BottomRight,
536 );
537 assert!((ctm[4] - 395.0).abs() < 0.001);
538 assert!((ctm[5]).abs() < 0.001);
539 }
540
541 #[test]
542 fn test_compute_ctm_top_left() {
543 let ctm = compute_ctm(595.0, 842.0, 200.0, 200.0, 1.0, &OverlayPosition::TopLeft);
544 assert!((ctm[4]).abs() < 0.001);
545 assert!((ctm[5] - 642.0).abs() < 0.001);
546 }
547
548 #[test]
549 fn test_compute_ctm_top_right() {
550 let ctm = compute_ctm(595.0, 842.0, 200.0, 200.0, 1.0, &OverlayPosition::TopRight);
551 assert!((ctm[4] - 395.0).abs() < 0.001);
552 assert!((ctm[5] - 642.0).abs() < 0.001);
553 }
554
555 #[test]
556 fn test_compute_ctm_custom_position() {
557 let ctm = compute_ctm(
558 595.0,
559 842.0,
560 200.0,
561 200.0,
562 1.0,
563 &OverlayPosition::Custom(100.0, 150.0),
564 );
565 assert!((ctm[4] - 100.0).abs() < 0.001);
566 assert!((ctm[5] - 150.0).abs() < 0.001);
567 }
568
569 #[test]
570 fn test_overlay_position_default() {
571 assert_eq!(OverlayPosition::default(), OverlayPosition::Center);
572 }
573
574 #[test]
575 fn test_overlay_position_equality() {
576 assert_eq!(OverlayPosition::Center, OverlayPosition::Center);
577 assert_eq!(
578 OverlayPosition::Custom(1.0, 2.0),
579 OverlayPosition::Custom(1.0, 2.0)
580 );
581 assert_ne!(OverlayPosition::Center, OverlayPosition::TopLeft);
582 }
583
584 #[test]
586 fn test_unresolvable_reference_degrades_to_null() {
587 use crate::objects::Object as WObj;
588 use crate::parser::objects::{PdfDictionary, PdfName, PdfObject as PObj};
589
590 let mut dict = PdfDictionary::new();
592 dict.0
593 .insert(PdfName::new("SMask".to_string()), PObj::Reference(99999, 0));
594 dict.0
595 .insert(PdfName::new("Width".to_string()), PObj::Integer(100));
596
597 let mut doc_builder = crate::Document::new();
599 let page = crate::Page::a4();
600 doc_builder.add_page(page);
601 let pdf_bytes = doc_builder.to_bytes().unwrap();
602
603 let reader = crate::parser::PdfReader::new(std::io::Cursor::new(pdf_bytes)).unwrap();
604 let pdf_doc = crate::parser::PdfDocument::new(reader);
605
606 let result = convert_parser_dict_to_objects_dict(&dict, &pdf_doc);
607
608 let smask_key = "SMask";
610 let smask_val = result.get(smask_key);
611 assert!(
612 matches!(smask_val, Some(WObj::Null)),
613 "Unresolvable reference should become Null, got: {:?}",
614 smask_val
615 );
616
617 let width_val = result.get("Width");
619 assert!(
620 matches!(width_val, Some(WObj::Integer(100))),
621 "Normal integer should convert, got: {:?}",
622 width_val
623 );
624 }
625}