Skip to main content

ebook_rs/
layout.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct SyntheticSpread {
5    pub left_index: usize,
6    pub right_index: Option<usize>,
7    pub combined_html: String,
8    pub width: f64,
9    pub height: f64,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum LayoutMode {
15    Reflowable,
16    PrePaginated,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum FlowMode {
22    Paginated,
23    Scrolled,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum SpreadMode {
29    Auto,
30    None,
31    Double,
32    Single,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum Theme {
38    Light,
39    Dark,
40    Sepia,
41    Solarized,
42    HighContrast,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ViewportManagerConfig {
47    pub preload_count: usize,
48    pub continuous: bool,
49    pub intersection_observer: bool,
50}
51
52impl Default for ViewportManagerConfig {
53    fn default() -> Self {
54        Self {
55            preload_count: 2,
56            continuous: true,
57            intersection_observer: true,
58        }
59    }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum AssetDeliveryStrategy {
65    InlinedBase64,
66    ResourceStream,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
70#[serde(rename_all = "kebab-case")]
71pub enum WritingMode {
72    #[default]
73    HorizontalLtr,
74    HorizontalRtl,
75    VerticalRl,
76    VerticalLr,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct RenditionLayout {
81    pub layout_mode: LayoutMode,
82    pub flow_mode: FlowMode,
83    pub spread_mode: SpreadMode,
84    pub writing_mode: WritingMode,
85    pub theme: Theme,
86    pub font_family: String,
87    pub font_size_px: u32,
88    pub line_height: f32,
89    pub margin_px: u32,
90    pub allow_scripted_content: bool,
91    pub viewport_config: ViewportManagerConfig,
92    pub asset_delivery: AssetDeliveryStrategy,
93    pub custom_font_family: Option<String>,
94    pub custom_font_url: Option<String>,
95}
96
97impl Default for RenditionLayout {
98    fn default() -> Self {
99        Self {
100            layout_mode: LayoutMode::Reflowable,
101            flow_mode: FlowMode::Paginated,
102            spread_mode: SpreadMode::Auto,
103            writing_mode: WritingMode::HorizontalLtr,
104            theme: Theme::Light,
105            font_family: "Inter, system-ui, -apple-system, sans-serif".to_string(),
106            font_size_px: 16,
107            line_height: 1.6,
108            margin_px: 32,
109            allow_scripted_content: false,
110            viewport_config: ViewportManagerConfig::default(),
111            asset_delivery: AssetDeliveryStrategy::InlinedBase64,
112            custom_font_family: None,
113            custom_font_url: None,
114        }
115    }
116}
117
118impl RenditionLayout {
119    /// Inject custom reader font family and font URL (F5 Fix) with CSS injection sanitization.
120    pub fn set_custom_font(&mut self, font_family: &str, font_url_or_b64: &str) {
121        // Sanitize font family name: allow alphanumeric, spaces, hyphens, and underscores only
122        let safe_family: String = font_family
123            .chars()
124            .filter(|c| c.is_alphanumeric() || *c == ' ' || *c == '-' || *c == '_')
125            .collect();
126
127        // Sanitize font URL/Base64: reject control chars, quotes, braces, semicolons, and javascript: URIs
128        let is_unsafe_url = font_url_or_b64
129            .contains(['\'', '"', ';', '{', '}', '<', '>', '\n', '\r'])
130            || font_url_or_b64
131                .trim()
132                .to_ascii_lowercase()
133                .starts_with("javascript:");
134
135        if !safe_family.is_empty() && !is_unsafe_url {
136            self.custom_font_family = Some(safe_family);
137            self.custom_font_url = Some(font_url_or_b64.trim().to_string());
138        }
139    }
140
141    /// Generate dynamic CSS rules to inject into section HTML.
142    pub fn to_css_override(&self) -> String {
143        let (bg, fg, link) = match self.theme {
144            Theme::Light => ("#ffffff", "#1e293b", "#2563eb"),
145            Theme::Dark => ("#0f172a", "#f8fafc", "#60a5fa"),
146            Theme::Sepia => ("#fef3c7", "#451a03", "#b45309"),
147            Theme::Solarized => ("#073642", "#839496", "#268bd2"),
148            Theme::HighContrast => ("#000000", "#ffffff", "#ffff00"),
149        };
150
151        let font_rule =
152            if let (Some(fam), Some(url)) = (&self.custom_font_family, &self.custom_font_url) {
153                format!(
154                    r#"
155                @font-face {{
156                    font-family: '{}';
157                    src: url('{}');
158                }}
159                "#,
160                    fam, url
161                )
162            } else {
163                "".to_string()
164            };
165
166        let active_font = self
167            .custom_font_family
168            .as_deref()
169            .unwrap_or(&self.font_family);
170
171        let mode_css = match self.writing_mode {
172            WritingMode::HorizontalLtr => "direction: ltr;",
173            WritingMode::HorizontalRtl => "direction: rtl;",
174            WritingMode::VerticalRl => {
175                "writing-mode: vertical-rl; -webkit-writing-mode: vertical-rl;"
176            }
177            WritingMode::VerticalLr => {
178                "writing-mode: vertical-lr; -webkit-writing-mode: vertical-lr;"
179            }
180        };
181
182        format!(
183            r#"
184            {}
185            :root {{
186                --reader-bg: {};
187                --reader-fg: {};
188                --reader-link: {};
189            }}
190            body {{
191                background-color: var(--reader-bg) !important;
192                color: var(--reader-fg) !important;
193                font-family: {} !important;
194                font-size: {}px !important;
195                line-height: {} !important;
196                padding: {}px !important;
197                margin: 0 auto !important;
198                max-width: 850px !important;
199                box-sizing: border-box !important;
200                word-wrap: break-word !important;
201                {}
202            }}
203            a {{
204                color: var(--reader-link) !important;
205            }}
206            img, svg, video {{
207                max-width: 100% !important;
208                height: auto !important;
209            }}
210            "#,
211            font_rule,
212            bg,
213            fg,
214            link,
215            active_font,
216            self.font_size_px,
217            self.line_height,
218            self.margin_px,
219            mode_css
220        )
221    }
222
223    /// Calculate Fixed Layout (FXL) scale factor and CSS transform matrix string
224    /// given target page dimensions (vp_width, vp_height) and screen container bounds.
225    pub fn compute_fxl_scale(
226        &self,
227        vp_width: f64,
228        vp_height: f64,
229        container_width: f64,
230        container_height: f64,
231    ) -> Option<(f64, String)> {
232        if vp_width <= 0.0 || vp_height <= 0.0 || container_width <= 0.0 || container_height <= 0.0
233        {
234            return None;
235        }
236
237        let scale_w = container_width / vp_width;
238        let scale_h = container_height / vp_height;
239        let scale = scale_w.min(scale_h);
240
241        let css_transform = format!(
242            "width: {}px; height: {}px; transform: scale({}); transform-origin: 0 0;",
243            vp_width, vp_height, scale
244        );
245
246        Some((scale, css_transform))
247    }
248}