img_gen_spec/validators/layers/typography/
line.rs1use std::num::NonZeroI32;
2
3#[cfg(feature = "pyo3")]
4use pyo3::prelude::*;
5
6use serde::{Deserialize, Serialize};
7
8use crate::{ImgGenSpecError, Result};
9
10#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
12pub struct LineHeight(f32);
13
14impl Default for LineHeight {
15 fn default() -> Self {
16 Self(1.0)
17 }
18}
19
20impl LineHeight {
21 pub fn new(height: f32) -> Option<Self> {
23 if height == 0.0 {
24 None
25 } else {
26 Some(Self(height))
27 }
28 }
29
30 pub fn get(&self) -> f32 {
32 self.0
33 }
34
35 pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Self, D::Error>
37 where
38 D: serde::Deserializer<'de>,
39 {
40 use serde::Deserialize;
41
42 let val = f32::deserialize(deserializer)?;
43 Self::new(val).ok_or_else(|| {
44 serde::de::Error::custom(format!("LineHeight must not be zero, got {val}"))
45 })
46 }
47}
48
49#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Line {
54 #[serde(default = "Line::default_line_amount")]
58 pub amount: NonZeroI32,
59 #[serde(
63 default = "LineHeight::default",
64 deserialize_with = "LineHeight::deserialize"
65 )]
66 pub height: LineHeight,
67}
68
69impl Line {
70 const fn default_line_amount() -> NonZeroI32 {
71 #[allow(clippy::unwrap_used, reason = "1 != 0, and this is a const fn")]
72 NonZeroI32::new(1).unwrap()
73 }
74
75 pub fn get_font_size(&self, height: u32, border_width: Option<u32>) -> Result<u32> {
77 if height == 0 {
78 return Err(ImgGenSpecError::InvalidLayerHeight);
79 }
80 let available_height = height.saturating_sub(border_width.unwrap_or_default()) as f32;
81 let max_size = available_height / (self.amount.get() as f32 * self.height.get());
82 Ok(max_size.max(1.0) as u32)
83 }
84}
85
86impl Default for Line {
87 fn default() -> Self {
88 Self {
89 amount: Self::default_line_amount(),
90 height: LineHeight::default(),
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 #![allow(clippy::unwrap_used)]
98
99 use super::*;
100
101 #[test]
102 fn test_line_height() {
103 assert!(LineHeight::new(1.0).is_some());
104 assert!(LineHeight::new(0.0).is_none());
105 assert!(LineHeight::new(-1.0).is_some());
106 }
107
108 #[test]
109 fn test_line_font_size() {
110 let line = Line {
111 amount: NonZeroI32::new(2).unwrap(),
112 height: LineHeight(1.5),
113 };
114 let err = line.get_font_size(0, Some(10)).unwrap_err();
115 assert!(matches!(err, ImgGenSpecError::InvalidLayerHeight));
116 }
117
118 #[test]
119 fn deserialize_bad_line_height() {
120 let json = r#"
121 {
122 "amount": 2,
123 "height": 0.0
124 }
125 "#;
126 let err = serde_json::from_str::<Line>(json).unwrap_err();
127 assert!(err.to_string().contains("LineHeight must not be zero"));
128 }
129}