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