1use std::cell::RefCell;
2use std::rc::Rc;
3
4use kurbo::{Affine, BezPath};
5use oxframe_core::Color;
6
7#[derive(Debug)]
9pub struct RasterImage {
10 pub width: u32,
12 pub height: u32,
14 pub rgba: Vec<u8>,
16 pub id: u64,
18}
19
20impl RasterImage {
21 pub fn new(width: u32, height: u32, rgba: Vec<u8>) -> Self {
24 let mut id: u64 = 0xcbf2_9ce4_8422_2325;
25 for &b in &rgba {
26 id = (id ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
27 }
28 Self {
29 width,
30 height,
31 rgba,
32 id,
33 }
34 }
35}
36
37#[derive(Debug)]
40pub struct VectorImage {
41 pub width: f64,
43 pub height: f64,
45 pub shapes: Vec<VectorShape>,
47}
48
49#[derive(Debug, Clone)]
51pub struct VectorShape {
52 pub path: BezPath,
54 pub fill: Option<Color>,
56 pub stroke: Option<(Color, f64)>,
58}
59
60#[derive(Debug, Clone)]
62pub enum ImageSource {
63 Raster(Rc<RasterImage>),
65 Vector(Rc<VectorImage>),
67}
68
69impl ImageSource {
70 pub fn natural_size(&self) -> (f64, f64) {
72 match self {
73 Self::Raster(r) => (f64::from(r.width), f64::from(r.height)),
74 Self::Vector(v) => (v.width, v.height),
75 }
76 }
77
78 pub fn decode(bytes: &[u8]) -> Result<Self, String> {
85 let key = fnv(bytes);
86 if let Some(hit) = CACHE.with(|c| {
87 c.borrow()
88 .iter()
89 .find(|(k, _)| *k == key)
90 .map(|e| e.1.clone())
91 }) {
92 return Ok(hit);
93 }
94 let src = if looks_like_svg(bytes) {
95 Self::Vector(Rc::new(decode_svg(bytes)?))
96 } else {
97 let img = image::load_from_memory(bytes)
98 .map_err(|e| e.to_string())?
99 .to_rgba8();
100 Self::Raster(Rc::new(RasterImage {
101 width: img.width(),
102 height: img.height(),
103 rgba: img.into_raw(),
104 id: key,
105 }))
106 };
107 CACHE.with(|c| {
108 let mut c = c.borrow_mut();
109 if c.len() >= 64 {
110 c.remove(0);
111 }
112 c.push((key, src.clone()));
113 });
114 Ok(src)
115 }
116}
117
118thread_local! {
119 static CACHE: RefCell<Vec<(u64, ImageSource)>> = const { RefCell::new(Vec::new()) };
120}
121
122fn fnv(bytes: &[u8]) -> u64 {
123 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
124 for b in bytes {
125 h = (h ^ u64::from(*b)).wrapping_mul(0x0100_0000_01b3);
126 }
127 h ^ bytes.len() as u64
128}
129
130fn looks_like_svg(bytes: &[u8]) -> bool {
131 let head = &bytes[..bytes.len().min(512)];
132 let text = String::from_utf8_lossy(head);
133 let t = text.trim_start_matches('\u{feff}').trim_start();
134 t.starts_with('<') && (t.contains("<svg") || t.starts_with("<?xml"))
135}
136
137fn decode_svg(bytes: &[u8]) -> Result<VectorImage, String> {
138 let tree =
139 usvg::Tree::from_data(bytes, &usvg::Options::default()).map_err(|e| e.to_string())?;
140 let size = tree.size();
141 let mut shapes = Vec::new();
142 collect(tree.root(), 1.0, &mut shapes);
143 Ok(VectorImage {
144 width: f64::from(size.width()),
145 height: f64::from(size.height()),
146 shapes,
147 })
148}
149
150fn collect(group: &usvg::Group, opacity: f32, out: &mut Vec<VectorShape>) {
151 let opacity = opacity * group.opacity().get();
152 for node in group.children() {
153 match node {
154 usvg::Node::Group(g) => collect(g, opacity, out),
155 usvg::Node::Path(p) if p.is_visible() => {
156 let t = p.abs_transform();
157 let tf = Affine::new([t.sx, t.ky, t.kx, t.sy, t.tx, t.ty].map(f64::from));
158 let path = tf * to_bez(p.data());
159 let paint = |paint: &usvg::Paint, a: f32| {
160 let c = match paint {
161 usvg::Paint::Color(c) => *c,
162 usvg::Paint::LinearGradient(g) => g.stops().first().map(|s| s.color())?,
163 usvg::Paint::RadialGradient(g) => g.stops().first().map(|s| s.color())?,
164 usvg::Paint::Pattern(_) => return None,
165 };
166 Some(Color::from_rgba8(
167 c.red,
168 c.green,
169 c.blue,
170 (a * opacity * 255.0).round() as u8,
171 ))
172 };
173 let fill = p.fill().and_then(|f| paint(f.paint(), f.opacity().get()));
174 let scale = (tf.determinant().abs()).sqrt();
175 let stroke = p.stroke().and_then(|s| {
176 paint(s.paint(), s.opacity().get())
177 .map(|c| (c, f64::from(s.width().get()) * scale))
178 });
179 out.push(VectorShape { path, fill, stroke });
180 }
181 _ => {}
182 }
183 }
184}
185
186pub(crate) fn to_bez(p: &usvg::tiny_skia_path::Path) -> BezPath {
187 use usvg::tiny_skia_path::PathSegment;
188 let pt = |p: usvg::tiny_skia_path::Point| (f64::from(p.x), f64::from(p.y));
189 let mut out = BezPath::new();
190 for seg in p.segments() {
191 match seg {
192 PathSegment::MoveTo(a) => out.move_to(pt(a)),
193 PathSegment::LineTo(a) => out.line_to(pt(a)),
194 PathSegment::QuadTo(a, b) => out.quad_to(pt(a), pt(b)),
195 PathSegment::CubicTo(a, b, c) => out.curve_to(pt(a), pt(b), pt(c)),
196 PathSegment::Close => out.close_path(),
197 }
198 }
199 out
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn decodes_svg_to_paths() {
208 let svg = br##"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10">
209 <rect x="0" y="0" width="10" height="10" fill="#ff0000"/>
210 <circle cx="15" cy="5" r="5" fill="none" stroke="#00ff00" stroke-width="2"/>
211 </svg>"##;
212 let ImageSource::Vector(v) = ImageSource::decode(svg).unwrap() else {
213 panic!("expected vector")
214 };
215 assert_eq!((v.width, v.height), (20.0, 10.0));
216 assert_eq!(v.shapes.len(), 2);
217 assert_eq!(v.shapes[0].fill.unwrap().to_rgba8(), [255, 0, 0, 255]);
218 assert!(v.shapes[1].fill.is_none());
219 assert_eq!(v.shapes[1].stroke.unwrap().1, 2.0);
220 }
221
222 #[test]
223 fn decodes_png() {
224 let mut png = Vec::new();
225 let img = image::RgbaImage::from_pixel(3, 2, image::Rgba([1, 2, 3, 255]));
226 img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
227 .unwrap();
228 let ImageSource::Raster(r) = ImageSource::decode(&png).unwrap() else {
229 panic!("expected raster")
230 };
231 assert_eq!((r.width, r.height), (3, 2));
232 assert_eq!(&r.rgba[..4], &[1, 2, 3, 255]);
233 }
234}