1use crate::{Error, Result};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum LogoClearShape {
6 Square,
7 Circle,
8}
9
10impl std::fmt::Display for LogoClearShape {
11 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12 match self {
13 Self::Square => write!(f, "square"),
14 Self::Circle => write!(f, "circle"),
15 }
16 }
17}
18
19impl std::str::FromStr for LogoClearShape {
20 type Err = String;
21
22 fn from_str(s: &str) -> std::result::Result<Self, String> {
23 match s.to_ascii_lowercase().as_str() {
24 "square" => Ok(Self::Square),
25 "circle" => Ok(Self::Circle),
26 _ => Err(format!(
27 "unknown clear shape: {s} (expected square or circle)"
28 )),
29 }
30 }
31}
32
33#[derive(Clone)]
35pub struct Logo {
36 pub pixels: Vec<u8>,
38 pub width: u32,
39 pub height: u32,
40 pub fraction: f64,
43 pub clear_border: usize,
46 pub clear_shape: LogoClearShape,
48}
49
50impl Logo {
51 pub fn from_svg(
54 svg_data: &[u8],
55 fraction: f64,
56 clear_border: usize,
57 clear_shape: LogoClearShape,
58 ) -> Result<Self> {
59 let fraction = validate_fraction(fraction)?;
60 let clear_border = validate_clear_border(clear_border)?;
61
62 let tree = resvg::usvg::Tree::from_data(
63 svg_data,
64 &resvg::usvg::Options::default(),
65 )
66 .map_err(|e| Error::SvgRender(format!("SVG parse: {e}")))?;
67
68 let render_size = 512u32;
69 let mut pixmap =
70 resvg::tiny_skia::Pixmap::new(render_size, render_size)
71 .ok_or_else(|| {
72 Error::SvgRender("failed to allocate pixmap".into())
73 })?;
74
75 let svg_size = tree.size();
76 let sx = render_size as f32 / svg_size.width();
77 let sy = render_size as f32 / svg_size.height();
78 let scale = sx.min(sy);
79 let tx = (render_size as f32 - svg_size.width() * scale) / 2.0;
80 let ty = (render_size as f32 - svg_size.height() * scale) / 2.0;
81 let transform = resvg::tiny_skia::Transform::from_scale(scale, scale)
82 .post_translate(tx, ty);
83
84 resvg::render(&tree, transform, &mut pixmap.as_mut());
85
86 let pixels = demultiply_alpha(pixmap.data());
88
89 Ok(Self {
90 pixels,
91 width: render_size,
92 height: render_size,
93 fraction,
94 clear_border,
95 clear_shape,
96 })
97 }
98
99 pub fn from_rgba(
101 pixels: Vec<u8>,
102 width: u32,
103 height: u32,
104 fraction: f64,
105 clear_border: usize,
106 clear_shape: LogoClearShape,
107 ) -> Result<Self> {
108 let fraction = validate_fraction(fraction)?;
109 let clear_border = validate_clear_border(clear_border)?;
110 if pixels.len() != (width * height * 4) as usize {
111 return Err(Error::InvalidParameter(format!(
112 "pixel buffer size {} doesn't match {}x{}x4",
113 pixels.len(),
114 width,
115 height
116 )));
117 }
118 Ok(Self {
119 pixels,
120 width,
121 height,
122 fraction,
123 clear_border,
124 clear_shape,
125 })
126 }
127
128 pub fn from_image_bytes(
130 data: &[u8],
131 fraction: f64,
132 clear_border: usize,
133 clear_shape: LogoClearShape,
134 ) -> Result<Self> {
135 let fraction = validate_fraction(fraction)?;
136 let clear_border = validate_clear_border(clear_border)?;
137
138 let img = image::load_from_memory(data)
139 .map_err(|e| {
140 Error::ImageEncode(format!("failed to decode image: {e}"))
141 })?
142 .into_rgba8();
143
144 let width = img.width();
145 let height = img.height();
146 let pixels = img.into_raw();
147
148 Ok(Self {
149 pixels,
150 width,
151 height,
152 fraction,
153 clear_border,
154 clear_shape,
155 })
156 }
157}
158
159fn validate_fraction(f: f64) -> Result<f64> {
160 if !(0.01..=0.99).contains(&f) {
161 return Err(Error::InvalidParameter(format!(
162 "logo fraction must be 0.01–0.99, got {f}"
163 )));
164 }
165 Ok(f)
166}
167
168fn validate_clear_border(b: usize) -> Result<usize> {
169 if b > 5 {
170 return Err(Error::InvalidParameter(format!(
171 "clear_border must be 0–5, got {b}"
172 )));
173 }
174 Ok(b)
175}
176
177fn demultiply_alpha(data: &[u8]) -> Vec<u8> {
179 let mut out = vec![0u8; data.len()];
180 for i in (0..data.len()).step_by(4) {
181 let a = data[i + 3] as u16;
182 if a == 0 {
183 out[i] = 0;
185 out[i + 1] = 0;
186 out[i + 2] = 0;
187 out[i + 3] = 0;
188 } else if a == 255 {
189 out[i..i + 4].copy_from_slice(&data[i..i + 4]);
190 } else {
191 out[i] = ((data[i] as u16 * 255 + a / 2) / a) as u8;
192 out[i + 1] = ((data[i + 1] as u16 * 255 + a / 2) / a) as u8;
193 out[i + 2] = ((data[i + 2] as u16 * 255 + a / 2) / a) as u8;
194 out[i + 3] = a as u8;
195 }
196 }
197 out
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn fraction_validation() {
206 assert!(validate_fraction(0.25).is_ok());
207 assert!(validate_fraction(0.0).is_err());
208 assert!(validate_fraction(1.0).is_err());
209 }
210
211 #[test]
212 fn clear_border_validation() {
213 assert!(validate_clear_border(0).is_ok());
214 assert!(validate_clear_border(5).is_ok());
215 assert!(validate_clear_border(6).is_err());
216 }
217
218 #[test]
219 fn demultiply_identity() {
220 let data = vec![255, 128, 0, 255]; let out = demultiply_alpha(&data);
222 assert_eq!(out, data);
223 }
224
225 #[test]
226 fn demultiply_transparent() {
227 let data = vec![0, 0, 0, 0];
228 let out = demultiply_alpha(&data);
229 assert_eq!(out, vec![0, 0, 0, 0]);
230 }
231}