feat: Lancsoz downsampler when target size is relatively too small
This commit is contained in:
parent
cc5ca1ce29
commit
511180679b
4 changed files with 233 additions and 2 deletions
|
|
@ -1,6 +1,8 @@
|
|||
//! Load and operate on images.
|
||||
#[cfg(feature = "image")]
|
||||
use crate::core::Bytes;
|
||||
#[cfg(feature = "image")]
|
||||
use crate::core::Size;
|
||||
|
||||
use crate::core::Color;
|
||||
use crate::core::Radians;
|
||||
|
|
@ -180,3 +182,105 @@ fn to_error(error: ::image::ImageError) -> image::Error {
|
|||
error => image::Error::Invalid(Arc::new(error)),
|
||||
}
|
||||
}
|
||||
|
||||
/// resample a raster image to this target
|
||||
#[cfg(feature = "image")]
|
||||
pub fn downsample_target(
|
||||
native: Size<u32>,
|
||||
bounds: Size<f32>,
|
||||
) -> Option<Size<u32>> {
|
||||
if !(bounds.width >= 1.0 && bounds.height >= 1.0) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Round up to 4px so an animated resize does not make a copy per pixel
|
||||
// TODO: maybe find a better approach
|
||||
let quantize = |length: f32| (length.ceil() as u32).div_ceil(4) * 4;
|
||||
|
||||
let target = Size::new(
|
||||
quantize(bounds.width).min(native.width),
|
||||
quantize(bounds.height).min(native.height),
|
||||
);
|
||||
|
||||
let minified = native.width as f32 >= target.width as f32 * 1.25
|
||||
|| native.height as f32 >= target.height as f32 * 1.25;
|
||||
|
||||
// If the target is big then Lanczos will be too expensive, and sampler is good enough
|
||||
let small = u64::from(target.width) * u64::from(target.height) <= 1 << 18;
|
||||
|
||||
(minified && small).then_some(target)
|
||||
}
|
||||
|
||||
/// Resamples premultiplied pixels down to `target`.
|
||||
#[cfg(feature = "image")]
|
||||
pub fn downsample_premultiplied(
|
||||
pixels: &[u8],
|
||||
size: Size<u32>,
|
||||
target: Size<u32>,
|
||||
) -> Vec<u8> {
|
||||
let image =
|
||||
::image::RgbaImage::from_raw(size.width, size.height, pixels.to_vec())
|
||||
.expect("pixels hold width * height RGBA pixels");
|
||||
|
||||
resize(&image, target).into_raw()
|
||||
}
|
||||
|
||||
#[cfg(feature = "image")]
|
||||
fn resize(image: &::image::RgbaImage, target: Size<u32>) -> ::image::RgbaImage {
|
||||
use ::image::imageops::{self, FilterType};
|
||||
use std::borrow::Cow;
|
||||
|
||||
// if image is too big compared to target, box average it to double the target
|
||||
// and then do Lanczos. The final result is almost the same, but lanczos is too
|
||||
// expensive on large images.
|
||||
let image = if image.width() >= target.width * 4
|
||||
&& image.height() >= target.height * 4
|
||||
{
|
||||
Cow::Owned(imageops::thumbnail(
|
||||
image,
|
||||
target.width * 2,
|
||||
target.height * 2,
|
||||
))
|
||||
} else {
|
||||
Cow::Borrowed(image)
|
||||
};
|
||||
|
||||
imageops::resize(&*image, target.width, target.height, FilterType::Lanczos3)
|
||||
}
|
||||
|
||||
/// Resamples RGBA pixels down to `target`.
|
||||
///
|
||||
/// Premultiplies the image, to avoid fringing the edges of an icon when interpolating
|
||||
/// transparent pixels.
|
||||
#[cfg(feature = "image")]
|
||||
pub fn downsample(image: &Buffer, target: Size<u32>) -> ::image::RgbaImage {
|
||||
let mut image = ::image::RgbaImage::from_raw(
|
||||
image.width(),
|
||||
image.height(),
|
||||
image.as_raw().to_vec(),
|
||||
)
|
||||
.expect("buffer holds width * height RGBA pixels");
|
||||
|
||||
for pixel in image.pixels_mut() {
|
||||
let alpha = u32::from(pixel[3]);
|
||||
|
||||
for channel in &mut pixel.0[..3] {
|
||||
*channel = ((u32::from(*channel) * alpha + 127) / 255) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
let mut image = resize(&image, target);
|
||||
|
||||
for pixel in image.pixels_mut() {
|
||||
let alpha = u32::from(pixel[3]);
|
||||
|
||||
if alpha > 0 {
|
||||
for channel in &mut pixel.0[..3] {
|
||||
*channel = ((u32::from(*channel) * 255 + alpha / 2) / alpha)
|
||||
.min(255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
image
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,21 @@ impl Pipeline {
|
|||
) {
|
||||
let mut cache = self.cache.borrow_mut();
|
||||
|
||||
let Ok(mut image) = cache.allocate(handle) else {
|
||||
let target = {
|
||||
let Ok(image) = cache.allocate(handle) else {
|
||||
return;
|
||||
};
|
||||
|
||||
graphics::image::downsample_target(
|
||||
Size::new(image.width(), image.height()),
|
||||
bounds.size(),
|
||||
)
|
||||
};
|
||||
|
||||
let Ok(mut image) = (match target {
|
||||
Some(target) => cache.allocate_resampled(handle, target),
|
||||
None => cache.allocate(handle),
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
|
@ -110,6 +124,8 @@ impl Pipeline {
|
|||
struct Cache {
|
||||
entries: FxHashMap<raster::Id, Option<Entry>>,
|
||||
hits: FxHashSet<raster::Id>,
|
||||
resampled: FxHashMap<(raster::Id, u32, u32), Entry>,
|
||||
resampled_hits: FxHashSet<(raster::Id, u32, u32)>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
|
|
@ -166,9 +182,59 @@ impl Cache {
|
|||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Like [`Self::allocate`], resampled to `target`. Call after
|
||||
/// [`Self::allocate`] has decoded the image.
|
||||
pub fn allocate_resampled(
|
||||
&mut self,
|
||||
handle: &raster::Handle,
|
||||
target: Size<u32>,
|
||||
) -> Result<tiny_skia::PixmapRef<'_>, raster::Error> {
|
||||
let key = (handle.id(), target.width, target.height);
|
||||
|
||||
if !self.resampled.contains_key(&key) {
|
||||
let native = self
|
||||
.entries
|
||||
.get(&handle.id())
|
||||
.and_then(Option::as_ref)
|
||||
.ok_or(raster::Error::Empty)?;
|
||||
|
||||
// Stored pixels are already premultiplied, so resample them as is.
|
||||
let pixels = graphics::image::downsample_premultiplied(
|
||||
bytemuck::cast_slice(&native.pixels),
|
||||
Size::new(native.width, native.height),
|
||||
target,
|
||||
);
|
||||
|
||||
let _ = self.resampled.insert(
|
||||
key,
|
||||
Entry {
|
||||
width: target.width,
|
||||
height: target.height,
|
||||
pixels: pixels
|
||||
.chunks_exact(4)
|
||||
.map(|p| u32::from_ne_bytes([p[0], p[1], p[2], p[3]]))
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let _ = self.resampled_hits.insert(key);
|
||||
let entry = &self.resampled[&key];
|
||||
|
||||
Ok(tiny_skia::PixmapRef::from_bytes(
|
||||
bytemuck::cast_slice(&entry.pixels),
|
||||
entry.width,
|
||||
entry.height,
|
||||
)
|
||||
.expect("Build pixmap from image bytes"))
|
||||
}
|
||||
|
||||
fn trim(&mut self) {
|
||||
self.entries.retain(|key, _| self.hits.contains(key));
|
||||
self.resampled
|
||||
.retain(|key, _| self.resampled_hits.contains(key));
|
||||
self.hits.clear();
|
||||
self.resampled_hits.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use crate::image::atlas::{self, Atlas};
|
|||
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
|
||||
use worker::Worker;
|
||||
|
||||
#[cfg(feature = "image")]
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
#[cfg(feature = "image")]
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -38,6 +40,9 @@ impl Cache {
|
|||
raster: Raster {
|
||||
cache: crate::image::raster::Cache::default(),
|
||||
pending: HashMap::new(),
|
||||
resampled: FxHashMap::default(),
|
||||
resampled_hits: FxHashSet::default(),
|
||||
should_trim: false,
|
||||
belt: wgpu::util::StagingBelt::new(
|
||||
device.clone(),
|
||||
2 * 1024 * 1024,
|
||||
|
|
@ -209,7 +214,9 @@ impl Cache {
|
|||
encoder: &mut wgpu::CommandEncoder,
|
||||
belt: &mut wgpu::util::StagingBelt,
|
||||
handle: &core::image::Handle,
|
||||
bounds: Size<f32>,
|
||||
) -> Option<(&atlas::Entry, &Arc<wgpu::BindGroup>)> {
|
||||
use crate::graphics::image::{downsample, downsample_target, load};
|
||||
use crate::image::raster::Memory;
|
||||
|
||||
self.receive();
|
||||
|
|
@ -223,6 +230,34 @@ impl Cache {
|
|||
None,
|
||||
)?;
|
||||
|
||||
if let Some(target) = downsample_target(memory.dimensions(), bounds) {
|
||||
let key = (handle.id(), target.width, target.height);
|
||||
|
||||
if !self.raster.resampled.contains_key(&key) {
|
||||
let image = memory.host().or_else(|| load(handle).ok())?;
|
||||
let image = downsample(&image, target);
|
||||
|
||||
let entry = self.atlas.upload(
|
||||
device,
|
||||
encoder,
|
||||
belt,
|
||||
target.width,
|
||||
target.height,
|
||||
&image,
|
||||
)?;
|
||||
|
||||
let _ = self.raster.resampled.insert(key, entry);
|
||||
self.raster.should_trim = true;
|
||||
}
|
||||
|
||||
let _ = self.raster.resampled_hits.insert(key);
|
||||
|
||||
return Some((
|
||||
self.raster.resampled.get(&key)?,
|
||||
self.atlas.bind_group(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Memory::Device {
|
||||
entry, bind_group, ..
|
||||
} = memory
|
||||
|
|
@ -303,6 +338,23 @@ impl Cache {
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.worker.drop(_bind_group);
|
||||
});
|
||||
|
||||
if self.raster.should_trim {
|
||||
let hits = &self.raster.resampled_hits;
|
||||
let atlas = &mut self.atlas;
|
||||
|
||||
self.raster.resampled.retain(|key, entry| {
|
||||
let retain = hits.contains(key);
|
||||
|
||||
if !retain {
|
||||
atlas.remove(entry);
|
||||
}
|
||||
|
||||
retain
|
||||
});
|
||||
self.raster.resampled_hits.clear();
|
||||
self.raster.should_trim = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
|
|
@ -376,6 +428,9 @@ impl Drop for Cache {
|
|||
struct Raster {
|
||||
cache: crate::image::raster::Cache,
|
||||
pending: HashMap<core::image::Id, Vec<Callback>>,
|
||||
resampled: FxHashMap<(core::image::Id, u32, u32), atlas::Entry>,
|
||||
resampled_hits: FxHashSet<(core::image::Id, u32, u32)>,
|
||||
should_trim: bool,
|
||||
belt: wgpu::util::StagingBelt,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -267,7 +267,13 @@ impl State {
|
|||
clip_bounds,
|
||||
} => {
|
||||
if let Some((atlas_entry, bind_group)) = cache
|
||||
.upload_raster(device, encoder, belt, &image.handle)
|
||||
.upload_raster(
|
||||
device,
|
||||
encoder,
|
||||
belt,
|
||||
&image.handle,
|
||||
bounds.size() * scale,
|
||||
)
|
||||
{
|
||||
match atlas.as_mut() {
|
||||
None => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue