mod content;
mod metadata_frags;
mod navigation;
mod positions;
mod resources;
mod survey;
use content::*;
use metadata_frags::*;
use navigation::*;
use positions::*;
use resources::*;
use survey::*;
use std::collections::{BTreeSet, HashMap};
use std::io::{self, Seek, Write};
use crate::export::Exporter;
use crate::import::ChapterId;
use crate::kfx::auxiliary::build_auxiliary_data_fragment;
use crate::kfx::context::{ExportContext, LandmarkTarget};
use crate::kfx::cover::{
COVER_SECTION_NAME, build_cover_section, is_image_only_chapter, needs_standalone_cover,
normalize_cover_path,
};
use crate::kfx::fragment::KfxFragment;
use crate::kfx::ion::IonValue;
use crate::kfx::metadata::{
MetadataCategory, MetadataContext, build_category_entries, generate_book_id,
};
use crate::kfx::serialization::{
SerializedEntity, create_entity_data, generate_container_id, serialize_annotated_ion,
serialize_container,
};
use crate::kfx::symbols::KfxSymbol;
use crate::kfx::transforms::format_to_kfx_symbol;
use crate::model::{
AnchorTarget, Book, Chapter, GlobalNodeId, LandmarkType, NodeId, ResolvedLinks, Role,
};
use crate::util::detect_media_format;
#[derive(Default)]
pub struct KfxExporter;
impl KfxExporter {
pub fn new() -> Self {
Self
}
}
impl Exporter for KfxExporter {
fn export<W: Write + Seek>(&self, book: &Book, writer: &mut W) -> crate::Result<()> {
let data = build_kfx_container(book)?;
writer.write_all(&data)?;
Ok(())
}
}
fn build_kfx_container(book: &Book) -> crate::Result<Vec<u8>> {
let meta = book.metadata();
let container_id = generate_container_id(&format!("{}\n{}", meta.identifier, meta.title));
let mut ctx = ExportContext::new();
let (standalone_cover_path, spine_info) = survey_book(book, &mut ctx)?;
let mut fragments = Vec::new();
fragments.push(build_content_features_fragment());
fragments.push(build_book_metadata_fragment(book, &container_id, &ctx));
fragments.push(build_metadata_fragment(&ctx));
let document_data_index = fragments.len();
let (section_fragments, storyline_fragments, content_fragments) = build_spine_entities(
book,
&spine_info,
standalone_cover_path.as_deref(),
&mut ctx,
);
ctx.fix_landmark_content_ids();
fragments.push(build_book_navigation_fragment_with_positions(book, &ctx));
fragments.extend(section_fragments);
fragments.extend(storyline_fragments);
fragments.extend(content_fragments);
fragments.extend(build_style_fragments(&mut ctx));
fragments.extend(build_anchor_fragments(&mut ctx));
append_auxiliary_data_fragments(
&mut fragments,
&spine_info,
standalone_cover_path.is_some(),
&mut ctx,
);
append_resource_fragments(book, &mut fragments, &mut ctx);
fragments.extend(build_font_fragments(book, &mut ctx));
fragments.push(build_position_map_fragment(&ctx));
fragments.push(build_position_id_map_fragment(&ctx));
fragments.push(build_location_map_fragment(&ctx));
fragments.push(build_resource_path_fragment());
fragments.push(build_container_entity_map_fragment(
&container_id,
&fragments,
&ctx,
));
fragments.insert(document_data_index, build_document_data_fragment(&ctx));
Ok(serialize_book(&container_id, &fragments, &ctx))
}
type SpineInfo = Vec<(ChapterId, String)>;
fn survey_book(book: &Book, ctx: &mut ExportContext) -> crate::Result<(Option<String>, SpineInfo)> {
let standalone_cover_path = detect_standalone_cover(book);
let spine_info = collect_spine_info(book, standalone_cover_path.is_some());
if standalone_cover_path.is_some() {
register_standalone_cover_section(ctx);
}
let resolved = book.resolve_links()?;
register_link_targets(book, &spine_info, &resolved, ctx)?;
let source_to_chapter = survey_spine_chapters(book, &spine_info, ctx);
resolve_landmarks(book, &spine_info, &source_to_chapter, &resolved, ctx);
register_nav_and_resource_symbols(book, ctx);
Ok((standalone_cover_path, spine_info))
}
fn detect_standalone_cover(book: &Book) -> Option<String> {
let asset_paths = book.list_assets();
let cover_image = book.metadata().cover_image.clone();
let first_chapter_id = book.spine().first().map(|e| e.id);
match (cover_image, first_chapter_id) {
(Some(cover_img), Some(first_id)) => {
let normalized = normalize_cover_path(&cover_img, asset_paths);
book.load_chapter_cached(first_id)
.ok()
.and_then(|first_chapter| {
if needs_standalone_cover(&normalized, &first_chapter) {
Some(normalized)
} else {
None
}
})
}
_ => None,
}
}
fn collect_spine_info(book: &Book, has_standalone_cover: bool) -> SpineInfo {
let section_offset = if has_standalone_cover { 1 } else { 0 };
book.spine()
.iter()
.enumerate()
.map(|(idx, entry)| {
let section_name = format!("c{}", idx + section_offset);
(entry.id, section_name)
})
.collect()
}
fn register_standalone_cover_section(ctx: &mut ExportContext) {
ctx.register_section(COVER_SECTION_NAME);
let cover_section_id = ctx.next_fragment_id();
ctx.cover_fragment_id = Some(cover_section_id);
ctx.landmark_fragments.insert(
LandmarkType::Cover,
LandmarkTarget {
fragment_id: cover_section_id,
offset: 0,
label: "cover-nav-unit".to_string(),
},
);
}
fn survey_spine_chapters(
book: &Book,
spine_info: &[(ChapterId, String)],
ctx: &mut ExportContext,
) -> HashMap<String, ChapterId> {
let mut source_to_chapter: HashMap<String, ChapterId> = HashMap::new();
for (chapter_id, section_name) in spine_info {
let _section_id = ctx.register_spine_section(section_name, *chapter_id);
let source_path = book.source_id(*chapter_id).unwrap_or("").to_string();
if !source_path.is_empty() {
source_to_chapter.insert(source_path.clone(), *chapter_id);
}
if let Ok(chapter) = book.load_chapter_cached(*chapter_id) {
ctx.position_map.reserve(chapter.node_count());
survey_chapter(&chapter, *chapter_id, &source_path, ctx);
}
}
source_to_chapter
}
fn resolve_landmarks(
book: &Book,
spine_info: &[(ChapterId, String)],
source_to_chapter: &HashMap<String, ChapterId>,
resolved: &ResolvedLinks,
ctx: &mut ExportContext,
) {
resolve_landmarks_from_ir(book, source_to_chapter, resolved, ctx);
let has_cover = ctx.landmark_fragments.contains_key(&LandmarkType::Cover);
let has_srl = ctx
.landmark_fragments
.contains_key(&LandmarkType::StartReading);
if !has_cover || !has_srl {
for (chapter_id, _section_name) in spine_info {
if let Ok(chapter) = book.load_chapter_cached(*chapter_id) {
let is_cover = is_image_only_chapter(&chapter);
let fragment_id = ctx.chapter_fragments.get(chapter_id).copied();
if let Some(fid) = fragment_id {
if is_cover && !ctx.landmark_fragments.contains_key(&LandmarkType::Cover) {
ctx.landmark_fragments.insert(
LandmarkType::Cover,
LandmarkTarget {
fragment_id: fid,
offset: 0,
label: "cover-nav-unit".to_string(),
},
);
} else if !is_cover
&& !ctx
.landmark_fragments
.contains_key(&LandmarkType::StartReading)
{
ctx.landmark_fragments.insert(
LandmarkType::StartReading,
LandmarkTarget {
fragment_id: fid,
offset: 0,
label: book.metadata().title.clone(),
},
);
}
}
if ctx.landmark_fragments.contains_key(&LandmarkType::Cover)
&& ctx
.landmark_fragments
.contains_key(&LandmarkType::StartReading)
{
break;
}
}
}
}
}
fn register_nav_and_resource_symbols(book: &Book, ctx: &mut ExportContext) {
ctx.nav_container_symbols.toc = ctx.symbols.get_or_intern("toc");
ctx.nav_container_symbols.headings = ctx.symbols.get_or_intern("headings");
ctx.nav_container_symbols.landmarks = ctx.symbols.get_or_intern("landmarks");
let asset_paths = book.list_assets();
for asset_path in asset_paths {
if is_media_asset(book, asset_path) {
ctx.resource_registry.register(asset_path, &mut ctx.symbols);
let short_name = ctx.resource_registry.get_or_create_name(asset_path);
ctx.symbols.get_or_intern(&short_name);
}
}
}
fn build_spine_entities(
book: &Book,
spine_info: &[(ChapterId, String)],
standalone_cover_path: Option<&str>,
ctx: &mut ExportContext,
) -> (Vec<KfxFragment>, Vec<KfxFragment>, Vec<KfxFragment>) {
let mut section_fragments = Vec::new();
let mut storyline_fragments = Vec::new();
let mut content_fragments = Vec::new();
if let Some(cover_path) = standalone_cover_path {
let section_id = ctx
.cover_fragment_id
.expect("cover_fragment_id should be set in Pass 1");
let cover_content_id = ctx.fragment_ids.peek();
ctx.cover_content_id = Some(cover_content_id);
let (section, storyline) = build_cover_section(cover_path, section_id, ctx);
section_fragments.push(section);
storyline_fragments.push(storyline);
if let Some(target) = ctx.landmark_fragments.get_mut(&LandmarkType::Cover) {
target.fragment_id = cover_content_id;
}
}
for (chapter_id, section_name) in spine_info {
if let Ok(chapter) = book.load_chapter_cached(*chapter_id) {
ctx.begin_chapter_export(*chapter_id);
let (section, storyline, content) =
build_chapter_entities_grouped(&chapter, *chapter_id, section_name, ctx);
section_fragments.push(section);
storyline_fragments.push(storyline);
if let Some(c) = content {
content_fragments.push(c);
}
for node_id in chapter.iter_dfs() {
if let Some(node) = chapter.node(node_id)
&& node.role == crate::model::Role::Image
&& let Some(src) = chapter.semantics.src(node_id)
{
let short_name = ctx.resource_registry.get_or_create_name(src);
ctx.record_section_image_ref(section_name, &short_name);
}
}
}
}
(section_fragments, storyline_fragments, content_fragments)
}
fn append_auxiliary_data_fragments(
fragments: &mut Vec<KfxFragment>,
spine_info: &[(ChapterId, String)],
has_standalone_cover: bool,
ctx: &mut ExportContext,
) {
if has_standalone_cover {
fragments.push(build_auxiliary_data_fragment(COVER_SECTION_NAME, ctx));
}
for (_, section_name) in spine_info {
fragments.push(build_auxiliary_data_fragment(section_name, ctx));
}
}
fn append_resource_fragments(
book: &Book,
fragments: &mut Vec<KfxFragment>,
ctx: &mut ExportContext,
) {
for asset_path in book.list_assets() {
if is_media_asset(book, asset_path)
&& let Ok(data) = book.load_asset(asset_path)
{
fragments.push(build_external_resource_fragment(asset_path, &data, ctx));
fragments.push(build_resource_fragment(asset_path, data, ctx));
}
}
}
fn serialize_book(container_id: &str, fragments: &[KfxFragment], ctx: &ExportContext) -> Vec<u8> {
let local_syms = ctx.symbols.local_symbols();
let symtab_ion = build_symbol_table_ion(local_syms);
let format_caps_ion = build_format_capabilities_ion();
let entities = serialize_fragments(fragments, ctx.symbols.local_symbols());
serialize_container(container_id, &entities, &symtab_ion, &format_caps_ion)
}
fn serialize_fragments<'a>(
fragments: &'a [KfxFragment],
local_symbols: &[String],
) -> Vec<SerializedEntity<'a>> {
let symbol_index: rustc_hash::FxHashMap<&str, usize> = local_symbols
.iter()
.enumerate()
.map(|(i, s)| (s.as_str(), i))
.collect();
fragments
.iter()
.map(|frag| {
let id = if frag.is_singleton() {
KfxSymbol::Null as u32 } else {
symbol_index
.get(frag.fid.as_str())
.map(|&i| (crate::kfx::symbols::KFX_SYMBOL_TABLE_SIZE + i) as u32)
.unwrap_or(0)
};
let (data, raw) = match &frag.data {
crate::kfx::fragment::FragmentData::Ion(value) => (create_entity_data(value), None),
crate::kfx::fragment::FragmentData::Raw(bytes) => (
crate::kfx::serialization::create_raw_media_header(),
Some(bytes.as_slice()),
),
};
SerializedEntity {
id,
entity_type: frag.ftype as u32,
data,
raw,
}
})
.collect()
}