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 merge;
7pub mod rotate;
8pub mod split;
9
10pub use merge::{merge_pdf_files, merge_pdfs, MergeInput, MergeOptions, PdfMerger};
11pub use rotate::{rotate_all_pages, rotate_pdf_pages, PageRotator, RotateOptions, RotationAngle};
12pub use split::{split_into_pages, split_pdf, PdfSplitter, SplitMode, SplitOptions};
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
96                .trim()
97                .parse::<usize>()
98                .map_err(|_| OperationError::InvalidPageRange(format!("Invalid start: {start}")))?;
99            let end = end
100                .trim()
101                .parse::<usize>()
102                .map_err(|_| OperationError::InvalidPageRange(format!("Invalid end: {end}")))?;
103
104            if start == 0 || end == 0 {
105                return Err(OperationError::InvalidPageRange(
106                    "Page numbers start at 1".to_string(),
107                ));
108            }
109
110            if start > end {
111                return Err(OperationError::InvalidPageRange(format!(
112                    "Start {start} is greater than end {end}"
113                )));
114            }
115
116            return Ok(PageRange::Range(start - 1, end - 1));
117        }
118
119        // Try list (e.g., "1,3,5")
120        if s.contains(',') {
121            let pages: Result<Vec<usize>, _> = s
122                .split(',')
123                .map(|p| {
124                    let page = p.trim().parse::<usize>().map_err(|_| {
125                        OperationError::InvalidPageRange(format!("Invalid page: {p}"))
126                    })?;
127                    if page == 0 {
128                        return Err(OperationError::InvalidPageRange(
129                            "Page numbers start at 1".to_string(),
130                        ));
131                    }
132                    Ok(page - 1)
133                })
134                .collect();
135
136            return Ok(PageRange::List(pages?));
137        }
138
139        Err(OperationError::InvalidPageRange(format!(
140            "Invalid format: {s}"
141        )))
142    }
143
144    /// Get the page indices for this range
145    pub fn get_indices(&self, total_pages: usize) -> Result<Vec<usize>, OperationError> {
146        match self {
147            PageRange::All => Ok((0..total_pages).collect()),
148            PageRange::Single(idx) => {
149                if *idx >= total_pages {
150                    Err(OperationError::PageIndexOutOfBounds(*idx, total_pages))
151                } else {
152                    Ok(vec![*idx])
153                }
154            }
155            PageRange::Range(start, end) => {
156                if *start >= total_pages {
157                    Err(OperationError::PageIndexOutOfBounds(*start, total_pages))
158                } else if *end >= total_pages {
159                    Err(OperationError::PageIndexOutOfBounds(*end, total_pages))
160                } else {
161                    Ok((*start..=*end).collect())
162                }
163            }
164            PageRange::List(pages) => {
165                for &page in pages {
166                    if page >= total_pages {
167                        return Err(OperationError::PageIndexOutOfBounds(page, total_pages));
168                    }
169                }
170                Ok(pages.clone())
171            }
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_page_range_parsing() {
182        assert!(matches!(PageRange::parse("all").unwrap(), PageRange::All));
183        assert!(matches!(PageRange::parse("ALL").unwrap(), PageRange::All));
184
185        match PageRange::parse("5").unwrap() {
186            PageRange::Single(idx) => assert_eq!(idx, 4),
187            _ => panic!("Expected Single"),
188        }
189
190        match PageRange::parse("2-5").unwrap() {
191            PageRange::Range(start, end) => {
192                assert_eq!(start, 1);
193                assert_eq!(end, 4);
194            }
195            _ => panic!("Expected Range"),
196        }
197
198        match PageRange::parse("1,3,5,7").unwrap() {
199            PageRange::List(pages) => {
200                assert_eq!(pages, vec![0, 2, 4, 6]);
201            }
202            _ => panic!("Expected List"),
203        }
204
205        assert!(PageRange::parse("0").is_err());
206        assert!(PageRange::parse("5-2").is_err());
207        assert!(PageRange::parse("invalid").is_err());
208    }
209
210    #[test]
211    fn test_page_range_indices() {
212        let total = 10;
213
214        assert_eq!(
215            PageRange::All.get_indices(total).unwrap(),
216            vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
217        );
218
219        assert_eq!(PageRange::Single(5).get_indices(total).unwrap(), vec![5]);
220
221        assert_eq!(
222            PageRange::Range(2, 5).get_indices(total).unwrap(),
223            vec![2, 3, 4, 5]
224        );
225
226        assert_eq!(
227            PageRange::List(vec![1, 3, 5]).get_indices(total).unwrap(),
228            vec![1, 3, 5]
229        );
230
231        assert!(PageRange::Single(10).get_indices(total).is_err());
232        assert!(PageRange::Range(8, 15).get_indices(total).is_err());
233    }
234}