Keep every part when bookmark titles repeat in doc_split (#800) (#863)

This commit is contained in:
MarekSpodBiedry authored and GitHub committed 2026-10-10 15:39:29 -04:00
1 parent 3cd619e937
commit d258764abc
5 files changed
+62 -3

No files matched your search

+9 -1
View File
@@ -1328,13 +1328,21 @@ impl Automation {
let dir = self.resolve(a.str("out_dir")?, true)?;
std::fs::create_dir_all(&dir).map_err(|e| failed(format!("{}: {e}", dir.display())))?;
let mut files = Vec::new();
let mut used = std::collections::HashSet::new();
for (i, (first, last, bytes)) in parts.iter().enumerate() {
let safe = |t: &str| t.chars().map(|c| if c.is_alphanumeric() || " -_.,()".contains(c) { c } else { '_' }).collect::<String>();
let file = match titles.iter().find(|t| t.0 + 1 == *first) {
Some((_, t)) => format!("{stem}-{}.pdf", safe(t)),
None => format!("{stem}-part{}.pdf", i + 1),
};
let path = child(&dir, &file);
// Equal titles (or titles that sanitize alike) get -2, -3, ... so no part overwrites another; compared case-insensitively.
let mut unique = file.clone();
let mut n = 1;
while !used.insert(unique.to_lowercase()) {
n += 1;
unique = format!("{}-{n}.pdf", file.trim_end_matches(".pdf"));
}
let path = child(&dir, &unique);
write_atomic(&path, bytes)?;
files.push(json!({ "path": path.to_string_lossy(), "first_page": first, "last_page": last }));
}
+1 -1
View File
@@ -215,7 +215,7 @@ pub fn tools() -> Vec<ToolDef> {
}),
&["paths"],
)),
t("doc_split", "Split a document", "Split into several files written to out_dir: every N pages, before given pages, at top-level bookmarks (bookmarks: true; files named after them), or by file size (max_mb). Files are <name>-partK.pdf.")
t("doc_split", "Split a document", "Split into several files written to out_dir: every N pages, before given pages, at top-level bookmarks (bookmarks: true; files named after them, repeated titles get -2, -3), or by file size (max_mb). Files are <name>-partK.pdf.")
.cmd("page.split")
.with(schema(
json!({ "doc": doc(), "every": { "type": "integer", "minimum": 1 }, "before": pages("that start a new part"), "bookmarks": { "type": "boolean" }, "max_mb": { "type": "number", "exclusiveMinimum": 0 }, "out_dir": { "type": "string" } }),
+21
View File
@@ -2334,6 +2334,27 @@ fn stamps_through_tools() {
assert!(matches!(a.call("stamp_custom", &json!({ "doc": doc, "page": 1, "path": "nope.png", "at": [1, 1] })), Err(ToolError::Failed(_))));
}
#[test]
fn bookmark_split_with_equal_titles_keeps_every_part() {
let dir = workdir("organize_dup_titles");
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
ok(&mut a, "bookmark_add", json!({ "doc": doc, "title": "Same", "page": 1 }));
ok(&mut a, "bookmark_add", json!({ "doc": doc, "title": "Same", "page": 2 }));
ok(&mut a, "bookmark_add", json!({ "doc": doc, "title": "same", "page": 3 }));
let s = ok(&mut a, "doc_split", json!({ "doc": doc, "bookmarks": true, "out_dir": "parts" }));
let files: Vec<String> = s["files"]
.as_array()
.unwrap()
.iter()
.map(|f| std::path::Path::new(f["path"].as_str().unwrap()).file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(files, ["a-Same.pdf", "a-Same-2.pdf", "a-same-3.pdf"]);
for f in &files {
assert!(dir.join("parts").join(f).exists(), "{f} missing");
}
}
#[test]
fn organizing_with_filters_bookmark_splits_and_extract_options() {
let dir = workdir("organize2");
+9 -1
View File
@@ -597,6 +597,7 @@ impl PdfCraftApp {
}
};
let safe = |t: &str| t.chars().map(|c| if c.is_alphanumeric() || " -_.,()".contains(c) { c } else { '_' }).collect::<String>();
let mut used = std::collections::HashSet::new();
let named: Vec<(String, Arc<Vec<u8>>)> = parts
.into_iter()
.map(|(a, b, bytes)| {
@@ -607,7 +608,14 @@ impl PdfCraftApp {
None if a == b => format!("{stem} (page {a}).pdf"),
None => format!("{stem} (pages {a}-{b}).pdf"),
};
(name, bytes)
// Equal titles (or titles that sanitize alike) get (2), (3), ... so no part overwrites another; compared case-insensitively.
let mut unique = name.clone();
let mut n = 1;
while !used.insert(unique.to_lowercase()) {
n += 1;
unique = format!("{} ({n}).pdf", name.trim_end_matches(".pdf"));
}
(unique, bytes)
})
.collect();
self.write_files(&named, "Choose a folder for the split files");
+22
View File
@@ -1380,6 +1380,28 @@ fn split_dialog_writes_one_file_per_part() {
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn split_at_bookmarks_with_equal_titles_keeps_every_part() {
let dir = temp_path("split-dup-titles");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut h = organize(3);
h.state_mut().export_dir_override = Some(dir.to_string_lossy().into_owned());
for (index, title) in [(0, "Same"), (1, "Same"), (2, "same")] {
h.state_mut().apply_edit(pdfcraft_engine::Edit::AddBookmark { parent: vec![], index, title: title.into(), page: index });
}
h.state_mut().split_draft.mode = pdfcraft_ui_egui::SplitMode::Bookmarks;
h.state_mut().run_command("page.split");
h.run_steps(3);
h.get_by_label_contains("Creates 3 files from 3 pages");
h.get_by_label("Split").click();
h.run_steps(3);
let mut names: Vec<String> = std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect();
names.sort();
assert_eq!(names, ["doc - Same (2).pdf", "doc - Same.pdf", "doc - same (3).pdf"]);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn split_before_selected_pages() {
let dir = temp_path("split-sel");