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,
generate_content_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(&ctx));
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);
let used = collect_used_symbols(&fragments);
fragments.extend(build_style_fragments(&mut ctx, &used.styles));
fragments.extend(build_anchor_fragments(&mut ctx, &used.anchors));
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));
let referenced_fonts: BTreeSet<String> = fragments
.iter()
.filter(|f| f.ftype == KfxSymbol::Font as u64)
.filter_map(|f| match &f.data {
crate::kfx::fragment::FragmentData::Ion(IonValue::Struct(fields)) => fields
.iter()
.find(|(k, _)| *k == KfxSymbol::Location as u64)
.and_then(|(_, v)| match v {
IonValue::String(loc) => Some(loc.clone()),
_ => None,
}),
_ => None,
})
.collect();
fragments
.retain(|f| f.ftype != KfxSymbol::Bcrawfont as u64 || referenced_fonts.contains(&f.fid));
fragments[0] = build_content_features_fragment(&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");
ctx.nav_container_symbols.page_list = ctx.symbols.get_or_intern("page_list");
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();
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) =
build_chapter_entities_grouped(&chapter, *chapter_id, section_name, ctx);
section_fragments.push(section);
storyline_fragments.push(storyline);
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);
}
}
}
}
let content_fragments = build_content_fragments(ctx);
(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,
) {
let used_resource_symbols = collect_used_symbols(fragments).resources;
for asset_path in book.list_assets() {
if is_media_asset(book, asset_path)
&& let Ok(data) = book.load_asset(asset_path)
{
if detect_media_format(asset_path, &data).is_font() {
fragments.push(build_font_data_fragment(asset_path, data, ctx));
continue;
}
let referenced = ctx
.resource_registry
.get_name(asset_path)
.and_then(|name| ctx.symbols.get(name))
.is_some_and(|sym| used_resource_symbols.contains(&sym));
if !referenced {
continue;
}
record_media_features(&data, ctx);
fragments.push(build_external_resource_fragment(asset_path, &data, ctx));
fragments.push(build_resource_fragment(asset_path, data, ctx));
}
}
}
#[derive(Default)]
struct UsedSymbols {
resources: BTreeSet<u64>,
styles: BTreeSet<u64>,
anchors: BTreeSet<u64>,
}
fn collect_used_symbols(fragments: &[KfxFragment]) -> UsedSymbols {
fn walk(value: &IonValue, used: &mut UsedSymbols) {
match value {
IonValue::Struct(fields) => {
for (key, val) in fields {
if let IonValue::Symbol(sym) = val {
if *key == KfxSymbol::ResourceName as u64 {
used.resources.insert(*sym);
} else if *key == KfxSymbol::Style as u64 {
used.styles.insert(*sym);
} else if *key == KfxSymbol::LinkTo as u64 {
used.anchors.insert(*sym);
}
}
walk(val, used);
}
}
IonValue::List(items) => {
for item in items {
walk(item, used);
}
}
IonValue::Annotated(_, inner) => walk(inner, used),
_ => {}
}
}
let mut used = UsedSymbols::default();
for fragment in fragments {
if let crate::kfx::fragment::FragmentData::Ion(value) = &fragment.data {
walk(value, &mut used);
}
}
used
}
fn record_media_features(data: &[u8], ctx: &mut ExportContext) {
if !ctx.has_hdv_image
&& let Some((width, height)) = crate::util::extract_image_dimensions(data)
&& (width > 1920 || height > 1920)
{
ctx.has_hdv_image = true;
}
if !ctx.jpg_rst_marker_present && data.starts_with(&[0xFF, 0xD8]) {
let mut i = 0;
while let Some(pos) = memchr::memchr(0xFF, &data[i..]) {
let at = i + pos;
match data.get(at + 1) {
Some(b) if (0xD0..=0xD7).contains(b) => {
ctx.jpg_rst_marker_present = true;
break;
}
Some(_) => i = at + 1,
None => break,
}
}
}
}
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 has_text_content = fragments
.iter()
.any(|f| f.ftype == KfxSymbol::Content as u64);
let format_caps_ion = build_format_capabilities_ion(has_text_content);
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()
}