oxidize_pdf/operations/
mod.rs

1//! PDF operations module
2//! 
3//! This module provides high-level operations for manipulating PDF documents
4//! such as splitting, merging, rotating pages, and reordering.
5
6pub mod split;
7pub mod merge;
8pub mod rotate;
9
10pub use split::{PdfSplitter, SplitOptions, SplitMode, split_pdf, split_into_pages};
11pub use merge::{PdfMerger, MergeOptions, MergeInput, merge_pdfs, merge_pdf_files};
12pub use rotate::{PageRotator, RotationAngle, RotateOptions, rotate_pdf_pages, rotate_all_pages};
13
14use crate::error::PdfError;
15
16/// Result type for operations
17pub type OperationResult<T> = Result<T, OperationError>;
18
19/// Operation-specific errors
20#[derive(Debug, thiserror::Error)]
21pub enum OperationError {
22    /// Page index out of bounds
23    #[error("Page index {0} out of bounds (document has {1} pages)")]
24    PageIndexOutOfBounds(usize, usize),
25    
26    /// Invalid page range
27    #[error("Invalid page range: {0}")]
28    InvalidPageRange(String),
29    
30    /// No pages to process
31    #[error("No pages to process")]
32    NoPagesToProcess,
33    
34    /// Resource conflict during merge
35    #[error("Resource conflict: {0}")]
36    ResourceConflict(String),
37    
38    /// Invalid rotation angle
39    #[error("Invalid rotation angle: {0} (must be 0, 90, 180, or 270)")]
40    InvalidRotation(i32),
41    
42    /// Parse error
43    #[error("Parse error: {0}")]
44    ParseError(String),
45    
46    /// IO error
47    #[error("IO error: {0}")]
48    Io(#[from] std::io::Error),
49    
50    /// Core PDF error
51    #[error("PDF error: {0}")]
52    PdfError(#[from] PdfError),
53}
54
55/// Page range specification
56#[derive(Debug, Clone)]
57pub enum PageRange {
58    /// All pages
59    All,
60    /// Single page (0-based index)
61    Single(usize),
62    /// Range of pages (inclusive, 0-based)
63    Range(usize, usize),
64    /// List of specific pages (0-based indices)
65    List(Vec<usize>),
66}
67
68impl PageRange {
69    /// Parse a page range from a string
70    /// 
71    /// Examples:
72    /// - "all" -> All pages
73    /// - "1" -> Single page (converts to 0-based)
74    /// - "1-5" -> Range of pages (converts to 0-based)
75    /// - "1,3,5" -> List of pages (converts to 0-based)
76    pub fn parse(s: &str) -> Result<Self, OperationError> {
77        let s = s.trim();
78        
79        if s.eq_ignore_ascii_case("all") {
80            return Ok(PageRange::All);
81        }
82        
83        // Try single page
84        if let Ok(page) = s.parse::<usize>() {
85            if page == 0 {
86                return Err(OperationError::InvalidPageRange(
87                    "Page numbers start at 1".to_string()
88                ));
89            }
90            return Ok(PageRange::Single(page - 1));
91        }
92        
93        // Try range (e.g., "1-5")
94        if let Some((start, end)) = s.split_once('-') {
95            let start = start.trim().parse::<usize>()
96                .map_err(|_| OperationError::InvalidPageRange(format!("Invalid start: {}", start)))?;
97            let end = end.trim().parse::<usize>()
98                .map_err(|_| OperationError::InvalidPageRange(format!("Invalid end: {}", end)))?;
99            
100            if start == 0 || end == 0 {
101                return Err(OperationError::InvalidPageRange(
102                    "Page numbers start at 1".to_string()
103                ));
104            }
105            
106            if start > end {
107                return Err(OperationError::InvalidPageRange(
108                    format!("Start {} is greater than end {}", start, end)
109                ));
110            }
111            
112            return Ok(PageRange::Range(start - 1, end - 1));
113        }
114        
115        // Try list (e.g., "1,3,5")
116        if s.contains(',') {
117            let pages: Result<Vec<usize>, _> = s.split(',')
118                .map(|p| {
119                    let page = p.trim().parse::<usize>()
120                        .map_err(|_| OperationError::InvalidPageRange(
121                            format!("Invalid page: {}", p)
122                        ))?;
123                    if page == 0 {
124                        return Err(OperationError::InvalidPageRange(
125                            "Page numbers start at 1".to_string()
126                        ));
127                    }
128                    Ok(page - 1)
129                })
130                .collect();
131            
132            return Ok(PageRange::List(pages?));
133        }
134        
135        Err(OperationError::InvalidPageRange(format!("Invalid format: {}", s)))
136    }
137    
138    /// Get the page indices for this range
139    pub fn get_indices(&self, total_pages: usize) -> Result<Vec<usize>, OperationError> {
140        match self {
141            PageRange::All => Ok((0..total_pages).collect()),
142            PageRange::Single(idx) => {
143                if *idx >= total_pages {
144                    Err(OperationError::PageIndexOutOfBounds(*idx, total_pages))
145                } else {
146                    Ok(vec![*idx])
147                }
148            }
149            PageRange::Range(start, end) => {
150                if *start >= total_pages {
151                    Err(OperationError::PageIndexOutOfBounds(*start, total_pages))
152                } else if *end >= total_pages {
153                    Err(OperationError::PageIndexOutOfBounds(*end, total_pages))
154                } else {
155                    Ok((*start..=*end).collect())
156                }
157            }
158            PageRange::List(pages) => {
159                for &page in pages {
160                    if page >= total_pages {
161                        return Err(OperationError::PageIndexOutOfBounds(page, total_pages));
162                    }
163                }
164                Ok(pages.clone())
165            }
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    
174    #[test]
175    fn test_page_range_parsing() {
176        assert!(matches!(PageRange::parse("all").unwrap(), PageRange::All));
177        assert!(matches!(PageRange::parse("ALL").unwrap(), PageRange::All));
178        
179        match PageRange::parse("5").unwrap() {
180            PageRange::Single(idx) => assert_eq!(idx, 4),
181            _ => panic!("Expected Single"),
182        }
183        
184        match PageRange::parse("2-5").unwrap() {
185            PageRange::Range(start, end) => {
186                assert_eq!(start, 1);
187                assert_eq!(end, 4);
188            }
189            _ => panic!("Expected Range"),
190        }
191        
192        match PageRange::parse("1,3,5,7").unwrap() {
193            PageRange::List(pages) => {
194                assert_eq!(pages, vec![0, 2, 4, 6]);
195            }
196            _ => panic!("Expected List"),
197        }
198        
199        assert!(PageRange::parse("0").is_err());
200        assert!(PageRange::parse("5-2").is_err());
201        assert!(PageRange::parse("invalid").is_err());
202    }
203    
204    #[test]
205    fn test_page_range_indices() {
206        let total = 10;
207        
208        assert_eq!(
209            PageRange::All.get_indices(total).unwrap(),
210            vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
211        );
212        
213        assert_eq!(
214            PageRange::Single(5).get_indices(total).unwrap(),
215            vec![5]
216        );
217        
218        assert_eq!(
219            PageRange::Range(2, 5).get_indices(total).unwrap(),
220            vec![2, 3, 4, 5]
221        );
222        
223        assert_eq!(
224            PageRange::List(vec![1, 3, 5]).get_indices(total).unwrap(),
225            vec![1, 3, 5]
226        );
227        
228        assert!(PageRange::Single(10).get_indices(total).is_err());
229        assert!(PageRange::Range(8, 15).get_indices(total).is_err());
230    }
231}