feat(layout): detect reflowable vs fixed-layout EPUB, fix pagination overflow
This commit is contained in:
37
src/book.rs
37
src/book.rs
@@ -187,22 +187,57 @@ pub struct Section {
|
|||||||
pub pages: Vec<usize>,
|
pub pages: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum BookLayout {
|
||||||
|
Reflowable,
|
||||||
|
FixedLayout,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BookLayout {
|
||||||
|
pub fn label(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
BookLayout::Reflowable => "重排",
|
||||||
|
BookLayout::FixedLayout => "固定",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Book {
|
pub struct Book {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub cover: Option<Vec<u8>>,
|
pub cover: Option<Vec<u8>>,
|
||||||
|
pub layout: BookLayout,
|
||||||
pub sections: Vec<Section>,
|
pub sections: Vec<Section>,
|
||||||
pub toc: Vec<TocEntry>,
|
pub toc: Vec<TocEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn detect_layout(doc: &mut epub::doc::EpubDoc<std::io::BufReader<std::fs::File>>) -> BookLayout {
|
||||||
|
if let Some(vals) = doc.metadata.get("rendition:layout") {
|
||||||
|
if vals.iter().any(|v| v == "pre-paginated") {
|
||||||
|
return BookLayout::FixedLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(opf) = doc.get_resource_str_by_path(&doc.root_file.clone()) {
|
||||||
|
if opf.contains("rendition:layout") && opf.contains("pre-paginated") {
|
||||||
|
return BookLayout::FixedLayout;
|
||||||
|
}
|
||||||
|
if opf.contains("rendition:layout-pre-paginated") {
|
||||||
|
return BookLayout::FixedLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BookLayout::Reflowable
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_epub(path: impl AsRef<Path>) -> Result<Book, String> {
|
pub fn load_epub(path: impl AsRef<Path>) -> Result<Book, String> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let mut doc = epub::doc::EpubDoc::new(path)
|
let mut doc = epub::doc::EpubDoc::new(path)
|
||||||
.map_err(|e| format!("无法打开文件: {}", e))?;
|
.map_err(|e| format!("无法打开文件: {}", e))?;
|
||||||
|
|
||||||
|
let layout = detect_layout(&mut doc);
|
||||||
|
|
||||||
let title = doc.mdata("title").unwrap_or_else(|| "未知标题".to_string());
|
let title = doc.mdata("title").unwrap_or_else(|| "未知标题".to_string());
|
||||||
let author = doc.mdata("creator").unwrap_or_else(|| "未知作者".to_string());
|
let author = doc.mdata("creator").unwrap_or_else(|| "未知作者".to_string());
|
||||||
let cover = doc.get_cover().ok();
|
let cover = doc.get_cover().ok();
|
||||||
@@ -225,7 +260,7 @@ pub fn load_epub(path: impl AsRef<Path>) -> Result<Book, String> {
|
|||||||
let raw_toc = std::mem::take(&mut doc.toc);
|
let raw_toc = std::mem::take(&mut doc.toc);
|
||||||
let toc = build_toc(&raw_toc, &spine);
|
let toc = build_toc(&raw_toc, &spine);
|
||||||
|
|
||||||
Ok(Book { title, author, cover, sections, toc })
|
Ok(Book { title, author, cover, layout, sections, toc })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_title(html: &str) -> Option<String> {
|
fn extract_title(html: &str) -> Option<String> {
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ use crate::theme::{self, BgType, Theme};
|
|||||||
|
|
||||||
pub fn recalculate_pages(book: &mut Book, font_size: f32, line_height: f32, panel_width: f32, panel_height: f32, style: &StyleProfile) {
|
pub fn recalculate_pages(book: &mut Book, font_size: f32, line_height: f32, panel_width: f32, panel_height: f32, style: &StyleProfile) {
|
||||||
let char_width = font_size * 1.0;
|
let char_width = font_size * 1.0;
|
||||||
let safety = 0.95;
|
let safety_w = 0.95;
|
||||||
|
let safety_h = 0.96;
|
||||||
let chars_per_line = if char_width > 0.0 {
|
let chars_per_line = if char_width > 0.0 {
|
||||||
((panel_width / char_width) * safety).max(1.0) as usize
|
((panel_width / char_width) * safety_w).max(1.0) as usize
|
||||||
} else {
|
} else {
|
||||||
1
|
1
|
||||||
};
|
};
|
||||||
let lines_per_page = if line_height > 0.0 {
|
let lines_per_page = if line_height > 0.0 {
|
||||||
((panel_height / line_height) * safety).max(1.0) as usize
|
((panel_height / line_height) * safety_h).max(1.0) as usize
|
||||||
} else {
|
} else {
|
||||||
1
|
1
|
||||||
};
|
};
|
||||||
@@ -101,7 +102,8 @@ pub fn reading_view(
|
|||||||
action.go_back = true;
|
action.go_back = true;
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label(&book.title);
|
ui.label(format!("《{}》", &book.title));
|
||||||
|
ui.label(format!("[{}]", book.layout.label()));
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
let (theme_icon, theme_hint) = match theme {
|
let (theme_icon, theme_hint) = match theme {
|
||||||
Theme::Dark => ("🌞", "切换到浅色主题"),
|
Theme::Dark => ("🌞", "切换到浅色主题"),
|
||||||
@@ -412,9 +414,9 @@ pub fn calculate_pages(text: &str, chars_per_page: usize) -> Vec<usize> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search backward from next for paragraph (\n\n) or line (\n) breaks
|
// 在页面后半段查找段落边界(\n\n)或行边界(\n),不回超理想页长
|
||||||
let search_start = pos + chars_per_page / 2;
|
let search_start = pos + chars_per_page / 2;
|
||||||
let search_end = (next + chars_per_page / 2).min(total_chars);
|
let search_end = next.min(total_chars);
|
||||||
let mut split = next;
|
let mut split = next;
|
||||||
|
|
||||||
// Prefer double newline (paragraph), then single newline
|
// Prefer double newline (paragraph), then single newline
|
||||||
|
|||||||
Reference in New Issue
Block a user