fix: TOC navigation and pagination improvements

- Fix recalculate_pages missing heading_top_spacing in page height calculation
- Improve build_toc path matching: extract filename first, fall back to substring
- Filter out EPUB3 nav.xhtml from content sections
- Skip Windows resource compilation when windres is not available
- Add unit tests for TOC filename matching and nav filtering
This commit is contained in:
Developer
2026-05-21 22:32:18 +08:00
parent 8e8ba01336
commit f12297d580
4 changed files with 281 additions and 13 deletions

186
Cargo.lock generated
View File

@@ -1138,6 +1138,20 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "embed-resource"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d506610004cfc74a6f5ee7e8c632b355de5eca1f03ee5e5e0ec11b77d4eb3d61"
dependencies = [
"cc",
"memchr",
"rustc_version",
"toml",
"vswhom",
"winreg",
]
[[package]]
name = "endi"
version = "1.1.1"
@@ -1207,6 +1221,7 @@ name = "epub-read"
version = "0.1.0"
dependencies = [
"eframe",
"embed-resource",
"epub",
"image",
"rfd",
@@ -2827,7 +2842,7 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
"toml_edit 0.25.11+spec-1.1.0",
]
[[package]]
@@ -3293,6 +3308,15 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -3626,6 +3650,27 @@ dependencies = [
"zerovec",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime 0.6.11",
"toml_edit 0.22.27",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
@@ -3635,6 +3680,20 @@ dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.15",
]
[[package]]
name = "toml_edit"
version = "0.25.11+spec-1.1.0"
@@ -3642,9 +3701,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
dependencies = [
"indexmap",
"toml_datetime",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"winnow",
"winnow 1.0.2",
]
[[package]]
@@ -3653,9 +3712,15 @@ version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow",
"winnow 1.0.2",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tracing"
version = "0.1.44"
@@ -3797,6 +3862,26 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vswhom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
dependencies = [
"libc",
"vswhom-sys",
]
[[package]]
name = "vswhom-sys"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -4301,6 +4386,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -4337,6 +4431,21 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -4370,6 +4479,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -4382,6 +4497,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -4394,6 +4515,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -4418,6 +4545,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -4430,6 +4563,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -4442,6 +4581,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -4454,6 +4599,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -4518,6 +4669,15 @@ dependencies = [
"xkbcommon-dl",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "1.0.2"
@@ -4527,6 +4687,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
@@ -4796,7 +4966,7 @@ dependencies = [
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow",
"winnow 1.0.2",
"zbus_macros 5.15.0",
"zbus_names 4.3.2",
"zvariant 5.11.0",
@@ -4872,7 +5042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
dependencies = [
"serde",
"winnow",
"winnow 1.0.2",
"zvariant 5.11.0",
]
@@ -5028,7 +5198,7 @@ dependencies = [
"enumflags2",
"serde",
"url",
"winnow",
"winnow 1.0.2",
"zvariant_derive 5.11.0",
"zvariant_utils 3.3.1",
]
@@ -5080,5 +5250,5 @@ dependencies = [
"quote",
"serde",
"syn",
"winnow",
"winnow 1.0.2",
]

View File

@@ -1,6 +1,16 @@
#[cfg(target_os = "windows")]
fn main() {
embed_resource::compile("read.rc", embed_resource::NONE);
let windres_available = std::process::Command::new("where")
.arg("windres")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if windres_available {
embed_resource::compile("read.rc", embed_resource::NONE);
} else {
println!("cargo:warning=windres not found, skipping Windows resource (icon) compilation");
}
}
#[cfg(not(target_os = "windows"))]

View File

@@ -248,7 +248,19 @@ pub fn load_epub(path: impl AsRef<Path>) -> Result<Book, String> {
let title = doc.mdata("title").unwrap_or_else(|| "未知标题".to_string());
let author = doc.mdata("creator").unwrap_or_else(|| "未知作者".to_string());
let cover = doc.get_cover().ok();
let spine = doc.spine.clone();
let spine: Vec<String> = doc.spine.iter()
.filter(|id| {
if id.as_str() == "nav" { return false; }
if let Some((path, _)) = doc.resources.get(*id) {
let path_str = path.to_string_lossy().to_lowercase();
if path_str.ends_with("nav.xhtml") || path_str.ends_with("nav.html") {
return false;
}
}
true
})
.cloned()
.collect();
let mut sections = Vec::new();
for (i, href) in spine.iter().enumerate() {
@@ -290,6 +302,11 @@ fn extract_title(html: &str) -> Option<String> {
None
}
fn extract_filename(path: &str) -> &str {
let path = path.trim_end_matches('/');
path.rsplit('/').next().unwrap_or(path)
}
fn build_toc(
entries: &[epub::doc::NavPoint],
spine: &[String],
@@ -298,10 +315,15 @@ fn build_toc(
.iter()
.map(|e| {
let content_str = e.content.to_string_lossy();
let content_file = extract_filename(&content_str);
let section = spine
.iter()
.position(|s| content_str.contains(s.trim_end_matches('/')))
// unwrap_or(0) is safe: a real TOC entry should always match a spine item
.position(|s| {
let spine_file = extract_filename(s);
spine_file == content_file
|| content_str.contains(s.as_str())
|| s.contains(content_str.as_ref())
})
.unwrap_or(0);
TocEntry {
label: e.label.clone(),
@@ -439,4 +461,66 @@ mod tests {
let toc = build_toc(&[], &[]);
assert!(toc.is_empty());
}
#[test]
fn test_load_sample_epub_nav_filtered() {
let book = load_epub("sample-short.epub").expect("Failed to load sample epub");
// Nav document is filtered out, leaving only chapter_0 as the single section
assert_eq!(book.sections.len(), 1);
assert_eq!(book.sections[0].title, "Understanding Digital Formats");
}
#[test]
fn test_toc_section_bounds() {
let book = load_epub("sample-short.epub").expect("Failed to load sample epub");
// All TOC section indices should be within sections range
fn check_bounds(entries: &[TocEntry], max: usize) {
for e in entries {
assert!(e.section < max, "TOC entry '{}' maps to section {} but only {} sections exist", e.label, e.section, max);
check_bounds(&e.children, max);
}
}
check_bounds(&book.toc, book.sections.len());
}
#[test]
fn test_build_toc_filename_matching() {
use epub::doc::NavPoint;
let spine = vec![
"OEBPS/Text/chapter1.xhtml".to_string(),
"OEBPS/Text/chapter2.xhtml".to_string(),
];
let nav_points = vec![
NavPoint {
label: "Chapter 2".to_string(),
content: std::path::PathBuf::from("Text/chapter2.xhtml"),
play_order: 1,
children: vec![],
},
];
let toc = build_toc(&nav_points, &spine);
assert_eq!(toc.len(), 1);
assert_eq!(toc[0].section, 1); // maps to spine index 1
assert_eq!(toc[0].label, "Chapter 2");
}
#[test]
fn test_build_toc_exact_path_match() {
use epub::doc::NavPoint;
let spine = vec![
"chapter1.xhtml".to_string(),
"chapter2.xhtml".to_string(),
];
let nav_points = vec![
NavPoint {
label: "Chapter 1".to_string(),
content: std::path::PathBuf::from("chapter1.xhtml"),
play_order: 1,
children: vec![],
},
];
let toc = build_toc(&nav_points, &spine);
assert_eq!(toc.len(), 1);
assert_eq!(toc[0].section, 0);
}
}

View File

@@ -151,7 +151,11 @@ pub fn recalculate_pages(
let block_height = measure_block_height(ctx, &display_text, block_font_size, available_width);
let spacing = if i > page_start_block && i > 0 {
para_spacing
if block.heading_level > 0 {
para_spacing + heading_top_spacing(para_spacing, block.heading_level)
} else {
para_spacing
}
} else {
0.0
};