Skip to main content

oxframe_2d/
image.rs

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