1pub mod extract_images;
7pub mod merge;
8pub mod page_analysis;
9pub mod page_extraction;
10pub mod reorder;
11pub mod rotate;
12pub mod split;
13
14pub use extract_images::{
15 extract_images_from_pages, extract_images_from_pdf, ExtractImagesOptions, ExtractedImage,
16 ImageExtractor,
17};
18pub use merge::{merge_pdf_files, merge_pdfs, MergeInput, MergeOptions, PdfMerger};
19pub use page_analysis::{AnalysisOptions, ContentAnalysis, PageContentAnalyzer, PageType};
20pub use page_extraction::{
21 extract_page, extract_page_range, extract_page_range_to_file, extract_page_to_file,
22 extract_pages, extract_pages_to_file, PageExtractionOptions, PageExtractor,
23};
24pub use reorder::{
25 move_pdf_page, reorder_pdf_pages, reverse_pdf_pages, swap_pdf_pages, PageReorderer,
26 ReorderOptions,
27};
28pub use rotate::{rotate_all_pages, rotate_pdf_pages, PageRotator, RotateOptions, RotationAngle};
29pub use split::{split_into_pages, split_pdf, PdfSplitter, SplitMode, SplitOptions};
30
31use crate::error::PdfError;
32
33pub type OperationResult<T> = Result<T, OperationError>;
35
36#[derive(Debug, thiserror::Error)]
38pub enum OperationError {
39 #[error("Page index {0} out of bounds (document has {1} pages)")]
41 PageIndexOutOfBounds(usize, usize),
42
43 #[error("Invalid page range: {0}")]
45 InvalidPageRange(String),
46
47 #[error("No pages to process")]
49 NoPagesToProcess,
50
51 #[error("Resource conflict: {0}")]
53 ResourceConflict(String),
54
55 #[error("Invalid rotation angle: {0} (must be 0, 90, 180, or 270)")]
57 InvalidRotation(i32),
58
59 #[error("Parse error: {0}")]
61 ParseError(String),
62
63 #[error("IO error: {0}")]
65 Io(#[from] std::io::Error),
66
67 #[error("PDF error: {0}")]
69 PdfError(#[from] PdfError),
70
71 #[error("Processing error: {0}")]
73 ProcessingError(String),
74}
75
76#[derive(Debug, Clone)]
78pub enum PageRange {
79 All,
81 Single(usize),
83 Range(usize, usize),
85 List(Vec<usize>),
87}
88
89impl PageRange {
90 pub fn parse(s: &str) -> Result<Self, OperationError> {
98 let s = s.trim();
99
100 if s.eq_ignore_ascii_case("all") {
101 return Ok(PageRange::All);
102 }
103
104 if let Ok(page) = s.parse::<usize>() {
106 if page == 0 {
107 return Err(OperationError::InvalidPageRange(
108 "Page numbers start at 1".to_string(),
109 ));
110 }
111 return Ok(PageRange::Single(page - 1));
112 }
113
114 if let Some((start, end)) = s.split_once('-') {
116 let start = start
117 .trim()
118 .parse::<usize>()
119 .map_err(|_| OperationError::InvalidPageRange(format!("Invalid start: {start}")))?;
120 let end = end
121 .trim()
122 .parse::<usize>()
123 .map_err(|_| OperationError::InvalidPageRange(format!("Invalid end: {end}")))?;
124
125 if start == 0 || end == 0 {
126 return Err(OperationError::InvalidPageRange(
127 "Page numbers start at 1".to_string(),
128 ));
129 }
130
131 if start > end {
132 return Err(OperationError::InvalidPageRange(format!(
133 "Start {start} is greater than end {end}"
134 )));
135 }
136
137 return Ok(PageRange::Range(start - 1, end - 1));
138 }
139
140 if s.contains(',') {
142 let pages: Result<Vec<usize>, _> = s
143 .split(',')
144 .map(|p| {
145 let page = p.trim().parse::<usize>().map_err(|_| {
146 OperationError::InvalidPageRange(format!("Invalid page: {p}"))
147 })?;
148 if page == 0 {
149 return Err(OperationError::InvalidPageRange(
150 "Page numbers start at 1".to_string(),
151 ));
152 }
153 Ok(page - 1)
154 })
155 .collect();
156
157 return Ok(PageRange::List(pages?));
158 }
159
160 Err(OperationError::InvalidPageRange(format!(
161 "Invalid format: {s}"
162 )))
163 }
164
165 pub fn get_indices(&self, total_pages: usize) -> Result<Vec<usize>, OperationError> {
167 match self {
168 PageRange::All => Ok((0..total_pages).collect()),
169 PageRange::Single(idx) => {
170 if *idx >= total_pages {
171 Err(OperationError::PageIndexOutOfBounds(*idx, total_pages))
172 } else {
173 Ok(vec![*idx])
174 }
175 }
176 PageRange::Range(start, end) => {
177 if *start >= total_pages {
178 Err(OperationError::PageIndexOutOfBounds(*start, total_pages))
179 } else if *end >= total_pages {
180 Err(OperationError::PageIndexOutOfBounds(*end, total_pages))
181 } else {
182 Ok((*start..=*end).collect())
183 }
184 }
185 PageRange::List(pages) => {
186 for &page in pages {
187 if page >= total_pages {
188 return Err(OperationError::PageIndexOutOfBounds(page, total_pages));
189 }
190 }
191 Ok(pages.clone())
192 }
193 }
194 }
195}
196
197#[cfg(test)]
198mod error_tests;
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_page_range_parsing() {
206 assert!(matches!(PageRange::parse("all").unwrap(), PageRange::All));
207 assert!(matches!(PageRange::parse("ALL").unwrap(), PageRange::All));
208
209 match PageRange::parse("5").unwrap() {
210 PageRange::Single(idx) => assert_eq!(idx, 4),
211 _ => panic!("Expected Single"),
212 }
213
214 match PageRange::parse("2-5").unwrap() {
215 PageRange::Range(start, end) => {
216 assert_eq!(start, 1);
217 assert_eq!(end, 4);
218 }
219 _ => panic!("Expected Range"),
220 }
221
222 match PageRange::parse("1,3,5,7").unwrap() {
223 PageRange::List(pages) => {
224 assert_eq!(pages, vec![0, 2, 4, 6]);
225 }
226 _ => panic!("Expected List"),
227 }
228
229 assert!(PageRange::parse("0").is_err());
230 assert!(PageRange::parse("5-2").is_err());
231 assert!(PageRange::parse("invalid").is_err());
232 }
233
234 #[test]
235 fn test_page_range_indices() {
236 let total = 10;
237
238 assert_eq!(
239 PageRange::All.get_indices(total).unwrap(),
240 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
241 );
242
243 assert_eq!(PageRange::Single(5).get_indices(total).unwrap(), vec![5]);
244
245 assert_eq!(
246 PageRange::Range(2, 5).get_indices(total).unwrap(),
247 vec![2, 3, 4, 5]
248 );
249
250 assert_eq!(
251 PageRange::List(vec![1, 3, 5]).get_indices(total).unwrap(),
252 vec![1, 3, 5]
253 );
254
255 assert!(PageRange::Single(10).get_indices(total).is_err());
256 assert!(PageRange::Range(8, 15).get_indices(total).is_err());
257 }
258
259 #[test]
260 fn test_module_exports() {
261 use super::extract_images::ExtractImagesOptions;
266 use super::merge::MergeOptions;
267 use super::page_analysis::{AnalysisOptions, PageType};
268 use super::page_extraction::PageExtractionOptions;
269 use super::rotate::{RotateOptions, RotationAngle};
270 use super::split::{SplitOptions, SplitMode};
271
272 let _extract: ExtractImagesOptions;
274 let _merge: MergeOptions;
275 let _analysis: AnalysisOptions;
276 let _extraction: PageExtractionOptions;
277 let _rotate: RotateOptions;
278 let _split: SplitOptions;
279 let _angle: RotationAngle;
280 let _page_type: PageType;
281 let _mode: SplitMode;
282 }
283
284 #[test]
285 fn test_operation_error_variants() {
286 let errors = vec![
287 OperationError::PageIndexOutOfBounds(5, 3),
288 OperationError::InvalidPageRange("test".to_string()),
289 OperationError::NoPagesToProcess,
290 OperationError::ResourceConflict("test".to_string()),
291 OperationError::InvalidRotation(45),
292 OperationError::ParseError("test".to_string()),
293 OperationError::ProcessingError("test".to_string()),
294 ];
295
296 for error in errors {
297 let message = error.to_string();
298 assert!(!message.is_empty());
299 }
300 }
301}