1pub use graphitepdf_font::{
2 FontDescriptor, FontSource, FontStyle, FontWeight as FontVariantWeight, StandardFont,
3};
4pub use graphitepdf_layout::EdgeInsets;
5use graphitepdf_primitives::{Color, Pt};
6
7pub use graphitepdf_stylesheet::{
8 Container as StylesheetContainer, ExpandedStyle as StylesheetExpandedStyle,
9 SafeStyle as StylesheetSafeStyle, Style as StylesheetMap, StyleValue, Stylesheet,
10};
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct Style {
14 pub width: Option<Pt>,
15 pub height: Option<Pt>,
16 pub margin: EdgeInsets,
17 pub padding: EdgeInsets,
18 pub background_color: Option<Color>,
19 pub color: Option<Color>,
20 pub font_size: Option<Pt>,
21 pub font_family: Option<String>,
22 pub font_style: Option<FontStyle>,
23 pub font_weight: Option<FontVariantWeight>,
24 pub font_source: Option<FontSource>,
25 pub flex_direction: FlexDirection,
26 pub justify_content: JustifyContent,
27 pub align_items: AlignItems,
28}
29
30impl Default for Style {
31 fn default() -> Self {
32 Self {
33 width: None,
34 height: None,
35 margin: EdgeInsets::default(),
36 padding: EdgeInsets::default(),
37 background_color: None,
38 color: Some(Color::BLACK),
39 font_size: Some(Pt::new(12.0)),
40 font_family: None,
41 font_style: None,
42 font_weight: None,
43 font_source: None,
44 flex_direction: FlexDirection::default(),
45 justify_content: JustifyContent::default(),
46 align_items: AlignItems::default(),
47 }
48 }
49}
50
51impl Style {
52 pub fn from_stylesheet(container: &StylesheetContainer, stylesheet: &Stylesheet) -> Self {
53 let mut style = Self::default();
54 style.apply_stylesheet(container, stylesheet);
55 style
56 }
57
58 pub fn apply_stylesheet(&mut self, container: &StylesheetContainer, stylesheet: &Stylesheet) {
59 let resolved = stylesheet.resolve(container);
60 self.apply_resolved_stylesheet(&resolved);
61 }
62
63 pub fn apply_resolved_stylesheet(&mut self, style: &StylesheetMap) {
64 if let Some(value) = stylesheet_pt(style, "width") {
65 self.width = Some(value);
66 }
67 if let Some(value) = stylesheet_pt(style, "height") {
68 self.height = Some(value);
69 }
70
71 apply_edge_insets(
72 &mut self.margin,
73 style,
74 ["marginTop", "marginRight", "marginBottom", "marginLeft"],
75 );
76 apply_edge_insets(
77 &mut self.padding,
78 style,
79 ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"],
80 );
81
82 if let Some(value) = stylesheet_color(style, "backgroundColor") {
83 self.background_color = Some(value);
84 }
85 if let Some(value) = stylesheet_color(style, "color") {
86 self.color = Some(value);
87 }
88 if let Some(value) = stylesheet_pt(style, "fontSize") {
89 self.font_size = Some(value);
90 }
91 if let Some(value) = stylesheet_string(style, "fontFamily") {
92 self.font_family = Some(value.to_string());
93 }
94 if let Some(value) = stylesheet_font_style(style, "fontStyle") {
95 self.font_style = Some(value);
96 }
97 if let Some(value) = stylesheet_font_weight(style, "fontWeight") {
98 self.font_weight = Some(value);
99 }
100 if let Some(value) = stylesheet_string(style, "fontSource") {
101 self.font_source = Some(FontSource::remote(value));
102 }
103 if let Some(value) = stylesheet_string(style, "fontSourceLocal") {
104 self.font_source = Some(FontSource::local(value));
105 }
106 if let Some(value) = stylesheet_string(style, "fontSourceDataUri") {
107 self.font_source = Some(FontSource::data_uri(value));
108 }
109 if let Some(value) = stylesheet_standard_font(style, "fontSourceStandard") {
110 self.font_source = Some(FontSource::standard(value));
111 }
112 if let Some(value) = stylesheet_flex_direction(style, "flexDirection") {
113 self.flex_direction = value;
114 }
115 if let Some(value) = stylesheet_justify_content(style, "justifyContent") {
116 self.justify_content = value;
117 }
118 if let Some(value) = stylesheet_align_items(style, "alignItems") {
119 self.align_items = value;
120 }
121 }
122
123 pub fn font_descriptor(&self) -> Option<FontDescriptor> {
124 let mut descriptor = FontDescriptor::new(self.font_family.clone()?);
125
126 if let Some(value) = self.font_style {
127 descriptor = descriptor.with_style(value);
128 }
129 if let Some(value) = self.font_weight {
130 descriptor = descriptor.with_weight(value);
131 }
132
133 Some(descriptor)
134 }
135
136 pub fn to_layout_style(&self) -> graphitepdf_layout::LayoutStyle {
137 graphitepdf_layout::LayoutStyle {
138 width: self.width,
139 height: self.height,
140 margin: Some(self.margin),
141 padding: Some(self.padding),
142 background_color: self.background_color,
143 color: self.color,
144 font_family: self.font_family.clone(),
145 font_style: self.font_style,
146 font_weight: self.font_weight,
147 font_source: self.font_source.clone(),
148 font_size: self.font_size,
149 line_height: None,
150 z_index: None,
151 page_break_before: None,
152 page_break_after: None,
153 }
154 }
155
156 pub fn from_layout_style(style: &graphitepdf_layout::LayoutStyle) -> Self {
157 Self {
158 width: style.width,
159 height: style.height,
160 margin: style.margin.unwrap_or_default(),
161 padding: style.padding.unwrap_or_default(),
162 background_color: style.background_color,
163 color: style.color,
164 font_size: style.font_size,
165 font_family: style.font_family.clone(),
166 font_style: style.font_style,
167 font_weight: style.font_weight,
168 font_source: style.font_source.clone(),
169 flex_direction: FlexDirection::default(),
170 justify_content: JustifyContent::default(),
171 align_items: AlignItems::default(),
172 }
173 }
174}
175
176#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
177pub enum FlexDirection {
178 #[default]
179 Column,
180 Row,
181}
182
183#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
184pub enum JustifyContent {
185 #[default]
186 Start,
187 Center,
188 End,
189 SpaceBetween,
190}
191
192#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
193pub enum AlignItems {
194 #[default]
195 Start,
196 Center,
197 End,
198 Stretch,
199}
200
201fn apply_edge_insets(target: &mut EdgeInsets, style: &StylesheetMap, keys: [&str; 4]) {
202 if let Some(value) = stylesheet_pt(style, keys[0]) {
203 target.top = value;
204 }
205 if let Some(value) = stylesheet_pt(style, keys[1]) {
206 target.right = value;
207 }
208 if let Some(value) = stylesheet_pt(style, keys[2]) {
209 target.bottom = value;
210 }
211 if let Some(value) = stylesheet_pt(style, keys[3]) {
212 target.left = value;
213 }
214}
215
216fn stylesheet_pt(style: &StylesheetMap, key: &str) -> Option<Pt> {
217 stylesheet_f32(style, key).map(Pt::new)
218}
219
220fn stylesheet_f32(style: &StylesheetMap, key: &str) -> Option<f32> {
221 match style.get(key)? {
222 StyleValue::Number(value) => Some(*value as f32),
223 StyleValue::String(value) => value.trim().parse::<f32>().ok(),
224 _ => None,
225 }
226}
227
228fn stylesheet_string<'a>(style: &'a StylesheetMap, key: &str) -> Option<&'a str> {
229 match style.get(key)? {
230 StyleValue::String(value) => Some(value.as_str()),
231 _ => None,
232 }
233}
234
235fn stylesheet_color(style: &StylesheetMap, key: &str) -> Option<Color> {
236 let value = stylesheet_string(style, key)?;
237 parse_color(value)
238}
239
240fn stylesheet_font_style(style: &StylesheetMap, key: &str) -> Option<FontStyle> {
241 match stylesheet_string(style, key)?
242 .trim()
243 .to_ascii_lowercase()
244 .as_str()
245 {
246 "normal" => Some(FontStyle::Normal),
247 "italic" => Some(FontStyle::Italic),
248 "oblique" => Some(FontStyle::Oblique),
249 _ => None,
250 }
251}
252
253fn stylesheet_font_weight(style: &StylesheetMap, key: &str) -> Option<FontVariantWeight> {
254 let value = style.get(key)?;
255 let number = match value {
256 StyleValue::Number(value) => *value as u16,
257 StyleValue::String(value) => value.trim().parse::<u16>().ok()?,
258 _ => return None,
259 };
260
261 FontVariantWeight::new(number).ok()
262}
263
264fn stylesheet_standard_font(style: &StylesheetMap, key: &str) -> Option<StandardFont> {
265 match stylesheet_string(style, key)?.trim() {
266 "Times-Roman" => Some(StandardFont::TimesRoman),
267 "Times-Bold" => Some(StandardFont::TimesBold),
268 "Times-Italic" => Some(StandardFont::TimesItalic),
269 "Times-BoldItalic" => Some(StandardFont::TimesBoldItalic),
270 "Helvetica" => Some(StandardFont::Helvetica),
271 "Helvetica-Bold" => Some(StandardFont::HelveticaBold),
272 "Helvetica-Oblique" => Some(StandardFont::HelveticaOblique),
273 "Helvetica-BoldOblique" => Some(StandardFont::HelveticaBoldOblique),
274 "Courier" => Some(StandardFont::Courier),
275 "Courier-Bold" => Some(StandardFont::CourierBold),
276 "Courier-Oblique" => Some(StandardFont::CourierOblique),
277 "Courier-BoldOblique" => Some(StandardFont::CourierBoldOblique),
278 "Symbol" => Some(StandardFont::Symbol),
279 "ZapfDingbats" => Some(StandardFont::ZapfDingbats),
280 _ => None,
281 }
282}
283
284fn stylesheet_flex_direction(style: &StylesheetMap, key: &str) -> Option<FlexDirection> {
285 match stylesheet_string(style, key)?.trim() {
286 "column" => Some(FlexDirection::Column),
287 "row" => Some(FlexDirection::Row),
288 _ => None,
289 }
290}
291
292fn stylesheet_justify_content(style: &StylesheetMap, key: &str) -> Option<JustifyContent> {
293 match stylesheet_string(style, key)?.trim() {
294 "start" | "flex-start" => Some(JustifyContent::Start),
295 "center" => Some(JustifyContent::Center),
296 "end" | "flex-end" => Some(JustifyContent::End),
297 "space-between" => Some(JustifyContent::SpaceBetween),
298 _ => None,
299 }
300}
301
302fn stylesheet_align_items(style: &StylesheetMap, key: &str) -> Option<AlignItems> {
303 match stylesheet_string(style, key)?.trim() {
304 "start" | "flex-start" => Some(AlignItems::Start),
305 "center" => Some(AlignItems::Center),
306 "end" | "flex-end" => Some(AlignItems::End),
307 "stretch" => Some(AlignItems::Stretch),
308 _ => None,
309 }
310}
311
312fn parse_color(value: &str) -> Option<Color> {
313 let trimmed = value.trim();
314
315 match trimmed {
316 "black" => return Some(Color::BLACK),
317 "white" => return Some(Color::WHITE),
318 _ => {}
319 }
320
321 let hex = trimmed.strip_prefix('#')?;
322 match hex.len() {
323 6 => Some(Color::rgb(
324 u8::from_str_radix(&hex[0..2], 16).ok()?,
325 u8::from_str_radix(&hex[2..4], 16).ok()?,
326 u8::from_str_radix(&hex[4..6], 16).ok()?,
327 )),
328 8 => Some(Color::rgba(
329 u8::from_str_radix(&hex[0..2], 16).ok()?,
330 u8::from_str_radix(&hex[2..4], 16).ok()?,
331 u8::from_str_radix(&hex[4..6], 16).ok()?,
332 u8::from_str_radix(&hex[6..8], 16).ok()?,
333 )),
334 _ => None,
335 }
336}
337
338impl From<&Style> for graphitepdf_layout::LayoutStyle {
339 fn from(value: &Style) -> Self {
340 value.to_layout_style()
341 }
342}
343
344impl From<Style> for graphitepdf_layout::LayoutStyle {
345 fn from(value: Style) -> Self {
346 value.to_layout_style()
347 }
348}
349
350impl From<graphitepdf_layout::LayoutStyle> for Style {
351 fn from(value: graphitepdf_layout::LayoutStyle) -> Self {
352 Self::from_layout_style(&value)
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 fn stylesheet_style(entries: [(&str, StyleValue); 11]) -> StylesheetMap {
361 entries
362 .into_iter()
363 .map(|(key, value)| (key.to_string(), value))
364 .collect()
365 }
366
367 #[test]
368 fn builds_style_from_stylesheet_and_exposes_font_descriptor() {
369 let container = StylesheetContainer {
370 width: 200.0,
371 height: 300.0,
372 dpi: None,
373 rem_base: Some(10.0),
374 orientation: None,
375 };
376 let stylesheet = Stylesheet::new(StyleValue::Object(stylesheet_style([
377 ("width", 24.into()),
378 ("marginTop", 12.into()),
379 ("marginRight", 14.into()),
380 ("paddingLeft", 8.into()),
381 ("backgroundColor", "#112233".into()),
382 ("color", "#AABBCCDD".into()),
383 ("fontFamily", "Inter".into()),
384 ("fontStyle", "italic".into()),
385 ("fontWeight", 600.into()),
386 ("fontSourceStandard", "Helvetica-Bold".into()),
387 ("justifyContent", "center".into()),
388 ])));
389
390 let style = Style::from_stylesheet(&container, &stylesheet);
391
392 assert_eq!(style.width, Some(Pt::new(24.0)));
393 assert_eq!(style.margin.top, Pt::new(12.0));
394 assert_eq!(style.margin.right, Pt::new(14.0));
395 assert_eq!(style.padding.left, Pt::new(8.0));
396 assert_eq!(style.background_color, Some(Color::rgb(0x11, 0x22, 0x33)));
397 assert_eq!(style.color, Some(Color::rgba(0xAA, 0xBB, 0xCC, 0xDD)));
398 assert_eq!(style.font_style, Some(FontStyle::Italic));
399 assert_eq!(style.font_weight, Some(FontVariantWeight::SEMI_BOLD));
400 assert_eq!(
401 style.font_source,
402 Some(FontSource::standard(StandardFont::HelveticaBold))
403 );
404 assert_eq!(style.justify_content, JustifyContent::Center);
405
406 let descriptor = style
407 .font_descriptor()
408 .expect("font descriptor should exist");
409 assert_eq!(descriptor.family(), "Inter");
410 assert_eq!(descriptor.font_style(), FontStyle::Italic);
411 assert_eq!(descriptor.font_weight(), FontVariantWeight::SEMI_BOLD);
412
413 let layout_style = style.to_layout_style();
414 assert_eq!(layout_style.font_family.as_deref(), Some("Inter"));
415 assert_eq!(layout_style.padding.unwrap_or_default().left, Pt::new(8.0));
416 }
417
418 #[test]
419 fn applying_partial_stylesheet_preserves_existing_values() {
420 let mut style = Style {
421 width: Some(Pt::new(42.0)),
422 font_family: Some(String::from("Existing")),
423 ..Style::default()
424 };
425 let resolved = stylesheet_style([
426 ("height", 100.into()),
427 ("marginTop", 3.into()),
428 ("marginRight", 0.into()),
429 ("paddingLeft", 0.into()),
430 ("backgroundColor", "#000000".into()),
431 ("color", "#FFFFFFFF".into()),
432 ("fontFamily", StyleValue::Null),
433 ("fontStyle", StyleValue::Null),
434 ("fontWeight", StyleValue::Null),
435 ("fontSourceStandard", StyleValue::Null),
436 ("alignItems", "stretch".into()),
437 ]);
438
439 style.apply_resolved_stylesheet(&resolved);
440
441 assert_eq!(style.width, Some(Pt::new(42.0)));
442 assert_eq!(style.height, Some(Pt::new(100.0)));
443 assert_eq!(style.margin.top, Pt::new(3.0));
444 assert_eq!(style.font_family.as_deref(), Some("Existing"));
445 assert_eq!(style.align_items, AlignItems::Stretch);
446 }
447
448 #[test]
449 fn converts_layout_style_back_into_compat_style() {
450 let layout_style = graphitepdf_layout::LayoutStyle::new()
451 .with_width(Pt::new(72.0))
452 .with_height(Pt::new(24.0))
453 .with_margin(EdgeInsets::all(Pt::new(6.0)))
454 .with_padding(EdgeInsets::all(Pt::new(4.0)))
455 .with_font_family("Inter")
456 .with_font_style(FontStyle::Italic)
457 .with_font_weight(FontVariantWeight::BOLD)
458 .with_font_source(FontSource::standard(StandardFont::HelveticaBold))
459 .with_font_size(Pt::new(14.0))
460 .with_background_color(Color::rgb(0x11, 0x22, 0x33));
461
462 let style = Style::from_layout_style(&layout_style);
463
464 assert_eq!(style.width, Some(Pt::new(72.0)));
465 assert_eq!(style.height, Some(Pt::new(24.0)));
466 assert_eq!(style.margin, EdgeInsets::all(Pt::new(6.0)));
467 assert_eq!(style.padding, EdgeInsets::all(Pt::new(4.0)));
468 assert_eq!(style.font_family.as_deref(), Some("Inter"));
469 assert_eq!(style.font_style, Some(FontStyle::Italic));
470 assert_eq!(style.font_weight, Some(FontVariantWeight::BOLD));
471 assert_eq!(
472 style.font_source,
473 Some(FontSource::standard(StandardFont::HelveticaBold))
474 );
475 assert_eq!(style.font_size, Some(Pt::new(14.0)));
476 assert_eq!(style.background_color, Some(Color::rgb(0x11, 0x22, 0x33)));
477 }
478}