1pub mod chunk_page_mapper;
7pub mod extract_images;
8pub mod merge;
9pub mod overlay;
10pub mod page_analysis;
11pub mod page_extraction;
12pub mod pdf_ocr_converter;
13pub mod reorder;
14pub mod rotate;
15mod semantic_preservation;
16pub mod semantic_redactor;
17pub mod source_highlighter;
18pub mod split;
19
20pub use chunk_page_mapper::ChunkPageMapper;
21pub use extract_images::{
22 extract_images_from_pages, extract_images_from_pdf, ExtractImagesOptions, ExtractedImage,
23 ExtractedImageData, ImageExtractionError, ImageExtractionLimits, ImageExtractionResult,
24 ImageExtractor, ImagePreprocessingOptions,
25};
26pub use merge::{merge_pdf_files, merge_pdfs, MergeInput, MergeOptions, PdfMerger};
27pub use overlay::{overlay_pdf, OverlayOptions, OverlayPosition, PdfOverlay};
28pub use page_analysis::{AnalysisOptions, ContentAnalysis, PageContentAnalyzer, PageType};
29pub use page_extraction::{
30 extract_page, extract_page_range, extract_page_range_to_file, extract_page_to_file,
31 extract_pages, extract_pages_to_file, PageExtractionOptions, PageExtractor,
32};
33pub use pdf_ocr_converter::{ConversionOptions, ConversionResult, PdfOcrConverter};
34pub use reorder::{
35 move_pdf_page, mutate_pdf_pages_lossless, plan_pdf_page_mutations, reorder_pdf_pages,
36 reorder_pdf_pages_lossless, reverse_pdf_pages, swap_pdf_pages, PageMutation, PageMutationBatch,
37 PageMutationReport, PageReorderer, ReorderOptions,
38};
39pub use rotate::{rotate_all_pages, rotate_pdf_pages, PageRotator, RotateOptions, RotationAngle};
40pub use semantic_preservation::{
41 extract_pdf_pages_lossless, merge_pdfs_lossless, plan_extract_pdf_pages_lossless,
42 plan_merge_pdfs_lossless, plan_split_pdf_lossless, split_pdf_lossless, DocumentStructure,
43 DocumentStructurePolicy, ExistingDocumentEngine, ExistingDocumentExecutionPlan,
44 ExistingDocumentMergeInput, ExistingDocumentPolicy, InputSemanticReport, InputSemanticRole,
45 LosslessMergeInput, PreserveBasePolicy, ReconstructMetadataPolicy, ReconstructPolicy,
46 SecondaryStructurePolicy, SemanticPreservationReport, StructureDisposition,
47 StructureSemanticReport,
48};
49pub use semantic_redactor::{
50 RedactionConfig, RedactionEntry, RedactionMode, RedactionReport, RedactionStyle,
51 SemanticRedactor, SemanticRedactorError, SemanticRedactorResult,
52};
53pub use source_highlighter::{
54 fragment_to_highlight_rect, HighlightStyle, IndexedFragment, SourceHighlighter,
55 SourceHighlighterError, SourceHighlighterResult, TextPositionIndex,
56};
57pub use split::{split_into_pages, split_pdf, PdfSplitter, SplitMode, SplitOptions};
58
59pub mod existing_document {
65 pub use super::semantic_preservation::{
66 extract_pdf_pages, merge_pdfs, plan_extract_pdf_pages, plan_merge_pdfs, plan_split_pdf,
67 split_pdf, DocumentStructure, DocumentStructurePolicy, ExistingDocumentEngine,
68 ExistingDocumentExecutionPlan, ExistingDocumentMergeInput, ExistingDocumentPolicy,
69 InputSemanticReport, InputSemanticRole, PreserveBasePolicy, ReconstructMetadataPolicy,
70 ReconstructPolicy, SecondaryStructurePolicy, SemanticPreservationReport,
71 StructureDisposition, StructureSemanticReport,
72 };
73}
74
75pub mod reconstruct {
80 pub use super::merge::{merge_pdf_files, MergeInput, MetadataMode};
81 pub use super::page_extraction::{
82 extract_page, extract_page_range, extract_page_range_to_file, extract_page_to_file,
83 extract_pages, extract_pages_to_file,
84 };
85 pub use super::split::{split_into_pages, SplitMode};
86
87 use super::{merge, split, OperationResult};
88 use std::path::{Path, PathBuf};
89
90 pub fn merge_pdfs(
92 inputs: Vec<MergeInput>,
93 output: impl AsRef<Path>,
94 metadata_mode: MetadataMode,
95 ) -> OperationResult<()> {
96 merge::merge_pdfs(
97 inputs,
98 output,
99 merge::MergeOptions {
100 metadata_mode,
101 ..merge::MergeOptions::default()
102 },
103 )
104 }
105
106 pub fn split_pdf(
108 input: impl AsRef<Path>,
109 mode: SplitMode,
110 output_pattern: impl Into<String>,
111 preserve_metadata: bool,
112 ) -> OperationResult<Vec<PathBuf>> {
113 split::split_pdf(
114 input,
115 split::SplitOptions {
116 mode,
117 output_pattern: output_pattern.into(),
118 preserve_metadata,
119 optimize: false,
120 },
121 )
122 }
123}
124
125use crate::error::PdfError;
126
127pub type OperationResult<T> = Result<T, OperationError>;
129
130#[derive(Debug, thiserror::Error)]
132pub enum OperationError {
133 #[error("Page index {0} out of bounds (document has {1} pages)")]
135 PageIndexOutOfBounds(usize, usize),
136
137 #[error("Invalid page range: {0}")]
139 InvalidPageRange(String),
140
141 #[error("No pages to process")]
143 NoPagesToProcess,
144
145 #[error("Resource conflict: {0}")]
147 ResourceConflict(String),
148
149 #[error("Invalid rotation angle: {0} (must be 0, 90, 180, or 270)")]
151 InvalidRotation(i32),
152
153 #[error("Parse error: {0}")]
155 ParseError(String),
156
157 #[error("Invalid file path: {reason}")]
159 InvalidPath { reason: String },
160
161 #[error("IO error: {0}")]
163 Io(#[from] std::io::Error),
164
165 #[error("PDF error: {0}")]
167 PdfError(#[from] PdfError),
168
169 #[error("Processing error: {0}")]
171 ProcessingError(String),
172}
173
174#[derive(Debug, Clone)]
176pub enum PageRange {
177 All,
179 Single(usize),
181 Range(usize, usize),
183 List(Vec<usize>),
185}
186
187impl PageRange {
188 pub fn parse(s: &str) -> Result<Self, OperationError> {
196 let s = s.trim();
197
198 if s.eq_ignore_ascii_case("all") {
199 return Ok(PageRange::All);
200 }
201
202 if let Ok(page) = s.parse::<usize>() {
204 if page == 0 {
205 return Err(OperationError::InvalidPageRange(
206 "Page numbers start at 1".to_string(),
207 ));
208 }
209 return Ok(PageRange::Single(page - 1));
210 }
211
212 if let Some((start, end)) = s.split_once('-') {
214 let start = start
215 .trim()
216 .parse::<usize>()
217 .map_err(|_| OperationError::InvalidPageRange(format!("Invalid start: {start}")))?;
218 let end = end
219 .trim()
220 .parse::<usize>()
221 .map_err(|_| OperationError::InvalidPageRange(format!("Invalid end: {end}")))?;
222
223 if start == 0 || end == 0 {
224 return Err(OperationError::InvalidPageRange(
225 "Page numbers start at 1".to_string(),
226 ));
227 }
228
229 if start > end {
230 return Err(OperationError::InvalidPageRange(format!(
231 "Start {start} is greater than end {end}"
232 )));
233 }
234
235 return Ok(PageRange::Range(start - 1, end - 1));
236 }
237
238 if s.contains(',') {
240 let pages: Result<Vec<usize>, _> = s
241 .split(',')
242 .map(|p| {
243 let page = p.trim().parse::<usize>().map_err(|_| {
244 OperationError::InvalidPageRange(format!("Invalid page: {p}"))
245 })?;
246 if page == 0 {
247 return Err(OperationError::InvalidPageRange(
248 "Page numbers start at 1".to_string(),
249 ));
250 }
251 Ok(page - 1)
252 })
253 .collect();
254
255 return Ok(PageRange::List(pages?));
256 }
257
258 Err(OperationError::InvalidPageRange(format!(
259 "Invalid format: {s}"
260 )))
261 }
262
263 pub fn get_indices(&self, total_pages: usize) -> Result<Vec<usize>, OperationError> {
265 match self {
266 PageRange::All => Ok((0..total_pages).collect()),
267 PageRange::Single(idx) => {
268 if *idx >= total_pages {
269 Err(OperationError::PageIndexOutOfBounds(*idx, total_pages))
270 } else {
271 Ok(vec![*idx])
272 }
273 }
274 PageRange::Range(start, end) => {
275 if *start >= total_pages {
276 Err(OperationError::PageIndexOutOfBounds(*start, total_pages))
277 } else if *end >= total_pages {
278 Err(OperationError::PageIndexOutOfBounds(*end, total_pages))
279 } else {
280 Ok((*start..=*end).collect())
281 }
282 }
283 PageRange::List(pages) => {
284 for &page in pages {
285 if page >= total_pages {
286 return Err(OperationError::PageIndexOutOfBounds(page, total_pages));
287 }
288 }
289 Ok(pages.clone())
290 }
291 }
292 }
293}
294
295#[cfg(test)]
296mod error_tests;
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn test_page_range_parsing() {
304 assert!(matches!(PageRange::parse("all").unwrap(), PageRange::All));
305 assert!(matches!(PageRange::parse("ALL").unwrap(), PageRange::All));
306
307 match PageRange::parse("5").unwrap() {
308 PageRange::Single(idx) => assert_eq!(idx, 4),
309 _ => panic!("Expected Single"),
310 }
311
312 match PageRange::parse("2-5").unwrap() {
313 PageRange::Range(start, end) => {
314 assert_eq!(start, 1);
315 assert_eq!(end, 4);
316 }
317 _ => panic!("Expected Range"),
318 }
319
320 match PageRange::parse("1,3,5,7").unwrap() {
321 PageRange::List(pages) => {
322 assert_eq!(pages, vec![0, 2, 4, 6]);
323 }
324 _ => panic!("Expected List"),
325 }
326
327 assert!(PageRange::parse("0").is_err());
328 assert!(PageRange::parse("5-2").is_err());
329 assert!(PageRange::parse("invalid").is_err());
330 }
331
332 #[test]
333 fn test_page_range_indices() {
334 let total = 10;
335
336 assert_eq!(
337 PageRange::All.get_indices(total).unwrap(),
338 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
339 );
340
341 assert_eq!(PageRange::Single(5).get_indices(total).unwrap(), vec![5]);
342
343 assert_eq!(
344 PageRange::Range(2, 5).get_indices(total).unwrap(),
345 vec![2, 3, 4, 5]
346 );
347
348 assert_eq!(
349 PageRange::List(vec![1, 3, 5]).get_indices(total).unwrap(),
350 vec![1, 3, 5]
351 );
352
353 assert!(PageRange::Single(10).get_indices(total).is_err());
354 assert!(PageRange::Range(8, 15).get_indices(total).is_err());
355 }
356
357 #[test]
358 fn test_page_range_empty_list() {
359 let result = PageRange::parse("");
361 assert!(result.is_err());
362
363 let result2 = PageRange::parse(",,");
365 assert!(result2.is_err());
366 }
367
368 #[test]
369 fn test_page_range_list_with_zero() {
370 let result = PageRange::parse("1,0,3");
372 assert!(result.is_err());
373 if let Err(e) = result {
374 match e {
375 OperationError::InvalidPageRange(msg) => {
376 assert!(msg.contains("Page numbers start at 1"));
377 }
378 _ => panic!("Expected InvalidPageRange error"),
379 }
380 }
381 }
382
383 #[test]
384 fn test_page_range_with_extra_spaces() {
385 match PageRange::parse(" 1 , 3 , 5 ").unwrap() {
387 PageRange::List(pages) => {
388 assert_eq!(pages, vec![0, 2, 4]);
389 }
390 _ => panic!("Expected List"),
391 }
392
393 match PageRange::parse(" 2 - 5 ").unwrap() {
395 PageRange::Range(start, end) => {
396 assert_eq!(start, 1);
397 assert_eq!(end, 4);
398 }
399 _ => panic!("Expected Range"),
400 }
401 }
402
403 #[test]
404 fn test_page_range_equal_start_end() {
405 match PageRange::parse("5-5").unwrap() {
407 PageRange::Range(start, end) => {
408 assert_eq!(start, 4);
409 assert_eq!(end, 4);
410 }
411 _ => panic!("Expected Range"),
412 }
413
414 let range = PageRange::Range(4, 4);
416 assert_eq!(range.get_indices(10).unwrap(), vec![4]);
417 }
418
419 #[test]
420 fn test_page_range_list_out_of_bounds() {
421 let pages = PageRange::List(vec![2, 5, 15]);
423 let result = pages.get_indices(10);
424 assert!(result.is_err());
425 if let Err(e) = result {
426 match e {
427 OperationError::PageIndexOutOfBounds(idx, total) => {
428 assert_eq!(idx, 15);
429 assert_eq!(total, 10);
430 }
431 _ => panic!("Expected PageIndexOutOfBounds error"),
432 }
433 }
434 }
435
436 #[test]
437 fn test_page_range_empty_document() {
438 let total = 0;
440
441 assert_eq!(
443 PageRange::All.get_indices(total).unwrap(),
444 Vec::<usize>::new()
445 );
446
447 assert!(PageRange::Single(0).get_indices(total).is_err());
449
450 assert!(PageRange::Range(0, 1).get_indices(total).is_err());
452
453 assert_eq!(
455 PageRange::List(vec![]).get_indices(total).unwrap(),
456 Vec::<usize>::new()
457 );
458 }
459
460 #[test]
461 fn test_page_range_additional_invalid_formats() {
462 assert!(PageRange::parse("1-2-3").is_err()); assert!(PageRange::parse("abc").is_err()); assert!(PageRange::parse("1.5").is_err()); assert!(PageRange::parse("-5").is_err()); assert!(PageRange::parse("1-").is_err()); assert!(PageRange::parse("-").is_err()); }
470
471 #[test]
472 fn test_module_exports() {
473 use super::extract_images::ExtractImagesOptions;
478 use super::merge::MergeOptions;
479 use super::page_analysis::{AnalysisOptions, PageType};
480 use super::page_extraction::PageExtractionOptions;
481 use super::rotate::{RotateOptions, RotationAngle};
482 use super::split::{SplitMode, SplitOptions};
483
484 let _extract: ExtractImagesOptions;
486 let _merge: MergeOptions;
487 let _analysis: AnalysisOptions;
488 let _extraction: PageExtractionOptions;
489 let _rotate: RotateOptions;
490 let _split: SplitOptions;
491 let _angle: RotationAngle;
492 let _page_type: PageType;
493 let _mode: SplitMode;
494 }
495
496 #[test]
497 fn test_operation_error_variants() {
498 let errors = vec![
499 OperationError::PageIndexOutOfBounds(5, 3),
500 OperationError::InvalidPageRange("test".to_string()),
501 OperationError::NoPagesToProcess,
502 OperationError::ResourceConflict("test".to_string()),
503 OperationError::InvalidRotation(45),
504 OperationError::ParseError("test".to_string()),
505 OperationError::ProcessingError("test".to_string()),
506 ];
507
508 for error in errors {
509 let message = error.to_string();
510 assert!(!message.is_empty());
511 }
512 }
513
514 #[test]
515 fn test_page_range_edge_cases() {
516 assert!(matches!(
518 PageRange::parse(" all ").unwrap(),
519 PageRange::All
520 ));
521 assert!(matches!(
522 PageRange::parse(" 5 ").unwrap(),
523 PageRange::Single(4)
524 ));
525
526 match PageRange::parse(" 1 , 3 , 5 ").unwrap() {
528 PageRange::List(pages) => assert_eq!(pages, vec![0, 2, 4]),
529 _ => panic!("Expected List"),
530 }
531
532 match PageRange::parse(" 2 - 5 ").unwrap() {
534 PageRange::Range(start, end) => {
535 assert_eq!(start, 1);
536 assert_eq!(end, 4);
537 }
538 _ => panic!("Expected Range"),
539 }
540 }
541
542 #[test]
543 fn test_page_range_invalid_formats() {
544 assert!(PageRange::parse("").is_err());
546 assert!(PageRange::parse("abc").is_err());
547 assert!(PageRange::parse("1-").is_err());
548 assert!(PageRange::parse("-5").is_err());
549 assert!(PageRange::parse("1-2-3").is_err());
550 assert!(PageRange::parse("1,0,3").is_err());
551 assert!(PageRange::parse("0-5").is_err());
552 assert!(PageRange::parse("5-0").is_err());
553 assert!(PageRange::parse("1,,3").is_err());
554 assert!(PageRange::parse("1.5").is_err());
555 }
556
557 #[test]
558 fn test_page_range_get_indices_empty_document() {
559 let total = 0;
560
561 assert_eq!(
562 PageRange::All.get_indices(total).unwrap(),
563 vec![] as Vec<usize>
564 );
565 assert!(PageRange::Single(0).get_indices(total).is_err());
566 assert!(PageRange::Range(0, 1).get_indices(total).is_err());
567 assert!(PageRange::List(vec![0]).get_indices(total).is_err());
568 }
569
570 #[test]
571 fn test_page_range_get_indices_single_page_document() {
572 let total = 1;
573
574 assert_eq!(PageRange::All.get_indices(total).unwrap(), vec![0]);
575 assert_eq!(PageRange::Single(0).get_indices(total).unwrap(), vec![0]);
576 assert!(PageRange::Single(1).get_indices(total).is_err());
577 assert_eq!(PageRange::Range(0, 0).get_indices(total).unwrap(), vec![0]);
578 assert!(PageRange::Range(0, 1).get_indices(total).is_err());
579 }
580
581 #[test]
582 fn test_page_range_list_duplicates() {
583 match PageRange::parse("1,1,2,2,3").unwrap() {
585 PageRange::List(pages) => {
586 assert_eq!(pages, vec![0, 0, 1, 1, 2]);
587 }
588 _ => panic!("Expected List"),
589 }
590 }
591
592 #[test]
593 fn test_page_range_list_unordered() {
594 match PageRange::parse("5,2,8,1,3").unwrap() {
596 PageRange::List(pages) => {
597 assert_eq!(pages, vec![4, 1, 7, 0, 2]);
598 }
599 _ => panic!("Expected List"),
600 }
601 }
602
603 #[test]
604 fn test_operation_error_display() {
605 let error = OperationError::PageIndexOutOfBounds(10, 5);
606 assert_eq!(
607 error.to_string(),
608 "Page index 10 out of bounds (document has 5 pages)"
609 );
610
611 let error = OperationError::InvalidRotation(45);
612 assert_eq!(
613 error.to_string(),
614 "Invalid rotation angle: 45 (must be 0, 90, 180, or 270)"
615 );
616
617 let error = OperationError::NoPagesToProcess;
618 assert_eq!(error.to_string(), "No pages to process");
619 }
620
621 #[test]
622 fn test_page_range_large_document() {
623 let total = 1000;
624
625 let indices = PageRange::All.get_indices(total).unwrap();
627 assert_eq!(indices.len(), 1000);
628 assert_eq!(indices[0], 0);
629 assert_eq!(indices[999], 999);
630
631 let indices = PageRange::Range(100, 200).get_indices(total).unwrap();
633 assert_eq!(indices.len(), 101);
634 assert_eq!(indices[0], 100);
635 assert_eq!(indices[100], 200);
636 }
637
638 #[test]
639 fn test_page_range_parse_case_insensitive() {
640 assert!(matches!(PageRange::parse("all").unwrap(), PageRange::All));
641 assert!(matches!(PageRange::parse("ALL").unwrap(), PageRange::All));
642 assert!(matches!(PageRange::parse("All").unwrap(), PageRange::All));
643 assert!(matches!(PageRange::parse("aLL").unwrap(), PageRange::All));
644 }
645
646 #[test]
647 fn test_operation_result_type() {
648 fn test_function() -> OperationResult<usize> {
650 Ok(42)
651 }
652
653 fn test_error_function() -> OperationResult<usize> {
654 Err(OperationError::NoPagesToProcess)
655 }
656
657 assert_eq!(test_function().unwrap(), 42);
658 assert!(test_error_function().is_err());
659 }
660
661 #[test]
662 fn test_page_range_boundary_values() {
663 let large_page = usize::MAX / 2;
665
666 match PageRange::parse(&large_page.to_string()).unwrap() {
667 PageRange::Single(idx) => assert_eq!(idx, large_page - 1),
668 _ => panic!("Expected Single"),
669 }
670
671 let indices = PageRange::Single(5).get_indices(10).unwrap();
673 assert_eq!(indices, vec![5]);
674
675 let indices = PageRange::Range(0, 9).get_indices(10).unwrap();
677 assert_eq!(indices.len(), 10);
678 }
679
680 #[test]
681 fn test_error_from_io() {
682 use std::io;
683
684 let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
685 let op_error: OperationError = io_error.into();
686
687 match op_error {
688 OperationError::Io(_) => {}
689 _ => panic!("Expected Io variant"),
690 }
691 }
692
693 #[test]
694 fn test_page_range_fmt_debug() {
695 let range = PageRange::All;
697 let debug_str = format!("{:?}", range);
698 assert!(debug_str.contains("All"));
699
700 let range = PageRange::Single(5);
701 let debug_str = format!("{:?}", range);
702 assert!(debug_str.contains("Single"));
703 assert!(debug_str.contains("5"));
704
705 let range = PageRange::Range(1, 10);
706 let debug_str = format!("{:?}", range);
707 assert!(debug_str.contains("Range"));
708
709 let range = PageRange::List(vec![1, 2, 3]);
710 let debug_str = format!("{:?}", range);
711 assert!(debug_str.contains("List"));
712 }
713
714 #[test]
715 fn test_page_range_clone() {
716 let original = PageRange::List(vec![1, 2, 3]);
717 let cloned = original.clone();
718
719 match (original, cloned) {
720 (PageRange::List(orig), PageRange::List(clone)) => {
721 assert_eq!(orig, clone);
722 }
723 _ => panic!("Clone failed"),
724 }
725 }
726}