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::collections::{HashMap, HashSet};
18use std::io::{Read, Seek};
19use std::path::Path;
20
21#[derive(Debug, Clone, PartialEq)]
23pub enum OverlayPosition {
24 Center,
26 TopLeft,
28 TopRight,
30 BottomLeft,
32 BottomRight,
34 Custom(f64, f64),
36}
37
38impl Default for OverlayPosition {
39 fn default() -> Self {
40 Self::Center
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct OverlayOptions {
47 pub pages: PageRange,
49 pub position: OverlayPosition,
51 pub opacity: f64,
53 pub scale: f64,
55 pub repeat: bool,
57}
58
59impl Default for OverlayOptions {
60 fn default() -> Self {
61 Self {
62 pages: PageRange::All,
63 position: OverlayPosition::Center,
64 opacity: 1.0,
65 scale: 1.0,
66 repeat: false,
67 }
68 }
69}
70
71impl OverlayOptions {
72 pub fn validate(&self) -> OperationResult<()> {
74 if self.scale <= 0.0 {
75 return Err(OperationError::ProcessingError(
76 "Overlay scale must be greater than 0".to_string(),
77 ));
78 }
79 Ok(())
80 }
81
82 fn clamped_opacity(&self) -> f64 {
84 self.opacity.clamp(0.0, 1.0)
85 }
86}
87
88pub(crate) fn compute_ctm(
94 base_w: f64,
95 base_h: f64,
96 overlay_w: f64,
97 overlay_h: f64,
98 scale: f64,
99 position: &OverlayPosition,
100) -> [f64; 6] {
101 let scaled_w = overlay_w * scale;
102 let scaled_h = overlay_h * scale;
103
104 let (tx, ty) = match position {
105 OverlayPosition::Center => ((base_w - scaled_w) / 2.0, (base_h - scaled_h) / 2.0),
106 OverlayPosition::TopLeft => (0.0, base_h - scaled_h),
107 OverlayPosition::TopRight => (base_w - scaled_w, base_h - scaled_h),
108 OverlayPosition::BottomLeft => (0.0, 0.0),
109 OverlayPosition::BottomRight => (base_w - scaled_w, 0.0),
110 OverlayPosition::Custom(x, y) => (*x, *y),
111 };
112
113 [scale, 0.0, 0.0, scale, tx, ty]
114}
115
116fn convert_parser_dict_to_objects_dict<R: Read + Seek>(
123 parser_dict: &crate::parser::objects::PdfDictionary,
124 doc: &PdfDocument<R>,
125) -> crate::objects::Dictionary {
126 let mut result = crate::objects::Dictionary::new();
127 for (key, value) in &parser_dict.0 {
128 let converted = convert_parser_obj_to_objects_obj(value, doc);
129 result.set(key.as_str(), converted);
130 }
131 result
132}
133
134fn convert_parser_obj_to_objects_obj<R: Read + Seek>(
141 obj: &crate::parser::objects::PdfObject,
142 doc: &PdfDocument<R>,
143) -> crate::objects::Object {
144 use crate::objects::Object as WObj;
145 use crate::parser::objects::PdfObject as PObj;
146
147 match obj {
148 PObj::Null => WObj::Null,
149 PObj::Boolean(b) => WObj::Boolean(*b),
150 PObj::Integer(i) => WObj::Integer(*i),
151 PObj::Real(r) => WObj::Real(*r),
152 PObj::String(s) => WObj::String(s.to_text()),
157 PObj::Name(n) => WObj::Name(n.as_str().to_string()),
158 PObj::Array(arr) => {
159 let items: Vec<WObj> = arr
160 .0
161 .iter()
162 .map(|item| convert_parser_obj_to_objects_obj(item, doc))
163 .collect();
164 WObj::Array(items)
165 }
166 PObj::Dictionary(dict) => WObj::Dictionary(convert_parser_dict_to_objects_dict(dict, doc)),
167 PObj::Stream(stream) => {
168 let dict = convert_parser_dict_to_objects_dict(&stream.dict, doc);
169 WObj::Stream(dict, stream.data.clone())
170 }
171 PObj::Reference(num, gen) => {
172 match doc.get_object(*num, *gen as u16) {
177 Ok(resolved) => convert_parser_obj_to_objects_obj(&resolved, doc),
178 Err(_) => {
179 tracing::warn!(
180 "Could not resolve reference {} {} R from overlay; replacing with Null",
181 num,
182 gen
183 );
184 WObj::Null
185 }
186 }
187 }
188 }
189}
190
191pub struct PdfOverlay<R: Read + Seek> {
193 base_doc: PdfDocument<R>,
194 overlay_doc: PdfDocument<R>,
195}
196
197impl<R: Read + Seek> PdfOverlay<R> {
198 pub fn new(base_doc: PdfDocument<R>, overlay_doc: PdfDocument<R>) -> Self {
200 Self {
201 base_doc,
202 overlay_doc,
203 }
204 }
205
206 pub fn apply(&self, options: &OverlayOptions) -> OperationResult<Document> {
208 options.validate()?;
209
210 let base_count =
211 self.base_doc
212 .page_count()
213 .map_err(|e| OperationError::ParseError(e.to_string()))? as usize;
214
215 if base_count == 0 {
216 return Err(OperationError::NoPagesToProcess);
217 }
218
219 let overlay_count =
220 self.overlay_doc
221 .page_count()
222 .map_err(|e| OperationError::ParseError(e.to_string()))? as usize;
223
224 if overlay_count == 0 {
225 return Err(OperationError::ProcessingError(
226 "Overlay PDF has no pages".to_string(),
227 ));
228 }
229
230 let target_indices = options.pages.get_indices(base_count)?;
231 let clamped_opacity = options.clamped_opacity();
232
233 let mut output_doc = Document::new();
234
235 for page_idx in 0..base_count {
236 let parsed_base = self
237 .base_doc
238 .get_page(page_idx as u32)
239 .map_err(|e| OperationError::ParseError(e.to_string()))?;
240
241 let mut page = Page::from_parsed_with_content(&parsed_base, &self.base_doc)
242 .map_err(OperationError::PdfError)?;
243
244 if target_indices.contains(&page_idx) {
245 let target_pos = target_indices
247 .iter()
248 .position(|&i| i == page_idx)
249 .unwrap_or(0);
250
251 let overlay_page_idx = if options.repeat || overlay_count == 1 {
252 target_pos % overlay_count
253 } else if target_pos < overlay_count {
254 target_pos
255 } else {
256 output_doc.add_page(page);
258 continue;
259 };
260
261 self.apply_overlay_to_page(
262 &mut page,
263 overlay_page_idx,
264 &parsed_base,
265 clamped_opacity,
266 options.scale,
267 &options.position,
268 )?;
269 }
270
271 output_doc.add_page(page);
272 }
273
274 Ok(output_doc)
275 }
276
277 fn apply_overlay_to_page(
279 &self,
280 page: &mut Page,
281 overlay_page_idx: usize,
282 parsed_base: &crate::parser::page_tree::ParsedPage,
283 opacity: f64,
284 scale: f64,
285 position: &OverlayPosition,
286 ) -> OperationResult<()> {
287 let parsed_overlay = self
288 .overlay_doc
289 .get_page(overlay_page_idx as u32)
290 .map_err(|e| OperationError::ParseError(e.to_string()))?;
291
292 let overlay_streams = self
294 .overlay_doc
295 .get_page_content_streams(&parsed_overlay)
296 .map_err(|e| OperationError::ParseError(e.to_string()))?;
297
298 let mut overlay_content = Vec::new();
299 for stream in &overlay_streams {
300 overlay_content.extend_from_slice(stream);
301 overlay_content.push(b'\n');
302 }
303
304 let ov_w = parsed_overlay.width();
306 let ov_h = parsed_overlay.height();
307 let bbox = Rectangle::new(Point::new(0.0, 0.0), Point::new(ov_w, ov_h));
308
309 let mut form = FormXObject::new(bbox).with_content(overlay_content);
310
311 if let Some(resources) = parsed_overlay.get_resources() {
313 let writer_dict = convert_parser_dict_to_objects_dict(resources, &self.overlay_doc);
314 form = form.with_resources(writer_dict);
315 }
316
317 let xobj_name = format!("Overlay{}", overlay_page_idx);
318 page.add_form_xobject(&xobj_name, form)?;
322
323 let base_w = parsed_base.width();
325 let base_h = parsed_base.height();
326 let ctm = compute_ctm(base_w, base_h, ov_w, ov_h, scale, position);
327
328 let mut ops = String::new();
330 ops.push_str("q\n");
331
332 if (opacity - 1.0).abs() > f64::EPSILON {
334 let mut state = ExtGState::new();
335 state.alpha_fill = Some(opacity);
336 state.alpha_stroke = Some(opacity);
337
338 let registered_name = page
339 .graphics()
340 .extgstate_manager_mut()
341 .add_state(state)
342 .map_err(|e| OperationError::ProcessingError(format!("ExtGState error: {e}")))?;
343
344 ops.push_str(&format!("/{} gs\n", registered_name));
345 }
346
347 ops.push_str(&format!(
349 "{} {} {} {} {} {} cm\n",
350 ctm[0], ctm[1], ctm[2], ctm[3], ctm[4], ctm[5]
351 ));
352
353 ops.push_str(&format!("/{} Do\n", xobj_name));
355 ops.push_str("Q\n");
356
357 let font_usage: HashMap<String, HashSet<char>> = HashMap::new();
370 page.append_raw_content(ops.as_bytes(), &font_usage);
371
372 Ok(())
373 }
374}
375
376pub fn overlay_pdf<P, Q, R>(
406 base_path: P,
407 overlay_path: Q,
408 output_path: R,
409 options: OverlayOptions,
410) -> OperationResult<()>
411where
412 P: AsRef<Path>,
413 Q: AsRef<Path>,
414 R: AsRef<Path>,
415{
416 let base_reader = PdfReader::open(base_path.as_ref())
417 .map_err(|e| OperationError::ParseError(format!("Failed to open base PDF: {e}")))?;
418 let base_doc = PdfDocument::new(base_reader);
419
420 let overlay_reader = PdfReader::open(overlay_path.as_ref())
421 .map_err(|e| OperationError::ParseError(format!("Failed to open overlay PDF: {e}")))?;
422 let overlay_doc = PdfDocument::new(overlay_reader);
423
424 let overlay_applicator = PdfOverlay::new(base_doc, overlay_doc);
425 let mut doc = overlay_applicator.apply(&options)?;
426 doc.save(output_path)?;
427 Ok(())
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn test_overlay_options_default() {
436 let opts = OverlayOptions::default();
437 assert_eq!(opts.opacity, 1.0);
438 assert_eq!(opts.scale, 1.0);
439 assert!(!opts.repeat);
440 assert!(matches!(opts.position, OverlayPosition::Center));
441 assert!(matches!(opts.pages, PageRange::All));
442 }
443
444 #[test]
445 fn test_overlay_options_validate_ok() {
446 let opts = OverlayOptions::default();
447 assert!(opts.validate().is_ok());
448 }
449
450 #[test]
451 fn test_overlay_options_validate_zero_scale() {
452 let opts = OverlayOptions {
453 scale: 0.0,
454 ..Default::default()
455 };
456 assert!(opts.validate().is_err());
457 }
458
459 #[test]
460 fn test_overlay_options_validate_negative_scale() {
461 let opts = OverlayOptions {
462 scale: -1.0,
463 ..Default::default()
464 };
465 assert!(opts.validate().is_err());
466 }
467
468 #[test]
469 fn test_overlay_options_validate_high_opacity_ok() {
470 let opts = OverlayOptions {
471 opacity: 2.5,
472 ..Default::default()
473 };
474 assert!(opts.validate().is_ok());
476 assert_eq!(opts.clamped_opacity(), 1.0);
477 }
478
479 #[test]
480 fn test_overlay_options_clamped_opacity() {
481 assert_eq!(
482 OverlayOptions {
483 opacity: -0.5,
484 ..Default::default()
485 }
486 .clamped_opacity(),
487 0.0
488 );
489 assert_eq!(
490 OverlayOptions {
491 opacity: 0.5,
492 ..Default::default()
493 }
494 .clamped_opacity(),
495 0.5
496 );
497 assert_eq!(
498 OverlayOptions {
499 opacity: 3.0,
500 ..Default::default()
501 }
502 .clamped_opacity(),
503 1.0
504 );
505 }
506
507 #[test]
508 fn test_compute_ctm_center_same_size() {
509 let ctm = compute_ctm(595.0, 842.0, 595.0, 842.0, 1.0, &OverlayPosition::Center);
510 assert_eq!(ctm[0], 1.0);
511 assert_eq!(ctm[3], 1.0);
512 assert!((ctm[4] - 0.0).abs() < 0.001);
513 assert!((ctm[5] - 0.0).abs() < 0.001);
514 }
515
516 #[test]
517 fn test_compute_ctm_center_different_sizes() {
518 let ctm = compute_ctm(595.0, 842.0, 200.0, 200.0, 1.0, &OverlayPosition::Center);
519 assert!((ctm[4] - 197.5).abs() < 0.001);
520 assert!((ctm[5] - 321.0).abs() < 0.001);
521 }
522
523 #[test]
524 fn test_compute_ctm_with_scale() {
525 let ctm = compute_ctm(595.0, 842.0, 595.0, 842.0, 0.5, &OverlayPosition::Center);
526 assert!((ctm[0] - 0.5).abs() < 0.001);
527 assert!((ctm[3] - 0.5).abs() < 0.001);
528 assert!((ctm[4] - 148.75).abs() < 0.001);
530 assert!((ctm[5] - 210.5).abs() < 0.001);
531 }
532
533 #[test]
534 fn test_compute_ctm_bottom_left() {
535 let ctm = compute_ctm(
536 595.0,
537 842.0,
538 200.0,
539 200.0,
540 1.0,
541 &OverlayPosition::BottomLeft,
542 );
543 assert!((ctm[4]).abs() < 0.001);
544 assert!((ctm[5]).abs() < 0.001);
545 }
546
547 #[test]
548 fn test_compute_ctm_bottom_right() {
549 let ctm = compute_ctm(
550 595.0,
551 842.0,
552 200.0,
553 200.0,
554 1.0,
555 &OverlayPosition::BottomRight,
556 );
557 assert!((ctm[4] - 395.0).abs() < 0.001);
558 assert!((ctm[5]).abs() < 0.001);
559 }
560
561 #[test]
562 fn test_compute_ctm_top_left() {
563 let ctm = compute_ctm(595.0, 842.0, 200.0, 200.0, 1.0, &OverlayPosition::TopLeft);
564 assert!((ctm[4]).abs() < 0.001);
565 assert!((ctm[5] - 642.0).abs() < 0.001);
566 }
567
568 #[test]
569 fn test_compute_ctm_top_right() {
570 let ctm = compute_ctm(595.0, 842.0, 200.0, 200.0, 1.0, &OverlayPosition::TopRight);
571 assert!((ctm[4] - 395.0).abs() < 0.001);
572 assert!((ctm[5] - 642.0).abs() < 0.001);
573 }
574
575 #[test]
576 fn test_compute_ctm_custom_position() {
577 let ctm = compute_ctm(
578 595.0,
579 842.0,
580 200.0,
581 200.0,
582 1.0,
583 &OverlayPosition::Custom(100.0, 150.0),
584 );
585 assert!((ctm[4] - 100.0).abs() < 0.001);
586 assert!((ctm[5] - 150.0).abs() < 0.001);
587 }
588
589 #[test]
590 fn test_overlay_position_default() {
591 assert_eq!(OverlayPosition::default(), OverlayPosition::Center);
592 }
593
594 #[test]
595 fn test_overlay_position_equality() {
596 assert_eq!(OverlayPosition::Center, OverlayPosition::Center);
597 assert_eq!(
598 OverlayPosition::Custom(1.0, 2.0),
599 OverlayPosition::Custom(1.0, 2.0)
600 );
601 assert_ne!(OverlayPosition::Center, OverlayPosition::TopLeft);
602 }
603
604 #[test]
606 fn test_unresolvable_reference_degrades_to_null() {
607 use crate::objects::Object as WObj;
608 use crate::parser::objects::{PdfDictionary, PdfName, PdfObject as PObj};
609
610 let mut dict = PdfDictionary::new();
612 dict.0
613 .insert(PdfName::new("SMask".to_string()), PObj::Reference(99999, 0));
614 dict.0
615 .insert(PdfName::new("Width".to_string()), PObj::Integer(100));
616
617 let mut doc_builder = crate::Document::new();
619 let page = crate::Page::a4();
620 doc_builder.add_page(page);
621 let pdf_bytes = doc_builder.to_bytes().unwrap();
622
623 let reader = crate::parser::PdfReader::new(std::io::Cursor::new(pdf_bytes)).unwrap();
624 let pdf_doc = crate::parser::PdfDocument::new(reader);
625
626 let result = convert_parser_dict_to_objects_dict(&dict, &pdf_doc);
627
628 let smask_key = "SMask";
630 let smask_val = result.get(smask_key);
631 assert!(
632 matches!(smask_val, Some(WObj::Null)),
633 "Unresolvable reference should become Null, got: {:?}",
634 smask_val
635 );
636
637 let width_val = result.get("Width");
639 assert!(
640 matches!(width_val, Some(WObj::Integer(100))),
641 "Normal integer should convert, got: {:?}",
642 width_val
643 );
644 }
645}