use crate::core::image as raster; use crate::core::{Rectangle, Size}; use crate::graphics; use rustc_hash::{FxHashMap, FxHashSet}; use std::cell::RefCell; use std::collections::hash_map; #[derive(Debug)] pub struct Pipeline { cache: RefCell, } impl Pipeline { pub fn new() -> Self { Self { cache: RefCell::new(Cache::default()), } } pub fn load( &self, handle: &raster::Handle, ) -> Result { let mut cache = self.cache.borrow_mut(); let image = cache.allocate(handle)?; #[allow(unsafe_code)] Ok(unsafe { raster::allocate(handle, Size::new(image.width(), image.height())) }) } pub fn dimensions(&self, handle: &raster::Handle) -> Option> { let mut cache = self.cache.borrow_mut(); let image = cache.allocate(handle).ok()?; Some(Size::new(image.width(), image.height())) } pub fn draw( &mut self, handle: &raster::Handle, filter_method: raster::FilterMethod, bounds: Rectangle, opacity: f32, pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, clip_mask: Option<&tiny_skia::Mask>, border_radius: [f32; 4], ) { let mut cache = self.cache.borrow_mut(); let Ok(mut image) = cache.allocate(handle) else { return; }; let width_scale = bounds.width / image.width() as f32; let height_scale = bounds.height / image.height() as f32; let quality = match filter_method { raster::FilterMethod::Linear => tiny_skia::FilterQuality::Bilinear, raster::FilterMethod::Nearest => tiny_skia::FilterQuality::Nearest, }; let mut scratch; // Round the borders if a border radius is defined if border_radius.iter().any(|&corner| corner != 0.0) { scratch = image.to_owned(); round(&mut scratch.as_mut(), { let [a, b, c, d] = border_radius; let scale_by = width_scale.min(height_scale); let max_radius = image.width().min(image.height()) / 2; [ ((a / scale_by) as u32).max(1).min(max_radius), ((b / scale_by) as u32).max(1).min(max_radius), ((c / scale_by) as u32).max(1).min(max_radius), ((d / scale_by) as u32).max(1).min(max_radius), ] }); image = scratch.as_ref(); } let transform = transform.pre_scale(width_scale, height_scale); let quality = match filter_method { raster::FilterMethod::Linear => tiny_skia::FilterQuality::Bilinear, raster::FilterMethod::Nearest => tiny_skia::FilterQuality::Nearest, }; pixels.draw_pixmap( (bounds.x / width_scale) as i32, (bounds.y / height_scale) as i32, image, &tiny_skia::PixmapPaint { quality, opacity, ..Default::default() }, transform, clip_mask, ); } pub fn trim_cache(&mut self) { self.cache.borrow_mut().trim(); } } #[derive(Debug, Default)] struct Cache { entries: FxHashMap>, hits: FxHashSet, } impl Cache { pub fn allocate( &mut self, handle: &raster::Handle, ) -> Result, raster::Error> { let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) { let image = match graphics::image::load(handle) { Ok(image) => image, Err(error) => { let _ = entry.insert(None); return Err(error); } }; if image.width() == 0 || image.height() == 0 { return Err(raster::Error::Empty); } let mut buffer = vec![0u32; image.width() as usize * image.height() as usize]; for (i, pixel) in image.pixels().enumerate() { let [r, g, b, a] = pixel.0; buffer[i] = bytemuck::cast( tiny_skia::ColorU8::from_rgba(b, g, r, a).premultiply(), ); } let _ = entry.insert(Some(Entry { width: image.width(), height: image.height(), pixels: buffer, })); } let _ = self.hits.insert(id); Ok(self .entries .get(&id) .unwrap() .as_ref() .map(|entry| { tiny_skia::PixmapRef::from_bytes( bytemuck::cast_slice(&entry.pixels), entry.width, entry.height, ) .expect("Build pixmap from image bytes") }) .expect("Image should be allocated")) } fn trim(&mut self) { self.entries.retain(|key, _| self.hits.contains(key)); self.hits.clear(); } } #[derive(Debug)] struct Entry { width: u32, height: u32, pixels: Vec, } // https://users.rust-lang.org/t/how-to-trim-image-to-circle-image-without-jaggy/70374/2 fn round(img: &mut tiny_skia::PixmapMut<'_>, radius: [u32; 4]) { let (width, height) = (img.width(), img.height()); assert!(radius[0] + radius[1] <= width); assert!(radius[3] + radius[2] <= width); assert!(radius[0] + radius[3] <= height); assert!(radius[1] + radius[2] <= height); // top left border_radius(img, radius[0], |x, y| (x - 1, y - 1)); // top right border_radius(img, radius[1], |x, y| (width - x, y - 1)); // bottom right border_radius(img, radius[2], |x, y| (width - x, height - y)); // bottom left border_radius(img, radius[3], |x, y| (x - 1, height - y)); } fn border_radius( img: &mut tiny_skia::PixmapMut<'_>, r: u32, coordinates: impl Fn(u32, u32) -> (u32, u32), ) { if r == 0 { return; } let r0 = r; // 16x antialiasing: 16x16 grid creates 256 possible shades, great for u8! let r = 16 * r; let mut x = 0; let mut y = r - 1; let mut p: i32 = 2 - r as i32; // ... let mut alpha: u16 = 0; let mut skip_draw = true; fn pixel_id(width: u32, (x, y): (u32, u32)) -> usize { ((width as usize * y as usize) + x as usize) * 4 } let clear_pixel = |img: &mut tiny_skia::PixmapMut<'_>, (x, y): (u32, u32)| { let pixel = pixel_id(img.width(), (x, y)); img.data_mut()[pixel..pixel + 4].copy_from_slice(&[0; 4]); }; let draw = |img: &mut tiny_skia::PixmapMut<'_>, alpha, x, y| { debug_assert!((1..=256).contains(&alpha)); let pixel = pixel_id(img.width(), coordinates(r0 - x, r0 - y)); let pixel_alpha = &mut img.data_mut()[pixel + 3]; *pixel_alpha = ((alpha * *pixel_alpha as u16 + 128) / 256) as u8; }; 'l: loop { // (comments for bottom_right case:) // remove contents below current position { let i = x / 16; for j in y / 16 + 1..r0 { clear_pixel(img, coordinates(r0 - i, r0 - j)); } } // remove contents right of current position mirrored { let j = x / 16; for i in y / 16 + 1..r0 { clear_pixel(img, coordinates(r0 - i, r0 - j)); } } // draw when moving to next pixel in x-direction if !skip_draw { draw(img, alpha, x / 16 - 1, y / 16); draw(img, alpha, y / 16, x / 16 - 1); alpha = 0; } for _ in 0..16 { skip_draw = false; if x >= y { break 'l; } alpha += y as u16 % 16 + 1; if p < 0 { x += 1; p += (2 * x + 2) as i32; } else { // draw when moving to next pixel in y-direction if y % 16 == 0 { draw(img, alpha, x / 16, y / 16); draw(img, alpha, y / 16, x / 16); skip_draw = true; alpha = (x + 1) as u16 % 16 * 16; } x += 1; p -= (2 * (y - x) + 2) as i32; y -= 1; } } } // one corner pixel left if x / 16 == y / 16 { // column under current position possibly not yet accounted if x == y { alpha += y as u16 % 16 + 1; } let s = y as u16 % 16 + 1; let alpha = 2 * alpha - s * s; draw(img, alpha, x / 16, y / 16); } // remove remaining square of content in the corner let range = y / 16 + 1..r0; for i in range.clone() { for j in range.clone() { clear_pixel(img, coordinates(r0 - i, r0 - j)); } } }