1pub 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
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
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 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 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}