iced_tiny_skia/
raster.rs

1use crate::core::image as raster;
2use crate::core::{Rectangle, Size};
3use crate::graphics;
4
5use rustc_hash::{FxHashMap, FxHashSet};
6use std::cell::RefCell;
7use std::collections::hash_map;
8
9#[derive(Debug)]
10pub struct Pipeline {
11    cache: RefCell<Cache>,
12}
13
14impl Pipeline {
15    pub fn new() -> Self {
16        Self {
17            cache: RefCell::new(Cache::default()),
18        }
19    }
20
21    pub fn dimensions(&self, handle: &raster::Handle) -> Size<u32> {
22        if let Some(image) = self.cache.borrow_mut().allocate(handle) {
23            Size::new(image.width(), image.height())
24        } else {
25            Size::new(0, 0)
26        }
27    }
28
29    pub fn draw(
30        &mut self,
31        handle: &raster::Handle,
32        filter_method: raster::FilterMethod,
33        bounds: Rectangle,
34        opacity: f32,
35        pixels: &mut tiny_skia::PixmapMut<'_>,
36        transform: tiny_skia::Transform,
37        clip_mask: Option<&tiny_skia::Mask>,
38        border_radius: [f32; 4],
39    ) {
40        if let Some(mut image) = self.cache.borrow_mut().allocate(handle) {
41            let width_scale = bounds.width / image.width() as f32;
42            let height_scale = bounds.height / image.height() as f32;
43
44            let transform = transform.pre_scale(width_scale, height_scale);
45
46            let quality = match filter_method {
47                raster::FilterMethod::Linear => {
48                    tiny_skia::FilterQuality::Bilinear
49                }
50                raster::FilterMethod::Nearest => {
51                    tiny_skia::FilterQuality::Nearest
52                }
53            };
54            let mut scratch;
55
56            // Round the borders if a border radius is defined
57            if border_radius.iter().any(|&corner| corner != 0.0) {
58                scratch = image.to_owned();
59                round(&mut scratch.as_mut(), {
60                    let [a, b, c, d] = border_radius;
61                    let scale_by = width_scale.min(height_scale);
62                    let max_radius = image.width().min(image.height()) / 2;
63                    [
64                        ((a / scale_by) as u32).max(1).min(max_radius),
65                        ((b / scale_by) as u32).max(1).min(max_radius),
66                        ((c / scale_by) as u32).max(1).min(max_radius),
67                        ((d / scale_by) as u32).max(1).min(max_radius),
68                    ]
69                });
70                image = scratch.as_ref();
71            }
72
73            pixels.draw_pixmap(
74                (bounds.x / width_scale) as i32,
75                (bounds.y / height_scale) as i32,
76                image,
77                &tiny_skia::PixmapPaint {
78                    quality,
79                    opacity,
80                    ..Default::default()
81                },
82                transform,
83                clip_mask,
84            );
85        }
86    }
87
88    pub fn trim_cache(&mut self) {
89        self.cache.borrow_mut().trim();
90    }
91}
92
93#[derive(Debug, Default)]
94struct Cache {
95    entries: FxHashMap<raster::Id, Option<Entry>>,
96    hits: FxHashSet<raster::Id>,
97}
98
99impl Cache {
100    pub fn allocate(
101        &mut self,
102        handle: &raster::Handle,
103    ) -> Option<tiny_skia::PixmapRef<'_>> {
104        let id = handle.id();
105
106        if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) {
107            let image = graphics::image::load(handle).ok()?;
108
109            let mut buffer =
110                vec![0u32; image.width() as usize * image.height() as usize];
111
112            for (i, pixel) in image.pixels().enumerate() {
113                let [r, g, b, a] = pixel.0;
114
115                buffer[i] = bytemuck::cast(
116                    tiny_skia::ColorU8::from_rgba(b, g, r, a).premultiply(),
117                );
118            }
119
120            let _ = entry.insert(Some(Entry {
121                width: image.width(),
122                height: image.height(),
123                pixels: buffer,
124            }));
125        }
126
127        let _ = self.hits.insert(id);
128        self.entries.get(&id).unwrap().as_ref().map(|entry| {
129            tiny_skia::PixmapRef::from_bytes(
130                bytemuck::cast_slice(&entry.pixels),
131                entry.width,
132                entry.height,
133            )
134            .expect("Build pixmap from image bytes")
135        })
136    }
137
138    fn trim(&mut self) {
139        self.entries.retain(|key, _| self.hits.contains(key));
140        self.hits.clear();
141    }
142}
143
144#[derive(Debug)]
145struct Entry {
146    width: u32,
147    height: u32,
148    pixels: Vec<u32>,
149}
150
151// https://users.rust-lang.org/t/how-to-trim-image-to-circle-image-without-jaggy/70374/2
152fn round(img: &mut tiny_skia::PixmapMut<'_>, radius: [u32; 4]) {
153    let (width, height) = (img.width(), img.height());
154    assert!(radius[0] + radius[1] <= width);
155    assert!(radius[3] + radius[2] <= width);
156    assert!(radius[0] + radius[3] <= height);
157    assert!(radius[1] + radius[2] <= height);
158
159    // top left
160    border_radius(img, radius[0], |x, y| (x - 1, y - 1));
161    // top right
162    border_radius(img, radius[1], |x, y| (width - x, y - 1));
163    // bottom right
164    border_radius(img, radius[2], |x, y| (width - x, height - y));
165    // bottom left
166    border_radius(img, radius[3], |x, y| (x - 1, height - y));
167}
168
169fn border_radius(
170    img: &mut tiny_skia::PixmapMut<'_>,
171    r: u32,
172    coordinates: impl Fn(u32, u32) -> (u32, u32),
173) {
174    if r == 0 {
175        return;
176    }
177    let r0 = r;
178
179    // 16x antialiasing: 16x16 grid creates 256 possible shades, great for u8!
180    let r = 16 * r;
181
182    let mut x = 0;
183    let mut y = r - 1;
184    let mut p: i32 = 2 - r as i32;
185
186    // ...
187
188    let mut alpha: u16 = 0;
189    let mut skip_draw = true;
190
191    fn pixel_id(width: u32, (x, y): (u32, u32)) -> usize {
192        ((width as usize * y as usize) + x as usize) * 4
193    }
194
195    let clear_pixel = |img: &mut tiny_skia::PixmapMut<'_>,
196                       (x, y): (u32, u32)| {
197        let pixel = pixel_id(img.width(), (x, y));
198        img.data_mut()[pixel..pixel + 4].copy_from_slice(&[0; 4]);
199    };
200
201    let draw = |img: &mut tiny_skia::PixmapMut<'_>, alpha, x, y| {
202        debug_assert!((1..=256).contains(&alpha));
203        let pixel = pixel_id(img.width(), coordinates(r0 - x, r0 - y));
204        let pixel_alpha = &mut img.data_mut()[pixel + 3];
205        *pixel_alpha = ((alpha * *pixel_alpha as u16 + 128) / 256) as u8;
206    };
207
208    'l: loop {
209        // (comments for bottom_right case:)
210        // remove contents below current position
211        {
212            let i = x / 16;
213            for j in y / 16 + 1..r0 {
214                clear_pixel(img, coordinates(r0 - i, r0 - j));
215            }
216        }
217        // remove contents right of current position mirrored
218        {
219            let j = x / 16;
220            for i in y / 16 + 1..r0 {
221                clear_pixel(img, coordinates(r0 - i, r0 - j));
222            }
223        }
224
225        // draw when moving to next pixel in x-direction
226        if !skip_draw {
227            draw(img, alpha, x / 16 - 1, y / 16);
228            draw(img, alpha, y / 16, x / 16 - 1);
229            alpha = 0;
230        }
231
232        for _ in 0..16 {
233            skip_draw = false;
234
235            if x >= y {
236                break 'l;
237            }
238
239            alpha += y as u16 % 16 + 1;
240            if p < 0 {
241                x += 1;
242                p += (2 * x + 2) as i32;
243            } else {
244                // draw when moving to next pixel in y-direction
245                if y % 16 == 0 {
246                    draw(img, alpha, x / 16, y / 16);
247                    draw(img, alpha, y / 16, x / 16);
248                    skip_draw = true;
249                    alpha = (x + 1) as u16 % 16 * 16;
250                }
251
252                x += 1;
253                p -= (2 * (y - x) + 2) as i32;
254                y -= 1;
255            }
256        }
257    }
258
259    // one corner pixel left
260    if x / 16 == y / 16 {
261        // column under current position possibly not yet accounted
262        if x == y {
263            alpha += y as u16 % 16 + 1;
264        }
265        let s = y as u16 % 16 + 1;
266        let alpha = 2 * alpha - s * s;
267        draw(img, alpha, x / 16, y / 16);
268    }
269
270    // remove remaining square of content in the corner
271    let range = y / 16 + 1..r0;
272    for i in range.clone() {
273        for j in range.clone() {
274            clear_pixel(img, coordinates(r0 - i, r0 - j));
275        }
276    }
277}