use std::{error::Error, fmt};
use fast_image_resize::{
FilterType, PixelType, ResizeAlg, ResizeOptions as FirResizeOptions, Resizer,
images::Image as FirImage,
};
use crate::model::{AnimationFrame, CanvasSize};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ResizeFilter {
#[default]
Bilinear,
CatmullRom,
Lanczos3,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResizeOptions {
pub maximum: CanvasSize,
pub allow_upscale: bool,
pub filter: ResizeFilter,
pub max_output_rgba_bytes: usize,
}
impl ResizeOptions {
pub const fn contain(maximum: CanvasSize) -> Self {
Self {
maximum,
allow_upscale: false,
filter: ResizeFilter::Bilinear,
max_output_rgba_bytes: usize::MAX,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResizePlan {
source: CanvasSize,
destination: CanvasSize,
filter: ResizeFilter,
}
impl ResizePlan {
pub fn new(source: CanvasSize, options: ResizeOptions) -> Result<Self, ResizeError> {
validate_size("source", source)?;
validate_size("maximum", options.maximum)?;
let destination = destination_size(source, options.maximum, options.allow_upscale);
let output_bytes = destination
.rgba_bytes()
.ok_or(ResizeError::OutputSizeOverflow)?;
if output_bytes > options.max_output_rgba_bytes {
return Err(ResizeError::OutputTooLarge {
actual: output_bytes,
maximum: options.max_output_rgba_bytes,
});
}
Ok(Self {
source,
destination,
filter: options.filter,
})
}
pub const fn source(&self) -> CanvasSize {
self.source
}
pub const fn destination(&self) -> CanvasSize {
self.destination
}
pub const fn is_noop(&self) -> bool {
self.source.width == self.destination.width && self.source.height == self.destination.height
}
pub fn transform_rgba(&self, source_rgba: &[u8]) -> Result<Vec<u8>, ResizeError> {
let mut source = source_rgba.to_vec();
let mut workspace = self.workspace()?;
workspace.transform_rgba(&mut source)?;
Ok(workspace.pixels().to_vec())
}
pub fn workspace(&self) -> Result<ResizeWorkspace, ResizeError> {
let destination_bytes = self
.destination
.rgba_bytes()
.ok_or(ResizeError::OutputSizeOverflow)?;
Ok(ResizeWorkspace {
plan: *self,
resizer: Resizer::new(),
destination: vec![0; destination_bytes],
})
}
pub fn transform_frame(&self, frame: AnimationFrame) -> Result<AnimationFrame, ResizeError> {
if frame.canvas != self.source {
return Err(ResizeError::UnexpectedFrameCanvas {
actual: frame.canvas,
expected: self.source,
});
}
Ok(AnimationFrame {
rgba: self.transform_rgba(&frame.rgba)?,
canvas: self.destination,
duration: frame.duration,
})
}
}
pub struct ResizeWorkspace {
plan: ResizePlan,
resizer: Resizer,
destination: Vec<u8>,
}
impl ResizeWorkspace {
pub fn transform_rgba(&mut self, source_rgba: &mut [u8]) -> Result<(), ResizeError> {
let expected = self
.plan
.source
.rgba_bytes()
.ok_or(ResizeError::SourceSizeOverflow)?;
if source_rgba.len() != expected {
return Err(ResizeError::InvalidSourceBufferLength {
actual: source_rgba.len(),
expected,
});
}
if self.plan.is_noop() {
self.destination.copy_from_slice(source_rgba);
return Ok(());
}
let has_transparency = source_rgba.chunks_exact(4).any(|pixel| pixel[3] != 255);
let source = FirImage::from_slice_u8(
self.plan.source.width,
self.plan.source.height,
source_rgba,
PixelType::U8x4,
)
.map_err(|error| ResizeError::ImageResize(error.to_string()))?;
let mut destination = FirImage::from_slice_u8(
self.plan.destination.width,
self.plan.destination.height,
&mut self.destination,
PixelType::U8x4,
)
.map_err(|error| ResizeError::ImageResize(error.to_string()))?;
let filter = match self.plan.filter {
ResizeFilter::Bilinear => FilterType::Bilinear,
ResizeFilter::CatmullRom => FilterType::CatmullRom,
ResizeFilter::Lanczos3 => FilterType::Lanczos3,
};
let options = FirResizeOptions::new()
.resize_alg(ResizeAlg::Convolution(filter))
.use_alpha(has_transparency);
self.resizer
.resize(&source, &mut destination, &options)
.map_err(|error| ResizeError::ImageResize(error.to_string()))?;
Ok(())
}
pub fn pixels(&self) -> &[u8] {
&self.destination
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ResizeError {
InvalidCanvasSize {
which: &'static str,
size: CanvasSize,
},
SourceSizeOverflow,
OutputSizeOverflow,
OutputTooLarge {
actual: usize,
maximum: usize,
},
InvalidSourceBufferLength {
actual: usize,
expected: usize,
},
UnexpectedFrameCanvas {
actual: CanvasSize,
expected: CanvasSize,
},
ImageResize(String),
}
impl fmt::Display for ResizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidCanvasSize { which, size } => write!(
f,
"{which} canvas has invalid dimensions {}x{}",
size.width, size.height
),
Self::SourceSizeOverflow => {
f.write_str("source RGBA size overflows the host address space")
}
Self::OutputSizeOverflow => {
f.write_str("output RGBA size overflows the host address space")
}
Self::OutputTooLarge { actual, maximum } => write!(
f,
"output is {actual} bytes, exceeding the {maximum}-byte limit"
),
Self::InvalidSourceBufferLength { actual, expected } => {
write!(f, "source buffer is {actual} bytes; expected {expected}")
}
Self::UnexpectedFrameCanvas { actual, expected } => write!(
f,
"frame canvas {}x{} does not match plan source {}x{}",
actual.width, actual.height, expected.width, expected.height
),
Self::ImageResize(message) => write!(f, "RGBA resize failed: {message}"),
}
}
}
impl Error for ResizeError {}
fn validate_size(which: &'static str, size: CanvasSize) -> Result<(), ResizeError> {
if size.width == 0 || size.height == 0 {
return Err(ResizeError::InvalidCanvasSize { which, size });
}
Ok(())
}
fn destination_size(source: CanvasSize, maximum: CanvasSize, allow_upscale: bool) -> CanvasSize {
let width_limited = u64::from(maximum.width) * u64::from(source.height)
<= u64::from(maximum.height) * u64::from(source.width);
let (numerator, denominator) = if width_limited {
(maximum.width, source.width)
} else {
(maximum.height, source.height)
};
if !allow_upscale && numerator >= denominator {
return source;
}
CanvasSize {
width: u32::try_from(
(u64::from(source.width) * u64::from(numerator) / u64::from(denominator)).max(1),
)
.expect("scaled width fits u32"),
height: u32::try_from(
(u64::from(source.height) * u64::from(numerator) / u64::from(denominator)).max(1),
)
.expect("scaled height fits u32"),
}
}