ebook-rs 0.13.0

Pure Rust multi-format eBook engine (EPUB2/3, MOBI, AZW3, FB2, LIT, CBZ, PDF, ODT, TXT, MD) with SpeechSynthesis TTS Word Synchronizer, Legacy Charset Decoding, Auto Language Detection, Zstd State Caching, Universal EPUB3 Exporter, Zero-Copy mmap, and Readium LCP/Locator APIs.
Documentation
use crate::archive::EpubArchive;
use crate::book::Book;
use crate::deobfuscate::FontDeobfuscator;
use crate::layout::RenditionLayout;
use crate::metadata::{Metadata, PageProgressionDirection, SpineItem};
use crate::nav::NavPoint;
use crate::opf::OpfPackage;
use crate::section::{Section, extract_plain_text};
use std::collections::HashMap;

/// Microsoft Reader LIT Format Parser.
pub struct LitBook;

impl LitBook {
    /// Parse Microsoft Reader LIT file bytes into a `Book` struct.
    pub fn parse(bytes: &[u8]) -> Result<Book, String> {
        if bytes.len() < 8 {
            return Err("File too small for LIT header".to_string());
        }

        // LIT files start with ITOL / ITLS header or raw container stream
        if !bytes.starts_with(b"ITOL")
            && !bytes.starts_with(b"ITLS")
            && !bytes.contains_str(b"ITOL")
        {
            return Err("Not a valid Microsoft Reader LIT container".to_string());
        }

        // Extract HTML text strings embedded inside LIT binary container streams
        let text_content = extract_html_from_lit_bytes(bytes);
        let plain_text = extract_plain_text(&text_content);
        let plain_text_lower = plain_text.to_lowercase();
        let char_count = plain_text.chars().count();

        let mut sections = Vec::new();
        let mut spine = Vec::new();
        let mut toc = Vec::new();

        sections.push(Section {
            index: 0,
            idref: "lit_sec_0".to_string(),
            href: "lit_sec_0.html".to_string(),
            full_path: "lit_sec_0.html".to_string(),
            raw_html: text_content.clone(),
            processed_html: text_content,
            plain_text,
            plain_text_lower,
            char_count,
            viewport_width: None,
            viewport_height: None,
        });

        spine.push(SpineItem {
            index: 0,
            idref: "lit_sec_0".to_string(),
            href: "lit_sec_0.html".to_string(),
            linear: true,
            media_type: "text/html".to_string(),
            properties: Vec::new(),
        });

        toc.push(NavPoint {
            id: "toc_0".to_string(),
            label: "Chapter 1".to_string(),
            href: "lit_sec_0.html".to_string(),
            full_path: "lit_sec_0.html".to_string(),
            subitems: Vec::new(),
        });

        let metadata = Metadata {
            title: "LIT Document".to_string(),
            creators: Vec::new(),
            publishers: Vec::new(),
            languages: vec!["en".to_string()],
            rights: None,
            description: None,
            identifier: None,
            pub_date: None,
            modified_date: None,
            subjects: Vec::new(),
            cover_id: None,
            cover_href: None,
            direction: PageProgressionDirection::Ltr,
            meta_properties: HashMap::new(),
            accessibility: Default::default(),
        };

        let opf = OpfPackage {
            version: "2.0".to_string(),
            opf_path: "content.opf".to_string(),
            opf_dir: "".to_string(),
            metadata,
            manifest: ahash::AHashMap::new(),
            spine,
            guide: Vec::new(),
            toc_item_id: None,
            nav_item_id: None,
        };

        let mut book = Book {
            archive: EpubArchive::empty(),
            opf,
            layout: RenditionLayout::default(),
            toc,
            landmarks: Vec::new(),
            page_list: Vec::new(),
            sections,
            locations: crate::locations::Locations::default(),
            annotations: crate::annotations::AnnotationManager::default(),
            before_display_hooks: Vec::new(),
            font_deobfuscator: FontDeobfuscator::parse_encryption_xml(""),
            media_overlays: HashMap::new(),
            render_cache: parking_lot::Mutex::new(HashMap::new()),
        };

        book.generate_locations(1000);
        Ok(book)
    }
}

trait ContainsStr {
    fn contains_str(&self, needle: &[u8]) -> bool;
}

impl ContainsStr for [u8] {
    fn contains_str(&self, needle: &[u8]) -> bool {
        self.windows(needle.len()).any(|w| w == needle)
    }
}

fn extract_html_from_lit_bytes(bytes: &[u8]) -> String {
    let mut out = String::new();

    // Check if container bytes are UTF-16LE encoded
    let text = if bytes.windows(2).any(|w| w == b"<\0") {
        let u16_data: Vec<u16> = bytes
            .chunks_exact(2)
            .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
            .collect();
        String::from_utf16_lossy(&u16_data)
    } else {
        String::from_utf8_lossy(bytes).to_string()
    };

    for line in text.lines() {
        if line.contains('<') && line.contains('>') {
            out.push_str(line);
            out.push('\n');
        }
    }

    if out.is_empty() {
        "<p>LIT Document Content</p>".to_string()
    } else {
        out
    }
}