use std::collections::{HashMap, HashSet};
use std::io::{self, Seek, Write};
use flate2::Compression;
use flate2::write::ZlibEncoder;
use crate::mobi::index::{
GuideBuildEntry, NcxBuildEntry, build_chunk_indx, build_cncx, build_guide_indx, build_ncx_indx,
build_skel_indx, calculate_cncx_offsets,
};
use crate::mobi::skeleton::{Chunker, ChunkerResult};
use crate::mobi::writer_transform::{
rewrite_css_references_fast, rewrite_html_references_fast, write_base32_4, write_base32_10,
};
use crate::model::{Book, Resource, TocEntry};
use crate::util::guess_media_type;
use super::Exporter;
const RECORD_SIZE: usize = 4096;
const NULL_INDEX: u32 = 0xFFFF_FFFF;
const XOR_KEY_LEN: usize = 20;
mod guide;
mod kf8;
use kf8::Kf8Builder;
#[derive(Debug, Clone, Default)]
pub struct Azw3Config {
pub normalize: bool,
}
pub struct Azw3Exporter {
config: Azw3Config,
}
impl Azw3Exporter {
pub fn new() -> Self {
Self {
config: Azw3Config::default(),
}
}
pub fn with_config(mut self, config: Azw3Config) -> Self {
self.config = config;
self
}
}
impl Default for Azw3Exporter {
fn default() -> Self {
Self::new()
}
}
impl Exporter for Azw3Exporter {
fn export<W: Write + Seek>(&self, book: &Book, writer: &mut W) -> crate::Result<()> {
let normalize = self.config.normalize || book.requires_normalized_export();
let builder = Kf8Builder::new(book, normalize)?;
Ok(builder.write(writer)?)
}
}
struct BookContext {
resources: HashMap<String, Resource>,
spine: Vec<SpineItem>,
toc: Vec<TocEntry>,
metadata: crate::model::Metadata,
landmarks: Vec<crate::model::Landmark>,
}
impl BookContext {
fn landmarks(&self) -> &[crate::model::Landmark] {
&self.landmarks
}
}
struct SpineItem {
href: String,
}
impl BookContext {
fn from_book(book: &Book, normalize: bool) -> crate::Result<Self> {
if normalize {
Self::from_normalized(book)
} else {
Self::from_raw(book)
}
}
fn from_raw(book: &Book) -> crate::Result<Self> {
book.resolve_toc();
let metadata = book.metadata().clone();
let toc = book.toc().to_vec();
let spine_entries = book.spine();
let mut spine = Vec::with_capacity(spine_entries.len());
let mut resources = HashMap::new();
for entry in spine_entries {
let href = book
.source_id(entry.id)
.unwrap_or("unknown.xhtml")
.to_string();
let data = book.load_raw(entry.id)?;
let media_type = match guess_media_type(&href) {
"application/octet-stream" => "application/xhtml+xml",
other => other,
};
resources.insert(href.clone(), Resource { data, media_type });
spine.push(SpineItem { href });
}
let asset_paths = book.list_assets();
for path in asset_paths {
if resources.contains_key(path) {
continue;
}
let data = book.load_asset(path)?;
let media_type = guess_media_type(path);
resources.insert(path.clone(), Resource { data, media_type });
}
Ok(Self {
resources,
spine,
toc,
metadata,
landmarks: book.landmarks().to_vec(),
})
}
fn from_normalized(book: &Book) -> crate::Result<Self> {
use super::normalize::normalize_book;
book.resolve_toc();
let normalized = normalize_book(book)?;
let metadata = book.metadata().clone();
let toc = normalized.rewrite_toc(book.toc());
let landmarks: Vec<crate::model::Landmark> = book
.landmarks()
.iter()
.map(|lm| {
let mut lm = lm.clone();
lm.href = normalized.rewrite_link(&lm.href);
lm
})
.collect();
let mut resources = HashMap::new();
if !normalized.css.is_empty() {
resources.insert(
"style.css".to_string(),
Resource {
data: normalized.css.into_bytes(),
media_type: "text/css",
},
);
}
let mut spine = Vec::with_capacity(normalized.chapters.len());
for (i, chapter) in normalized.chapters.iter().enumerate() {
let href = format!("chapter_{}.xhtml", i);
resources.insert(
href.clone(),
Resource {
data: chapter.document.as_bytes().to_vec(),
media_type: "application/xhtml+xml",
},
);
spine.push(SpineItem { href });
}
for asset_path in &normalized.assets {
if let Ok(data) = book.load_asset(asset_path) {
let media_type = guess_media_type(asset_path);
resources.insert(asset_path.clone(), Resource { data, media_type });
}
}
Ok(Self {
resources,
spine,
toc,
metadata,
landmarks,
})
}
}