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;
15pub mod semantic_redactor;
16pub mod source_highlighter;
17pub mod split;
18
19pub use chunk_page_mapper::ChunkPageMapper;
20pub use extract_images::{
21 extract_images_from_pages, extract_images_from_pdf, ExtractImagesOptions, ExtractedImage,
22 ExtractedImageData, ImageExtractionError, ImageExtractionLimits, ImageExtractionResult,
23 ImageExtractor, ImagePreprocessingOptions,
24};
25pub use merge::{merge_pdf_files, merge_pdfs, MergeInput, MergeOptions, PdfMerger};
26pub use overlay::{overlay_pdf, OverlayOptions, OverlayPosition, PdfOverlay};
27pub use page_analysis::{AnalysisOptions, ContentAnalysis, PageContentAnalyzer, PageType};
28pub use page_extraction::{
29 extract_page, extract_page_range, extract_page_range_to_file, extract_page_to_file,
30 extract_pages, extract_pages_to_file, PageExtractionOptions, PageExtractor,
31};
32pub use pdf_ocr_converter::{ConversionOptions, ConversionResult, PdfOcrConverter};
33pub use reorder::{
34 move_pdf_page, reorder_pdf_pages, reverse_pdf_pages, swap_pdf_pages, PageReorderer,
35 ReorderOptions,
36};
37pub use rotate::{rotate_all_pages, rotate_pdf_pages, PageRotator, RotateOptions, RotationAngle};
38pub use semantic_redactor::{
39 RedactionConfig, RedactionEntry, RedactionReport, RedactionStyle, SemanticRedactor,
40 SemanticRedactorError, SemanticRedactorResult,
41};
42pub use source_highlighter::{
43 fragment_to_highlight_rect, HighlightStyle, IndexedFragment, SourceHighlighter,
44 SourceHighlighterError, SourceHighlighterResult, TextPositionIndex,
45};
46pub use split::{split_into_pages, split_pdf, PdfSplitter, SplitMode, SplitOptions};
47
48use crate::error::PdfError;
49
50pub type OperationResult<T> = Result<T, OperationError>;
52
53#[derive(Debug, thiserror::Error)]
55pub enum OperationError {
56 #[error("Page index {0} out of bounds (document has {1} pages)")]
58 PageIndexOutOfBounds(usize, usize),
59
60 #[error("Invalid page range: {0}")]
62 InvalidPageRange(String),
63
64 #[error("No pages to process")]
66 NoPagesToProcess,
67
68 #[error("Resource conflict: {0}")]
70 ResourceConflict(String),
71
72 #[error("Invalid rotation angle: {0} (must be 0, 90, 180, or 270)")]
74 InvalidRotation(i32),
75
76 #[error("Parse error: {0}")]
78 ParseError(String),
79
80 #[error("Invalid file path: {reason}")]
82 InvalidPath { reason: String },
83
84 #[error("IO error: {0}")]
86 Io(#[from] std::io::Error),
87
88 #[error("PDF error: {0}")]
90 PdfError(#[from] PdfError),
91
92 #[error("Processing error: {0}")]
94 ProcessingError(String),
95}
96
97#[derive(Debug, Clone)]
99pub enum PageRange {
100 All,
102 Single(usize),
104 Range(usize, usize),
106 List(Vec<usize>),
108}
109
110impl PageRange {
111 pub fn parse(s: &str) -> Result<Self, OperationError> {
119 let s = s.trim();
120
121 if s.eq_ignore_ascii_case("all") {
122 return Ok(PageRange::All);
123 }
124
125 if let Ok(page) = s.parse::<usize>() {
127 if page == 0 {
128 return Err(OperationError::InvalidPageRange(
129 "Page numbers start at 1".to_string(),
130 ));
131 }
132 return Ok(PageRange::Single(page - 1));
133 }
134
135 if let Some((start, end)) = s.split_once('-') {
137 let start = start
138 .trim()
139 .parse::<usize>()
140 .map_err(|_| OperationError::InvalidPageRange(format!("Invalid start: {start}")))?;
141 let end = end
142 .trim()
143 .parse::<usize>()
144 .map_err(|_| OperationError::InvalidPageRange(format!("Invalid end: {end}")))?;
145
146 if start == 0 || end == 0 {
147 return Err(OperationError::InvalidPageRange(
148 "Page numbers start at 1".to_string(),
149 ));
150 }
151
152 if start > end {
153 return Err(OperationError::InvalidPageRange(format!(
154 "Start {start} is greater than end {end}"
155 )));
156 }
157
158 return Ok(PageRange::Range(start - 1, end - 1));
159 }
160
161 if s.contains(',') {
163 let pages: Result<Vec<usize>, _> = s
164 .split(',')
165 .map(|p| {
166 let page = p.trim().parse::<usize>().map_err(|_| {
167 OperationError::InvalidPageRange(format!("Invalid page: {p}"))
168 })?;
169 if page == 0 {
170 return Err(OperationError::InvalidPageRange(
171 "Page numbers start at 1".to_string(),
172 ));
173 }
174 Ok(page - 1)
175 })
176 .collect();
177
178 return Ok(PageRange::List(pages?));
179 }
180
181 Err(OperationError::InvalidPageRange(format!(
182 "Invalid format: {s}"
183 )))
184 }
185
186 pub fn get_indices(&self, total_pages: usize) -> Result<Vec<usize>, OperationError> {
188 match self {
189 PageRange::All => Ok((0..total_pages).collect()),
190 PageRange::Single(idx) => {
191 if *idx >= total_pages {
192 Err(OperationError::PageIndexOutOfBounds(*idx, total_pages))
193 } else {
194 Ok(vec![*idx])
195 }
196 }
197 PageRange::Range(start, end) => {
198 if *start >= total_pages {
199 Err(OperationError::PageIndexOutOfBounds(*start, total_pages))
200 } else if *end >= total_pages {
201 Err(OperationError::PageIndexOutOfBounds(*end, total_pages))
202 } else {
203 Ok((*start..=*end).collect())
204 }
205 }
206 PageRange::List(pages) => {
207 for &page in pages {
208 if page >= total_pages {
209 return Err(OperationError::PageIndexOutOfBounds(page, total_pages));
210 }
211 }
212 Ok(pages.clone())
213 }
214 }
215 }
216}
217
218#[cfg(test)]
219mod error_tests;
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_page_range_parsing() {
227 assert!(matches!(PageRange::parse("all").unwrap(), PageRange::All));
228 assert!(matches!(PageRange::parse("ALL").unwrap(), PageRange::All));
229
230 match PageRange::parse("5").unwrap() {
231 PageRange::Single(idx) => assert_eq!(idx, 4),
232 _ => panic!("Expected Single"),
233 }
234
235 match PageRange::parse("2-5").unwrap() {
236 PageRange::Range(start, end) => {
237 assert_eq!(start, 1);
238 assert_eq!(end, 4);
239 }
240 _ => panic!("Expected Range"),
241 }
242
243 match PageRange::parse("1,3,5,7").unwrap() {
244 PageRange::List(pages) => {
245 assert_eq!(pages, vec![0, 2, 4, 6]);
246 }
247 _ => panic!("Expected List"),
248 }
249
250 assert!(PageRange::parse("0").is_err());
251 assert!(PageRange::parse("5-2").is_err());
252 assert!(PageRange::parse("invalid").is_err());
253 }
254
255 #[test]
256 fn test_page_range_indices() {
257 let total = 10;
258
259 assert_eq!(
260 PageRange::All.get_indices(total).unwrap(),
261 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
262 );
263
264 assert_eq!(PageRange::Single(5).get_indices(total).unwrap(), vec![5]);
265
266 assert_eq!(
267 PageRange::Range(2, 5).get_indices(total).unwrap(),
268 vec![2, 3, 4, 5]
269 );
270
271 assert_eq!(
272 PageRange::List(vec![1, 3, 5]).get_indices(total).unwrap(),
273 vec![1, 3, 5]
274 );
275
276 assert!(PageRange::Single(10).get_indices(total).is_err());
277 assert!(PageRange::Range(8, 15).get_indices(total).is_err());
278 }
279
280 #[test]
281 fn test_page_range_empty_list() {
282 let result = PageRange::parse("");
284 assert!(result.is_err());
285
286 let result2 = PageRange::parse(",,");
288 assert!(result2.is_err());
289 }
290
291 #[test]
292 fn test_page_range_list_with_zero() {
293 let result = PageRange::parse("1,0,3");
295 assert!(result.is_err());
296 if let Err(e) = result {
297 match e {
298 OperationError::InvalidPageRange(msg) => {
299 assert!(msg.contains("Page numbers start at 1"));
300 }
301 _ => panic!("Expected InvalidPageRange error"),
302 }
303 }
304 }
305
306 #[test]
307 fn test_page_range_with_extra_spaces() {
308 match PageRange::parse(" 1 , 3 , 5 ").unwrap() {
310 PageRange::List(pages) => {
311 assert_eq!(pages, vec![0, 2, 4]);
312 }
313 _ => panic!("Expected List"),
314 }
315
316 match PageRange::parse(" 2 - 5 ").unwrap() {
318 PageRange::Range(start, end) => {
319 assert_eq!(start, 1);
320 assert_eq!(end, 4);
321 }
322 _ => panic!("Expected Range"),
323 }
324 }
325
326 #[test]
327 fn test_page_range_equal_start_end() {
328 match PageRange::parse("5-5").unwrap() {
330 PageRange::Range(start, end) => {
331 assert_eq!(start, 4);
332 assert_eq!(end, 4);
333 }
334 _ => panic!("Expected Range"),
335 }
336
337 let range = PageRange::Range(4, 4);
339 assert_eq!(range.get_indices(10).unwrap(), vec![4]);
340 }
341
342 #[test]
343 fn test_page_range_list_out_of_bounds() {
344 let pages = PageRange::List(vec![2, 5, 15]);
346 let result = pages.get_indices(10);
347 assert!(result.is_err());
348 if let Err(e) = result {
349 match e {
350 OperationError::PageIndexOutOfBounds(idx, total) => {
351 assert_eq!(idx, 15);
352 assert_eq!(total, 10);
353 }
354 _ => panic!("Expected PageIndexOutOfBounds error"),
355 }
356 }
357 }
358
359 #[test]
360 fn test_page_range_empty_document() {
361 let total = 0;
363
364 assert_eq!(
366 PageRange::All.get_indices(total).unwrap(),
367 Vec::<usize>::new()
368 );
369
370 assert!(PageRange::Single(0).get_indices(total).is_err());
372
373 assert!(PageRange::Range(0, 1).get_indices(total).is_err());
375
376 assert_eq!(
378 PageRange::List(vec![]).get_indices(total).unwrap(),
379 Vec::<usize>::new()
380 );
381 }
382
383 #[test]
384 fn test_page_range_additional_invalid_formats() {
385 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()); }
393
394 #[test]
395 fn test_module_exports() {
396 use super::extract_images::ExtractImagesOptions;
401 use super::merge::MergeOptions;
402 use super::page_analysis::{AnalysisOptions, PageType};
403 use super::page_extraction::PageExtractionOptions;
404 use super::rotate::{RotateOptions, RotationAngle};
405 use super::split::{SplitMode, SplitOptions};
406
407 let _extract: ExtractImagesOptions;
409 let _merge: MergeOptions;
410 let _analysis: AnalysisOptions;
411 let _extraction: PageExtractionOptions;
412 let _rotate: RotateOptions;
413 let _split: SplitOptions;
414 let _angle: RotationAngle;
415 let _page_type: PageType;
416 let _mode: SplitMode;
417 }
418
419 #[test]
420 fn test_operation_error_variants() {
421 let errors = vec![
422 OperationError::PageIndexOutOfBounds(5, 3),
423 OperationError::InvalidPageRange("test".to_string()),
424 OperationError::NoPagesToProcess,
425 OperationError::ResourceConflict("test".to_string()),
426 OperationError::InvalidRotation(45),
427 OperationError::ParseError("test".to_string()),
428 OperationError::ProcessingError("test".to_string()),
429 ];
430
431 for error in errors {
432 let message = error.to_string();
433 assert!(!message.is_empty());
434 }
435 }
436
437 #[test]
438 fn test_page_range_edge_cases() {
439 assert!(matches!(
441 PageRange::parse(" all ").unwrap(),
442 PageRange::All
443 ));
444 assert!(matches!(
445 PageRange::parse(" 5 ").unwrap(),
446 PageRange::Single(4)
447 ));
448
449 match PageRange::parse(" 1 , 3 , 5 ").unwrap() {
451 PageRange::List(pages) => assert_eq!(pages, vec![0, 2, 4]),
452 _ => panic!("Expected List"),
453 }
454
455 match PageRange::parse(" 2 - 5 ").unwrap() {
457 PageRange::Range(start, end) => {
458 assert_eq!(start, 1);
459 assert_eq!(end, 4);
460 }
461 _ => panic!("Expected Range"),
462 }
463 }
464
465 #[test]
466 fn test_page_range_invalid_formats() {
467 assert!(PageRange::parse("").is_err());
469 assert!(PageRange::parse("abc").is_err());
470 assert!(PageRange::parse("1-").is_err());
471 assert!(PageRange::parse("-5").is_err());
472 assert!(PageRange::parse("1-2-3").is_err());
473 assert!(PageRange::parse("1,0,3").is_err());
474 assert!(PageRange::parse("0-5").is_err());
475 assert!(PageRange::parse("5-0").is_err());
476 assert!(PageRange::parse("1,,3").is_err());
477 assert!(PageRange::parse("1.5").is_err());
478 }
479
480 #[test]
481 fn test_page_range_get_indices_empty_document() {
482 let total = 0;
483
484 assert_eq!(
485 PageRange::All.get_indices(total).unwrap(),
486 vec![] as Vec<usize>
487 );
488 assert!(PageRange::Single(0).get_indices(total).is_err());
489 assert!(PageRange::Range(0, 1).get_indices(total).is_err());
490 assert!(PageRange::List(vec![0]).get_indices(total).is_err());
491 }
492
493 #[test]
494 fn test_page_range_get_indices_single_page_document() {
495 let total = 1;
496
497 assert_eq!(PageRange::All.get_indices(total).unwrap(), vec![0]);
498 assert_eq!(PageRange::Single(0).get_indices(total).unwrap(), vec![0]);
499 assert!(PageRange::Single(1).get_indices(total).is_err());
500 assert_eq!(PageRange::Range(0, 0).get_indices(total).unwrap(), vec![0]);
501 assert!(PageRange::Range(0, 1).get_indices(total).is_err());
502 }
503
504 #[test]
505 fn test_page_range_list_duplicates() {
506 match PageRange::parse("1,1,2,2,3").unwrap() {
508 PageRange::List(pages) => {
509 assert_eq!(pages, vec![0, 0, 1, 1, 2]);
510 }
511 _ => panic!("Expected List"),
512 }
513 }
514
515 #[test]
516 fn test_page_range_list_unordered() {
517 match PageRange::parse("5,2,8,1,3").unwrap() {
519 PageRange::List(pages) => {
520 assert_eq!(pages, vec![4, 1, 7, 0, 2]);
521 }
522 _ => panic!("Expected List"),
523 }
524 }
525
526 #[test]
527 fn test_operation_error_display() {
528 let error = OperationError::PageIndexOutOfBounds(10, 5);
529 assert_eq!(
530 error.to_string(),
531 "Page index 10 out of bounds (document has 5 pages)"
532 );
533
534 let error = OperationError::InvalidRotation(45);
535 assert_eq!(
536 error.to_string(),
537 "Invalid rotation angle: 45 (must be 0, 90, 180, or 270)"
538 );
539
540 let error = OperationError::NoPagesToProcess;
541 assert_eq!(error.to_string(), "No pages to process");
542 }
543
544 #[test]
545 fn test_page_range_large_document() {
546 let total = 1000;
547
548 let indices = PageRange::All.get_indices(total).unwrap();
550 assert_eq!(indices.len(), 1000);
551 assert_eq!(indices[0], 0);
552 assert_eq!(indices[999], 999);
553
554 let indices = PageRange::Range(100, 200).get_indices(total).unwrap();
556 assert_eq!(indices.len(), 101);
557 assert_eq!(indices[0], 100);
558 assert_eq!(indices[100], 200);
559 }
560
561 #[test]
562 fn test_page_range_parse_case_insensitive() {
563 assert!(matches!(PageRange::parse("all").unwrap(), PageRange::All));
564 assert!(matches!(PageRange::parse("ALL").unwrap(), PageRange::All));
565 assert!(matches!(PageRange::parse("All").unwrap(), PageRange::All));
566 assert!(matches!(PageRange::parse("aLL").unwrap(), PageRange::All));
567 }
568
569 #[test]
570 fn test_operation_result_type() {
571 fn test_function() -> OperationResult<usize> {
573 Ok(42)
574 }
575
576 fn test_error_function() -> OperationResult<usize> {
577 Err(OperationError::NoPagesToProcess)
578 }
579
580 assert_eq!(test_function().unwrap(), 42);
581 assert!(test_error_function().is_err());
582 }
583
584 #[test]
585 fn test_page_range_boundary_values() {
586 let large_page = usize::MAX / 2;
588
589 match PageRange::parse(&large_page.to_string()).unwrap() {
590 PageRange::Single(idx) => assert_eq!(idx, large_page - 1),
591 _ => panic!("Expected Single"),
592 }
593
594 let indices = PageRange::Single(5).get_indices(10).unwrap();
596 assert_eq!(indices, vec![5]);
597
598 let indices = PageRange::Range(0, 9).get_indices(10).unwrap();
600 assert_eq!(indices.len(), 10);
601 }
602
603 #[test]
604 fn test_error_from_io() {
605 use std::io;
606
607 let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
608 let op_error: OperationError = io_error.into();
609
610 match op_error {
611 OperationError::Io(_) => {}
612 _ => panic!("Expected Io variant"),
613 }
614 }
615
616 #[test]
617 fn test_page_range_fmt_debug() {
618 let range = PageRange::All;
620 let debug_str = format!("{:?}", range);
621 assert!(debug_str.contains("All"));
622
623 let range = PageRange::Single(5);
624 let debug_str = format!("{:?}", range);
625 assert!(debug_str.contains("Single"));
626 assert!(debug_str.contains("5"));
627
628 let range = PageRange::Range(1, 10);
629 let debug_str = format!("{:?}", range);
630 assert!(debug_str.contains("Range"));
631
632 let range = PageRange::List(vec![1, 2, 3]);
633 let debug_str = format!("{:?}", range);
634 assert!(debug_str.contains("List"));
635 }
636
637 #[test]
638 fn test_page_range_clone() {
639 let original = PageRange::List(vec![1, 2, 3]);
640 let cloned = original.clone();
641
642 match (original, cloned) {
643 (PageRange::List(orig), PageRange::List(clone)) => {
644 assert_eq!(orig, clone);
645 }
646 _ => panic!("Clone failed"),
647 }
648 }
649}