A fork of pulp-os for the xteink4 adding custom apps
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove smol-epub from kernel crate; improve EpubState encapsulation Or if you want a two-line version with a body: Remove smol-epub from kernel crate; improve EpubState encapsulation Inline fnv1a_icase, define DecodedImage locally, make image decode pluggable via registered callback. Move epub pipeline methods to impl EpubState and privatize internal fields (name_hash, archive_size, chapter_sizes). Kernel now compiles with zero format-specific dependencies.

hansmrtn 9b1f3353 70aa004d

+294 -222
-1
Cargo.lock
··· 1305 1305 "esp-hal", 1306 1306 "log", 1307 1307 "nb 1.1.0", 1308 - "smol-epub", 1309 1308 "static_cell", 1310 1309 ] 1311 1310
-1
kernel/Cargo.toml
··· 24 24 static_cell.workspace = true 25 25 nb.workspace = true 26 26 log.workspace = true 27 - smol-epub.workspace = true 28 27 esp-alloc = { version = "0.9.0", features = ["internal-heap-stats"] }
+3 -3
kernel/src/drivers/sdcard.rs
··· 27 27 // 28 28 // sync SdCard uses RefCell internally, takes &self for BlockDevice 29 29 // methods; we delegate AsyncBlockDevice &mut self to the inner &self 30 - // methods -- all resolve immediately since SPI is DMA-blocking 30 + // methods. All resolve immediately since SPI is DMA-blocking 31 31 32 32 pub(crate) struct BlockDeviceAdapter<D: BlockDevice>(D); 33 33 ··· 107 107 // num_bytes() to force init and verify the card responds 108 108 // 109 109 // pub so Board::init can run this before other SPI peripherals 110 - // touch the bus -- SD spec requires a clean 400 kHz bus for CMD0 110 + // touch the bus - SD spec requires a clean 400 kHz bus for CMD0 111 111 pub fn init_card(spi_device: SdSpiDevice) -> Option<SyncSdCard> { 112 112 let sd = SdCard::new(spi_device, esp_hal::delay::Delay::new()); 113 113 ··· 188 188 // drive a future to completion in exactly one poll 189 189 // 190 190 // correct because the SPI bus is blocking and the sync SdCard 191 - // completes every operation before returning -- no inner .await 191 + // completes every operation before returning - no inner .await 192 192 // ever returns Pending 193 193 // 194 194 // only use for file-level operations (open, read, write, seek,
+1 -1
kernel/src/kernel/app.rs
··· 9 9 // distros define their own AppId enum and implement AppIdType for 10 10 // it. the kernel never knows which specific apps exist. 11 11 // 12 - // QuickAction types also live here -- they are pure data describing 12 + // QuickAction types also live here - they are pure data describing 13 13 // what actions an app exposes; the renderer (QuickMenu widget) is 14 14 // app-side, but the protocol is kernel-side 15 15
+9 -1
kernel/src/kernel/bookmarks.rs
··· 8 8 9 9 use crate::drivers::sdcard::SdStorage; 10 10 use crate::drivers::storage; 11 - pub use smol_epub::cache::fnv1a_icase; 11 + // FNV-1a hash with ASCII case folding, used for bookmark filename lookups. 12 + pub fn fnv1a_icase(data: &[u8]) -> u32 { 13 + let mut h: u32 = 0x811c_9dc5; 14 + for &b in data { 15 + h ^= b.to_ascii_lowercase() as u32; 16 + h = h.wrapping_mul(0x0100_0193); 17 + } 18 + h 19 + } 12 20 13 21 // little-endian helpers for binary record encoding 14 22 #[inline]
+33 -8
kernel/src/kernel/work_queue.rs
··· 18 18 use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; 19 19 use embassy_sync::channel::Channel; 20 20 21 - use smol_epub::DecodedImage; 21 + // 1-bit decoded image 22 + pub struct DecodedImage { 23 + pub width: u16, 24 + pub height: u16, 25 + pub data: Vec<u8>, 26 + pub stride: usize, 27 + } 28 + 29 + impl core::fmt::Debug for DecodedImage { 30 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 31 + f.debug_struct("DecodedImage") 32 + .field("width", &self.width) 33 + .field("height", &self.height) 34 + .field("data_len", &self.data.len()) 35 + .field("stride", &self.stride) 36 + .finish() 37 + } 38 + } 39 + 40 + pub type ImageDecodeFn = fn(&[u8], bool, u16, u16) -> Result<DecodedImage, &'static str>; 41 + 42 + static IMAGE_DECODER: Mutex<Cell<Option<ImageDecodeFn>>> = Mutex::new(Cell::new(None)); 43 + 44 + pub fn register_image_decoder(f: ImageDecodeFn) { 45 + critical_section::with(|cs| IMAGE_DECODER.borrow(cs).set(Some(f))); 46 + } 47 + 48 + fn get_image_decoder() -> ImageDecodeFn { 49 + critical_section::with(|cs| IMAGE_DECODER.borrow(cs).get()) 50 + .expect("work_queue: no image decoder registered") 51 + } 22 52 23 53 #[derive(Clone, Copy, PartialEq, Eq, Debug)] 24 54 #[repr(u8)] ··· 133 163 static WORK_OUT: Channel<CriticalSectionRawMutex, WorkResult, 2> = Channel::new(); 134 164 135 165 // true if the input channel has room for at least one more item. 136 - // use before expensive extraction to avoid wasted work when the 137 - // worker hasn't consumed its previous item yet. 138 166 #[inline] 139 167 pub fn can_submit() -> bool { 140 168 !WORK_IN.is_full() ··· 204 232 g, 205 233 ); 206 234 207 - let result = if is_jpeg { 208 - smol_epub::jpeg::decode_jpeg_fit(&data, max_w, max_h) 209 - } else { 210 - smol_epub::png::decode_png_fit(&data, max_w, max_h) 211 - }; 235 + let decode = get_image_decoder(); 236 + let result = decode(&data, is_jpeg, max_w, max_h); 212 237 drop(data); 213 238 214 239 if g != active_generation() {
+1 -2
src/apps/mod.rs
··· 1 1 // app modules, AppId definition, and re-exports from kernel::app 2 2 // 3 - // AppId is defined here (the distro side) -- the kernel is generic 4 - // over AppIdType and never knows which concrete apps exist 3 + // AppId is defined here (the distro side) the kernel attempts to be generic 5 4 6 5 pub mod files; 7 6 pub mod home;
+166 -168
src/apps/reader/epubs.rs
··· 1 1 // epub init, chapter cache pipeline, and background cache state machine 2 + // 3 + // pure epub-state methods live on impl EpubState (init_zip, 4 + // check_cache, finish_cache, cache_chapter_async, try_cache_chapter). 5 + // methods that also touch PageState or ReaderApp fields stay on 6 + // impl ReaderApp (epub_init_opf, epub_index_chapter, bg_cache_step). 2 7 3 8 use alloc::vec::Vec; 4 9 use core::cell::RefCell; ··· 10 15 use crate::kernel::KernelHandle; 11 16 use crate::kernel::work_queue; 12 17 13 - use super::{BgCacheState, CHAPTER_CACHE_MAX, EOCD_TAIL, PAGE_BUF, ReaderApp, ZipIndex}; 18 + use super::{BgCacheState, CHAPTER_CACHE_MAX, EOCD_TAIL, EpubState, PAGE_BUF, ReaderApp, ZipIndex}; 14 19 15 20 // one cell shared between reader and writer; safe because 16 21 // stream_strip_entry_async never borrows both simultaneously ··· 35 40 } 36 41 } 37 42 38 - impl ReaderApp { 39 - pub(super) fn epub_init_zip(&mut self, k: &mut KernelHandle<'_>) -> crate::error::Result<()> { 40 - let (nb, nl) = self.name_copy(); 41 - let name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 42 - 43 + impl EpubState { 44 + pub(super) fn init_zip( 45 + &mut self, 46 + k: &mut KernelHandle<'_>, 47 + name: &str, 48 + scratch: &mut [u8], 49 + ) -> crate::error::Result<()> { 43 50 let epub_size = k.file_size(name)?; 44 51 if epub_size < 22 { 45 52 return Err(Error::new( ··· 47 54 "epub_init_zip: too small", 48 55 )); 49 56 } 50 - self.epub.archive_size = epub_size; 51 - self.epub.name_hash = cache::fnv1a(name.as_bytes()); 52 - self.epub.cache_dir = cache::dir_name_for_hash(self.epub.name_hash); 57 + self.archive_size = epub_size; 58 + self.name_hash = cache::fnv1a(name.as_bytes()); 59 + self.cache_dir = cache::dir_name_for_hash(self.name_hash); 53 60 54 61 let tail_size = (epub_size as usize).min(EOCD_TAIL); 55 62 let tail_offset = epub_size - tail_size as u32; 56 - let n = k.read_chunk(name, tail_offset, &mut self.pg.buf[..tail_size])?; 57 - let (cd_offset, cd_size) = ZipIndex::parse_eocd(&self.pg.buf[..n], epub_size) 63 + let n = k.read_chunk(name, tail_offset, &mut scratch[..tail_size])?; 64 + let (cd_offset, cd_size) = ZipIndex::parse_eocd(&scratch[..n], epub_size) 58 65 .map_err(|_| Error::new(ErrorKind::ParseFailed, "epub_init_zip: EOCD"))?; 59 66 60 67 log::info!( ··· 70 77 .map_err(|_| Error::new(ErrorKind::OutOfMemory, "epub_init_zip: CD alloc"))?; 71 78 cd_buf.resize(cd_size as usize, 0); 72 79 super::read_full(k, name, cd_offset, &mut cd_buf)?; 73 - self.epub.zip.clear(); 74 - self.epub 75 - .zip 80 + self.zip.clear(); 81 + self.zip 76 82 .parse_central_directory(&cd_buf) 77 83 .map_err(|_| Error::new(ErrorKind::ParseFailed, "epub_init_zip: CD parse"))?; 78 84 drop(cd_buf); 79 85 80 - log::info!("epub: {} entries in ZIP", self.epub.zip.count()); 86 + log::info!("epub: {} entries in ZIP", self.zip.count()); 81 87 82 88 Ok(()) 83 89 } 84 90 85 - pub(super) fn epub_init_opf(&mut self, k: &mut KernelHandle<'_>) -> crate::error::Result<()> { 86 - let (nb, nl) = self.name_copy(); 87 - let name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 88 - 89 - let mut opf_path_buf = [0u8; epub::OPF_PATH_CAP]; 90 - let opf_path_len = if let Some(container_idx) = self.epub.zip.find("META-INF/container.xml") 91 - { 92 - let container_data = super::extract_zip_entry(k, name, &self.epub.zip, container_idx) 93 - .map_err(|_| { 94 - Error::new(ErrorKind::ReadFailed, "epub_init_opf: container read") 95 - })?; 96 - let len = epub::parse_container(&container_data, &mut opf_path_buf).map_err(|_| { 97 - Error::new(ErrorKind::ParseFailed, "epub_init_opf: container parse") 98 - })?; 99 - drop(container_data); 100 - len 101 - } else { 102 - log::warn!("epub: no container.xml, scanning for .opf"); 103 - epub::find_opf_in_zip(&self.epub.zip, &mut opf_path_buf) 104 - .map_err(|_| Error::new(ErrorKind::NotFound, "epub_init_opf: no .opf in zip"))? 105 - }; 106 - 107 - let opf_path = core::str::from_utf8(&opf_path_buf[..opf_path_len]) 108 - .map_err(|_| Error::new(ErrorKind::BadEncoding, "epub_init_opf: OPF path"))?; 109 - 110 - log::info!("epub: OPF at {}", opf_path); 111 - 112 - let opf_idx = self 113 - .epub 114 - .zip 115 - .find(opf_path) 116 - .or_else(|| self.epub.zip.find_icase(opf_path)) 117 - .ok_or(Error::new(ErrorKind::NotFound, "epub_init_opf: OPF entry"))?; 118 - let opf_data = super::extract_zip_entry(k, name, &self.epub.zip, opf_idx) 119 - .map_err(|_| Error::new(ErrorKind::ReadFailed, "epub_init_opf: OPF read"))?; 120 - 121 - let opf_dir = opf_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(""); 122 - epub::parse_opf( 123 - &opf_data, 124 - opf_dir, 125 - &self.epub.zip, 126 - &mut self.epub.meta, 127 - &mut self.epub.spine, 128 - ) 129 - .map_err(|_| Error::new(ErrorKind::ParseFailed, "epub_init_opf: OPF parse"))?; 130 - 131 - // defer TOC to NeedToc to avoid stack overflow while OPF is live 132 - self.epub.toc_source = epub::find_toc_source(&opf_data, opf_dir, &self.epub.zip); 133 - drop(opf_data); 134 - 135 - log::info!( 136 - "epub: \"{}\" by {} -- {} chapters", 137 - self.epub.meta.title_str(), 138 - self.epub.meta.author_str(), 139 - self.epub.spine.len() 140 - ); 141 - 142 - let tlen = self.epub.meta.title_len as usize; 143 - if tlen > 0 { 144 - let n = tlen.min(self.title.len()); 145 - self.title[..n].copy_from_slice(&self.epub.meta.title[..n]); 146 - self.title_len = n; 147 - 148 - if let Err(e) = k.save_title(name, self.epub.meta.title_str()) { 149 - log::warn!("epub: failed to save title mapping: {}", e); 150 - } 151 - } 152 - 153 - self.epub.toc.clear(); 154 - 155 - Ok(()) 156 - } 157 - 158 - pub(super) fn epub_check_cache( 91 + pub(super) fn check_cache( 159 92 &mut self, 160 93 k: &mut KernelHandle<'_>, 94 + scratch: &mut [u8], 161 95 ) -> crate::error::Result<bool> { 162 - let dir_buf = self.epub.cache_dir; 96 + let dir_buf = self.cache_dir; 163 97 let dir = cache::dir_name_str(&dir_buf); 164 98 165 - // read into self.buf to avoid ~2 KB stack temporaries 166 - let meta_cap = cache::META_MAX_SIZE.min(self.pg.buf.len()); 167 - if let Ok(n) = 168 - k.read_app_subdir_chunk(dir, cache::META_FILE, 0, &mut self.pg.buf[..meta_cap]) 99 + let meta_cap = cache::META_MAX_SIZE.min(scratch.len()); 100 + if let Ok(n) = k.read_app_subdir_chunk(dir, cache::META_FILE, 0, &mut scratch[..meta_cap]) 169 101 && let Ok(count) = cache::parse_cache_meta( 170 - &self.pg.buf[..n], 171 - self.epub.archive_size, 172 - self.epub.name_hash, 173 - self.epub.spine.len(), 174 - &mut self.epub.chapter_sizes, 102 + &scratch[..n], 103 + self.archive_size, 104 + self.name_hash, 105 + self.spine.len(), 106 + &mut self.chapter_sizes, 175 107 ) 176 108 { 177 - self.epub.chapters_cached = true; 109 + self.chapters_cached = true; 178 110 for i in 0..count { 179 - self.epub.ch_cached[i] = true; 111 + self.ch_cached[i] = true; 180 112 } 181 113 log::info!("epub: cache hit ({} chapters)", count); 182 114 return Ok(true); 183 115 } 184 116 185 - log::info!( 186 - "epub: building cache for {} chapters", 187 - self.epub.spine.len() 188 - ); 117 + log::info!("epub: building cache for {} chapters", self.spine.len()); 189 118 k.ensure_app_subdir(dir)?; 190 - self.epub.cache_chapter = 0; 119 + self.cache_chapter = 0; 191 120 Ok(false) 192 121 } 193 122 194 - pub(super) fn epub_finish_cache( 195 - &mut self, 196 - k: &mut KernelHandle<'_>, 197 - ) -> crate::error::Result<bool> { 198 - let dir_buf = self.epub.cache_dir; 123 + pub(super) fn finish_cache(&mut self, k: &mut KernelHandle<'_>) -> crate::error::Result<bool> { 124 + let dir_buf = self.cache_dir; 199 125 let dir = cache::dir_name_str(&dir_buf); 200 - let spine_len = self.epub.spine.len(); 126 + let spine_len = self.spine.len(); 201 127 202 128 let mut meta_buf = [0u8; cache::META_MAX_SIZE]; 203 129 let meta_len = cache::encode_cache_meta( 204 - self.epub.archive_size, 205 - self.epub.name_hash, 206 - &self.epub.chapter_sizes[..spine_len], 130 + self.archive_size, 131 + self.name_hash, 132 + &self.chapter_sizes[..spine_len], 207 133 &mut meta_buf, 208 134 ); 209 135 k.write_app_subdir(dir, cache::META_FILE, &meta_buf[..meta_len])?; 210 136 211 - self.epub.chapters_cached = true; 137 + self.chapters_cached = true; 212 138 log::info!("epub: cache complete"); 213 139 Ok(false) 214 140 } 215 141 216 - // async streaming chapter cache; used for both initial and background 217 - // caching. decompresses, strips html, and writes chunks to sd without 218 - // ever holding full xhtml in ram. yields between decompression 219 - // iterations so the scheduler's select(run_background, input) can 220 - // interrupt on user input (e.g. pressing back during book open) 221 - pub(super) async fn epub_cache_chapter_async( 142 + // async streaming chapter cache: decompress, strip HTML, write to SD. 143 + pub(super) async fn cache_chapter_async( 222 144 &mut self, 223 145 k: &mut KernelHandle<'_>, 224 146 ch: usize, 147 + epub_name: &str, 225 148 ) -> crate::error::Result<()> { 226 - if ch >= self.epub.spine.len() || self.epub.ch_cached[ch] { 149 + if ch >= self.spine.len() || self.ch_cached[ch] { 227 150 return Ok(()); 228 151 } 229 152 230 - let dir_buf = self.epub.cache_dir; 153 + let dir_buf = self.cache_dir; 231 154 let dir = cache::dir_name_str(&dir_buf); 232 - let (nb, nl) = self.name_copy(); 233 - let epub_name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 234 - let entry_idx = self.epub.spine.items[ch] as usize; 235 - let entry = *self.epub.zip.entry(entry_idx); 155 + let entry_idx = self.spine.items[ch] as usize; 156 + let entry = *self.zip.entry(entry_idx); 236 157 let ch_file = cache::chapter_file_name(ch as u16); 237 158 let ch_str = cache::chapter_file_str(&ch_file); 238 159 ··· 251 172 &mut writer, 252 173 ) 253 174 .await 254 - .map_err(|msg| Error::from(msg).with_source("epub_cache_chapter_async: stream"))?; 175 + .map_err(|msg| Error::from(msg).with_source("cache_chapter_async: stream"))?; 255 176 256 - self.epub.chapter_sizes[ch] = text_size; 257 - self.epub.ch_cached[ch] = true; 177 + self.chapter_sizes[ch] = text_size; 178 + self.ch_cached[ch] = true; 258 179 259 180 log::info!( 260 181 "epub: cached ch{}/{} = {} bytes", 261 182 ch, 262 - self.epub.spine.len(), 183 + self.spine.len(), 263 184 text_size 264 185 ); 265 186 Ok(()) 266 187 } 267 188 268 - pub(super) fn epub_index_chapter(&mut self) { 269 - self.reset_paging(); 270 - // force reload; ch_cache may hold a different chapter's data 271 - // with the same byte count (try_cache_chapter only checks len) 272 - self.epub.ch_cache = Vec::new(); 273 - let ch = self.epub.chapter as usize; 274 - self.file_size = if ch < cache::MAX_CACHE_CHAPTERS { 275 - self.epub.chapter_sizes[ch] 276 - } else { 277 - 0 278 - }; 279 - log::info!( 280 - "epub: index chapter {}/{} ({} bytes cached text)", 281 - self.epub.chapter + 1, 282 - self.epub.spine.len(), 283 - self.file_size, 284 - ); 285 - } 286 - 287 189 pub(super) fn try_cache_chapter(&mut self, k: &mut KernelHandle<'_>) -> bool { 288 - if !self.is_epub || !self.epub.chapters_cached { 190 + if !self.chapters_cached { 289 191 return false; 290 192 } 291 193 292 - let ch = self.epub.chapter as usize; 194 + let ch = self.chapter as usize; 293 195 let ch_size = if ch < cache::MAX_CACHE_CHAPTERS { 294 - self.epub.chapter_sizes[ch] as usize 196 + self.chapter_sizes[ch] as usize 295 197 } else { 296 198 return false; 297 199 }; 298 200 299 201 if ch_size == 0 || ch_size > CHAPTER_CACHE_MAX { 300 - self.epub.ch_cache = Vec::new(); 202 + self.ch_cache = Vec::new(); 301 203 return false; 302 204 } 303 205 304 - if self.epub.ch_cache.len() == ch_size { 206 + if self.ch_cache.len() == ch_size { 305 207 log::info!("chapter cache: reusing {} bytes in RAM", ch_size); 306 208 return true; 307 209 } 308 210 309 - self.epub.ch_cache = Vec::new(); 310 - if self.epub.ch_cache.try_reserve_exact(ch_size).is_err() { 211 + self.ch_cache = Vec::new(); 212 + if self.ch_cache.try_reserve_exact(ch_size).is_err() { 311 213 log::info!("chapter cache: OOM for {} bytes", ch_size); 312 214 return false; 313 215 } 314 - self.epub.ch_cache.resize(ch_size, 0); 216 + self.ch_cache.resize(ch_size, 0); 315 217 316 - let dir_buf = self.epub.cache_dir; 218 + let dir_buf = self.cache_dir; 317 219 let dir = cache::dir_name_str(&dir_buf); 318 - let ch_file = cache::chapter_file_name(self.epub.chapter); 220 + let ch_file = cache::chapter_file_name(self.chapter); 319 221 let ch_str = cache::chapter_file_str(&ch_file); 320 222 321 223 let mut pos = 0usize; ··· 325 227 dir, 326 228 ch_str, 327 229 pos as u32, 328 - &mut self.epub.ch_cache[pos..pos + chunk], 230 + &mut self.ch_cache[pos..pos + chunk], 329 231 ) { 330 232 Ok(n) if n > 0 => pos += n, 331 233 Ok(_) => break, 332 234 Err(e) => { 333 235 log::info!("chapter cache: SD read failed at {}: {}", pos, e); 334 - self.epub.ch_cache = Vec::new(); 236 + self.ch_cache = Vec::new(); 335 237 return false; 336 238 } 337 239 } ··· 339 241 340 242 log::info!( 341 243 "chapter cache: loaded ch{} ({} bytes) into RAM", 342 - self.epub.chapter, 244 + self.chapter, 343 245 ch_size, 344 246 ); 345 247 true 346 248 } 347 249 250 + #[inline] 251 + pub(super) fn current_chapter_size(&self) -> u32 { 252 + self.chapter_size(self.chapter as usize) 253 + } 254 + } 255 + 256 + impl ReaderApp { 257 + pub(super) fn epub_init_opf(&mut self, k: &mut KernelHandle<'_>) -> crate::error::Result<()> { 258 + let (nb, nl) = self.name_copy(); 259 + let name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 260 + 261 + let mut opf_path_buf = [0u8; epub::OPF_PATH_CAP]; 262 + let opf_path_len = if let Some(container_idx) = self.epub.zip.find("META-INF/container.xml") 263 + { 264 + let container_data = super::extract_zip_entry(k, name, &self.epub.zip, container_idx) 265 + .map_err(|_| { 266 + Error::new(ErrorKind::ReadFailed, "epub_init_opf: container read") 267 + })?; 268 + let len = epub::parse_container(&container_data, &mut opf_path_buf).map_err(|_| { 269 + Error::new(ErrorKind::ParseFailed, "epub_init_opf: container parse") 270 + })?; 271 + drop(container_data); 272 + len 273 + } else { 274 + log::warn!("epub: no container.xml, scanning for .opf"); 275 + epub::find_opf_in_zip(&self.epub.zip, &mut opf_path_buf) 276 + .map_err(|_| Error::new(ErrorKind::NotFound, "epub_init_opf: no .opf in zip"))? 277 + }; 278 + 279 + let opf_path = core::str::from_utf8(&opf_path_buf[..opf_path_len]) 280 + .map_err(|_| Error::new(ErrorKind::BadEncoding, "epub_init_opf: OPF path"))?; 281 + 282 + log::info!("epub: OPF at {}", opf_path); 283 + 284 + let opf_idx = self 285 + .epub 286 + .zip 287 + .find(opf_path) 288 + .or_else(|| self.epub.zip.find_icase(opf_path)) 289 + .ok_or(Error::new(ErrorKind::NotFound, "epub_init_opf: OPF entry"))?; 290 + let opf_data = super::extract_zip_entry(k, name, &self.epub.zip, opf_idx) 291 + .map_err(|_| Error::new(ErrorKind::ReadFailed, "epub_init_opf: OPF read"))?; 292 + 293 + let opf_dir = opf_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(""); 294 + epub::parse_opf( 295 + &opf_data, 296 + opf_dir, 297 + &self.epub.zip, 298 + &mut self.epub.meta, 299 + &mut self.epub.spine, 300 + ) 301 + .map_err(|_| Error::new(ErrorKind::ParseFailed, "epub_init_opf: OPF parse"))?; 302 + 303 + // defer TOC to NeedToc to avoid stack overflow while OPF is live 304 + self.epub.toc_source = epub::find_toc_source(&opf_data, opf_dir, &self.epub.zip); 305 + drop(opf_data); 306 + 307 + log::info!( 308 + "epub: \"{}\" by {} -- {} chapters", 309 + self.epub.meta.title_str(), 310 + self.epub.meta.author_str(), 311 + self.epub.spine.len() 312 + ); 313 + 314 + let tlen = self.epub.meta.title_len as usize; 315 + if tlen > 0 { 316 + let n = tlen.min(self.title.len()); 317 + self.title[..n].copy_from_slice(&self.epub.meta.title[..n]); 318 + self.title_len = n; 319 + 320 + if let Err(e) = k.save_title(name, self.epub.meta.title_str()) { 321 + log::warn!("epub: failed to save title mapping: {}", e); 322 + } 323 + } 324 + 325 + self.epub.toc.clear(); 326 + 327 + Ok(()) 328 + } 329 + 330 + pub(super) fn epub_index_chapter(&mut self) { 331 + self.reset_paging(); 332 + // force reload; ch_cache may hold a different chapter's data 333 + // with the same byte count (try_cache_chapter only checks len) 334 + self.epub.ch_cache = Vec::new(); 335 + self.file_size = self.epub.current_chapter_size(); 336 + log::info!( 337 + "epub: index chapter {}/{} ({} bytes cached text)", 338 + self.epub.chapter + 1, 339 + self.epub.spine.len(), 340 + self.file_size, 341 + ); 342 + } 343 + 348 344 // run one step of background caching; async because CacheChapter 349 - // awaits epub_cache_chapter_async which yields during deflate 345 + // awaits cache_chapter_async which yields during deflate 350 346 pub(super) async fn bg_cache_step(&mut self, k: &mut KernelHandle<'_>) { 351 347 match self.epub.bg_cache { 352 348 BgCacheState::CacheChapter => { ··· 363 359 // before continuing the sequential scan; forward/backward 364 360 // nav stays instant 365 361 let reading_ch = self.epub.chapter as usize; 362 + let (nb, nl) = self.name_copy(); 363 + let name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 366 364 for &adj in &[reading_ch + 1, reading_ch.saturating_sub(1)] { 367 365 if adj < spine_len && adj != reading_ch && !self.epub.ch_cached[adj] { 368 366 log::info!( ··· 370 368 adj, 371 369 reading_ch, 372 370 ); 373 - if let Err(e) = self.epub_cache_chapter_async(k, adj).await { 371 + if let Err(e) = self.epub.cache_chapter_async(k, adj, &name).await { 374 372 log::warn!("epub: priority ch{} failed: {}", adj, e); 375 373 } 376 374 } ··· 378 376 379 377 let ch = self.epub.cache_chapter as usize; 380 378 if ch >= spine_len { 381 - let _ = self.epub_finish_cache(k); 379 + let _ = self.epub.finish_cache(k); 382 380 self.epub.img_cache_ch = self.epub.chapter; 383 381 self.epub.img_cache_offset = 0; 384 382 self.epub.img_scan_wrapped = false; ··· 386 384 return; 387 385 } 388 386 389 - match self.epub_cache_chapter_async(k, ch).await { 387 + match self.epub.cache_chapter_async(k, ch, &name).await { 390 388 Ok(()) => { 391 389 self.epub.cache_chapter += 1; 392 390 // try nearby image dispatch before next chapter
+19 -9
src/apps/reader/images.rs
··· 9 9 use alloc::vec::Vec; 10 10 use core::cell::RefCell; 11 11 12 - use smol_epub::DecodedImage; 12 + use crate::kernel::work_queue::DecodedImage; 13 13 use smol_epub::cache; 14 14 use smol_epub::epub; 15 15 use smol_epub::html_strip::{IMG_REF, MARKER}; ··· 22 22 use super::{ 23 23 DEFAULT_IMG_H, MAX_IMAGES_PER_PAGE, NO_PREFETCH, PAGE_BUF, PRECACHE_IMG_MAX, ReaderApp, 24 24 }; 25 + 26 + fn from_smol_image(img: smol_epub::DecodedImage) -> DecodedImage { 27 + DecodedImage { 28 + width: img.width, 29 + height: img.height, 30 + data: img.data, 31 + stride: img.stride, 32 + } 33 + } 25 34 26 35 // result of scanning a chapter for the next uncached image 27 36 enum ScanResult { ··· 216 225 let do_decode = |k_ref: &mut KernelHandle<'_>| -> Result<DecodedImage, &'static str> { 217 226 let k_cell = RefCell::new(k_ref); 218 227 let read_err = |e: Error| -> &'static str { e.into() }; 219 - if is_jpeg && entry.method == zip::METHOD_STORED { 228 + let raw = if is_jpeg && entry.method == zip::METHOD_STORED { 220 229 smol_epub::jpeg::decode_jpeg_sd( 221 230 |off, buf| { 222 231 k_cell ··· 269 278 img_max_w, 270 279 img_max_h, 271 280 ) 272 - } 281 + }; 282 + raw.map(from_smol_image) 273 283 }; 274 284 275 285 let result = do_decode(k); ··· 325 335 let ch_path = self.epub.zip.entry_name(ch_zip_idx); 326 336 let ch_dir = ch_path.rsplit_once('/').map(|(d, _)| d).unwrap_or(""); 327 337 328 - let dir_buf = self.epub.cache_dir; 329 - let dir = cache::dir_name_str(&dir_buf); 338 + let dir = self.epub.cache_dir_str(); 330 339 331 340 let (nb, nl) = self.name_copy(); 332 341 let epub_name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); ··· 412 421 { 413 422 return Ok(ScanResult::NoneFound); 414 423 } 415 - let ch_size = self.epub.chapter_sizes[ch] as usize; 424 + let ch_size = self.epub.chapter_size(ch) as usize; 416 425 if ch_size == 0 { 417 426 return Ok(ScanResult::NoneFound); 418 427 } ··· 676 685 677 686 match result.outcome { 678 687 work_queue::WorkOutcome::ImageReady { path_hash, image } => { 679 - let dir_buf = self.epub.cache_dir; 680 - let dir = cache::dir_name_str(&dir_buf); 688 + let dir = self.epub.cache_dir_str(); 681 689 let img_name = img_cache_name(path_hash); 682 690 let img_file = img_cache_str(&img_name); 683 691 ··· 816 824 max_h, 817 825 ) 818 826 }; 819 - result.map_err(|msg| Error::from(msg).with_source("decode_image_streaming")) 827 + result 828 + .map(from_smol_image) 829 + .map_err(|msg| Error::from(msg).with_source("decode_image_streaming")) 820 830 } 821 831 822 832 pub(super) fn load_cached_image(
+43 -20
src/apps/reader/mod.rs
··· 27 27 use crate::kernel::QuickAction; 28 28 use crate::kernel::bookmarks; 29 29 use crate::kernel::work_queue; 30 + use crate::kernel::work_queue::DecodedImage; 30 31 use crate::ui::{Alignment, CONTENT_TOP, HEADER_W, Region, StackFmt, TITLE_Y_OFFSET}; 31 - use smol_epub::DecodedImage; 32 32 use smol_epub::cache; 33 33 use smol_epub::epub::{self, EpubMeta, EpubSpine, EpubToc, TocSource}; 34 34 use smol_epub::html_strip::{ ··· 233 233 // epub-specific state: zip index, metadata, spine, toc, chapter 234 234 // cache, background cache progress, image cache scan position 235 235 pub(super) struct EpubState { 236 + // --- publicly accessible from sibling modules --- 236 237 pub(super) zip: ZipIndex, 237 238 pub(super) meta: EpubMeta, 238 239 pub(super) spine: EpubSpine, 239 240 pub(super) chapter: u16, 240 241 241 242 pub(super) cache_dir: [u8; 8], 242 - pub(super) name_hash: u32, 243 - pub(super) archive_size: u32, 244 - pub(super) chapter_sizes: [u32; cache::MAX_CACHE_CHAPTERS], 243 + chapter_sizes: [u32; cache::MAX_CACHE_CHAPTERS], 245 244 pub(super) chapters_cached: bool, 246 245 pub(super) cache_chapter: u16, 247 246 pub(super) ch_cached: [bool; cache::MAX_CACHE_CHAPTERS], ··· 258 257 pub(super) toc_source: Option<TocSource>, 259 258 pub(super) toc_selected: usize, 260 259 pub(super) toc_scroll: usize, 260 + 261 + // --- private: only accessed by impl EpubState methods --- 262 + name_hash: u32, 263 + archive_size: u32, 261 264 } 262 265 263 266 impl EpubState { ··· 284 287 toc_source: None, 285 288 toc_selected: 0, 286 289 toc_scroll: 0, 290 + } 291 + } 292 + 293 + #[inline] 294 + pub(super) fn cache_dir_str(&self) -> &str { 295 + cache::dir_name_str(&self.cache_dir) 296 + } 297 + 298 + #[inline] 299 + pub(super) fn chapter_size(&self, ch: usize) -> u32 { 300 + if ch < cache::MAX_CACHE_CHAPTERS { 301 + self.chapter_sizes[ch] 302 + } else { 303 + 0 287 304 } 288 305 } 289 306 } ··· 875 892 continue; 876 893 } 877 894 878 - State::NeedInit => match self.epub_init_zip(k) { 879 - Ok(()) => { 880 - self.state = State::NeedOpf; 881 - ctx.set_loading(LOADING_REGION, "Loading", 25); 882 - } 883 - Err(e) => { 884 - log::info!("reader: epub init (zip) failed: {}", e); 885 - self.enter_error(ctx, e); 895 + State::NeedInit => { 896 + let (nb, nl) = self.name_copy(); 897 + let name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 898 + match self.epub.init_zip(k, name, &mut self.pg.buf) { 899 + Ok(()) => { 900 + self.state = State::NeedOpf; 901 + ctx.set_loading(LOADING_REGION, "Loading", 25); 902 + } 903 + Err(e) => { 904 + log::info!("reader: epub init (zip) failed: {}", e); 905 + self.enter_error(ctx, e); 906 + } 886 907 } 887 - }, 908 + } 888 909 889 910 State::NeedOpf => match self.epub_init_opf(k) { 890 911 Ok(()) => { ··· 941 962 ctx.set_loading(LOADING_REGION, "Caching", 55); 942 963 } 943 964 944 - State::NeedCache => match self.epub_check_cache(k) { 965 + State::NeedCache => match self.epub.check_cache(k, &mut self.pg.buf) { 945 966 Ok(true) => { 946 967 self.state = State::NeedIndex; 947 968 ctx.set_loading(LOADING_REGION, "Indexing", 75); ··· 951 972 // during deflate so the scheduler's select can 952 973 // interrupt if the user presses back 953 974 let ch = self.epub.chapter as usize; 954 - match self.epub_cache_chapter_async(k, ch).await { 975 + let (nb, nl) = self.name_copy(); 976 + let epub_name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 977 + match self.epub.cache_chapter_async(k, ch, epub_name).await { 955 978 Ok(()) => { 956 979 self.epub.chapters_cached = true; 957 980 self.epub.cache_chapter = 0; ··· 990 1013 { 991 1014 // async version yields during deflate so the 992 1015 // scheduler's select can interrupt on input 993 - if let Err(e) = self 994 - .epub_cache_chapter_async(k, self.epub.chapter as usize) 995 - .await 996 - { 1016 + let ch = self.epub.chapter as usize; 1017 + let (nb, nl) = self.name_copy(); 1018 + let epub_name = core::str::from_utf8(&nb[..nl]).unwrap_or(""); 1019 + if let Err(e) = self.epub.cache_chapter_async(k, ch, epub_name).await { 997 1020 self.enter_error(ctx, e); 998 1021 break; 999 1022 } ··· 1004 1027 1005 1028 self.epub_index_chapter(); 1006 1029 1007 - if self.try_cache_chapter(k) { 1030 + if self.is_epub && self.epub.try_cache_chapter(k) { 1008 1031 self.preindex_all_pages(); 1009 1032 } 1010 1033
+2 -4
src/apps/reader/paging.rs
··· 133 133 self.pg.prefetch_page = NO_PREFETCH; 134 134 self.pg.prefetch_len = 0; 135 135 } else if self.is_epub && self.epub.chapters_cached { 136 - let dir_buf = self.epub.cache_dir; 137 - let dir = cache::dir_name_str(&dir_buf); 136 + let dir = self.epub.cache_dir_str(); 138 137 let ch_file = cache::chapter_file_name(self.epub.chapter); 139 138 let ch_str = cache::chapter_file_str(&ch_file); 140 139 let n = k.read_app_subdir_chunk( ··· 180 179 if self.pg.page + 1 < self.pg.total_pages { 181 180 let pf_offset = self.pg.offsets[self.pg.page + 1]; 182 181 let pf_result = if self.is_epub && self.epub.chapters_cached { 183 - let dir_buf = self.epub.cache_dir; 184 - let dir = cache::dir_name_str(&dir_buf); 182 + let dir = self.epub.cache_dir_str(); 185 183 let ch_file = cache::chapter_file_name(self.epub.chapter); 186 184 let ch_str = cache::chapter_file_str(&ch_file); 187 185 k.read_app_subdir_chunk(dir, ch_str, pf_offset, &mut self.pg.prefetch)
+16
src/bin/main.rs
··· 144 144 145 145 kernel.boot(&mut app_mgr).await; 146 146 147 + // register the image decoder so the kernel's worker task can 148 + // decode JPEG/PNG without depending on smol-epub directly 149 + work_queue::register_image_decoder(|data, is_jpeg, max_w, max_h| { 150 + let raw = if is_jpeg { 151 + smol_epub::jpeg::decode_jpeg_fit(data, max_w, max_h) 152 + } else { 153 + smol_epub::png::decode_png_fit(data, max_w, max_h) 154 + }; 155 + raw.map(|img| work_queue::DecodedImage { 156 + width: img.width, 157 + height: img.height, 158 + data: img.data, 159 + stride: img.stride, 160 + }) 161 + }); 162 + 147 163 spawner 148 164 .spawn(tasks::input_task(input)) 149 165 .expect("spawn input_task");
+1 -4
src/lib.rs
··· 1 - // pulp-os -- e-reader firmware for the XTEink X4 1 + // pulp-os - e-reader firmware for the XTEink X4 2 2 3 3 #![no_std] 4 4 5 5 extern crate alloc; 6 6 7 - // kernel crate re-exports -- keeps crate::board, crate::drivers, 8 - // crate::kernel, crate::error paths working in app code without 9 - // import changes 10 7 pub use pulp_kernel::board; 11 8 pub use pulp_kernel::drivers; 12 9 pub use pulp_kernel::error;