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