1use super::{OperationError, OperationResult, PageRange};
7use crate::parser::page_tree::ParsedPage;
8use crate::parser::{PdfDocument, PdfReader};
9use crate::{Document, Page};
10use std::fs::File;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone)]
15pub struct SplitOptions {
16 pub mode: SplitMode,
18 pub output_pattern: String,
20 pub preserve_metadata: bool,
22 pub optimize: bool,
24}
25
26impl Default for SplitOptions {
27 fn default() -> Self {
28 Self {
29 mode: SplitMode::SinglePages,
30 output_pattern: "page_{}.pdf".to_string(),
31 preserve_metadata: true,
32 optimize: false,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub enum SplitMode {
40 SinglePages,
42 Ranges(Vec<PageRange>),
44 ChunkSize(usize),
46 SplitAt(Vec<usize>),
48}
49
50pub struct PdfSplitter {
52 document: PdfDocument<File>,
53 options: SplitOptions,
54}
55
56impl PdfSplitter {
57 pub fn new(document: PdfDocument<File>, options: SplitOptions) -> Self {
59 Self { document, options }
60 }
61
62 pub fn split(&mut self) -> OperationResult<Vec<PathBuf>> {
64 let total_pages =
65 self.document
66 .page_count()
67 .map_err(|e| OperationError::ParseError(e.to_string()))? as usize;
68
69 if total_pages == 0 {
70 return Err(OperationError::NoPagesToProcess);
71 }
72
73 let ranges = match &self.options.mode {
74 SplitMode::SinglePages => {
75 (0..total_pages).map(PageRange::Single).collect()
77 }
78 SplitMode::Ranges(ranges) => ranges.clone(),
79 SplitMode::ChunkSize(size) => {
80 let mut ranges = Vec::new();
82 let mut start = 0;
83 while start < total_pages {
84 let end = (start + size - 1).min(total_pages - 1);
85 ranges.push(PageRange::Range(start, end));
86 start += size;
87 }
88 ranges
89 }
90 SplitMode::SplitAt(split_points) => {
91 let mut ranges = Vec::new();
93 let mut start = 0;
94
95 for &split_point in split_points {
96 if split_point > 0 && split_point < total_pages {
97 ranges.push(PageRange::Range(start, split_point - 1));
98 start = split_point;
99 }
100 }
101
102 if start < total_pages {
104 ranges.push(PageRange::Range(start, total_pages - 1));
105 }
106
107 ranges
108 }
109 };
110
111 let mut output_files = Vec::new();
113
114 for (index, range) in ranges.iter().enumerate() {
115 let output_path = self.format_output_path(index, range);
116 self.extract_range(range, &output_path)?;
117 output_files.push(output_path);
118 }
119
120 Ok(output_files)
121 }
122
123 fn extract_range(&mut self, range: &PageRange, output_path: &Path) -> OperationResult<()> {
125 let total_pages =
126 self.document
127 .page_count()
128 .map_err(|e| OperationError::ParseError(e.to_string()))? as usize;
129
130 let indices = range.get_indices(total_pages)?;
131 if indices.is_empty() {
132 return Err(OperationError::NoPagesToProcess);
133 }
134
135 let mut doc = Document::new();
137
138 if self.options.preserve_metadata {
140 if let Ok(metadata) = self.document.metadata() {
141 if let Some(title) = metadata.title {
142 doc.set_title(&title);
143 }
144 if let Some(author) = metadata.author {
145 doc.set_author(&author);
146 }
147 if let Some(subject) = metadata.subject {
148 doc.set_subject(&subject);
149 }
150 if let Some(keywords) = metadata.keywords {
151 doc.set_keywords(&keywords);
152 }
153 }
154 }
155
156 for &page_idx in &indices {
158 let parsed_page = self
159 .document
160 .get_page(page_idx as u32)
161 .map_err(|e| OperationError::ParseError(e.to_string()))?;
162
163 let page = self.convert_page(&parsed_page)?;
164 doc.add_page(page);
165 }
166
167 doc.save(output_path)?;
169
170 Ok(())
171 }
172
173 fn convert_page(&mut self, parsed_page: &ParsedPage) -> OperationResult<Page> {
183 Page::from_parsed_with_content(parsed_page, &self.document)
184 .map_err(|e| OperationError::ParseError(e.to_string()))
185 }
186
187 fn format_output_path(&self, index: usize, range: &PageRange) -> PathBuf {
189 let filename = match range {
190 PageRange::Single(page) => self
191 .options
192 .output_pattern
193 .replace("{}", &(page + 1).to_string())
194 .replace("{n}", &(index + 1).to_string())
195 .replace("{page}", &(page + 1).to_string()),
196 PageRange::Range(start, end) => self
197 .options
198 .output_pattern
199 .replace("{}", &format!("{}-{}", start + 1, end + 1))
200 .replace("{n}", &(index + 1).to_string())
201 .replace("{start}", &(start + 1).to_string())
202 .replace("{end}", &(end + 1).to_string()),
203 _ => self
204 .options
205 .output_pattern
206 .replace("{}", &(index + 1).to_string())
207 .replace("{n}", &(index + 1).to_string()),
208 };
209
210 PathBuf::from(filename)
211 }
212}
213
214pub fn split_pdf<P: AsRef<Path>>(
216 input_path: P,
217 options: SplitOptions,
218) -> OperationResult<Vec<PathBuf>> {
219 let document = PdfReader::open_document(input_path)
220 .map_err(|e| OperationError::ParseError(e.to_string()))?;
221
222 let mut splitter = PdfSplitter::new(document, options);
223 splitter.split()
224}
225
226pub fn split_into_pages<P: AsRef<Path>>(
228 input_path: P,
229 output_pattern: &str,
230) -> OperationResult<Vec<PathBuf>> {
231 let options = SplitOptions {
232 mode: SplitMode::SinglePages,
233 output_pattern: output_pattern.to_string(),
234 ..Default::default()
235 };
236
237 split_pdf(input_path, options)
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_split_options_default() {
246 let options = SplitOptions::default();
247 assert!(matches!(options.mode, SplitMode::SinglePages));
248 assert_eq!(options.output_pattern, "page_{}.pdf");
249 assert!(options.preserve_metadata);
250 assert!(!options.optimize);
251 }
252
253 #[test]
254 fn test_format_output_path() {
255 let _options = SplitOptions {
256 output_pattern: "output_page_{}.pdf".to_string(),
257 ..Default::default()
258 };
259
260 let _reader = PdfReader::open("test.pdf");
261 }
264
265 #[test]
268 fn test_split_mode_variants() {
269 let single_pages = SplitMode::SinglePages;
271 assert!(matches!(single_pages, SplitMode::SinglePages));
272
273 let ranges = SplitMode::Ranges(vec![
275 super::PageRange::Single(0),
276 super::PageRange::Range(5, 10),
277 ]);
278 assert!(matches!(ranges, SplitMode::Ranges(_)));
279
280 let chunk = SplitMode::ChunkSize(5);
282 if let SplitMode::ChunkSize(size) = chunk {
283 assert_eq!(size, 5);
284 } else {
285 panic!("Expected ChunkSize");
286 }
287
288 let split_at = SplitMode::SplitAt(vec![5, 10, 15]);
290 assert!(matches!(split_at, SplitMode::SplitAt(_)));
291 }
292
293 #[test]
294 fn test_split_options_with_modes() {
295 let options = SplitOptions {
296 mode: SplitMode::ChunkSize(10),
297 output_pattern: "chunk_{}.pdf".to_string(),
298 preserve_metadata: true,
299 optimize: true,
300 };
301
302 assert!(matches!(options.mode, SplitMode::ChunkSize(10)));
303 assert_eq!(options.output_pattern, "chunk_{}.pdf");
304 assert!(options.preserve_metadata);
305 assert!(options.optimize);
306 }
307
308 #[test]
309 fn test_split_options_page_range() {
310 let ranges = vec![
311 super::PageRange::All,
312 super::PageRange::Single(5),
313 super::PageRange::Range(10, 20),
314 super::PageRange::List(vec![1, 3, 5, 7, 9]),
315 ];
316
317 let options = SplitOptions {
318 mode: SplitMode::Ranges(ranges),
319 ..Default::default()
320 };
321
322 if let SplitMode::Ranges(r) = options.mode {
323 assert_eq!(r.len(), 4);
324 } else {
325 panic!("Expected Ranges mode");
326 }
327 }
328
329 #[test]
330 fn test_split_options_split_at() {
331 let split_points = vec![3, 6, 9, 12]; let options = SplitOptions {
334 mode: SplitMode::SplitAt(split_points.clone()),
335 output_pattern: "part_{}.pdf".to_string(),
336 ..Default::default()
337 };
338
339 if let SplitMode::SplitAt(points) = options.mode {
340 assert_eq!(points.len(), 4);
341 assert_eq!(points, split_points);
342 } else {
343 panic!("Expected SplitAt mode");
344 }
345 }
346
347 #[test]
348 fn test_output_pattern_formatting() {
349 let patterns = vec![
351 "output_{}.pdf",
352 "page_{}.pdf",
353 "document_part_{}.pdf",
354 "{}_split.pdf",
355 ];
356
357 for pattern in patterns {
358 let options = SplitOptions {
359 output_pattern: pattern.to_string(),
360 ..Default::default()
361 };
362 assert!(options.output_pattern.contains("{")); }
364 }
365
366 #[test]
367 fn test_split_options_preserve_metadata() {
368 let with_metadata = SplitOptions {
370 preserve_metadata: true,
371 ..Default::default()
372 };
373 assert!(with_metadata.preserve_metadata);
374
375 let without_metadata = SplitOptions {
376 preserve_metadata: false,
377 ..Default::default()
378 };
379 assert!(!without_metadata.preserve_metadata);
380 }
381
382 #[test]
383 fn test_split_single_pages_mode() {
384 let options = SplitOptions {
385 mode: SplitMode::SinglePages,
386 output_pattern: "page_{:04}.pdf".to_string(),
387 ..Default::default()
388 };
389
390 assert!(matches!(options.mode, SplitMode::SinglePages));
391 assert!(options.output_pattern.contains("{"));
392 }
393
394 #[test]
395 fn test_split_chunk_size_validation() {
396 let chunk_sizes = vec![1, 5, 10, 50, 100];
398
399 for size in chunk_sizes {
400 let options = SplitOptions {
401 mode: SplitMode::ChunkSize(size),
402 ..Default::default()
403 };
404
405 if let SplitMode::ChunkSize(s) = options.mode {
406 assert_eq!(s, size);
407 assert!(s > 0); }
409 }
410 }
411
412 #[test]
413 fn test_split_options_optimization() {
414 let optimized = SplitOptions {
415 optimize: true,
416 ..Default::default()
417 };
418 assert!(optimized.optimize);
419
420 let not_optimized = SplitOptions {
421 optimize: false,
422 ..Default::default()
423 };
424 assert!(!not_optimized.optimize);
425 }
426
427 #[test]
428 fn test_split_options_with_custom_pattern() {
429 let options = SplitOptions {
430 output_pattern: "document_part_{}.pdf".to_string(),
431 ..Default::default()
432 };
433 assert_eq!(options.output_pattern, "document_part_{}.pdf");
434 }
435
436 #[test]
437 fn test_split_mode_ranges() {
438 let ranges = vec![
439 PageRange::Single(0),
440 PageRange::Range(1, 3),
441 PageRange::Single(5),
442 ];
443 let mode = SplitMode::Ranges(ranges);
444
445 match mode {
446 SplitMode::Ranges(r) => {
447 assert_eq!(r.len(), 3);
448 assert!(matches!(r[0], PageRange::Single(0)));
449 assert!(matches!(r[1], PageRange::Range(1, 3)));
450 assert!(matches!(r[2], PageRange::Single(5)));
451 }
452 _ => panic!("Wrong mode"),
453 }
454 }
455
456 #[test]
457 fn test_split_mode_split_at() {
458 let split_points = vec![5, 10, 15];
459 let mode = SplitMode::SplitAt(split_points.clone());
460
461 match mode {
462 SplitMode::SplitAt(points) => assert_eq!(points, split_points),
463 _ => panic!("Wrong mode"),
464 }
465 }
466
467 #[test]
468 fn test_page_range_parse() {
469 let range = PageRange::parse("all").unwrap();
471 assert!(matches!(range, PageRange::All));
472
473 let range = PageRange::parse("5").unwrap();
475 assert!(matches!(range, PageRange::Single(4))); let range = PageRange::parse("3-7").unwrap();
479 assert!(matches!(range, PageRange::Range(2, 6))); let range = PageRange::parse("1,3,5").unwrap();
483 match range {
484 PageRange::List(pages) => assert_eq!(pages, vec![0, 2, 4]),
485 _ => panic!("Expected List"),
486 }
487 }
488
489 #[test]
490 fn test_page_range_invalid_parse() {
491 assert!(PageRange::parse("").is_err());
492 assert!(PageRange::parse("abc").is_err());
493 assert!(PageRange::parse("5-3").is_err()); assert!(PageRange::parse("0").is_err()); }
496
497 #[test]
498 fn test_split_options_all_fields() {
499 let options = SplitOptions {
500 mode: SplitMode::ChunkSize(5),
501 output_pattern: "chunk_{}.pdf".to_string(),
502 preserve_metadata: false,
503 optimize: true,
504 };
505
506 match options.mode {
507 SplitMode::ChunkSize(size) => assert_eq!(size, 5),
508 _ => panic!("Wrong mode"),
509 }
510 assert_eq!(options.output_pattern, "chunk_{}.pdf");
511 assert!(!options.preserve_metadata);
512 assert!(options.optimize);
513 }
514
515 #[test]
516 fn test_split_mode_chunk_size_edge_cases() {
517 let mode = SplitMode::ChunkSize(1);
519 match mode {
520 SplitMode::ChunkSize(size) => assert_eq!(size, 1),
521 _ => panic!("Wrong mode"),
522 }
523
524 let mode = SplitMode::ChunkSize(1000);
526 match mode {
527 SplitMode::ChunkSize(size) => assert_eq!(size, 1000),
528 _ => panic!("Wrong mode"),
529 }
530 }
531
532 #[test]
533 fn test_split_mode_empty_ranges() {
534 let ranges = Vec::new();
535 let mode = SplitMode::Ranges(ranges);
536
537 match mode {
538 SplitMode::Ranges(r) => assert!(r.is_empty()),
539 _ => panic!("Wrong mode"),
540 }
541 }
542
543 #[test]
544 fn test_split_mode_empty_split_points() {
545 let split_points = Vec::new();
546 let mode = SplitMode::SplitAt(split_points);
547
548 match mode {
549 SplitMode::SplitAt(points) => assert!(points.is_empty()),
550 _ => panic!("Wrong mode"),
551 }
552 }
553}
554
555#[cfg(test)]
556#[path = "split_tests.rs"]
557mod split_tests;