From f12297d580e6da85a450b68beb1b8c36f304f22c Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 21 May 2026 22:32:18 +0800 Subject: [PATCH] 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 --- Cargo.lock | 186 +++++++++++++++++++++++++++++++++++++++++++++++--- build.rs | 12 +++- src/book.rs | 90 +++++++++++++++++++++++- src/reader.rs | 6 +- 4 files changed, 281 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f84d60..bec96cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/build.rs b/build.rs index b249850..2d36543 100644 --- a/build.rs +++ b/build.rs @@ -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"))] diff --git a/src/book.rs b/src/book.rs index 6f4d328..3cb742e 100644 --- a/src/book.rs +++ b/src/book.rs @@ -248,7 +248,19 @@ pub fn load_epub(path: impl AsRef) -> Result { 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 = 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 { 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); + } } diff --git a/src/reader.rs b/src/reader.rs index 4e72b13..4ab8b71 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -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 };