diff --git a/crates/organize/src/labels.rs b/crates/organize/src/labels.rs index fc4affdeb..8ee04bed5 100644 --- a/crates/organize/src/labels.rs +++ b/crates/organize/src/labels.rs @@ -396,6 +396,31 @@ pub fn set_page_label_ranges(doc: &mut Document, ranges: &[LabelRange]) -> Resul Ok(()) } +/// After `deleted` pages (0-based, as they were numbered before the delete) are gone, move the label +/// ranges with the pages they label: a range starts as many pages earlier as were deleted before +/// it, and keeps its first number, so numbering continues over the gap as in Acrobat. A range whose +/// first page went starts on the next page left; one left with no pages gives way to the next. +/// A tree that can't be read is left as it is: the delete still goes ahead. +pub(crate) fn drop_deleted_pages(doc: &mut Document, deleted: &[usize]) -> Result<(), OrganizeError> { + let Ok(old) = checked_ranges(doc) else { return Ok(()) }; + let mut deleted = deleted.to_vec(); + deleted.sort_unstable(); + deleted.dedup(); + let mut ranges: Vec = Vec::with_capacity(old.len()); + for r in &old { + let start = r.start.saturating_sub(deleted.partition_point(|d| *d < r.start)); + match ranges.last_mut() { + // The later range is the one on the first page left; ranges stay sorted by start. + Some(last) if last.start == start => *last = LabelRange { start, ..r.clone() }, + _ => ranges.push(LabelRange { start, ..r.clone() }), + } + } + if ranges == old { + return Ok(()); + } + set_page_label_ranges(doc, &ranges) +} + /// Acrobat's "Number pages": label pages `from..=to` (0-based) with `style`, `prefix` and /// starting number `first`; pages after `to` keep the labels they had. pub fn number_pages(doc: &mut Document, from: usize, to: usize, style: LabelStyle, prefix: &str, first: u32) -> Result<(), OrganizeError> { diff --git a/crates/organize/src/lib.rs b/crates/organize/src/lib.rs index d73815e73..1d8a1e92d 100644 --- a/crates/organize/src/lib.rs +++ b/crates/organize/src/lib.rs @@ -181,6 +181,7 @@ pub fn delete_pages(doc: &mut Document, indices: &[usize]) -> Result<(), Organiz return Err(OrganizeError::WouldRemoveAllPages); } rebuild(doc, &keep)?; + labels::drop_deleted_pages(doc, indices)?; // The deleted pages go for good: bookmarks, links and widgets that still point at one would // otherwise keep it, with its content, in the saved file. Their form widgets leave the form, // and bookmarks and links that went to them lose that destination. diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index 69360a42e..3730b5998 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -1472,6 +1472,38 @@ fn number_pages_like_acrobat() { assert!(d.get(d.root().unwrap()).as_dict().unwrap().get(b"PageLabels").is_none()); } +/// Deleting pages moves the label ranges with the pages they label (#2091): later ranges start that +/// many pages earlier and keep numbering on, a range whose first page went starts on the next page +/// left, and a range left with no pages gives way to the one after it. +#[test] +fn deleting_pages_keeps_the_label_ranges_on_their_pages() { + use crate::{LabelRange, LabelStyle::*}; + let ten = || { + let mut d = doc_a(); + for at in 3..10 { + crate::insert_blank_page(&mut d, at, 300.0, 400.0).unwrap(); + } + let ranges = [ + LabelRange { start: 0, style: LowerRoman, prefix: String::new(), first: 1 }, + LabelRange { start: 3, style: Decimal, prefix: String::new(), first: 1 }, + LabelRange { start: 7, style: Decimal, prefix: "A-".into(), first: 1 }, + ]; + crate::set_page_label_ranges(&mut d, &ranges).unwrap(); + assert_eq!(crate::page_labels(&d).unwrap(), ["i", "ii", "iii", "1", "2", "3", "4", "A-1", "A-2", "A-3"]); + d + }; + for (deleted, expected) in [ + (&[4, 5][..], &["i", "ii", "iii", "1", "2", "A-1", "A-2", "A-3"][..]), + (&[3], &["i", "ii", "iii", "1", "2", "3", "A-1", "A-2", "A-3"]), + (&[3, 4, 5, 6], &["i", "ii", "iii", "A-1", "A-2", "A-3"]), + ] { + let mut d = ten(); + crate::delete_pages(&mut d, deleted).unwrap(); + assert_eq!(crate::page_labels(&d).unwrap(), expected, "after deleting {deleted:?}"); + assert_eq!(crate::page_labels(&full_roundtrip(&d)).unwrap(), expected, "reopened after deleting {deleted:?}"); + } +} + #[test] fn page_label_limits_reject_unsupported_edits_before_mutating() { use crate::LabelStyle::*;