1use epub_stream::navigation::NavPoint;
2use epub_stream::{
3 BlockRole, ChapterRef, EpubBook, FontPolicy, Locator, Navigation, RenderPrep, RenderPrepError,
4 RenderPrepOptions, StyledEventOrRun,
5};
6use serde::{Deserialize, Serialize};
7use std::collections::VecDeque;
8use std::fmt;
9use std::fs::{self, File, OpenOptions};
10use std::io::{self, BufWriter, Read, Write};
11use std::path::{Path, PathBuf};
12use std::sync::atomic::{AtomicUsize, Ordering};
13use std::sync::mpsc::{sync_channel, Receiver};
14use std::sync::{Arc, Mutex};
15use std::time::Instant;
16
17use crate::render_ir::{
18 DrawCommand, ImageObjectCommand, JustifyMode, OverlayContent, OverlayItem, OverlayRect,
19 OverlaySize, OverlaySlot, PageAnnotation, PageChromeCommand, PageChromeKind, PageMetrics,
20 PaginationProfileId, RectCommand, RenderPage, ResolvedTextStyle, RuleCommand, TextCommand,
21};
22use crate::render_layout::{
23 LayoutConfig, LayoutEngine, LayoutSession as CoreLayoutSession, TextMeasurer,
24};
25
26pub trait CancelToken {
28 fn is_cancelled(&self) -> bool;
29}
30
31#[derive(Clone, Copy, Debug, Default)]
33pub struct NeverCancel;
34
35impl CancelToken for NeverCancel {
36 fn is_cancelled(&self) -> bool {
37 false
38 }
39}
40
41#[derive(Clone, Debug, PartialEq)]
43pub enum RenderDiagnostic {
44 ReflowTimeMs(u32),
45 Cancelled,
46 MemoryLimitExceeded {
47 kind: &'static str,
48 actual: usize,
49 limit: usize,
50 },
51 CacheHit {
52 chapter_index: usize,
53 page_count: usize,
54 },
55 CacheMiss {
56 chapter_index: usize,
57 },
58}
59
60type DiagnosticCallback = Arc<Mutex<Box<dyn FnMut(RenderDiagnostic) + Send + 'static>>>;
61type DiagnosticSink = Option<DiagnosticCallback>;
62
63#[derive(Clone, Copy, Debug, Default, PartialEq)]
65pub struct RenderEngineOptions {
66 pub prep: RenderPrepOptions,
68 pub layout: LayoutConfig,
70}
71
72impl RenderEngineOptions {
73 pub fn for_display(width: i32, height: i32) -> Self {
75 Self {
76 prep: RenderPrepOptions::default(),
77 layout: LayoutConfig::for_display(width, height),
78 }
79 }
80}
81
82pub type PageRange = core::ops::Range<usize>;
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct RenderBookPageMapEntry {
88 pub chapter_index: usize,
90 pub chapter_href: String,
92 pub first_page_index: usize,
94 pub page_count: usize,
96}
97
98impl RenderBookPageMapEntry {
99 fn contains_global_page(&self, global_page_index: usize) -> bool {
100 if self.page_count == 0 {
101 return false;
102 }
103 let start = self.first_page_index;
104 let end = start.saturating_add(self.page_count);
105 global_page_index >= start && global_page_index < end
106 }
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum RenderLocatorTargetKind {
112 ChapterStart,
114 FragmentFallbackChapterStart,
116 FragmentAnchor,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct RenderLocatorPageTarget {
125 pub page_index: usize,
127 pub chapter_index: usize,
129 pub chapter_href: String,
131 pub fragment: Option<String>,
133 pub kind: RenderLocatorTargetKind,
135}
136
137#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
142pub struct RenderReadingPositionToken {
143 pub chapter_index: usize,
145 pub chapter_href: Option<String>,
147 pub chapter_page_index: usize,
149 pub chapter_page_count: usize,
151 pub chapter_progress: f32,
153 pub global_page_index: usize,
155 pub global_page_count: usize,
157}
158
159impl RenderReadingPositionToken {
160 fn normalized_chapter_progress(&self) -> f32 {
161 if self.chapter_page_count > 1 {
162 return page_progress_from_count(self.chapter_page_index, self.chapter_page_count);
163 }
164 normalize_progress(self.chapter_progress)
165 }
166
167 fn normalized_global_progress(&self) -> f32 {
168 page_progress_from_count(self.global_page_index, self.global_page_count)
169 }
170}
171
172#[derive(Clone, Debug, Default, PartialEq, Eq)]
174pub struct RenderBookPageMap {
175 entries: Vec<RenderBookPageMapEntry>,
176 total_pages: usize,
177}
178
179impl RenderBookPageMap {
180 pub fn from_chapter_page_counts(
185 chapters: &[ChapterRef],
186 chapter_page_counts: &[usize],
187 ) -> Self {
188 let mut entries = Vec::with_capacity(chapters.len());
189 let mut first_page_index = 0usize;
190
191 for (order_index, chapter) in chapters.iter().enumerate() {
192 let page_count = chapter_page_counts
193 .get(chapter.index)
194 .copied()
195 .or_else(|| chapter_page_counts.get(order_index).copied())
196 .unwrap_or(0);
197 entries.push(RenderBookPageMapEntry {
198 chapter_index: chapter.index,
199 chapter_href: chapter.href.clone(),
200 first_page_index,
201 page_count,
202 });
203 first_page_index = first_page_index.saturating_add(page_count);
204 }
205
206 Self {
207 entries,
208 total_pages: first_page_index,
209 }
210 }
211
212 pub fn entries(&self) -> &[RenderBookPageMapEntry] {
214 &self.entries
215 }
216
217 pub fn total_pages(&self) -> usize {
219 self.total_pages
220 }
221
222 pub fn chapter_start_page_index(&self, chapter_index: usize) -> Option<usize> {
226 self.chapter_entry_with_pages(chapter_index)
227 .map(|entry| entry.first_page_index)
228 }
229
230 pub fn chapter_page_range(&self, chapter_index: usize) -> Option<PageRange> {
234 self.chapter_entry_with_pages(chapter_index).map(|entry| {
235 entry.first_page_index..entry.first_page_index.saturating_add(entry.page_count)
236 })
237 }
238
239 pub fn resolve_href(&self, href: &str) -> Option<RenderLocatorPageTarget> {
244 self.resolve_href_with_fragment_progress(href, None)
245 }
246
247 pub fn resolve_href_with_fragment_progress(
253 &self,
254 href: &str,
255 fragment_progress: Option<f32>,
256 ) -> Option<RenderLocatorPageTarget> {
257 let (base_href, fragment) = split_href_fragment(href);
258 if base_href.is_empty() {
259 return None;
260 }
261
262 let chapter_index = self.find_chapter_index_for_href(base_href)?;
263 let chapter = self.chapter_entry_with_pages(chapter_index)?;
264 let mut page_index = chapter.first_page_index;
265 let kind = match fragment {
266 Some(_) => {
267 if let Some(progress) = fragment_progress {
268 let local_index = progress_to_page_index(progress, chapter.page_count.max(1));
269 page_index = chapter.first_page_index.saturating_add(local_index);
270 RenderLocatorTargetKind::FragmentAnchor
271 } else {
272 RenderLocatorTargetKind::FragmentFallbackChapterStart
273 }
274 }
275 None => RenderLocatorTargetKind::ChapterStart,
276 };
277
278 Some(RenderLocatorPageTarget {
279 page_index,
280 chapter_index: chapter.chapter_index,
281 chapter_href: chapter.chapter_href.clone(),
282 fragment: fragment.map(ToOwned::to_owned),
283 kind,
284 })
285 }
286
287 pub fn resolve_toc_href(&self, href: &str) -> Option<RenderLocatorPageTarget> {
289 self.resolve_href(href)
290 }
291
292 pub fn resolve_locator(
296 &self,
297 locator: &Locator,
298 navigation: Option<&Navigation>,
299 ) -> Option<RenderLocatorPageTarget> {
300 match locator {
301 Locator::Chapter(chapter_index) => self.resolve_chapter_start(*chapter_index, None),
302 Locator::Href(href) => self.resolve_href(href),
303 Locator::Fragment(_) => None,
304 Locator::TocId(id) => {
305 let navigation = navigation?;
306 let href = find_toc_href(navigation, id)?;
307 self.resolve_href(&href)
308 }
309 Locator::Position(pos) => self.resolve_position_locator(pos),
310 }
311 }
312
313 pub fn reading_position_token_for_page_index(
317 &self,
318 global_page_index: usize,
319 ) -> Option<RenderReadingPositionToken> {
320 if self.total_pages == 0 {
321 return None;
322 }
323
324 let clamped = global_page_index.min(self.total_pages.saturating_sub(1));
325 let chapter = self.entry_for_global_page(clamped)?;
326 let chapter_offset = clamped.saturating_sub(chapter.first_page_index);
327
328 Some(RenderReadingPositionToken {
329 chapter_index: chapter.chapter_index,
330 chapter_href: Some(chapter.chapter_href.clone()),
331 chapter_page_index: chapter_offset,
332 chapter_page_count: chapter.page_count.max(1),
333 chapter_progress: page_progress_from_count(chapter_offset, chapter.page_count.max(1)),
334 global_page_index: clamped,
335 global_page_count: self.total_pages.max(1),
336 })
337 }
338
339 pub fn remap_reading_position_token(
344 &self,
345 token: &RenderReadingPositionToken,
346 ) -> Option<usize> {
347 if self.total_pages == 0 {
348 return None;
349 }
350
351 if let Some(chapter) = self.chapter_entry_for_token(token) {
352 let local_index =
353 progress_to_page_index(token.normalized_chapter_progress(), chapter.page_count);
354 return Some(chapter.first_page_index.saturating_add(local_index));
355 }
356
357 Some(progress_to_page_index(
358 token.normalized_global_progress(),
359 self.total_pages,
360 ))
361 }
362
363 fn resolve_position_locator(
364 &self,
365 position: &epub_stream::ReadingPosition,
366 ) -> Option<RenderLocatorPageTarget> {
367 if let Some(href) = position.chapter_href.as_deref() {
368 if let Some(anchor) = position
369 .anchor
370 .as_deref()
371 .filter(|anchor| !anchor.is_empty())
372 {
373 let mut href_with_fragment = String::with_capacity(
374 href.len().saturating_add(anchor.len()).saturating_add(1),
375 );
376 href_with_fragment.push_str(href);
377 href_with_fragment.push('#');
378 href_with_fragment.push_str(anchor);
379 if let Some(target) = self.resolve_href(&href_with_fragment) {
380 return Some(target);
381 }
382 }
383 if let Some(target) = self.resolve_href(href) {
384 return Some(target);
385 }
386 }
387 self.resolve_chapter_start(position.chapter_index, position.anchor.as_deref())
388 }
389
390 fn resolve_chapter_start(
391 &self,
392 chapter_index: usize,
393 anchor: Option<&str>,
394 ) -> Option<RenderLocatorPageTarget> {
395 let chapter = self.chapter_entry_with_pages(chapter_index)?;
396 let has_anchor = anchor.is_some_and(|value| !value.is_empty());
397 let kind = if has_anchor {
398 RenderLocatorTargetKind::FragmentFallbackChapterStart
399 } else {
400 RenderLocatorTargetKind::ChapterStart
401 };
402
403 Some(RenderLocatorPageTarget {
404 page_index: chapter.first_page_index,
405 chapter_index: chapter.chapter_index,
406 chapter_href: chapter.chapter_href.clone(),
407 fragment: anchor.map(ToOwned::to_owned),
408 kind,
409 })
410 }
411
412 fn chapter_entry_for_token(
413 &self,
414 token: &RenderReadingPositionToken,
415 ) -> Option<&RenderBookPageMapEntry> {
416 if let Some(href) = token.chapter_href.as_deref() {
417 let (base_href, _) = split_href_fragment(href);
418 if let Some(chapter_index) = self.find_chapter_index_for_href(base_href) {
419 if let Some(chapter) = self.chapter_entry_with_pages(chapter_index) {
420 return Some(chapter);
421 }
422 }
423 }
424 self.chapter_entry_with_pages(token.chapter_index)
425 }
426
427 fn chapter_entry_with_pages(&self, chapter_index: usize) -> Option<&RenderBookPageMapEntry> {
428 self.entries
429 .iter()
430 .find(|entry| entry.chapter_index == chapter_index && entry.page_count > 0)
431 }
432
433 fn entry_for_global_page(&self, global_page_index: usize) -> Option<&RenderBookPageMapEntry> {
434 self.entries
435 .iter()
436 .find(|entry| entry.contains_global_page(global_page_index))
437 }
438
439 fn find_chapter_index_for_href(&self, base_href: &str) -> Option<usize> {
440 if base_href.is_empty() {
441 return None;
442 }
443
444 if let Some(entry) = self
445 .entries
446 .iter()
447 .find(|entry| entry.chapter_href == base_href)
448 {
449 return Some(entry.chapter_index);
450 }
451
452 let normalized_target = normalize_rel_path(base_href);
453 if let Some(entry) = self
454 .entries
455 .iter()
456 .find(|entry| normalize_rel_path(&entry.chapter_href) == normalized_target)
457 {
458 return Some(entry.chapter_index);
459 }
460
461 let target_basename = basename_of(&normalized_target);
462 if target_basename.is_empty() {
463 return None;
464 }
465 let mut candidate: Option<usize> = None;
466 for entry in &self.entries {
467 let normalized_entry = normalize_rel_path(&entry.chapter_href);
468 let entry_basename = basename_of(&normalized_entry);
469 if entry_basename != target_basename {
470 continue;
471 }
472 if candidate.is_some() {
473 return None;
474 }
475 candidate = Some(entry.chapter_index);
476 }
477 candidate
478 }
479}
480
481pub trait RenderCacheStore {
483 fn load_chapter_pages(
485 &self,
486 _profile: PaginationProfileId,
487 _chapter_index: usize,
488 ) -> Option<Vec<RenderPage>> {
489 None
490 }
491
492 fn store_chapter_pages(
494 &self,
495 _profile: PaginationProfileId,
496 _chapter_index: usize,
497 _pages: &[RenderPage],
498 ) {
499 }
500}
501
502const CACHE_SCHEMA_VERSION: u8 = 1;
503const DEFAULT_MAX_CACHE_FILE_BYTES: usize = 4 * 1024 * 1024;
504static CACHE_WRITE_NONCE: AtomicUsize = AtomicUsize::new(0);
505
506#[derive(Clone, Debug)]
515pub struct FileRenderCacheStore {
516 root: PathBuf,
517 max_file_bytes: usize,
518}
519
520impl FileRenderCacheStore {
521 pub fn new(root: impl Into<PathBuf>) -> Self {
523 Self {
524 root: root.into(),
525 max_file_bytes: DEFAULT_MAX_CACHE_FILE_BYTES,
526 }
527 }
528
529 pub fn with_max_file_bytes(mut self, max_file_bytes: usize) -> Self {
533 self.max_file_bytes = max_file_bytes.max(1);
534 self
535 }
536
537 pub fn cache_root(&self) -> &Path {
539 &self.root
540 }
541
542 pub fn max_file_bytes(&self) -> usize {
544 self.max_file_bytes
545 }
546
547 pub fn chapter_cache_path(
549 &self,
550 profile: PaginationProfileId,
551 chapter_index: usize,
552 ) -> PathBuf {
553 let profile_dir = profile_hex(profile);
554 self.root
555 .join(profile_dir)
556 .join(format!("chapter-{}.json", chapter_index))
557 }
558}
559
560impl RenderCacheStore for FileRenderCacheStore {
561 fn load_chapter_pages(
562 &self,
563 profile: PaginationProfileId,
564 chapter_index: usize,
565 ) -> Option<Vec<RenderPage>> {
566 let path = self.chapter_cache_path(profile, chapter_index);
567 let max_file_bytes = self.max_file_bytes as u64;
568 if fs::metadata(&path).ok()?.len() > max_file_bytes {
569 return None;
570 }
571
572 let file = File::open(path).ok()?;
573 let mut reader = file.take(max_file_bytes.saturating_add(1));
574 let mut payload = Vec::with_capacity(0);
575 if reader.read_to_end(&mut payload).is_err() {
576 return None;
577 }
578 if payload.len() > self.max_file_bytes {
579 return None;
580 }
581 let envelope: PersistedCacheEnvelope = serde_json::from_slice(&payload).ok()?;
582 envelope.into_render_pages()
583 }
584
585 fn store_chapter_pages(
586 &self,
587 profile: PaginationProfileId,
588 chapter_index: usize,
589 pages: &[RenderPage],
590 ) {
591 let final_path = self.chapter_cache_path(profile, chapter_index);
592 let Some(parent) = final_path.parent() else {
593 return;
594 };
595 if fs::create_dir_all(parent).is_err() {
596 return;
597 }
598
599 let nonce = CACHE_WRITE_NONCE.fetch_add(1, Ordering::Relaxed);
600 let temp_path = parent.join(format!(
601 "chapter-{}.json.tmp-{}-{}",
602 chapter_index,
603 std::process::id(),
604 nonce
605 ));
606
607 let envelope = PersistedCacheEnvelope::from_pages(pages);
608 let file = match OpenOptions::new()
609 .write(true)
610 .create_new(true)
611 .open(&temp_path)
612 {
613 Ok(file) => file,
614 Err(_) => return,
615 };
616 let writer = BufWriter::new(file);
617 let mut writer = CappedWriter::new(writer, self.max_file_bytes);
618 if serde_json::to_writer(&mut writer, &envelope).is_err() {
619 remove_file_quiet(&temp_path);
620 return;
621 }
622 if writer.flush().is_err() {
623 remove_file_quiet(&temp_path);
624 return;
625 }
626 let mut writer = writer.into_inner();
627 if writer.flush().is_err() {
628 remove_file_quiet(&temp_path);
629 return;
630 }
631 let file = match writer.into_inner() {
632 Ok(file) => file,
633 Err(_) => {
634 remove_file_quiet(&temp_path);
635 return;
636 }
637 };
638 if file.sync_all().is_err() {
639 remove_file_quiet(&temp_path);
640 return;
641 }
642 drop(file);
643 if fs::rename(&temp_path, &final_path).is_err() {
644 remove_file_quiet(&temp_path);
645 return;
646 }
647 sync_directory(parent);
648 }
649}
650
651pub fn remap_page_index_by_chapter_progress(
654 old_pages: &[RenderPage],
655 old_page_index: usize,
656 new_pages: &[RenderPage],
657) -> Option<usize> {
658 let target = chapter_progress_for_index(old_pages, old_page_index);
659 resolve_page_index_for_chapter_progress(target, new_pages)
660}
661
662pub fn resolve_page_index_for_chapter_progress(
666 chapter_progress: f32,
667 pages: &[RenderPage],
668) -> Option<usize> {
669 if pages.is_empty() {
670 return None;
671 }
672 let target = normalize_progress(chapter_progress);
673 let mut best_idx = 0usize;
674 let mut best_distance = f32::INFINITY;
675 let mut prev_progress = 0.0f32;
676
677 for (idx, page) in pages.iter().enumerate() {
678 let mut page_progress = page_progress_for_index(page, idx, pages.len());
679 if idx > 0 && page_progress < prev_progress {
680 page_progress = prev_progress;
681 }
682 prev_progress = page_progress;
683
684 let distance = (page_progress - target).abs();
685 if distance < best_distance {
686 best_distance = distance;
687 best_idx = idx;
688 continue;
689 }
690 if (distance - best_distance).abs() <= f32::EPSILON && idx < best_idx {
691 best_idx = idx;
692 continue;
693 }
694 if page_progress > target && distance > best_distance {
695 break;
696 }
697 }
698
699 Some(best_idx)
700}
701
702fn normalize_progress(progress: f32) -> f32 {
703 if progress.is_finite() {
704 return progress.clamp(0.0, 1.0);
705 }
706 0.0
707}
708
709fn page_progress_from_count(page_index: usize, page_count: usize) -> f32 {
710 if page_count <= 1 {
711 return 1.0;
712 }
713 let clamped = page_index.min(page_count.saturating_sub(1));
714 (clamped as f32 / (page_count - 1) as f32).clamp(0.0, 1.0)
715}
716
717fn progress_to_page_index(progress: f32, page_count: usize) -> usize {
718 if page_count <= 1 {
719 return 0;
720 }
721 let max_index = page_count.saturating_sub(1);
722 let scaled = normalize_progress(progress) * max_index as f32;
723 let rounded = scaled.round();
724 if !rounded.is_finite() || rounded <= 0.0 {
725 return 0;
726 }
727 let index = rounded as usize;
728 index.min(max_index)
729}
730
731fn split_href_fragment(href: &str) -> (&str, Option<&str>) {
732 let (base, fragment) = match href.split_once('#') {
733 Some((base, fragment)) => (base, Some(fragment)),
734 None => (href, None),
735 };
736 let fragment = fragment.filter(|value| !value.is_empty());
737 (base, fragment)
738}
739
740pub fn estimate_fragment_progress_in_html(chapter_html: &[u8], fragment: &str) -> Option<f32> {
746 if chapter_html.is_empty() {
747 return None;
748 }
749 let fragment = fragment.trim();
750 if fragment.is_empty() {
751 return None;
752 }
753 let needle = fragment.as_bytes();
754
755 let mut patterns: [Vec<u8>; 4] = [
756 Vec::with_capacity(needle.len() + 5),
757 Vec::with_capacity(needle.len() + 5),
758 Vec::with_capacity(needle.len() + 7),
759 Vec::with_capacity(needle.len() + 7),
760 ];
761 patterns[0].extend_from_slice(b"id=\"");
762 patterns[0].extend_from_slice(needle);
763 patterns[0].push(b'"');
764
765 patterns[1].extend_from_slice(b"id='");
766 patterns[1].extend_from_slice(needle);
767 patterns[1].push(b'\'');
768
769 patterns[2].extend_from_slice(b"name=\"");
770 patterns[2].extend_from_slice(needle);
771 patterns[2].push(b'"');
772
773 patterns[3].extend_from_slice(b"name='");
774 patterns[3].extend_from_slice(needle);
775 patterns[3].push(b'\'');
776
777 let mut best: Option<usize> = None;
778 for pattern in &patterns {
779 if let Some(pos) = find_bytes(chapter_html, pattern) {
780 best = Some(best.map_or(pos, |current| current.min(pos)));
781 }
782 }
783 let position = best?;
784 if chapter_html.len() <= 1 {
785 return Some(0.0);
786 }
787 Some((position as f32 / (chapter_html.len() - 1) as f32).clamp(0.0, 1.0))
788}
789
790fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
791 if needle.is_empty() || needle.len() > haystack.len() {
792 return None;
793 }
794 haystack
795 .windows(needle.len())
796 .position(|window| window == needle)
797}
798
799fn normalize_rel_path(path: &str) -> String {
800 let base = path
801 .split_once('?')
802 .map_or(path, |(without_query, _)| without_query);
803 let slash_normalized = base.replace('\\', "/");
804 let mut out_parts: Vec<&str> = Vec::with_capacity(0);
805
806 for part in slash_normalized.split('/') {
807 if part.is_empty() || part == "." {
808 continue;
809 }
810 if part == ".." {
811 let _ = out_parts.pop();
812 continue;
813 }
814 out_parts.push(part);
815 }
816
817 out_parts.join("/")
818}
819
820fn basename_of(path: &str) -> &str {
821 path.rsplit('/').next().unwrap_or(path)
822}
823
824fn find_toc_href(navigation: &Navigation, id: &str) -> Option<String> {
825 fn visit(points: &[NavPoint], id: &str) -> Option<String> {
826 for point in points {
827 let (_, fragment) = split_href_fragment(&point.href);
828 if point.label == id || fragment == Some(id) {
829 return Some(point.href.clone());
830 }
831 if let Some(hit) = visit(&point.children, id) {
832 return Some(hit);
833 }
834 }
835 None
836 }
837 visit(&navigation.toc, id)
838}
839
840fn chapter_progress_for_index(pages: &[RenderPage], page_index: usize) -> f32 {
841 if pages.is_empty() {
842 return 0.0;
843 }
844 let clamped_index = page_index.min(pages.len().saturating_sub(1));
845 page_progress_for_index(&pages[clamped_index], clamped_index, pages.len())
846}
847
848fn page_progress_for_index(page: &RenderPage, fallback_index: usize, fallback_total: usize) -> f32 {
849 if let Some(chapter_page_count) = page.metrics.chapter_page_count {
850 if chapter_page_count <= 1 {
851 return 1.0;
852 }
853 let chapter_page_index = page
854 .metrics
855 .chapter_page_index
856 .min(chapter_page_count.saturating_sub(1));
857 return (chapter_page_index as f32 / (chapter_page_count - 1) as f32).clamp(0.0, 1.0);
858 }
859 if page.metrics.progress_chapter.is_finite() {
860 return page.metrics.progress_chapter.clamp(0.0, 1.0);
861 }
862 if fallback_total <= 1 {
863 return 1.0;
864 }
865 (fallback_index as f32 / (fallback_total - 1) as f32).clamp(0.0, 1.0)
866}
867
868fn profile_hex(profile: PaginationProfileId) -> String {
869 const HEX: &[u8; 16] = b"0123456789abcdef";
870 let mut out = String::with_capacity(64);
871 for byte in profile.0 {
872 out.push(HEX[(byte >> 4) as usize] as char);
873 out.push(HEX[(byte & 0x0f) as usize] as char);
874 }
875 out
876}
877
878fn remove_file_quiet(path: &Path) {
879 let _ = fs::remove_file(path);
880}
881
882fn sync_directory(path: &Path) {
883 if let Ok(dir) = File::open(path) {
884 let _ = dir.sync_all();
885 }
886}
887
888struct CappedWriter<W> {
889 inner: W,
890 max_bytes: usize,
891 written: usize,
892}
893
894impl<W> CappedWriter<W> {
895 fn new(inner: W, max_bytes: usize) -> Self {
896 Self {
897 inner,
898 max_bytes: max_bytes.max(1),
899 written: 0,
900 }
901 }
902
903 fn into_inner(self) -> W {
904 self.inner
905 }
906}
907
908impl<W: Write> Write for CappedWriter<W> {
909 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
910 let remaining = self.max_bytes.saturating_sub(self.written);
911 if buf.len() > remaining {
912 return Err(io::Error::new(
913 io::ErrorKind::InvalidData,
914 "cache file exceeds max_file_bytes",
915 ));
916 }
917 self.inner.write_all(buf)?;
918 self.written = self.written.saturating_add(buf.len());
919 Ok(buf.len())
920 }
921
922 fn flush(&mut self) -> io::Result<()> {
923 self.inner.flush()
924 }
925}
926
927#[derive(Clone, Debug, Serialize, Deserialize)]
928struct PersistedCacheEnvelope {
929 version: u8,
930 pages: Vec<PersistedRenderPage>,
931}
932
933impl PersistedCacheEnvelope {
934 fn from_pages(pages: &[RenderPage]) -> Self {
935 Self {
936 version: CACHE_SCHEMA_VERSION,
937 pages: pages.iter().map(PersistedRenderPage::from).collect(),
938 }
939 }
940
941 fn into_render_pages(self) -> Option<Vec<RenderPage>> {
942 if self.version != CACHE_SCHEMA_VERSION {
943 return None;
944 }
945 Some(self.pages.into_iter().map(RenderPage::from).collect())
946 }
947}
948
949#[derive(Clone, Debug, Serialize, Deserialize)]
950struct PersistedRenderPage {
951 page_number: usize,
952 commands: Vec<PersistedDrawCommand>,
953 content_commands: Vec<PersistedDrawCommand>,
954 chrome_commands: Vec<PersistedDrawCommand>,
955 overlay_commands: Vec<PersistedDrawCommand>,
956 overlay_items: Vec<PersistedOverlayItem>,
957 annotations: Vec<PersistedPageAnnotation>,
958 metrics: PersistedPageMetrics,
959}
960
961impl From<&RenderPage> for PersistedRenderPage {
962 fn from(page: &RenderPage) -> Self {
963 Self {
964 page_number: page.page_number,
965 commands: page
966 .commands
967 .iter()
968 .map(PersistedDrawCommand::from)
969 .collect(),
970 content_commands: page
971 .content_commands
972 .iter()
973 .map(PersistedDrawCommand::from)
974 .collect(),
975 chrome_commands: page
976 .chrome_commands
977 .iter()
978 .map(PersistedDrawCommand::from)
979 .collect(),
980 overlay_commands: page
981 .overlay_commands
982 .iter()
983 .map(PersistedDrawCommand::from)
984 .collect(),
985 overlay_items: page
986 .overlay_items
987 .iter()
988 .map(PersistedOverlayItem::from)
989 .collect(),
990 annotations: page
991 .annotations
992 .iter()
993 .map(PersistedPageAnnotation::from)
994 .collect(),
995 metrics: page.metrics.into(),
996 }
997 }
998}
999
1000impl From<PersistedRenderPage> for RenderPage {
1001 fn from(value: PersistedRenderPage) -> Self {
1002 Self {
1003 page_number: value.page_number,
1004 commands: value.commands.into_iter().map(DrawCommand::from).collect(),
1005 content_commands: value
1006 .content_commands
1007 .into_iter()
1008 .map(DrawCommand::from)
1009 .collect(),
1010 chrome_commands: value
1011 .chrome_commands
1012 .into_iter()
1013 .map(DrawCommand::from)
1014 .collect(),
1015 overlay_commands: value
1016 .overlay_commands
1017 .into_iter()
1018 .map(DrawCommand::from)
1019 .collect(),
1020 overlay_items: value
1021 .overlay_items
1022 .into_iter()
1023 .map(OverlayItem::from)
1024 .collect(),
1025 annotations: value
1026 .annotations
1027 .into_iter()
1028 .map(PageAnnotation::from)
1029 .collect(),
1030 metrics: value.metrics.into(),
1031 }
1032 }
1033}
1034
1035#[derive(Clone, Debug, Serialize, Deserialize)]
1036struct PersistedOverlayItem {
1037 slot: PersistedOverlaySlot,
1038 z: i32,
1039 content: PersistedOverlayContent,
1040}
1041
1042impl From<&OverlayItem> for PersistedOverlayItem {
1043 fn from(value: &OverlayItem) -> Self {
1044 Self {
1045 slot: (&value.slot).into(),
1046 z: value.z,
1047 content: (&value.content).into(),
1048 }
1049 }
1050}
1051
1052impl From<PersistedOverlayItem> for OverlayItem {
1053 fn from(value: PersistedOverlayItem) -> Self {
1054 Self {
1055 slot: value.slot.into(),
1056 z: value.z,
1057 content: value.content.into(),
1058 }
1059 }
1060}
1061
1062#[derive(Clone, Debug, Serialize, Deserialize)]
1063enum PersistedOverlaySlot {
1064 TopLeft,
1065 TopCenter,
1066 TopRight,
1067 BottomLeft,
1068 BottomCenter,
1069 BottomRight,
1070 Custom(PersistedOverlayRect),
1071}
1072
1073impl From<&OverlaySlot> for PersistedOverlaySlot {
1074 fn from(value: &OverlaySlot) -> Self {
1075 match value {
1076 OverlaySlot::TopLeft => Self::TopLeft,
1077 OverlaySlot::TopCenter => Self::TopCenter,
1078 OverlaySlot::TopRight => Self::TopRight,
1079 OverlaySlot::BottomLeft => Self::BottomLeft,
1080 OverlaySlot::BottomCenter => Self::BottomCenter,
1081 OverlaySlot::BottomRight => Self::BottomRight,
1082 OverlaySlot::Custom(rect) => Self::Custom((*rect).into()),
1083 }
1084 }
1085}
1086
1087impl From<PersistedOverlaySlot> for OverlaySlot {
1088 fn from(value: PersistedOverlaySlot) -> Self {
1089 match value {
1090 PersistedOverlaySlot::TopLeft => Self::TopLeft,
1091 PersistedOverlaySlot::TopCenter => Self::TopCenter,
1092 PersistedOverlaySlot::TopRight => Self::TopRight,
1093 PersistedOverlaySlot::BottomLeft => Self::BottomLeft,
1094 PersistedOverlaySlot::BottomCenter => Self::BottomCenter,
1095 PersistedOverlaySlot::BottomRight => Self::BottomRight,
1096 PersistedOverlaySlot::Custom(rect) => Self::Custom(rect.into()),
1097 }
1098 }
1099}
1100
1101#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1102struct PersistedOverlayRect {
1103 x: i32,
1104 y: i32,
1105 width: u32,
1106 height: u32,
1107}
1108
1109impl From<OverlayRect> for PersistedOverlayRect {
1110 fn from(value: OverlayRect) -> Self {
1111 Self {
1112 x: value.x,
1113 y: value.y,
1114 width: value.width,
1115 height: value.height,
1116 }
1117 }
1118}
1119
1120impl From<PersistedOverlayRect> for OverlayRect {
1121 fn from(value: PersistedOverlayRect) -> Self {
1122 Self {
1123 x: value.x,
1124 y: value.y,
1125 width: value.width,
1126 height: value.height,
1127 }
1128 }
1129}
1130
1131#[derive(Clone, Debug, Serialize, Deserialize)]
1132enum PersistedOverlayContent {
1133 Text(String),
1134 Command(PersistedDrawCommand),
1135}
1136
1137impl From<&OverlayContent> for PersistedOverlayContent {
1138 fn from(value: &OverlayContent) -> Self {
1139 match value {
1140 OverlayContent::Text(text) => Self::Text(text.clone()),
1141 OverlayContent::Command(cmd) => Self::Command(cmd.into()),
1142 }
1143 }
1144}
1145
1146impl From<PersistedOverlayContent> for OverlayContent {
1147 fn from(value: PersistedOverlayContent) -> Self {
1148 match value {
1149 PersistedOverlayContent::Text(text) => Self::Text(text),
1150 PersistedOverlayContent::Command(cmd) => Self::Command(cmd.into()),
1151 }
1152 }
1153}
1154
1155#[derive(Clone, Debug, Serialize, Deserialize)]
1156struct PersistedPageAnnotation {
1157 kind: String,
1158 value: Option<String>,
1159}
1160
1161impl From<&PageAnnotation> for PersistedPageAnnotation {
1162 fn from(value: &PageAnnotation) -> Self {
1163 Self {
1164 kind: value.kind.clone(),
1165 value: value.value.clone(),
1166 }
1167 }
1168}
1169
1170impl From<PersistedPageAnnotation> for PageAnnotation {
1171 fn from(value: PersistedPageAnnotation) -> Self {
1172 Self {
1173 kind: value.kind,
1174 value: value.value,
1175 }
1176 }
1177}
1178
1179#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1180struct PersistedPageMetrics {
1181 chapter_index: usize,
1182 chapter_page_index: usize,
1183 chapter_page_count: Option<usize>,
1184 global_page_index: Option<usize>,
1185 global_page_count_estimate: Option<usize>,
1186 progress_chapter: f32,
1187 progress_book: Option<f32>,
1188}
1189
1190impl From<PageMetrics> for PersistedPageMetrics {
1191 fn from(value: PageMetrics) -> Self {
1192 Self {
1193 chapter_index: value.chapter_index,
1194 chapter_page_index: value.chapter_page_index,
1195 chapter_page_count: value.chapter_page_count,
1196 global_page_index: value.global_page_index,
1197 global_page_count_estimate: value.global_page_count_estimate,
1198 progress_chapter: normalize_progress(value.progress_chapter),
1199 progress_book: value.progress_book.map(normalize_progress),
1200 }
1201 }
1202}
1203
1204impl From<PersistedPageMetrics> for PageMetrics {
1205 fn from(value: PersistedPageMetrics) -> Self {
1206 Self {
1207 chapter_index: value.chapter_index,
1208 chapter_page_index: value.chapter_page_index,
1209 chapter_page_count: value.chapter_page_count,
1210 global_page_index: value.global_page_index,
1211 global_page_count_estimate: value.global_page_count_estimate,
1212 progress_chapter: normalize_progress(value.progress_chapter),
1213 progress_book: value.progress_book.map(normalize_progress),
1214 }
1215 }
1216}
1217
1218#[derive(Clone, Debug, Serialize, Deserialize)]
1219enum PersistedDrawCommand {
1220 Text(PersistedTextCommand),
1221 Rule(PersistedRuleCommand),
1222 ImageObject(PersistedImageObjectCommand),
1223 Rect(PersistedRectCommand),
1224 PageChrome(PersistedPageChromeCommand),
1225}
1226
1227impl From<&DrawCommand> for PersistedDrawCommand {
1228 fn from(value: &DrawCommand) -> Self {
1229 match value {
1230 DrawCommand::Text(cmd) => Self::Text(cmd.into()),
1231 DrawCommand::Rule(cmd) => Self::Rule((*cmd).into()),
1232 DrawCommand::ImageObject(cmd) => Self::ImageObject(cmd.into()),
1233 DrawCommand::Rect(cmd) => Self::Rect((*cmd).into()),
1234 DrawCommand::PageChrome(cmd) => Self::PageChrome(cmd.into()),
1235 }
1236 }
1237}
1238
1239impl From<PersistedDrawCommand> for DrawCommand {
1240 fn from(value: PersistedDrawCommand) -> Self {
1241 match value {
1242 PersistedDrawCommand::Text(cmd) => Self::Text(cmd.into()),
1243 PersistedDrawCommand::Rule(cmd) => Self::Rule(cmd.into()),
1244 PersistedDrawCommand::ImageObject(cmd) => Self::ImageObject(cmd.into()),
1245 PersistedDrawCommand::Rect(cmd) => Self::Rect(cmd.into()),
1246 PersistedDrawCommand::PageChrome(cmd) => Self::PageChrome(cmd.into()),
1247 }
1248 }
1249}
1250
1251#[derive(Clone, Debug, Serialize, Deserialize)]
1252struct PersistedTextCommand {
1253 x: i32,
1254 baseline_y: i32,
1255 text: String,
1256 font_id: Option<u32>,
1257 style: PersistedResolvedTextStyle,
1258}
1259
1260impl From<&TextCommand> for PersistedTextCommand {
1261 fn from(value: &TextCommand) -> Self {
1262 Self {
1263 x: value.x,
1264 baseline_y: value.baseline_y,
1265 text: value.text.clone(),
1266 font_id: value.font_id,
1267 style: (&value.style).into(),
1268 }
1269 }
1270}
1271
1272impl From<PersistedTextCommand> for TextCommand {
1273 fn from(value: PersistedTextCommand) -> Self {
1274 Self {
1275 x: value.x,
1276 baseline_y: value.baseline_y,
1277 text: value.text,
1278 font_id: value.font_id,
1279 style: value.style.into(),
1280 }
1281 }
1282}
1283
1284#[derive(Clone, Debug, Serialize, Deserialize)]
1285struct PersistedResolvedTextStyle {
1286 font_id: Option<u32>,
1287 family: String,
1288 weight: u16,
1289 italic: bool,
1290 size_px: f32,
1291 line_height: f32,
1292 letter_spacing: f32,
1293 role: PersistedBlockRole,
1294 justify_mode: PersistedJustifyMode,
1295}
1296
1297impl From<&ResolvedTextStyle> for PersistedResolvedTextStyle {
1298 fn from(value: &ResolvedTextStyle) -> Self {
1299 Self {
1300 font_id: value.font_id,
1301 family: value.family.clone(),
1302 weight: value.weight,
1303 italic: value.italic,
1304 size_px: value.size_px,
1305 line_height: value.line_height,
1306 letter_spacing: value.letter_spacing,
1307 role: value.role.into(),
1308 justify_mode: value.justify_mode.into(),
1309 }
1310 }
1311}
1312
1313impl From<PersistedResolvedTextStyle> for ResolvedTextStyle {
1314 fn from(value: PersistedResolvedTextStyle) -> Self {
1315 Self {
1316 font_id: value.font_id,
1317 family: value.family,
1318 weight: value.weight,
1319 italic: value.italic,
1320 size_px: value.size_px,
1321 line_height: value.line_height,
1322 letter_spacing: value.letter_spacing,
1323 role: value.role.into(),
1324 justify_mode: value.justify_mode.into(),
1325 }
1326 }
1327}
1328
1329#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1330enum PersistedBlockRole {
1331 Body,
1332 Paragraph,
1333 Heading(u8),
1334 ListItem,
1335 FigureCaption,
1336}
1337
1338impl From<BlockRole> for PersistedBlockRole {
1339 fn from(value: BlockRole) -> Self {
1340 match value {
1341 BlockRole::Body => Self::Body,
1342 BlockRole::Paragraph => Self::Paragraph,
1343 BlockRole::Heading(level) => Self::Heading(level),
1344 BlockRole::ListItem => Self::ListItem,
1345 BlockRole::FigureCaption => Self::FigureCaption,
1346 }
1347 }
1348}
1349
1350impl From<PersistedBlockRole> for BlockRole {
1351 fn from(value: PersistedBlockRole) -> Self {
1352 match value {
1353 PersistedBlockRole::Body => Self::Body,
1354 PersistedBlockRole::Paragraph => Self::Paragraph,
1355 PersistedBlockRole::Heading(level) => Self::Heading(level),
1356 PersistedBlockRole::ListItem => Self::ListItem,
1357 PersistedBlockRole::FigureCaption => Self::FigureCaption,
1358 }
1359 }
1360}
1361
1362#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1363enum PersistedJustifyMode {
1364 None,
1365 InterWord { extra_px_total: i32 },
1366 AlignRight { offset_px: i32 },
1367 AlignCenter { offset_px: i32 },
1368}
1369
1370impl From<JustifyMode> for PersistedJustifyMode {
1371 fn from(value: JustifyMode) -> Self {
1372 match value {
1373 JustifyMode::None => Self::None,
1374 JustifyMode::InterWord { extra_px_total } => Self::InterWord { extra_px_total },
1375 JustifyMode::AlignRight { offset_px } => Self::AlignRight { offset_px },
1376 JustifyMode::AlignCenter { offset_px } => Self::AlignCenter { offset_px },
1377 }
1378 }
1379}
1380
1381impl From<PersistedJustifyMode> for JustifyMode {
1382 fn from(value: PersistedJustifyMode) -> Self {
1383 match value {
1384 PersistedJustifyMode::None => Self::None,
1385 PersistedJustifyMode::InterWord { extra_px_total } => {
1386 Self::InterWord { extra_px_total }
1387 }
1388 PersistedJustifyMode::AlignRight { offset_px } => Self::AlignRight { offset_px },
1389 PersistedJustifyMode::AlignCenter { offset_px } => Self::AlignCenter { offset_px },
1390 }
1391 }
1392}
1393
1394#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1395struct PersistedRuleCommand {
1396 x: i32,
1397 y: i32,
1398 length: u32,
1399 thickness: u32,
1400 horizontal: bool,
1401}
1402
1403impl From<RuleCommand> for PersistedRuleCommand {
1404 fn from(value: RuleCommand) -> Self {
1405 Self {
1406 x: value.x,
1407 y: value.y,
1408 length: value.length,
1409 thickness: value.thickness,
1410 horizontal: value.horizontal,
1411 }
1412 }
1413}
1414
1415impl From<PersistedRuleCommand> for RuleCommand {
1416 fn from(value: PersistedRuleCommand) -> Self {
1417 Self {
1418 x: value.x,
1419 y: value.y,
1420 length: value.length,
1421 thickness: value.thickness,
1422 horizontal: value.horizontal,
1423 }
1424 }
1425}
1426
1427#[derive(Clone, Debug, Serialize, Deserialize)]
1428struct PersistedImageObjectCommand {
1429 src: String,
1430 alt: String,
1431 x: i32,
1432 y: i32,
1433 width: u32,
1434 height: u32,
1435}
1436
1437impl From<&ImageObjectCommand> for PersistedImageObjectCommand {
1438 fn from(value: &ImageObjectCommand) -> Self {
1439 Self {
1440 src: value.src.clone(),
1441 alt: value.alt.clone(),
1442 x: value.x,
1443 y: value.y,
1444 width: value.width,
1445 height: value.height,
1446 }
1447 }
1448}
1449
1450impl From<PersistedImageObjectCommand> for ImageObjectCommand {
1451 fn from(value: PersistedImageObjectCommand) -> Self {
1452 Self {
1453 src: value.src,
1454 alt: value.alt,
1455 x: value.x,
1456 y: value.y,
1457 width: value.width,
1458 height: value.height,
1459 }
1460 }
1461}
1462
1463#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1464struct PersistedRectCommand {
1465 x: i32,
1466 y: i32,
1467 width: u32,
1468 height: u32,
1469 fill: bool,
1470}
1471
1472impl From<RectCommand> for PersistedRectCommand {
1473 fn from(value: RectCommand) -> Self {
1474 Self {
1475 x: value.x,
1476 y: value.y,
1477 width: value.width,
1478 height: value.height,
1479 fill: value.fill,
1480 }
1481 }
1482}
1483
1484impl From<PersistedRectCommand> for RectCommand {
1485 fn from(value: PersistedRectCommand) -> Self {
1486 Self {
1487 x: value.x,
1488 y: value.y,
1489 width: value.width,
1490 height: value.height,
1491 fill: value.fill,
1492 }
1493 }
1494}
1495
1496#[derive(Clone, Debug, Serialize, Deserialize)]
1497struct PersistedPageChromeCommand {
1498 kind: PersistedPageChromeKind,
1499 text: Option<String>,
1500 current: Option<usize>,
1501 total: Option<usize>,
1502}
1503
1504impl From<&PageChromeCommand> for PersistedPageChromeCommand {
1505 fn from(value: &PageChromeCommand) -> Self {
1506 Self {
1507 kind: value.kind.into(),
1508 text: value.text.clone(),
1509 current: value.current,
1510 total: value.total,
1511 }
1512 }
1513}
1514
1515impl From<PersistedPageChromeCommand> for PageChromeCommand {
1516 fn from(value: PersistedPageChromeCommand) -> Self {
1517 Self {
1518 kind: value.kind.into(),
1519 text: value.text,
1520 current: value.current,
1521 total: value.total,
1522 }
1523 }
1524}
1525
1526#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1527enum PersistedPageChromeKind {
1528 Header,
1529 Footer,
1530 Progress,
1531}
1532
1533impl From<PageChromeKind> for PersistedPageChromeKind {
1534 fn from(value: PageChromeKind) -> Self {
1535 match value {
1536 PageChromeKind::Header => Self::Header,
1537 PageChromeKind::Footer => Self::Footer,
1538 PageChromeKind::Progress => Self::Progress,
1539 }
1540 }
1541}
1542
1543impl From<PersistedPageChromeKind> for PageChromeKind {
1544 fn from(value: PersistedPageChromeKind) -> Self {
1545 match value {
1546 PersistedPageChromeKind::Header => Self::Header,
1547 PersistedPageChromeKind::Footer => Self::Footer,
1548 PersistedPageChromeKind::Progress => Self::Progress,
1549 }
1550 }
1551}
1552
1553#[derive(Clone)]
1555pub struct RenderConfig<'a> {
1556 page_range: Option<PageRange>,
1557 cache: Option<&'a dyn RenderCacheStore>,
1558 cancel: Option<&'a dyn CancelToken>,
1559 embedded_fonts: bool,
1560 forced_font_family: Option<String>,
1561 text_measurer: Option<Arc<dyn TextMeasurer>>,
1562}
1563
1564impl<'a> Default for RenderConfig<'a> {
1565 fn default() -> Self {
1566 Self {
1567 page_range: None,
1568 cache: None,
1569 cancel: None,
1570 embedded_fonts: true,
1571 forced_font_family: None,
1572 text_measurer: None,
1573 }
1574 }
1575}
1576
1577impl<'a> RenderConfig<'a> {
1578 pub fn with_page_range(mut self, range: PageRange) -> Self {
1580 self.page_range = Some(range);
1581 self
1582 }
1583
1584 pub fn with_cache(mut self, cache: &'a dyn RenderCacheStore) -> Self {
1586 self.cache = Some(cache);
1587 self
1588 }
1589
1590 pub fn with_cancel(mut self, cancel: &'a dyn CancelToken) -> Self {
1592 self.cancel = Some(cancel);
1593 self
1594 }
1595
1596 pub fn with_embedded_fonts(mut self, enabled: bool) -> Self {
1601 self.embedded_fonts = enabled;
1602 self
1603 }
1604
1605 pub fn with_forced_font_family(mut self, family: impl Into<String>) -> Self {
1610 let family = family.into();
1611 let trimmed = family.trim();
1612 if trimmed.is_empty() {
1613 self.forced_font_family = None;
1614 return self;
1615 }
1616 self.forced_font_family = Some(trimmed.to_string());
1617 self.embedded_fonts = false;
1618 self
1619 }
1620
1621 pub fn with_text_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
1623 self.text_measurer = Some(measurer);
1624 self
1625 }
1626}
1627
1628#[derive(Clone)]
1630pub struct RenderEngine {
1631 opts: RenderEngineOptions,
1632 layout: LayoutEngine,
1633 diagnostic_sink: DiagnosticSink,
1634}
1635
1636impl fmt::Debug for RenderEngine {
1637 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1638 f.debug_struct("RenderEngine")
1639 .field("opts", &self.opts)
1640 .field("layout", &self.layout)
1641 .finish_non_exhaustive()
1642 }
1643}
1644
1645impl RenderEngine {
1646 pub fn new(opts: RenderEngineOptions) -> Self {
1648 Self {
1649 layout: LayoutEngine::new(opts.layout),
1650 opts,
1651 diagnostic_sink: None,
1652 }
1653 }
1654
1655 pub fn set_diagnostic_sink<F>(&mut self, sink: F)
1657 where
1658 F: FnMut(RenderDiagnostic) + Send + 'static,
1659 {
1660 self.diagnostic_sink = Some(Arc::new(Mutex::new(Box::new(sink))));
1661 }
1662
1663 fn emit_diagnostic(&self, diagnostic: RenderDiagnostic) {
1664 let Some(sink) = &self.diagnostic_sink else {
1665 return;
1666 };
1667 if let Ok(mut sink) = sink.lock() {
1668 sink(diagnostic);
1669 }
1670 }
1671
1672 pub fn pagination_profile_id(&self) -> PaginationProfileId {
1674 let payload = format!("{:?}|{:?}", self.opts.prep, self.opts.layout);
1675 PaginationProfileId::from_bytes(payload.as_bytes())
1676 }
1677
1678 pub fn begin<'a>(
1680 &'a self,
1681 chapter_index: usize,
1682 config: RenderConfig<'a>,
1683 ) -> LayoutSession<'a> {
1684 let profile = self.pagination_profile_id();
1685 let text_measurer = config.text_measurer.clone();
1686 let mut pending = VecDeque::new();
1687 let mut cached_hit = false;
1688 if let Some(cache) = config.cache {
1689 if let Some(pages) = cache.load_chapter_pages(profile, chapter_index) {
1690 cached_hit = true;
1691 self.emit_diagnostic(RenderDiagnostic::CacheHit {
1692 chapter_index,
1693 page_count: pages.len(),
1694 });
1695 let range = normalize_page_range(config.page_range.clone());
1696 let total_pages = pages.len();
1697 for (idx, mut page) in pages.into_iter().enumerate() {
1698 Self::annotate_page_for_chapter(&mut page, chapter_index);
1699 Self::annotate_page_metrics(&mut page, total_pages);
1700 if page_in_range(idx, &range) {
1701 pending.push_back(page);
1702 }
1703 }
1704 }
1705 }
1706 if config.cache.is_some() && !cached_hit {
1707 self.emit_diagnostic(RenderDiagnostic::CacheMiss { chapter_index });
1708 }
1709 LayoutSession {
1710 engine: self,
1711 chapter_index,
1712 profile,
1713 cfg: config,
1714 inner: if cached_hit {
1715 None
1716 } else {
1717 Some(self.layout.start_session_with_text_measurer(text_measurer))
1718 },
1719 pending_pages: pending,
1720 rendered_pages: Vec::with_capacity(0),
1721 page_index: 0,
1722 completed: cached_hit,
1723 }
1724 }
1725
1726 fn annotate_page_for_chapter(page: &mut RenderPage, chapter_index: usize) {
1727 page.metrics.chapter_index = chapter_index;
1728 page.metrics.chapter_page_index = page.page_number.saturating_sub(1);
1729 }
1730
1731 fn annotate_page_metrics(page: &mut RenderPage, chapter_page_count: usize) {
1732 let chapter_page_count = chapter_page_count.max(1);
1733 page.metrics.chapter_page_count = Some(chapter_page_count);
1734 page.metrics.global_page_index = Some(page.metrics.chapter_page_index);
1735 page.metrics.global_page_count_estimate = Some(chapter_page_count);
1736 page.metrics.progress_chapter = if chapter_page_count <= 1 {
1737 1.0
1738 } else {
1739 page.metrics.chapter_page_index as f32 / (chapter_page_count - 1) as f32
1740 }
1741 .clamp(0.0, 1.0);
1742 page.metrics.progress_book = Some(page.metrics.progress_chapter);
1743 }
1744
1745 pub fn prepare_chapter<R: std::io::Read + std::io::Seek>(
1747 &self,
1748 book: &mut EpubBook<R>,
1749 chapter_index: usize,
1750 ) -> Result<Vec<RenderPage>, RenderEngineError> {
1751 self.prepare_chapter_with_config_collect(book, chapter_index, RenderConfig::default())
1752 }
1753
1754 pub fn prepare_chapter_with_config_collect<R: std::io::Read + std::io::Seek>(
1756 &self,
1757 book: &mut EpubBook<R>,
1758 chapter_index: usize,
1759 config: RenderConfig<'_>,
1760 ) -> Result<Vec<RenderPage>, RenderEngineError> {
1761 let page_limit = self.opts.prep.memory.max_pages_in_memory;
1762 let mut pages = Vec::with_capacity(page_limit.min(8));
1763 let mut dropped_pages = 0usize;
1764 self.prepare_chapter_with_config(book, chapter_index, config, |page| {
1765 if pages.len() < page_limit {
1766 pages.push(page);
1767 } else {
1768 dropped_pages = dropped_pages.saturating_add(1);
1769 }
1770 })?;
1771 let chapter_total = pages.len().max(1);
1772 for page in pages.iter_mut() {
1773 Self::annotate_page_for_chapter(page, chapter_index);
1774 if page.metrics.chapter_page_count.is_none() {
1775 Self::annotate_page_metrics(page, chapter_total);
1776 }
1777 }
1778 if dropped_pages > 0 {
1779 self.emit_diagnostic(RenderDiagnostic::MemoryLimitExceeded {
1780 kind: "max_pages_in_memory",
1781 actual: pages.len().saturating_add(dropped_pages),
1782 limit: page_limit,
1783 });
1784 return Err(RenderEngineError::LimitExceeded {
1785 kind: "max_pages_in_memory",
1786 actual: pages.len().saturating_add(dropped_pages),
1787 limit: page_limit,
1788 });
1789 }
1790 Ok(pages)
1791 }
1792
1793 pub fn prepare_chapter_with<R, F>(
1795 &self,
1796 book: &mut EpubBook<R>,
1797 chapter_index: usize,
1798 on_page: F,
1799 ) -> Result<(), RenderEngineError>
1800 where
1801 R: std::io::Read + std::io::Seek,
1802 F: FnMut(RenderPage),
1803 {
1804 self.prepare_chapter_with_config(book, chapter_index, RenderConfig::default(), on_page)
1805 }
1806
1807 pub fn prepare_chapter_with_config<R, F>(
1809 &self,
1810 book: &mut EpubBook<R>,
1811 chapter_index: usize,
1812 config: RenderConfig<'_>,
1813 mut on_page: F,
1814 ) -> Result<(), RenderEngineError>
1815 where
1816 R: std::io::Read + std::io::Seek,
1817 F: FnMut(RenderPage),
1818 {
1819 let cancel = config.cancel.unwrap_or(&NeverCancel);
1820 self.prepare_chapter_with_cancel_and_config(book, chapter_index, cancel, config, |page| {
1821 on_page(page)
1822 })
1823 }
1824
1825 pub fn prepare_chapter_bytes_with<R, F>(
1830 &self,
1831 book: &mut EpubBook<R>,
1832 chapter_index: usize,
1833 html: &[u8],
1834 on_page: F,
1835 ) -> Result<(), RenderEngineError>
1836 where
1837 R: std::io::Read + std::io::Seek,
1838 F: FnMut(RenderPage),
1839 {
1840 self.prepare_chapter_bytes_with_config(
1841 book,
1842 chapter_index,
1843 html,
1844 RenderConfig::default(),
1845 on_page,
1846 )
1847 }
1848
1849 pub fn prepare_chapter_bytes_with_config<R, F>(
1851 &self,
1852 book: &mut EpubBook<R>,
1853 chapter_index: usize,
1854 html: &[u8],
1855 config: RenderConfig<'_>,
1856 on_page: F,
1857 ) -> Result<(), RenderEngineError>
1858 where
1859 R: std::io::Read + std::io::Seek,
1860 F: FnMut(RenderPage),
1861 {
1862 let cancel = config.cancel.unwrap_or(&NeverCancel);
1863 self.prepare_chapter_bytes_with_cancel_and_config(
1864 book,
1865 chapter_index,
1866 html,
1867 cancel,
1868 config,
1869 on_page,
1870 )
1871 }
1872
1873 pub fn prepare_chapter_with_cancel<R, C, F>(
1875 &self,
1876 book: &mut EpubBook<R>,
1877 chapter_index: usize,
1878 cancel: &C,
1879 on_page: F,
1880 ) -> Result<(), RenderEngineError>
1881 where
1882 R: std::io::Read + std::io::Seek,
1883 C: CancelToken,
1884 F: FnMut(RenderPage),
1885 {
1886 let config = RenderConfig::default().with_cancel(cancel);
1887 self.prepare_chapter_with_cancel_and_config(book, chapter_index, cancel, config, on_page)
1888 }
1889
1890 fn prepare_chapter_with_cancel_and_config<R, C, F>(
1891 &self,
1892 book: &mut EpubBook<R>,
1893 chapter_index: usize,
1894 cancel: &C,
1895 config: RenderConfig<'_>,
1896 mut on_page: F,
1897 ) -> Result<(), RenderEngineError>
1898 where
1899 R: std::io::Read + std::io::Seek,
1900 C: CancelToken + ?Sized,
1901 F: FnMut(RenderPage),
1902 {
1903 let embedded_fonts = config.embedded_fonts;
1904 let forced_font_family = config.forced_font_family.clone();
1905 let defer_emit_until_finish = config.page_range.is_some();
1906 let started = Instant::now();
1907 if cancel.is_cancelled() {
1908 self.emit_diagnostic(RenderDiagnostic::Cancelled);
1909 return Err(RenderEngineError::Cancelled);
1910 }
1911 let mut session = self.begin(chapter_index, config);
1912 session.set_hyphenation_language(book.language());
1913 if session.is_complete() {
1914 session.drain_pages(&mut on_page);
1915 return Ok(());
1916 }
1917 let mut prep = if let Some(family) = forced_font_family.as_deref() {
1918 RenderPrep::new(self.opts.prep).with_font_policy(forced_font_policy(family))
1919 } else {
1920 RenderPrep::new(self.opts.prep).with_serif_default()
1921 };
1922 if embedded_fonts {
1923 prep = prep.with_embedded_fonts_from_book(book)?;
1924 }
1925 let mut saw_cancelled = false;
1926 prep.prepare_chapter_with(book, chapter_index, |item| {
1927 if saw_cancelled || cancel.is_cancelled() {
1928 saw_cancelled = true;
1929 return;
1930 }
1931 if session.push(item).is_err() {
1932 saw_cancelled = true;
1933 return;
1934 }
1935 if !defer_emit_until_finish {
1936 session.drain_pages(&mut on_page);
1937 }
1938 })?;
1939 if saw_cancelled || cancel.is_cancelled() {
1940 self.emit_diagnostic(RenderDiagnostic::Cancelled);
1941 return Err(RenderEngineError::Cancelled);
1942 }
1943 session.finish()?;
1944 session.drain_pages(&mut on_page);
1945 let elapsed = started.elapsed().as_millis().min(u32::MAX as u128) as u32;
1946 self.emit_diagnostic(RenderDiagnostic::ReflowTimeMs(elapsed));
1947 Ok(())
1948 }
1949
1950 fn prepare_chapter_bytes_with_cancel_and_config<R, C, F>(
1951 &self,
1952 book: &mut EpubBook<R>,
1953 chapter_index: usize,
1954 html: &[u8],
1955 cancel: &C,
1956 config: RenderConfig<'_>,
1957 mut on_page: F,
1958 ) -> Result<(), RenderEngineError>
1959 where
1960 R: std::io::Read + std::io::Seek,
1961 C: CancelToken + ?Sized,
1962 F: FnMut(RenderPage),
1963 {
1964 let embedded_fonts = config.embedded_fonts;
1965 let forced_font_family = config.forced_font_family.clone();
1966 let defer_emit_until_finish = config.page_range.is_some();
1967 let started = Instant::now();
1968 if cancel.is_cancelled() {
1969 self.emit_diagnostic(RenderDiagnostic::Cancelled);
1970 return Err(RenderEngineError::Cancelled);
1971 }
1972 let mut session = self.begin(chapter_index, config);
1973 session.set_hyphenation_language(book.language());
1974 if session.is_complete() {
1975 session.drain_pages(&mut on_page);
1976 return Ok(());
1977 }
1978 let mut prep = if let Some(family) = forced_font_family.as_deref() {
1979 RenderPrep::new(self.opts.prep).with_font_policy(forced_font_policy(family))
1980 } else {
1981 RenderPrep::new(self.opts.prep).with_serif_default()
1982 };
1983 if embedded_fonts {
1984 prep = prep.with_embedded_fonts_from_book(book)?;
1985 }
1986 let mut saw_cancelled = false;
1987 prep.prepare_chapter_bytes_with(book, chapter_index, html, |item| {
1988 if saw_cancelled || cancel.is_cancelled() {
1989 saw_cancelled = true;
1990 return;
1991 }
1992 if session.push(item).is_err() {
1993 saw_cancelled = true;
1994 return;
1995 }
1996 if !defer_emit_until_finish {
1997 session.drain_pages(&mut on_page);
1998 }
1999 })?;
2000 if saw_cancelled || cancel.is_cancelled() {
2001 self.emit_diagnostic(RenderDiagnostic::Cancelled);
2002 return Err(RenderEngineError::Cancelled);
2003 }
2004 session.finish()?;
2005 session.drain_pages(&mut on_page);
2006 let elapsed = started.elapsed().as_millis().min(u32::MAX as u128) as u32;
2007 self.emit_diagnostic(RenderDiagnostic::ReflowTimeMs(elapsed));
2008 Ok(())
2009 }
2010
2011 pub fn prepare_chapter_page_range<R: std::io::Read + std::io::Seek>(
2016 &self,
2017 book: &mut EpubBook<R>,
2018 chapter_index: usize,
2019 start: usize,
2020 end: usize,
2021 ) -> Result<Vec<RenderPage>, RenderEngineError> {
2022 self.page_range(book, chapter_index, start..end)
2023 }
2024
2025 pub fn page_range<R: std::io::Read + std::io::Seek>(
2027 &self,
2028 book: &mut EpubBook<R>,
2029 chapter_index: usize,
2030 range: PageRange,
2031 ) -> Result<Vec<RenderPage>, RenderEngineError> {
2032 if range.start >= range.end {
2033 return Ok(Vec::with_capacity(0));
2034 }
2035 self.prepare_chapter_with_config_collect(
2036 book,
2037 chapter_index,
2038 RenderConfig::default().with_page_range(range),
2039 )
2040 }
2041
2042 pub fn prepare_chapter_iter<R: std::io::Read + std::io::Seek>(
2046 &self,
2047 book: &mut EpubBook<R>,
2048 chapter_index: usize,
2049 ) -> Result<RenderPageIter, RenderEngineError> {
2050 let pages = self.prepare_chapter(book, chapter_index)?;
2051 Ok(RenderPageIter {
2052 inner: pages.into_iter(),
2053 })
2054 }
2055
2056 pub fn prepare_chapter_iter_streaming<R>(
2062 &self,
2063 mut book: EpubBook<R>,
2064 chapter_index: usize,
2065 ) -> RenderPageStreamIter
2066 where
2067 R: std::io::Read + std::io::Seek + Send + 'static,
2068 {
2069 let (tx, rx) = sync_channel(1);
2070 let engine = self.clone();
2071
2072 std::thread::spawn(move || {
2073 let mut receiver_closed = false;
2074 let result = engine.prepare_chapter_with(&mut book, chapter_index, |page| {
2075 if receiver_closed {
2076 return;
2077 }
2078 if tx.send(StreamMessage::Page(page)).is_err() {
2079 receiver_closed = true;
2080 }
2081 });
2082
2083 if receiver_closed {
2084 return;
2085 }
2086 match result {
2087 Ok(()) => {
2088 let _ = tx.send(StreamMessage::Done);
2089 }
2090 Err(err) => {
2091 let _ = tx.send(StreamMessage::Error(err));
2092 }
2093 }
2094 });
2095
2096 RenderPageStreamIter {
2097 rx,
2098 finished: false,
2099 }
2100 }
2101
2102 pub fn prepare_chapter_with_overlay_composer<R, O, F>(
2104 &self,
2105 book: &mut EpubBook<R>,
2106 chapter_index: usize,
2107 viewport: OverlaySize,
2108 composer: &O,
2109 mut on_page: F,
2110 ) -> Result<(), RenderEngineError>
2111 where
2112 R: std::io::Read + std::io::Seek,
2113 O: crate::render_ir::OverlayComposer,
2114 F: FnMut(RenderPage),
2115 {
2116 self.prepare_chapter_with(book, chapter_index, |mut page| {
2117 let overlays = composer.compose(&page.metrics, viewport);
2118 for item in overlays {
2119 page.overlay_items.push(item.clone());
2120 if let OverlayContent::Command(cmd) = item.content {
2121 page.push_overlay_command(cmd);
2122 }
2123 }
2124 page.sync_commands();
2125 on_page(page);
2126 })
2127 }
2128}
2129
2130pub struct LayoutSession<'a> {
2132 engine: &'a RenderEngine,
2133 chapter_index: usize,
2134 profile: PaginationProfileId,
2135 cfg: RenderConfig<'a>,
2136 inner: Option<CoreLayoutSession>,
2137 pending_pages: VecDeque<RenderPage>,
2138 rendered_pages: Vec<RenderPage>,
2139 page_index: usize,
2140 completed: bool,
2141}
2142
2143impl LayoutSession<'_> {
2144 pub fn set_hyphenation_language(&mut self, language_tag: &str) {
2146 if let Some(inner) = self.inner.as_mut() {
2147 inner.set_hyphenation_language(language_tag);
2148 }
2149 }
2150
2151 pub fn push(&mut self, item: StyledEventOrRun) -> Result<(), RenderEngineError> {
2153 if self.completed {
2154 return Ok(());
2155 }
2156 if self.cfg.cancel.is_some_and(|cancel| cancel.is_cancelled()) {
2157 self.engine.emit_diagnostic(RenderDiagnostic::Cancelled);
2158 return Err(RenderEngineError::Cancelled);
2159 }
2160 if let Some(inner) = self.inner.as_mut() {
2161 let chapter = self.chapter_index;
2162 let range = normalize_page_range(self.cfg.page_range.clone());
2163 let rendered = &mut self.rendered_pages;
2164 let pending = &mut self.pending_pages;
2165 let page_index = &mut self.page_index;
2166 let capture_for_cache = self.cfg.cache.is_some();
2167 inner.push_item_with_pages(item, &mut |mut page| {
2168 RenderEngine::annotate_page_for_chapter(&mut page, chapter);
2169 if capture_for_cache {
2170 rendered.push(page.clone());
2171 }
2172 if page_in_range(*page_index, &range) {
2173 pending.push_back(page);
2174 }
2175 *page_index += 1;
2176 });
2177 }
2178 Ok(())
2179 }
2180
2181 pub fn drain_pages<F>(&mut self, mut on_page: F)
2183 where
2184 F: FnMut(RenderPage),
2185 {
2186 while let Some(page) = self.pending_pages.pop_front() {
2187 on_page(page);
2188 }
2189 }
2190
2191 pub fn finish(&mut self) -> Result<(), RenderEngineError> {
2193 if self.completed {
2194 return Ok(());
2195 }
2196 if self.cfg.cancel.is_some_and(|cancel| cancel.is_cancelled()) {
2197 self.engine.emit_diagnostic(RenderDiagnostic::Cancelled);
2198 return Err(RenderEngineError::Cancelled);
2199 }
2200 if let Some(inner) = self.inner.as_mut() {
2201 let chapter = self.chapter_index;
2202 let range = normalize_page_range(self.cfg.page_range.clone());
2203 let rendered = &mut self.rendered_pages;
2204 let pending = &mut self.pending_pages;
2205 let page_index = &mut self.page_index;
2206 let capture_for_cache = self.cfg.cache.is_some();
2207 inner.finish(&mut |mut page| {
2208 RenderEngine::annotate_page_for_chapter(&mut page, chapter);
2209 if capture_for_cache {
2210 rendered.push(page.clone());
2211 }
2212 if page_in_range(*page_index, &range) {
2213 pending.push_back(page);
2214 }
2215 *page_index += 1;
2216 });
2217 }
2218 let chapter_total = self.page_index.max(1);
2219 for page in self.pending_pages.iter_mut() {
2220 RenderEngine::annotate_page_metrics(page, chapter_total);
2221 }
2222 for page in self.rendered_pages.iter_mut() {
2223 RenderEngine::annotate_page_metrics(page, chapter_total);
2224 }
2225 if let Some(cache) = self.cfg.cache {
2226 if !self.rendered_pages.is_empty() {
2227 cache.store_chapter_pages(self.profile, self.chapter_index, &self.rendered_pages);
2228 }
2229 }
2230 self.completed = true;
2231 Ok(())
2232 }
2233
2234 fn is_complete(&self) -> bool {
2235 self.completed
2236 }
2237}
2238
2239fn normalize_page_range(range: Option<PageRange>) -> Option<PageRange> {
2240 match range {
2241 Some(r) if r.start < r.end => Some(r),
2242 Some(_) => Some(0..0),
2243 None => None,
2244 }
2245}
2246
2247fn page_in_range(idx: usize, range: &Option<PageRange>) -> bool {
2248 range.as_ref().map(|r| r.contains(&idx)).unwrap_or(true)
2249}
2250
2251fn forced_font_policy(family: &str) -> FontPolicy {
2252 let mut policy = FontPolicy::serif_default();
2253 let normalized = family
2254 .split(',')
2255 .next()
2256 .map(str::trim)
2257 .map(|part| part.trim_matches('"').trim_matches('\''))
2258 .filter(|part| !part.is_empty())
2259 .unwrap_or("serif");
2260 policy.preferred_families = vec![normalized.to_string()];
2261 policy.default_family = normalized.to_string();
2262 policy.allow_embedded_fonts = false;
2263 policy
2264}
2265
2266#[derive(Debug)]
2268pub struct RenderPageIter {
2269 inner: std::vec::IntoIter<RenderPage>,
2270}
2271
2272impl Iterator for RenderPageIter {
2273 type Item = RenderPage;
2274
2275 fn next(&mut self) -> Option<Self::Item> {
2276 self.inner.next()
2277 }
2278
2279 fn size_hint(&self) -> (usize, Option<usize>) {
2280 self.inner.size_hint()
2281 }
2282}
2283
2284impl ExactSizeIterator for RenderPageIter {
2285 fn len(&self) -> usize {
2286 self.inner.len()
2287 }
2288}
2289
2290impl std::iter::FusedIterator for RenderPageIter {}
2291
2292enum StreamMessage {
2293 Page(RenderPage),
2294 Error(RenderEngineError),
2295 Done,
2296}
2297
2298#[derive(Debug)]
2300pub struct RenderPageStreamIter {
2301 rx: Receiver<StreamMessage>,
2302 finished: bool,
2303}
2304
2305impl Iterator for RenderPageStreamIter {
2306 type Item = Result<RenderPage, RenderEngineError>;
2307
2308 fn next(&mut self) -> Option<Self::Item> {
2309 if self.finished {
2310 return None;
2311 }
2312 match self.rx.recv() {
2313 Ok(StreamMessage::Page(page)) => Some(Ok(page)),
2314 Ok(StreamMessage::Error(err)) => {
2315 self.finished = true;
2316 Some(Err(err))
2317 }
2318 Ok(StreamMessage::Done) | Err(_) => {
2319 self.finished = true;
2320 None
2321 }
2322 }
2323 }
2324}
2325
2326#[derive(Debug)]
2328pub enum RenderEngineError {
2329 Prep(RenderPrepError),
2331 Cancelled,
2333 LimitExceeded {
2335 kind: &'static str,
2336 actual: usize,
2337 limit: usize,
2338 },
2339}
2340
2341impl core::fmt::Display for RenderEngineError {
2342 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2343 match self {
2344 Self::Prep(err) => write!(f, "render prep failed: {}", err),
2345 Self::Cancelled => write!(f, "render cancelled"),
2346 Self::LimitExceeded {
2347 kind,
2348 actual,
2349 limit,
2350 } => write!(
2351 f,
2352 "render memory limit exceeded: {} (actual={} limit={})",
2353 kind, actual, limit
2354 ),
2355 }
2356 }
2357}
2358
2359impl std::error::Error for RenderEngineError {}
2360
2361impl From<RenderPrepError> for RenderEngineError {
2362 fn from(value: RenderPrepError) -> Self {
2363 Self::Prep(value)
2364 }
2365}
2366
2367#[cfg(test)]
2368mod tests {
2369 use super::*;
2370 use crate::render_ir::{
2371 DrawCommand, ImageObjectCommand, JustifyMode, OverlayItem, OverlayRect, OverlaySlot,
2372 PageAnnotation, PageChromeCommand, PageChromeKind, RectCommand, ResolvedTextStyle,
2373 RuleCommand, TextCommand,
2374 };
2375 use epub_stream::{BlockRole, ChapterRef, ComputedTextStyle, StyledEvent, StyledRun};
2376 use std::fs;
2377 use std::path::PathBuf;
2378
2379 fn body_run(text: &str) -> StyledEventOrRun {
2380 StyledEventOrRun::Run(StyledRun {
2381 text: text.to_string(),
2382 style: ComputedTextStyle {
2383 family_stack: vec!["serif".to_string()],
2384 weight: 400,
2385 italic: false,
2386 size_px: 16.0,
2387 line_height: 1.4,
2388 letter_spacing: 0.0,
2389 block_role: BlockRole::Body,
2390 },
2391 font_id: 0,
2392 resolved_family: "serif".to_string(),
2393 })
2394 }
2395
2396 fn cache_fixture_page(page_number: usize, chapter_page_count: usize) -> RenderPage {
2397 let mut page = RenderPage::new(page_number);
2398 page.push_content_command(DrawCommand::Text(TextCommand {
2399 x: 12,
2400 baseline_y: 24,
2401 text: format!("page-{page_number}"),
2402 font_id: Some(3),
2403 style: ResolvedTextStyle {
2404 font_id: Some(3),
2405 family: "serif".to_string(),
2406 weight: 400,
2407 italic: false,
2408 size_px: 16.0,
2409 line_height: 1.4,
2410 letter_spacing: 0.0,
2411 role: BlockRole::Body,
2412 justify_mode: JustifyMode::InterWord { extra_px_total: 6 },
2413 },
2414 }));
2415 page.push_content_command(DrawCommand::Rule(RuleCommand {
2416 x: 10,
2417 y: 28,
2418 length: 100,
2419 thickness: 1,
2420 horizontal: true,
2421 }));
2422 page.push_content_command(DrawCommand::ImageObject(ImageObjectCommand {
2423 src: "images/pic.png".to_string(),
2424 alt: "diagram".to_string(),
2425 x: 10,
2426 y: 40,
2427 width: 64,
2428 height: 48,
2429 }));
2430 page.push_chrome_command(DrawCommand::PageChrome(PageChromeCommand {
2431 kind: PageChromeKind::Footer,
2432 text: Some(format!("{page_number}/{chapter_page_count}")),
2433 current: Some(page_number),
2434 total: Some(chapter_page_count),
2435 }));
2436 page.push_overlay_command(DrawCommand::Rect(RectCommand {
2437 x: 6,
2438 y: 6,
2439 width: 18,
2440 height: 8,
2441 fill: true,
2442 }));
2443 page.overlay_items.push(OverlayItem {
2444 slot: OverlaySlot::TopRight,
2445 z: 1,
2446 content: OverlayContent::Text("bookmark".to_string()),
2447 });
2448 page.overlay_items.push(OverlayItem {
2449 slot: OverlaySlot::Custom(OverlayRect {
2450 x: 4,
2451 y: 5,
2452 width: 10,
2453 height: 11,
2454 }),
2455 z: 2,
2456 content: OverlayContent::Command(DrawCommand::Rule(RuleCommand {
2457 x: 2,
2458 y: 3,
2459 length: 12,
2460 thickness: 1,
2461 horizontal: false,
2462 })),
2463 });
2464 page.annotations.push(PageAnnotation {
2465 kind: "note".to_string(),
2466 value: Some(format!("a{page_number}")),
2467 });
2468 page.metrics.chapter_index = 4;
2469 page.metrics.chapter_page_index = page_number.saturating_sub(1);
2470 page.metrics.chapter_page_count = Some(chapter_page_count);
2471 page.metrics.global_page_index = Some(page_number.saturating_sub(1));
2472 page.metrics.global_page_count_estimate = Some(chapter_page_count);
2473 page.metrics.progress_chapter = if chapter_page_count <= 1 {
2474 1.0
2475 } else {
2476 page.metrics.chapter_page_index as f32 / (chapter_page_count - 1) as f32
2477 };
2478 page.metrics.progress_book = Some(page.metrics.progress_chapter);
2479 page.sync_commands();
2480 page
2481 }
2482
2483 fn progress_pages(count: usize) -> Vec<RenderPage> {
2484 let mut pages = Vec::with_capacity(count);
2485 for idx in 0..count {
2486 let mut page = RenderPage::new(idx + 1);
2487 page.metrics.chapter_page_index = idx;
2488 page.metrics.chapter_page_count = Some(count);
2489 page.metrics.progress_chapter = if count <= 1 {
2490 1.0
2491 } else {
2492 idx as f32 / (count - 1) as f32
2493 };
2494 page.metrics.progress_book = Some(page.metrics.progress_chapter);
2495 pages.push(page);
2496 }
2497 pages
2498 }
2499
2500 fn temp_cache_root(label: &str) -> PathBuf {
2501 let nonce = CACHE_WRITE_NONCE.fetch_add(1, Ordering::Relaxed);
2502 std::env::temp_dir().join(format!(
2503 "epub-stream-render-{label}-{}-{nonce}",
2504 std::process::id()
2505 ))
2506 }
2507
2508 fn sample_chapters() -> Vec<ChapterRef> {
2509 vec![
2510 ChapterRef {
2511 index: 0,
2512 idref: "c0".to_string(),
2513 href: "text/ch0.xhtml".to_string(),
2514 media_type: "application/xhtml+xml".to_string(),
2515 },
2516 ChapterRef {
2517 index: 1,
2518 idref: "c1".to_string(),
2519 href: "text/ch1.xhtml".to_string(),
2520 media_type: "application/xhtml+xml".to_string(),
2521 },
2522 ]
2523 }
2524
2525 #[test]
2526 fn begin_push_and_drain_pages_streams_incrementally() {
2527 let mut opts = RenderEngineOptions::for_display(300, 120);
2528 opts.layout.margin_top = 8;
2529 opts.layout.margin_bottom = 8;
2530 let engine = RenderEngine::new(opts);
2531
2532 let mut items = Vec::new();
2533 for _ in 0..40 {
2534 items.push(StyledEventOrRun::Event(StyledEvent::ParagraphStart));
2535 items.push(body_run("one two three four five six seven eight nine ten"));
2536 items.push(StyledEventOrRun::Event(StyledEvent::ParagraphEnd));
2537 }
2538
2539 let mut session = engine.begin(3, RenderConfig::default());
2540 let mut streamed = Vec::new();
2541 for item in &items {
2542 session.push(item.clone()).expect("push should pass");
2543 session.drain_pages(|page| streamed.push(page));
2544 }
2545 session.finish().expect("finish should pass");
2546 session.drain_pages(|page| streamed.push(page));
2547
2548 let mut expected = engine.layout.layout_items(items);
2549 for page in &mut expected {
2550 page.metrics.chapter_index = 3;
2551 }
2552 assert_eq!(streamed, expected);
2553 assert!(streamed.iter().all(|page| page.metrics.chapter_index == 3));
2554 }
2555
2556 #[test]
2557 fn forced_font_family_config_disables_embedded_fonts() {
2558 let cfg = RenderConfig::default().with_forced_font_family(" monospace ");
2559 assert_eq!(cfg.forced_font_family.as_deref(), Some("monospace"));
2560 assert!(!cfg.embedded_fonts);
2561 }
2562
2563 #[test]
2564 fn empty_forced_font_family_is_ignored() {
2565 let cfg = RenderConfig::default().with_forced_font_family(" ");
2566 assert!(cfg.forced_font_family.is_none());
2567 assert!(cfg.embedded_fonts);
2568 }
2569
2570 #[test]
2571 fn forced_font_policy_uses_first_family_entry() {
2572 let policy = forced_font_policy("Alegreya, serif");
2573 assert_eq!(policy.default_family, "Alegreya");
2574 assert_eq!(policy.preferred_families, vec!["Alegreya".to_string()]);
2575 assert!(!policy.allow_embedded_fonts);
2576 }
2577
2578 #[test]
2579 fn cache_roundtrip_load_store() {
2580 let root = temp_cache_root("cache-roundtrip");
2581 let store = FileRenderCacheStore::new(&root).with_max_file_bytes(256 * 1024);
2582 let profile = PaginationProfileId::from_bytes(b"profile-a");
2583 let chapter_index = 9;
2584 let pages = vec![cache_fixture_page(1, 2), cache_fixture_page(2, 2)];
2585
2586 store.store_chapter_pages(profile, chapter_index, &pages);
2587 let cache_path = store.chapter_cache_path(profile, chapter_index);
2588 assert!(cache_path.exists());
2589
2590 let loaded = store.load_chapter_pages(profile, chapter_index);
2591 assert_eq!(loaded, Some(pages.clone()));
2592
2593 let tiny_cap = FileRenderCacheStore::new(&root).with_max_file_bytes(48);
2594 tiny_cap.store_chapter_pages(profile, chapter_index + 1, &pages);
2595 assert!(tiny_cap
2596 .load_chapter_pages(profile, chapter_index + 1)
2597 .is_none());
2598
2599 let _ = fs::remove_dir_all(root);
2600 }
2601
2602 #[test]
2603 fn remap_helpers_monotonicity_and_bounds() {
2604 let old_pages = progress_pages(7);
2605 let new_pages = progress_pages(11);
2606
2607 let mut prev = 0usize;
2608 for old_idx in 0..old_pages.len() {
2609 let mapped = remap_page_index_by_chapter_progress(&old_pages, old_idx, &new_pages)
2610 .expect("new pages should resolve");
2611 assert!(mapped < new_pages.len());
2612 assert!(mapped >= prev);
2613 prev = mapped;
2614 }
2615
2616 assert_eq!(
2617 remap_page_index_by_chapter_progress(&old_pages, usize::MAX, &new_pages),
2618 Some(new_pages.len() - 1)
2619 );
2620
2621 let mut prev_resolved = 0usize;
2622 for step in 0..=50 {
2623 let progress = step as f32 / 50.0;
2624 let mapped = resolve_page_index_for_chapter_progress(progress, &new_pages)
2625 .expect("new pages should resolve");
2626 assert!(mapped < new_pages.len());
2627 assert!(mapped >= prev_resolved);
2628 prev_resolved = mapped;
2629 }
2630
2631 assert_eq!(
2632 resolve_page_index_for_chapter_progress(-10.0, &new_pages),
2633 Some(0)
2634 );
2635 assert_eq!(
2636 resolve_page_index_for_chapter_progress(10.0, &new_pages),
2637 Some(new_pages.len() - 1)
2638 );
2639 assert_eq!(
2640 resolve_page_index_for_chapter_progress(f32::NAN, &new_pages),
2641 Some(0)
2642 );
2643 assert_eq!(resolve_page_index_for_chapter_progress(0.5, &[]), None);
2644 }
2645
2646 #[test]
2647 fn resolve_href_with_fragment_progress_maps_anchor_inside_chapter() {
2648 let map = RenderBookPageMap::from_chapter_page_counts(&sample_chapters(), &[3, 5]);
2649 let target = map
2650 .resolve_href_with_fragment_progress("text/ch1.xhtml#intro", Some(0.5))
2651 .expect("target should resolve");
2652 assert_eq!(target.chapter_index, 1);
2653 assert_eq!(target.page_index, 5);
2654 assert_eq!(target.kind, RenderLocatorTargetKind::FragmentAnchor);
2655 assert_eq!(target.fragment.as_deref(), Some("intro"));
2656
2657 let fallback = map
2658 .resolve_href("text/ch1.xhtml#intro")
2659 .expect("fallback should resolve");
2660 assert_eq!(fallback.page_index, 3);
2661 assert_eq!(
2662 fallback.kind,
2663 RenderLocatorTargetKind::FragmentFallbackChapterStart
2664 );
2665 }
2666
2667 #[test]
2668 fn estimate_fragment_progress_in_html_matches_id_and_name_patterns() {
2669 let html = br#"
2670 <html><body>
2671 <p>intro text</p>
2672 <h2 id="middle">Middle</h2>
2673 <a name='end-anchor'>End</a>
2674 </body></html>
2675 "#;
2676 let middle = estimate_fragment_progress_in_html(html, "middle")
2677 .expect("middle anchor should resolve");
2678 let end = estimate_fragment_progress_in_html(html, "end-anchor")
2679 .expect("end anchor should resolve");
2680 assert!(middle > 0.0);
2681 assert!(end > middle);
2682 assert!(estimate_fragment_progress_in_html(html, "missing").is_none());
2683 assert!(estimate_fragment_progress_in_html(&[], "middle").is_none());
2684 assert!(estimate_fragment_progress_in_html(html, "").is_none());
2685 }
2686}