1pub 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
16pub type OperationResult<T> = Result<T, OperationError>;
18
19#[derive(Debug, thiserror::Error)]
21pub enum OperationError {
22 #[error("Page index {0} out of bounds (document has {1} pages)")]
24 PageIndexOutOfBounds(usize, usize),
25
26 #[error("Invalid page range: {0}")]
28 InvalidPageRange(String),
29
30 #[error("No pages to process")]
32 NoPagesToProcess,
33
34 #[error("Resource conflict: {0}")]
36 ResourceConflict(String),
37
38 #[error("Invalid rotation angle: {0} (must be 0, 90, 180, or 270)")]
40 InvalidRotation(i32),
41
42 #[error("Parse error: {0}")]
44 ParseError(String),
45
46 #[error("IO error: {0}")]
48 Io(#[from] std::io::Error),
49
50 #[error("PDF error: {0}")]
52 PdfError(#[from] PdfError),
53}
54
55#[derive(Debug, Clone)]
57pub enum PageRange {
58 All,
60 Single(usize),
62 Range(usize, usize),
64 List(Vec<usize>),
66}
67
68impl PageRange {
69 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 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 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 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 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}