1use std::collections::HashSet;
35
36use lopdf::{Dictionary, Document, Object, ObjectId};
37
38use crate::inspect::{deref_obj, load_pdf, resolve_rotation};
39use crate::merge::{load, materialize_inherited, MergeError, SharedValues};
40
41#[derive(Debug, thiserror::Error)]
42pub enum OrganizeError {
43 #[error("document has no pages")]
44 Empty,
45 #[error("input could not be parsed: {0}")]
46 Parse(String),
47 #[error("input is encrypted")]
48 Encrypted,
49 #[error("page {0} is out of range")]
50 PageOutOfRange(u32),
51 #[error("page order is not a permutation of 1..={0}")]
52 NotAPermutation(u32),
53 #[error("deleting these pages would leave no page")]
54 DeletesEveryPage,
55 #[error("rotation {0} is not a multiple of 90 degrees")]
56 InvalidRotation(i32),
57 #[error("insert position {at} is past the end of a {pages}-page document")]
58 InvalidPosition { at: u32, pages: u32 },
59 #[error("no pages selected")]
60 NoPagesSelected,
61 #[error("result failed verification: {0}")]
62 Verification(String),
63 #[error("serialize failed: {0}")]
64 Write(String),
65}
66
67impl OrganizeError {
68 pub fn code(&self) -> &'static str {
70 match self {
71 OrganizeError::Empty => "empty",
72 OrganizeError::Parse(_) => "parse",
73 OrganizeError::Encrypted => "encrypted",
74 OrganizeError::PageOutOfRange(_) => "page_out_of_range",
75 OrganizeError::NotAPermutation(_) => "not_a_permutation",
76 OrganizeError::DeletesEveryPage => "deletes_every_page",
77 OrganizeError::InvalidRotation(_) => "invalid_rotation",
78 OrganizeError::InvalidPosition { .. } => "invalid_position",
79 OrganizeError::NoPagesSelected => "no_pages_selected",
80 OrganizeError::Verification(_) => "verification_failed",
81 OrganizeError::Write(_) => "write",
82 }
83 }
84}
85
86fn from_merge(error: MergeError) -> OrganizeError {
87 match error {
88 MergeError::Encrypted(_) => OrganizeError::Encrypted,
89 MergeError::Empty => OrganizeError::Empty,
90 other => OrganizeError::Parse(other.to_string()),
91 }
92}
93
94struct Open {
96 doc: Document,
97 pages: Vec<ObjectId>,
98}
99
100fn open(index: usize, bytes: &[u8]) -> Result<Open, OrganizeError> {
101 let mut doc = load(index, bytes).map_err(from_merge)?;
102 let pages: Vec<ObjectId> = doc.get_pages().into_values().collect();
103 if pages.is_empty() {
104 return Err(OrganizeError::Empty);
105 }
106 let mut shared = SharedValues::new();
107 for id in &pages {
108 materialize_inherited(&mut doc, *id, &mut shared);
109 }
110 Ok(Open { doc, pages })
111}
112
113#[derive(Clone, Debug)]
119struct LabelRange {
120 start: usize,
121 dict: Dictionary,
122}
123
124const MAX_TREE_DEPTH: usize = 32;
125
126fn collect_nums(
127 doc: &Document,
128 node: &Dictionary,
129 out: &mut Vec<LabelRange>,
130 depth: usize,
131 visited: &mut HashSet<ObjectId>,
132) {
133 if depth > MAX_TREE_DEPTH {
134 return;
135 }
136 if let Some(Object::Array(nums)) = node.get(b"Nums").ok().and_then(|o| deref_obj(doc, o)) {
137 for pair in nums.chunks(2) {
138 let [key, value] = pair else { continue };
139 let start = deref_obj(doc, key).and_then(|o| o.as_i64().ok());
140 let dict = deref_obj(doc, value).and_then(|o| o.as_dict().ok());
141 if let (Some(start), Some(dict)) = (start, dict) {
142 if start >= 0 {
143 out.push(LabelRange {
144 start: start as usize,
145 dict: dict.clone(),
146 });
147 }
148 }
149 }
150 }
151 if let Some(Object::Array(kids)) = node.get(b"Kids").ok().and_then(|o| deref_obj(doc, o)) {
152 for kid in kids {
153 let Object::Reference(id) = kid else { continue };
154 if !visited.insert(*id) {
155 continue;
156 }
157 if let Ok(Object::Dictionary(child)) = doc.get_object(*id) {
158 collect_nums(doc, child, out, depth + 1, visited);
159 }
160 }
161 }
162}
163
164fn read_labels(doc: &Document) -> Vec<LabelRange> {
165 let mut out = Vec::new();
166 let tree = doc
167 .catalog()
168 .ok()
169 .and_then(|c| c.get(b"PageLabels").ok())
170 .and_then(|o| deref_obj(doc, o))
171 .and_then(|o| o.as_dict().ok());
172 if let Some(tree) = tree {
173 collect_nums(doc, tree, &mut out, 0, &mut HashSet::new());
174 }
175 out.sort_by_key(|range| range.start);
176 out.dedup_by_key(|range| range.start);
177 out
178}
179
180fn range_for(ranges: &[LabelRange], index: usize) -> (usize, Dictionary) {
183 match ranges.iter().rev().find(|range| range.start <= index) {
184 Some(range) => (range.start, range.dict.clone()),
185 None => {
186 let mut dict = Dictionary::new();
187 dict.set("S", Object::Name(b"D".to_vec()));
188 (0, dict)
189 }
190 }
191}
192
193#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195enum Origin {
196 Dest(usize),
198 Source(usize),
200}
201
202fn build_labels(
205 plan: &[Origin],
206 dest: &[LabelRange],
207 source: &[LabelRange],
208) -> Option<Vec<LabelRange>> {
209 if dest.is_empty() && source.is_empty() {
210 return None;
211 }
212 let mut out: Vec<LabelRange> = Vec::new();
213 let mut previous: Option<(Origin, usize)> = None;
214 for (new_index, origin) in plan.iter().enumerate() {
215 let (ranges, old_index) = match origin {
216 Origin::Dest(i) => (dest, *i),
217 Origin::Source(i) => (source, *i),
218 };
219 let (range_start, dict) = range_for(ranges, old_index);
220 let continues = match (previous, origin) {
221 (Some((Origin::Dest(p), start)), Origin::Dest(i)) => {
222 p + 1 == *i && start == range_start
223 }
224 (Some((Origin::Source(p), start)), Origin::Source(i)) => {
225 p + 1 == *i && start == range_start
226 }
227 _ => false,
228 };
229 if !continues {
230 let mut dict = dict;
232 let base = dict
233 .get(b"St")
234 .ok()
235 .and_then(|o| o.as_i64().ok())
236 .unwrap_or(1);
237 dict.set(
238 "St",
239 Object::Integer(base + old_index.saturating_sub(range_start) as i64),
240 );
241 out.push(LabelRange {
242 start: new_index,
243 dict,
244 });
245 }
246 previous = Some((*origin, range_start));
247 }
248 Some(out)
249}
250
251fn labels_object(ranges: &[LabelRange]) -> Object {
252 let mut nums = Vec::with_capacity(ranges.len() * 2);
253 for range in ranges {
254 nums.push(Object::Integer(range.start as i64));
255 nums.push(Object::Dictionary(range.dict.clone()));
256 }
257 let mut tree = Dictionary::new();
258 tree.set("Nums", Object::Array(nums));
259 Object::Dictionary(tree)
260}
261
262fn outline_items(doc: &Document) -> Vec<ObjectId> {
268 let root = doc
269 .catalog()
270 .ok()
271 .and_then(|c| c.get(b"Outlines").ok())
272 .and_then(|o| o.as_reference().ok());
273 let mut items = Vec::new();
274 let mut seen: HashSet<ObjectId> = HashSet::new();
275 let mut stack: Vec<ObjectId> = Vec::new();
276 if let Some(root) = root {
277 if let Ok(dict) = doc.get_dictionary(root) {
278 if let Ok(Object::Reference(first)) = dict.get(b"First") {
279 stack.push(*first);
280 }
281 }
282 }
283 while let Some(id) = stack.pop() {
284 if !seen.insert(id) || items.len() >= 1_000_000 {
285 continue;
286 }
287 let Ok(dict) = doc.get_dictionary(id) else {
288 continue;
289 };
290 items.push(id);
291 for key in [&b"First"[..], b"Next"] {
292 if let Ok(Object::Reference(next)) = dict.get(key) {
293 stack.push(*next);
294 }
295 }
296 }
297 items
298}
299
300fn destination_is_dead(destination: Option<&Object>) -> bool {
302 matches!(destination, Some(Object::Array(items)) if matches!(items.first(), Some(Object::Null)))
303}
304
305fn clean_dead_destinations(doc: &mut Document) {
306 for id in outline_items(doc) {
307 let Ok(Object::Dictionary(item)) = doc.get_object_mut(id) else {
308 continue;
309 };
310 if destination_is_dead(item.get(b"Dest").ok()) {
311 item.remove(b"Dest");
312 }
313 let dead_action = match item.get(b"A") {
314 Ok(Object::Dictionary(action)) => {
315 action
316 .get(b"S")
317 .ok()
318 .and_then(|s| s.as_name().ok())
319 .is_some_and(|name| name == &b"GoTo"[..])
320 && destination_is_dead(action.get(b"D").ok())
321 }
322 _ => false,
323 };
324 if dead_action {
325 item.remove(b"A");
326 }
327 }
328}
329
330fn root_pages_id(doc: &Document) -> Option<ObjectId> {
335 doc.catalog()
336 .ok()
337 .and_then(|c| c.get(b"Pages").ok())
338 .and_then(|o| o.as_reference().ok())
339}
340
341fn finish(
344 mut doc: Document,
345 new_pages: &[ObjectId],
346 removed: &HashSet<ObjectId>,
347 labels: Option<Vec<LabelRange>>,
348) -> Result<Vec<u8>, OrganizeError> {
349 let root_id = doc
350 .trailer
351 .get(b"Root")
352 .ok()
353 .and_then(|o| o.as_reference().ok())
354 .ok_or_else(|| OrganizeError::Parse("no catalog".to_string()))?;
355 let pages_id = match root_pages_id(&doc) {
356 Some(id) => id,
357 None => {
358 let id = doc.new_object_id();
359 if let Ok(Object::Dictionary(catalog)) = doc.get_object_mut(root_id) {
360 catalog.set("Pages", Object::Reference(id));
361 }
362 id
363 }
364 };
365 let kids: Vec<Object> = new_pages.iter().map(|id| Object::Reference(*id)).collect();
366 let mut node = Dictionary::new();
367 node.set("Type", Object::Name(b"Pages".to_vec()));
368 node.set("Count", Object::Integer(kids.len() as i64));
369 node.set("Kids", Object::Array(kids));
370 doc.objects.insert(pages_id, Object::Dictionary(node));
371 for id in new_pages {
372 if let Ok(Object::Dictionary(page)) = doc.get_object_mut(*id) {
373 page.set("Parent", Object::Reference(pages_id));
374 }
375 }
376 if let Some(ranges) = labels {
377 if let Ok(Object::Dictionary(catalog)) = doc.get_object_mut(root_id) {
378 if ranges.is_empty() {
379 catalog.remove(b"PageLabels");
380 } else {
381 catalog.set("PageLabels", labels_object(&ranges));
382 }
383 }
384 }
385 if !removed.is_empty() {
386 let _ = doc.traverse_objects(|object| {
387 if let Object::Reference(id) = object {
388 if removed.contains(&*id) {
389 *object = Object::Null;
390 }
391 }
392 });
393 clean_dead_destinations(&mut doc);
394 }
395 doc.prune_objects();
396 let mut bytes = Vec::new();
397 doc.save_to(&mut bytes)
398 .map_err(|e| OrganizeError::Write(e.to_string()))?;
399 verify_structure(&bytes, new_pages.len())?;
400 Ok(bytes)
401}
402
403pub fn verify_structure(bytes: &[u8], expected_pages: usize) -> Result<(), OrganizeError> {
406 let fail = |message: String| OrganizeError::Verification(message);
407 let doc = load_pdf(bytes).map_err(|e| fail(format!("reload failed: {e}")))?;
408 let pages = doc.get_pages();
409 if pages.len() != expected_pages {
410 return Err(fail(format!(
411 "expected {expected_pages} pages, found {}",
412 pages.len()
413 )));
414 }
415 let root = root_pages_id(&doc).ok_or_else(|| fail("catalog has no /Pages".to_string()))?;
416 let mut seen: HashSet<ObjectId> = HashSet::new();
417 for (number, id) in &pages {
418 if !seen.insert(*id) {
419 return Err(fail(format!("page {number} repeats a page object")));
420 }
421 let dict = doc
422 .get_dictionary(*id)
423 .map_err(|_| fail(format!("page {number} is not a dictionary")))?;
424 if !dict.has_type(b"Page") {
425 return Err(fail(format!("page {number} is not a /Page")));
426 }
427 let parent = dict.get(b"Parent").ok().and_then(|p| p.as_reference().ok());
428 if parent != Some(root) {
429 return Err(fail(format!("page {number} is not under the root /Pages")));
430 }
431 }
432 let count = doc
433 .get_dictionary(root)
434 .ok()
435 .and_then(|d| d.get(b"Count").ok())
436 .and_then(|c| c.as_i64().ok());
437 if count != Some(expected_pages as i64) {
438 return Err(fail("root /Pages /Count is wrong".to_string()));
439 }
440 for id in outline_items(&doc) {
441 let Ok(item) = doc.get_dictionary(id) else {
442 continue;
443 };
444 let mut destinations: Vec<&Object> = Vec::new();
445 if let Ok(destination) = item.get(b"Dest") {
446 destinations.push(destination);
447 }
448 if let Ok(Object::Dictionary(action)) = item.get(b"A") {
449 if let Ok(destination) = action.get(b"D") {
450 destinations.push(destination);
451 }
452 }
453 for destination in destinations {
454 if let Object::Array(array) = destination {
455 if let Some(Object::Reference(page)) = array.first() {
456 if !seen.contains(page) {
457 return Err(fail("an outline item points at a missing page".to_string()));
458 }
459 }
460 }
461 }
462 }
463 let labels = read_labels(&doc);
464 if labels.iter().any(|range| range.start >= expected_pages) {
465 return Err(fail(
466 "a page-label range starts past the last page".to_string(),
467 ));
468 }
469 Ok(())
470}
471
472pub fn delete_pages(source: &[u8], pages: &[u32]) -> Result<Vec<u8>, OrganizeError> {
478 let Open { doc, pages: all } = open(0, source)?;
479 let count = all.len();
480 let mut drop: HashSet<usize> = HashSet::new();
481 for &page in pages {
482 if page == 0 || page as usize > count {
483 return Err(OrganizeError::PageOutOfRange(page));
484 }
485 drop.insert(page as usize - 1);
486 }
487 if drop.is_empty() {
488 return Err(OrganizeError::NoPagesSelected);
489 }
490 if drop.len() == count {
491 return Err(OrganizeError::DeletesEveryPage);
492 }
493 let dest_labels = read_labels(&doc);
494 let mut kept = Vec::new();
495 let mut plan = Vec::new();
496 let mut removed = HashSet::new();
497 for (index, id) in all.iter().enumerate() {
498 if drop.contains(&index) {
499 removed.insert(*id);
500 } else {
501 kept.push(*id);
502 plan.push(Origin::Dest(index));
503 }
504 }
505 let labels = build_labels(&plan, &dest_labels, &[]);
506 finish(doc, &kept, &removed, labels)
507}
508
509pub fn reorder_pages(source: &[u8], order: &[u32]) -> Result<Vec<u8>, OrganizeError> {
512 let Open { doc, pages: all } = open(0, source)?;
513 let count = all.len();
514 let mut seen = vec![false; count];
515 if order.len() != count {
516 return Err(OrganizeError::NotAPermutation(count as u32));
517 }
518 for &page in order {
519 let index = (page as usize)
520 .checked_sub(1)
521 .filter(|index| *index < count)
522 .ok_or(OrganizeError::NotAPermutation(count as u32))?;
523 if std::mem::replace(&mut seen[index], true) {
524 return Err(OrganizeError::NotAPermutation(count as u32));
525 }
526 }
527 let dest_labels = read_labels(&doc);
528 let plan: Vec<Origin> = order
529 .iter()
530 .map(|page| Origin::Dest(*page as usize - 1))
531 .collect();
532 let new_pages: Vec<ObjectId> = order.iter().map(|page| all[*page as usize - 1]).collect();
533 let labels = build_labels(&plan, &dest_labels, &[]);
534 finish(doc, &new_pages, &HashSet::new(), labels)
535}
536
537pub fn rotate_pages(source: &[u8], pages: &[u32], degrees: i32) -> Result<Vec<u8>, OrganizeError> {
540 if degrees % 90 != 0 {
541 return Err(OrganizeError::InvalidRotation(degrees));
542 }
543 let Open {
544 mut doc,
545 pages: all,
546 } = open(0, source)?;
547 if pages.is_empty() {
548 return Err(OrganizeError::NoPagesSelected);
549 }
550 let mut targets: Vec<usize> = Vec::new();
551 for &page in pages {
552 if page == 0 || page as usize > all.len() {
553 return Err(OrganizeError::PageOutOfRange(page));
554 }
555 if !targets.contains(&(page as usize - 1)) {
556 targets.push(page as usize - 1);
557 }
558 }
559 for index in targets {
560 let id = all[index];
561 let current = doc
562 .get_object(id)
563 .ok()
564 .and_then(|o| o.as_dict().ok())
565 .map(|page| resolve_rotation(&doc, page))
566 .unwrap_or(0);
567 let next = (current + degrees).rem_euclid(360);
568 if let Ok(Object::Dictionary(page)) = doc.get_object_mut(id) {
569 page.set("Rotate", Object::Integer(i64::from(next)));
570 }
571 }
572 finish(doc, &all, &HashSet::new(), None)
573}
574
575pub fn insert_pages(
579 dest: &[u8],
580 at: u32,
581 source: &[u8],
582 source_pages: Option<&[u32]>,
583) -> Result<Vec<u8>, OrganizeError> {
584 let Open {
585 mut doc,
586 pages: dest_pages,
587 } = open(0, dest)?;
588 if at as usize > dest_pages.len() {
589 return Err(OrganizeError::InvalidPosition {
590 at,
591 pages: dest_pages.len() as u32,
592 });
593 }
594 let Open {
595 doc: mut from,
596 pages: source_all,
597 } = open(1, source)?;
598 let selection: Vec<usize> = match source_pages {
599 None => (0..source_all.len()).collect(),
600 Some([]) => return Err(OrganizeError::NoPagesSelected),
601 Some(list) => {
602 let mut picked = Vec::with_capacity(list.len());
603 for &page in list {
604 if page == 0 || page as usize > source_all.len() {
605 return Err(OrganizeError::PageOutOfRange(page));
606 }
607 picked.push(page as usize - 1);
608 }
609 picked
610 }
611 };
612 let dest_labels = read_labels(&doc);
613 let source_labels = read_labels(&from);
614 from.renumber_objects_with(doc.max_id + 1);
615 let source_ids: Vec<ObjectId> = from.get_pages().into_values().collect();
616 doc.max_id = doc.max_id.max(from.max_id);
617 doc.objects.extend(from.objects);
618
619 let mut inserted: Vec<ObjectId> = Vec::with_capacity(selection.len());
620 let mut used: HashSet<ObjectId> = HashSet::new();
621 for index in &selection {
622 let id = *source_ids
623 .get(*index)
624 .ok_or(OrganizeError::PageOutOfRange(*index as u32 + 1))?;
625 if used.insert(id) {
626 inserted.push(id);
627 } else {
628 let copy = doc
630 .get_object(id)
631 .cloned()
632 .map_err(|_| OrganizeError::PageOutOfRange(*index as u32 + 1))?;
633 let new_id = doc.new_object_id();
634 doc.objects.insert(new_id, copy);
635 inserted.push(new_id);
636 }
637 }
638
639 let at = at as usize;
640 let mut new_pages: Vec<ObjectId> = Vec::new();
641 let mut plan: Vec<Origin> = Vec::new();
642 for (index, id) in dest_pages.iter().enumerate().take(at) {
643 new_pages.push(*id);
644 plan.push(Origin::Dest(index));
645 }
646 for (id, index) in inserted.iter().zip(&selection) {
647 new_pages.push(*id);
648 plan.push(Origin::Source(*index));
649 }
650 for (index, id) in dest_pages.iter().enumerate().skip(at) {
651 new_pages.push(*id);
652 plan.push(Origin::Dest(index));
653 }
654 let labels = build_labels(&plan, &dest_labels, &source_labels);
655 finish(doc, &new_pages, &HashSet::new(), labels)
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use crate::inspect::page_graph_from_bytes;
662 use lopdf::{dictionary, Stream, StringFormat};
663
664 fn numbered(n: usize, labels: bool, outline: bool) -> Vec<u8> {
667 let mut doc = Document::with_version("1.7");
668 let pages_id = doc.new_object_id();
669 let font = doc.add_object(dictionary! {
670 "Type" => "Font", "Subtype" => "Type1", "BaseFont" => "Helvetica",
671 "Encoding" => "WinAnsiEncoding",
672 });
673 let resources = doc.add_object(dictionary! { "Font" => dictionary! { "F1" => font } });
674 let mut page_ids = Vec::new();
675 for k in 1..=n {
676 let content = doc.add_object(Stream::new(
677 Dictionary::new(),
678 format!("BT /F1 12 Tf 72 700 Td (P{k}) Tj ET").into_bytes(),
679 ));
680 page_ids.push(doc.add_object(dictionary! {
681 "Type" => "Page", "Parent" => pages_id, "Contents" => content,
682 "Resources" => resources,
683 "MediaBox" => Object::Array(vec![
684 Object::Integer(0), Object::Integer(0), Object::Integer(612), Object::Integer(792),
685 ]),
686 }));
687 }
688 doc.objects.insert(
689 pages_id,
690 Object::Dictionary(dictionary! {
691 "Type" => "Pages",
692 "Kids" => Object::Array(page_ids.iter().map(|id| Object::Reference(*id)).collect()),
693 "Count" => n as i64,
694 }),
695 );
696 let mut catalog = dictionary! { "Type" => "Catalog", "Pages" => pages_id };
697 if labels {
698 catalog.set(
699 "PageLabels",
700 dictionary! { "Nums" => Object::Array(vec![
701 Object::Integer(0), Object::Dictionary(dictionary! { "S" => "r" }),
702 Object::Integer(2), Object::Dictionary(dictionary! { "S" => "D" }),
703 ]) },
704 );
705 }
706 if outline {
707 let outlines = doc.new_object_id();
708 let items: Vec<ObjectId> = (0..n).map(|_| doc.new_object_id()).collect();
709 for (k, id) in items.iter().enumerate() {
710 let mut item = dictionary! {
711 "Title" => Object::String(format!("Item {}", k + 1).into_bytes(), StringFormat::Literal),
712 "Parent" => outlines,
713 "Dest" => Object::Array(vec![
714 Object::Reference(page_ids[k]), Object::Name(b"Fit".to_vec()),
715 ]),
716 };
717 if k + 1 < n {
718 item.set("Next", Object::Reference(items[k + 1]));
719 }
720 if k > 0 {
721 item.set("Prev", Object::Reference(items[k - 1]));
722 }
723 doc.objects.insert(*id, Object::Dictionary(item));
724 }
725 doc.objects.insert(
726 outlines,
727 Object::Dictionary(dictionary! {
728 "Type" => "Outlines", "First" => items[0], "Last" => items[n - 1],
729 "Count" => n as i64,
730 }),
731 );
732 catalog.set("Outlines", Object::Reference(outlines));
733 }
734 let catalog_id = doc.add_object(catalog);
735 doc.trailer.set("Root", Object::Reference(catalog_id));
736 let mut out = Vec::new();
737 doc.save_to(&mut out).expect("fixture saves");
738 out
739 }
740
741 fn texts(bytes: &[u8]) -> Vec<String> {
742 let count = load_pdf(bytes).unwrap().get_pages().len();
743 (1..=count as u32)
744 .map(|page| {
745 page_graph_from_bytes(bytes, page)
746 .unwrap()
747 .islands
748 .iter()
749 .map(|i| i.text.clone())
750 .collect::<Vec<_>>()
751 .join("")
752 })
753 .collect()
754 }
755
756 #[test]
757 fn delete_keeps_the_others_and_drops_the_dead_outline_destination() {
758 let out = delete_pages(&numbered(4, false, true), &[2]).unwrap();
759 assert_eq!(texts(&out), ["P1", "P3", "P4"]);
760 let doc = load_pdf(&out).unwrap();
761 let items = outline_items(&doc);
762 assert_eq!(items.len(), 4, "the heading stays");
763 let with_dest = items
764 .iter()
765 .filter(|id| doc.get_dictionary(**id).unwrap().has(b"Dest"))
766 .count();
767 assert_eq!(with_dest, 3);
768 }
769
770 #[test]
771 fn deleted_page_content_does_not_survive() {
772 let out = delete_pages(&numbered(3, false, true), &[2]).unwrap();
773 let doc = load_pdf(&out).unwrap();
774 let has_p2 = doc.objects.values().any(|o| {
775 o.as_stream()
776 .ok()
777 .and_then(|s| s.decompressed_content().ok())
778 .is_some_and(|c| c.windows(4).any(|w| w == b"(P2)"))
779 });
780 assert!(!has_p2);
781 }
782
783 #[test]
784 fn reorder_rebuilds_the_label_ranges() {
785 let out = reorder_pages(&numbered(4, true, false), &[3, 4, 1, 2]).unwrap();
786 assert_eq!(texts(&out), ["P3", "P4", "P1", "P2"]);
787 let labels = read_labels(&load_pdf(&out).unwrap());
788 assert_eq!(labels.iter().map(|r| r.start).collect::<Vec<_>>(), [0, 2]);
789 assert_eq!(labels[0].dict.get(b"S").unwrap().as_name().unwrap(), b"D");
790 assert_eq!(labels[1].dict.get(b"S").unwrap().as_name().unwrap(), b"r");
791 }
792
793 #[test]
794 fn delete_shifts_st_so_pages_keep_their_labels() {
795 let out = delete_pages(&numbered(4, true, false), &[3]).unwrap();
797 let labels = read_labels(&load_pdf(&out).unwrap());
798 let last = labels.last().unwrap();
799 assert_eq!(last.start, 2);
800 assert_eq!(last.dict.get(b"St").unwrap().as_i64().unwrap(), 2);
801 }
802
803 #[test]
804 fn rotate_adds_to_the_effective_rotation() {
805 let source = numbered(2, false, false);
806 let once = rotate_pages(&source, &[1], 90).unwrap();
807 let twice = rotate_pages(&once, &[1], 270).unwrap();
808 let rotation = |bytes: &[u8], page: u32| {
809 let doc = load_pdf(bytes).unwrap();
810 let id = doc.get_pages()[&page];
811 resolve_rotation(&doc, doc.get_dictionary(id).unwrap())
812 };
813 assert_eq!(rotation(&once, 1), 90);
814 assert_eq!(rotation(&once, 2), 0);
815 assert_eq!(rotation(&twice, 1), 0);
816 }
817
818 #[test]
819 fn insert_places_pages_and_keeps_both_documents_text() {
820 let dest = numbered(2, false, true);
821 let source = numbered(3, false, false);
822 let out = insert_pages(&dest, 1, &source, Some(&[3, 3])).unwrap();
823 assert_eq!(texts(&out), ["P1", "P3", "P3", "P2"]);
824 let appended = insert_pages(&dest, 2, &source, None).unwrap();
825 assert_eq!(texts(&appended), ["P1", "P2", "P1", "P2", "P3"]);
826 }
827
828 #[test]
829 fn bad_requests_are_typed() {
830 let source = numbered(3, false, false);
831 assert!(matches!(
832 reorder_pages(&source, &[1, 1, 2]),
833 Err(OrganizeError::NotAPermutation(3))
834 ));
835 assert!(matches!(
836 delete_pages(&source, &[1, 2, 3]),
837 Err(OrganizeError::DeletesEveryPage)
838 ));
839 assert!(matches!(
840 delete_pages(&source, &[4]),
841 Err(OrganizeError::PageOutOfRange(4))
842 ));
843 assert!(matches!(
844 rotate_pages(&source, &[1], 45),
845 Err(OrganizeError::InvalidRotation(45))
846 ));
847 assert!(matches!(
848 insert_pages(&source, 9, &source, None),
849 Err(OrganizeError::InvalidPosition { at: 9, pages: 3 })
850 ));
851 }
852}