Skip to main content

epub_stream_render/
render_ir.rs

1use epub_stream::BlockRole;
2
3/// Page represented as backend-agnostic draw commands.
4#[derive(Clone, Debug, Default, PartialEq)]
5pub struct RenderPage {
6    /// 1-based page number.
7    pub page_number: usize,
8    /// Legacy merged command stream.
9    ///
10    /// This remains for compatibility and is kept in sync with
11    /// `content_commands`, `chrome_commands`, and `overlay_commands`.
12    pub commands: Vec<DrawCommand>,
13    /// Content-layer draw commands (deterministic pagination output).
14    pub content_commands: Vec<DrawCommand>,
15    /// Chrome-layer draw commands (header/footer/progress and similar).
16    pub chrome_commands: Vec<DrawCommand>,
17    /// Overlay draw commands attached after content/chrome layout.
18    pub overlay_commands: Vec<DrawCommand>,
19    /// Structured overlay items attached by composer APIs.
20    pub overlay_items: Vec<OverlayItem>,
21    /// Structured non-draw annotations associated with this page.
22    pub annotations: Vec<PageAnnotation>,
23    /// Per-page metrics for navigation/progress consumers.
24    pub metrics: PageMetrics,
25}
26
27impl RenderPage {
28    /// Create an empty page.
29    pub fn new(page_number: usize) -> Self {
30        Self {
31            page_number,
32            commands: Vec::with_capacity(0),
33            content_commands: Vec::with_capacity(0),
34            chrome_commands: Vec::with_capacity(0),
35            overlay_commands: Vec::with_capacity(0),
36            overlay_items: Vec::with_capacity(0),
37            annotations: Vec::with_capacity(0),
38            metrics: PageMetrics {
39                chapter_page_index: page_number.saturating_sub(1),
40                ..PageMetrics::default()
41            },
42        }
43    }
44
45    /// Push a content-layer command.
46    pub fn push_content_command(&mut self, cmd: DrawCommand) {
47        self.content_commands.push(cmd);
48    }
49
50    /// Push a chrome-layer command.
51    pub fn push_chrome_command(&mut self, cmd: DrawCommand) {
52        self.chrome_commands.push(cmd);
53    }
54
55    /// Push an overlay-layer command.
56    pub fn push_overlay_command(&mut self, cmd: DrawCommand) {
57        self.overlay_commands.push(cmd);
58    }
59
60    /// Rebuild legacy merged `commands` from split layers.
61    pub fn sync_commands(&mut self) {
62        #[cfg(target_os = "espidf")]
63        {
64            // On constrained targets, avoid duplicating command vectors.
65            // Embedded consumers render from split command layers directly.
66            self.commands.clear();
67            return;
68        }
69        #[cfg(not(target_os = "espidf"))]
70        {
71            self.commands.clear();
72            self.commands.extend(self.content_commands.iter().cloned());
73            self.commands.extend(self.chrome_commands.iter().cloned());
74            self.commands.extend(self.overlay_commands.iter().cloned());
75        }
76    }
77
78    /// Backward-compatible accessor alias for page metadata.
79    pub fn page_meta(&self) -> &PageMeta {
80        &self.metrics
81    }
82}
83
84/// Structured page annotation.
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct PageAnnotation {
87    /// Stable annotation kind/tag.
88    pub kind: String,
89    /// Optional annotation payload.
90    pub value: Option<String>,
91}
92
93/// Structured page metrics for progress and navigation.
94#[derive(Clone, Copy, Debug, Default, PartialEq)]
95pub struct PageMetrics {
96    /// Chapter index in the spine (0-based), when known.
97    pub chapter_index: usize,
98    /// Page index in chapter (0-based).
99    pub chapter_page_index: usize,
100    /// Total pages in chapter, when known.
101    pub chapter_page_count: Option<usize>,
102    /// Global page index across rendered stream (0-based), when known.
103    pub global_page_index: Option<usize>,
104    /// Estimated global page count, when known.
105    pub global_page_count_estimate: Option<usize>,
106    /// Chapter progress in range `[0.0, 1.0]`.
107    pub progress_chapter: f32,
108    /// Book progress in range `[0.0, 1.0]`, when known.
109    pub progress_book: Option<f32>,
110}
111
112/// Backward-compatible alias for page-level metadata.
113pub type PageMeta = PageMetrics;
114
115/// Stable pagination profile id.
116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
117pub struct PaginationProfileId(pub [u8; 32]);
118
119impl PaginationProfileId {
120    /// Build a deterministic profile id from arbitrary payload bytes.
121    pub fn from_bytes(bytes: &[u8]) -> Self {
122        fn fnv64(seed: u64, payload: &[u8]) -> u64 {
123            let mut hash = seed;
124            for b in payload {
125                hash ^= *b as u64;
126                hash = hash.wrapping_mul(0x100000001b3);
127            }
128            hash
129        }
130        let mut out = [0u8; 32];
131        let h0 = fnv64(0xcbf29ce484222325, bytes).to_le_bytes();
132        let h1 = fnv64(0x9e3779b97f4a7c15, bytes).to_le_bytes();
133        let h2 = fnv64(0xd6e8feb86659fd93, bytes).to_le_bytes();
134        let h3 = fnv64(0xa0761d6478bd642f, bytes).to_le_bytes();
135        out[0..8].copy_from_slice(&h0);
136        out[8..16].copy_from_slice(&h1);
137        out[16..24].copy_from_slice(&h2);
138        out[24..32].copy_from_slice(&h3);
139        Self(out)
140    }
141}
142
143/// Logical overlay slots for app/UI composition.
144#[derive(Clone, Debug, PartialEq)]
145pub enum OverlaySlot {
146    TopLeft,
147    TopCenter,
148    TopRight,
149    BottomLeft,
150    BottomCenter,
151    BottomRight,
152    Custom(OverlayRect),
153}
154
155/// Logical viewport size for overlay composition.
156#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
157pub struct OverlaySize {
158    pub width: u32,
159    pub height: u32,
160}
161
162/// Rectangle for custom overlay slot coordinates.
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
164pub struct OverlayRect {
165    pub x: i32,
166    pub y: i32,
167    pub width: u32,
168    pub height: u32,
169}
170
171/// Overlay content payload.
172#[derive(Clone, Debug, PartialEq)]
173pub enum OverlayContent {
174    /// Text payload (resolved by the app/backend).
175    Text(String),
176    /// Backend-agnostic draw command payload.
177    Command(DrawCommand),
178}
179
180/// Overlay item attached to a page.
181#[derive(Clone, Debug, PartialEq)]
182pub struct OverlayItem {
183    /// Destination slot.
184    pub slot: OverlaySlot,
185    /// Z-order.
186    pub z: i32,
187    /// Overlay payload.
188    pub content: OverlayContent,
189}
190
191/// Overlay composer API for app-driven overlay placement/content.
192pub trait OverlayComposer {
193    fn compose(&self, metrics: &PageMetrics, viewport: OverlaySize) -> Vec<OverlayItem>;
194}
195
196/// Layout output commands.
197#[derive(Clone, Debug, PartialEq)]
198pub enum DrawCommand {
199    /// Draw text.
200    Text(TextCommand),
201    /// Draw a line rule.
202    Rule(RuleCommand),
203    /// Draw an inline image object box.
204    ImageObject(ImageObjectCommand),
205    /// Draw rectangle.
206    Rect(RectCommand),
207    /// Draw page metadata/chrome.
208    PageChrome(PageChromeCommand),
209}
210
211/// Theme-aware render intent.
212#[derive(Clone, Copy, Debug, PartialEq, Eq)]
213pub struct RenderIntent {
214    /// Convert output to grayscale mode.
215    pub grayscale_mode: GrayscaleMode,
216    /// Optional dithering algorithm.
217    pub dither: DitherMode,
218    /// Contrast multiplier in percent (100 = neutral).
219    pub contrast_boost: u8,
220}
221
222impl Default for RenderIntent {
223    fn default() -> Self {
224        Self {
225            grayscale_mode: GrayscaleMode::Off,
226            dither: DitherMode::None,
227            contrast_boost: 100,
228        }
229    }
230}
231
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum GrayscaleMode {
234    Off,
235    Luminosity,
236}
237
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
239pub enum DitherMode {
240    None,
241    Ordered,
242    ErrorDiffusion,
243}
244
245/// Resolved style passed to renderer.
246#[derive(Clone, Debug, PartialEq)]
247pub struct ResolvedTextStyle {
248    /// Stable font identifier for this style.
249    pub font_id: Option<u32>,
250    /// Chosen family.
251    pub family: String,
252    /// Numeric weight.
253    pub weight: u16,
254    /// Italic flag.
255    pub italic: bool,
256    /// Size in pixels.
257    pub size_px: f32,
258    /// Line height multiplier.
259    pub line_height: f32,
260    /// Letter spacing in px.
261    pub letter_spacing: f32,
262    /// Semantic role.
263    pub role: BlockRole,
264    /// Justification mode from layout.
265    pub justify_mode: JustifyMode,
266}
267
268/// Justification mode determined during layout.
269#[derive(Clone, Copy, Debug, PartialEq, Eq)]
270pub enum JustifyMode {
271    /// Left/no justification.
272    None,
273    /// Inter-word with total extra px to distribute.
274    InterWord { extra_px_total: i32 },
275    /// Right alignment with horizontal offset.
276    AlignRight { offset_px: i32 },
277    /// Center alignment with horizontal offset.
278    AlignCenter { offset_px: i32 },
279}
280
281/// Text draw command.
282#[derive(Clone, Debug, PartialEq)]
283pub struct TextCommand {
284    /// Left x.
285    pub x: i32,
286    /// Baseline y.
287    pub baseline_y: i32,
288    /// Content.
289    pub text: String,
290    /// Font identifier for direct command-level lookup.
291    pub font_id: Option<u32>,
292    /// Resolved style.
293    pub style: ResolvedTextStyle,
294}
295
296/// Rule draw command.
297#[derive(Clone, Copy, Debug, PartialEq, Eq)]
298pub struct RuleCommand {
299    /// Start x.
300    pub x: i32,
301    /// Start y.
302    pub y: i32,
303    /// Length.
304    pub length: u32,
305    /// Thickness.
306    pub thickness: u32,
307    /// Horizontal if true; vertical if false.
308    pub horizontal: bool,
309}
310
311/// Rectangle command.
312#[derive(Clone, Copy, Debug, PartialEq, Eq)]
313pub struct RectCommand {
314    /// Left x.
315    pub x: i32,
316    /// Top y.
317    pub y: i32,
318    /// Width.
319    pub width: u32,
320    /// Height.
321    pub height: u32,
322    /// Fill rectangle when true.
323    pub fill: bool,
324}
325
326/// Inline image object command.
327#[derive(Clone, Debug, PartialEq, Eq)]
328pub struct ImageObjectCommand {
329    /// Resource href (OPF-relative).
330    pub src: String,
331    /// Optional alt/caption text.
332    pub alt: String,
333    /// Left x.
334    pub x: i32,
335    /// Top y.
336    pub y: i32,
337    /// Width.
338    pub width: u32,
339    /// Height.
340    pub height: u32,
341}
342
343/// Page-level metadata/chrome marker.
344#[derive(Clone, Debug, PartialEq, Eq)]
345pub struct PageChromeCommand {
346    /// Semantic chrome kind.
347    pub kind: PageChromeKind,
348    /// Optional text payload (e.g. footer text).
349    pub text: Option<String>,
350    /// Optional current value (e.g. for progress).
351    pub current: Option<usize>,
352    /// Optional total value (e.g. for progress).
353    pub total: Option<usize>,
354}
355
356/// Kind of page-level metadata/chrome.
357#[derive(Clone, Copy, Debug, PartialEq, Eq)]
358pub enum PageChromeKind {
359    /// Header marker.
360    Header,
361    /// Footer marker.
362    Footer,
363    /// Progress marker.
364    Progress,
365}
366
367/// Text style for header/footer chrome rendering.
368#[derive(Clone, Copy, Debug, PartialEq, Eq)]
369pub enum PageChromeTextStyle {
370    Regular,
371    Bold,
372    Italic,
373    BoldItalic,
374}
375
376/// Shared page-chrome policy and geometry configuration.
377#[derive(Clone, Copy, Debug, PartialEq, Eq)]
378pub struct PageChromeConfig {
379    /// Emit/draw page header text.
380    pub header_enabled: bool,
381    /// Emit/draw page footer text.
382    pub footer_enabled: bool,
383    /// Emit/draw page progress bar.
384    pub progress_enabled: bool,
385    /// Header text left x.
386    pub header_x: i32,
387    /// Header text baseline y.
388    pub header_baseline_y: i32,
389    /// Header text style.
390    pub header_style: PageChromeTextStyle,
391    /// Footer text left x.
392    pub footer_x: i32,
393    /// Footer text baseline offset from bottom edge.
394    pub footer_baseline_from_bottom: i32,
395    /// Footer text style.
396    pub footer_style: PageChromeTextStyle,
397    /// Progress bar left/right inset.
398    pub progress_x_inset: i32,
399    /// Progress bar top y offset from bottom edge.
400    pub progress_y_from_bottom: i32,
401    /// Progress bar height.
402    pub progress_height: u32,
403    /// Progress bar outline thickness.
404    pub progress_stroke_width: u32,
405}
406
407impl PageChromeConfig {
408    /// Default chrome geometry matching historical renderer behavior.
409    pub const fn geometry_defaults() -> Self {
410        Self {
411            header_enabled: true,
412            footer_enabled: true,
413            progress_enabled: true,
414            header_x: 8,
415            header_baseline_y: 16,
416            header_style: PageChromeTextStyle::Bold,
417            footer_x: 8,
418            footer_baseline_from_bottom: 8,
419            footer_style: PageChromeTextStyle::Regular,
420            progress_x_inset: 8,
421            progress_y_from_bottom: 20,
422            progress_height: 4,
423            progress_stroke_width: 1,
424        }
425    }
426
427    /// Defaults used by layout so chrome markers are opt-in.
428    pub const fn layout_defaults() -> Self {
429        let mut cfg = Self::geometry_defaults();
430        cfg.header_enabled = false;
431        cfg.footer_enabled = false;
432        cfg.progress_enabled = false;
433        cfg
434    }
435}
436
437impl Default for PageChromeConfig {
438    fn default() -> Self {
439        Self::layout_defaults()
440    }
441}
442
443/// Typography policy knobs for layout behavior.
444#[derive(Clone, Copy, Debug, Default, PartialEq)]
445pub struct TypographyConfig {
446    /// Hyphenation policy.
447    pub hyphenation: HyphenationConfig,
448    /// Widow/orphan control policy.
449    pub widow_orphan_control: WidowOrphanControl,
450    /// Justification policy.
451    pub justification: JustificationConfig,
452    /// Hanging punctuation policy.
453    pub hanging_punctuation: HangingPunctuationConfig,
454}
455
456/// Hyphenation behavior.
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
458pub struct HyphenationConfig {
459    /// Soft-hyphen handling policy.
460    pub soft_hyphen_policy: HyphenationMode,
461}
462
463impl Default for HyphenationConfig {
464    fn default() -> Self {
465        Self {
466            soft_hyphen_policy: HyphenationMode::Discretionary,
467        }
468    }
469}
470
471#[derive(Clone, Copy, Debug, PartialEq, Eq)]
472pub enum HyphenationMode {
473    Ignore,
474    Discretionary,
475}
476
477/// Widow/orphan policy.
478#[derive(Clone, Copy, Debug, PartialEq, Eq)]
479pub struct WidowOrphanControl {
480    /// Keep at least this many lines at paragraph start/end when possible.
481    pub min_lines: u8,
482    /// Enable widow/orphan controls.
483    pub enabled: bool,
484}
485
486impl Default for WidowOrphanControl {
487    fn default() -> Self {
488        Self {
489            min_lines: 2,
490            enabled: true,
491        }
492    }
493}
494
495/// Justification policy.
496#[derive(Clone, Copy, Debug, PartialEq)]
497pub struct JustificationConfig {
498    /// Enable justification/alignment policy.
499    pub enabled: bool,
500    /// Justification strategy for body/paragraph lines.
501    pub strategy: JustificationStrategy,
502    /// Minimum words required for justification.
503    pub min_words: usize,
504    /// Minimum fill ratio required for justification.
505    pub min_fill_ratio: f32,
506    /// Maximum stretch per space as a multiplier of measured space width.
507    ///
508    /// Used by adaptive inter-word mode to avoid visually noisy spacing.
509    /// Full inter-word mode ignores this cap.
510    pub max_space_stretch_ratio: f32,
511}
512
513impl Default for JustificationConfig {
514    fn default() -> Self {
515        Self {
516            enabled: true,
517            strategy: JustificationStrategy::AdaptiveInterWord,
518            min_words: 7,
519            min_fill_ratio: 0.75,
520            max_space_stretch_ratio: 0.45,
521        }
522    }
523}
524
525/// Justification/alignment strategy.
526#[derive(Clone, Copy, Debug, PartialEq, Eq)]
527pub enum JustificationStrategy {
528    /// Adaptive inter-word justification with quality thresholds.
529    AdaptiveInterWord,
530    /// Full inter-word justification that uses all line slack.
531    FullInterWord,
532    /// Left alignment (no inter-word expansion).
533    AlignLeft,
534    /// Right alignment.
535    AlignRight,
536    /// Center alignment.
537    AlignCenter,
538}
539
540/// Hanging punctuation policy.
541#[derive(Clone, Copy, Debug, PartialEq, Eq)]
542pub struct HangingPunctuationConfig {
543    /// Enable hanging punctuation (currently informational).
544    pub enabled: bool,
545}
546
547impl Default for HangingPunctuationConfig {
548    fn default() -> Self {
549        Self { enabled: true }
550    }
551}
552
553/// Non-text object layout policy knobs.
554#[derive(Clone, Copy, Debug, PartialEq)]
555pub struct ObjectLayoutConfig {
556    /// Max inline-image height ratio relative to content height.
557    pub max_inline_image_height_ratio: f32,
558    /// Policy for cover-like first-page images.
559    pub cover_page_mode: CoverPageMode,
560    /// Enable/disable float placement.
561    pub float_support: FloatSupport,
562    /// SVG placement mode.
563    pub svg_mode: SvgMode,
564    /// Emit alt-text fallback when object drawing is unavailable.
565    pub alt_text_fallback: bool,
566}
567
568impl Default for ObjectLayoutConfig {
569    fn default() -> Self {
570        Self {
571            max_inline_image_height_ratio: 0.5,
572            cover_page_mode: CoverPageMode::Contain,
573            float_support: FloatSupport::None,
574            svg_mode: SvgMode::RasterizeFallback,
575            alt_text_fallback: true,
576        }
577    }
578}
579
580/// Cover-image placement mode for cover-like first-page image resources.
581#[derive(Clone, Copy, Debug, PartialEq, Eq)]
582pub enum CoverPageMode {
583    /// Fit image within content area, preserve aspect ratio, no cropping.
584    Contain,
585    /// Fill viewport while preserving aspect ratio; crop overflow by viewport clip.
586    FullBleed,
587    /// Respect normal CSS/object layout behavior (no special cover handling).
588    RespectCss,
589}
590
591#[derive(Clone, Copy, Debug, PartialEq, Eq)]
592pub enum FloatSupport {
593    None,
594    Basic,
595}
596
597#[derive(Clone, Copy, Debug, PartialEq, Eq)]
598pub enum SvgMode {
599    Ignore,
600    RasterizeFallback,
601    Native,
602}