iced-yoda/tiny_skia/src/raster.rs

308 lines
8.9 KiB
Rust

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<Cache>,
}
impl Pipeline {
pub fn new() -> Self {
Self {
cache: RefCell::new(Cache::default()),
}
}
pub fn load(
&self,
handle: &raster::Handle,
) -> Result<raster::Allocation, raster::Error> {
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<Size<u32>> {
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<raster::Id, Option<Entry>>,
hits: FxHashSet<raster::Id>,
}
impl Cache {
pub fn allocate(
&mut self,
handle: &raster::Handle,
) -> Result<tiny_skia::PixmapRef<'_>, 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);
let Some(ret) = 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")
}) else {
return Err(raster::Error::Empty);
};
Ok(ret)
}
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<u32>,
}
// 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));
}
}
}