1use ab_glyph::{Font, FontArc, PxScale};
2use anyhow::{Result, anyhow};
3use image::{DynamicImage, GenericImageView};
4use log;
5
6use crate::color::Color;
7use crate::{F64_ALMOST_ZERO, FULL_WIDTH_SPACE, IMAGE_SIZE};
8
9#[derive(Debug, Clone, Default, PartialEq)]
12pub struct Element {
13 characteristics: Vec<f64>,
14 luminance: f64,
15 character: Option<char>,
16 image: Option<DynamicImage>,
17}
18
19impl Element {
20 pub fn new(
22 characteristics: Vec<f64>,
23 luminance: f64,
24 character: Option<char>,
25 image: Option<DynamicImage>,
26 ) -> Self {
27 Element {
28 characteristics,
29 luminance,
30 character,
31 image,
32 }
33 }
34
35 pub fn characteristics(&self) -> &[f64] {
37 &self.characteristics
38 }
39
40 pub fn luminance(&self) -> f64 {
42 self.luminance
43 }
44
45 pub fn character(&self) -> Option<char> {
47 self.character
48 }
49
50 #[allow(dead_code)]
52 pub fn image(&self) -> Option<&DynamicImage> {
53 self.image.as_ref()
54 }
55
56 pub fn from_char(font: &FontArc, character: char, scale: PxScale) -> Result<Self> {
59 let (width, height) = (IMAGE_SIZE, IMAGE_SIZE);
60 let mut characteristics = vec![1.0; (width * height) as usize];
61
62 let glyph = font.glyph_id(character).with_scale(scale);
63 let outline = match font.outline_glyph(glyph) {
64 Some(g) => g,
65 None => {
66 if character == FULL_WIDTH_SPACE {
67 return Ok(Element {
68 characteristics,
69 luminance: 1.0,
70 character: Some(' '),
71 image: None,
72 });
73 }
74 return Err(anyhow!(
75 "Failed to outline glyph for character: {}",
76 character
77 ));
78 }
79 };
80
81 let bounds = outline.px_bounds();
82
83 let glyph_center_x = bounds.min.x + bounds.width() / 2.0;
85 let glyph_center_y = bounds.min.y + bounds.height() / 2.0;
86
87 let canvas_center_x = width as f32 / 2.0;
88 let canvas_center_y = height as f32 / 2.0;
89
90 let offset_x = canvas_center_x - glyph_center_x;
91 let offset_y = canvas_center_y - glyph_center_y;
92
93 outline.draw(|x, y, c| {
94 let canvas_x = x as f32 + offset_x;
95 let canvas_y = y as f32 + offset_y;
96
97 if canvas_x >= 0.0
98 && canvas_x < width as f32
99 && canvas_y >= 0.0
100 && canvas_y < height as f32
101 {
102 let index = (canvas_y as u32 * width + canvas_x as u32) as usize;
103 characteristics[index] = 1.0 - (c as f64);
104 }
105 });
106
107 let luminance = characteristics.iter().sum::<f64>() / (width * height) as f64;
108
109 log::debug!(
110 "Character: '{character}', Width: {width}, Height: {height}, Luminance: {luminance}"
111 );
112
113 Ok(Element {
114 characteristics,
115 luminance,
116 character: Some(character),
117 image: None,
118 })
119 }
120
121 pub fn from_image(image: DynamicImage) -> Result<Self> {
123 let (width, height) = image.dimensions();
124 log::trace!("Image dimensions: {width}x{height}");
125 if width == 0 || height == 0 {
126 return Err(anyhow!("Image has zero width or height."));
127 }
128
129 let mut characteristics: Vec<f64> = vec![];
130 let mut total_luminance: f64 = 0.0;
131
132 for (_, _, rgba) in image.pixels() {
133 let l = Color::luminance_from_rgba(&rgba.0);
134 total_luminance += l;
135 characteristics.push(l);
136 }
137
138 let luminance = total_luminance / (width * height) as f64;
139
140 Ok(Element {
141 characteristics,
142 luminance,
143 character: None,
144 image: Some(image),
145 })
146 }
147
148 pub fn normalized(&mut self, min: f64, max: f64) -> Result<()> {
151 if min >= max {
152 return Err(anyhow!(
153 "Invalid range: min ({}) must be less than max ({})",
154 min,
155 max
156 ));
157 }
158
159 log::trace!(
160 "Normalizing element: character: {:?}, luminance: {}",
161 self.character,
162 self.luminance,
163 );
164
165 for value in &mut self.characteristics {
166 *value = Self::normalize(*value, min, max);
167 }
168 self.luminance = Self::normalize(self.luminance, min, max);
169
170 log::trace!(
171 "Normalized element: character: {:?}, luminance: {}",
172 self.character,
173 self.luminance,
174 );
175
176 Ok(())
177 }
178
179 fn normalize(value: f64, min: f64, max: f64) -> f64 {
181 if max - min < F64_ALMOST_ZERO {
182 0.0
183 } else {
184 (value - min) / (max - min)
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use std::fs;
193
194 const FONT_PATH: &str = "resources/NotoSansJP-Regular.otf";
195
196 #[test]
197 fn element_from_char() {
198 let font_data = fs::read(FONT_PATH).unwrap();
199 let font = FontArc::try_from_vec(font_data).unwrap();
200 let scale = PxScale::from(16.0);
201 let element = Element::from_char(&font, 'A', scale);
202 assert!(element.is_ok());
203 let element = element.unwrap();
204 assert_eq!(element.character, Some('A'));
205 assert!(!element.characteristics.is_empty());
206 }
207
208 #[test]
209 fn normalized_invalid_range_returns_err() {
210 let mut element = Element::new(vec![0.5, 0.6, 0.7], 0.6, Some('A'), None);
211 let result = element.normalized(0.7, 0.5);
212 assert!(result.is_err());
213 }
214
215 #[test]
216 fn normalized_valid_range_returns_ok() {
217 let mut element = Element::new(vec![0.5, 0.6, 0.7], 0.6, Some('A'), None);
218 let result = element.normalized(0.5, 0.7);
219 assert!(result.is_ok());
220 assert_eq!(element.characteristics, vec![0.0, 0.5, 1.0]);
221 assert_eq!(element.luminance, 0.5);
222 }
223}