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 pub fn set_custom_font(&mut self, font_family: &str, font_url_or_b64: &str) {
121 let safe_family: String = font_family
123 .chars()
124 .filter(|c| c.is_alphanumeric() || *c == ' ' || *c == '-' || *c == '_')
125 .collect();
126
127 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 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 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}