1use std::{error::Error, fmt};
2
3use fast_image_resize::{
4 FilterType, PixelType, ResizeAlg, ResizeOptions as FirResizeOptions, Resizer,
5 images::Image as FirImage,
6};
7
8use crate::model::{AnimationFrame, CanvasSize};
9
10#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
12pub enum ResizeFilter {
13 #[default]
15 Bilinear,
16 CatmullRom,
18 Lanczos3,
21}
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub struct ResizeOptions {
29 pub maximum: CanvasSize,
31 pub allow_upscale: bool,
33 pub filter: ResizeFilter,
35 pub max_output_rgba_bytes: usize,
37}
38
39impl ResizeOptions {
40 pub const fn contain(maximum: CanvasSize) -> Self {
42 Self {
43 maximum,
44 allow_upscale: false,
45 filter: ResizeFilter::Bilinear,
46 max_output_rgba_bytes: usize::MAX,
47 }
48 }
49}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub struct ResizePlan {
57 source: CanvasSize,
58 destination: CanvasSize,
59 filter: ResizeFilter,
60}
61
62impl ResizePlan {
63 pub fn new(source: CanvasSize, options: ResizeOptions) -> Result<Self, ResizeError> {
68 validate_size("source", source)?;
69 validate_size("maximum", options.maximum)?;
70
71 let destination = destination_size(source, options.maximum, options.allow_upscale);
72 let output_bytes = destination
73 .rgba_bytes()
74 .ok_or(ResizeError::OutputSizeOverflow)?;
75 if output_bytes > options.max_output_rgba_bytes {
76 return Err(ResizeError::OutputTooLarge {
77 actual: output_bytes,
78 maximum: options.max_output_rgba_bytes,
79 });
80 }
81
82 Ok(Self {
83 source,
84 destination,
85 filter: options.filter,
86 })
87 }
88
89 pub const fn source(&self) -> CanvasSize {
91 self.source
92 }
93
94 pub const fn destination(&self) -> CanvasSize {
96 self.destination
97 }
98
99 pub const fn is_noop(&self) -> bool {
101 self.source.width == self.destination.width && self.source.height == self.destination.height
102 }
103
104 pub fn transform_rgba(&self, source_rgba: &[u8]) -> Result<Vec<u8>, ResizeError> {
110 let mut source = source_rgba.to_vec();
111 let mut workspace = self.workspace()?;
112 workspace.transform_rgba(&mut source)?;
113 Ok(workspace.pixels().to_vec())
114 }
115
116 pub fn workspace(&self) -> Result<ResizeWorkspace, ResizeError> {
121 let destination_bytes = self
122 .destination
123 .rgba_bytes()
124 .ok_or(ResizeError::OutputSizeOverflow)?;
125 Ok(ResizeWorkspace {
126 plan: *self,
127 resizer: Resizer::new(),
128 destination: vec![0; destination_bytes],
129 })
130 }
131
132 pub fn transform_frame(&self, frame: AnimationFrame) -> Result<AnimationFrame, ResizeError> {
138 if frame.canvas != self.source {
139 return Err(ResizeError::UnexpectedFrameCanvas {
140 actual: frame.canvas,
141 expected: self.source,
142 });
143 }
144 Ok(AnimationFrame {
145 rgba: self.transform_rgba(&frame.rgba)?,
146 canvas: self.destination,
147 duration: frame.duration,
148 })
149 }
150}
151
152pub struct ResizeWorkspace {
157 plan: ResizePlan,
158 resizer: Resizer,
159 destination: Vec<u8>,
160}
161
162impl ResizeWorkspace {
163 pub fn transform_rgba(&mut self, source_rgba: &mut [u8]) -> Result<(), ResizeError> {
172 let expected = self
173 .plan
174 .source
175 .rgba_bytes()
176 .ok_or(ResizeError::SourceSizeOverflow)?;
177 if source_rgba.len() != expected {
178 return Err(ResizeError::InvalidSourceBufferLength {
179 actual: source_rgba.len(),
180 expected,
181 });
182 }
183 if self.plan.is_noop() {
184 self.destination.copy_from_slice(source_rgba);
185 return Ok(());
186 }
187 let has_transparency = source_rgba.chunks_exact(4).any(|pixel| pixel[3] != 255);
188 let source = FirImage::from_slice_u8(
189 self.plan.source.width,
190 self.plan.source.height,
191 source_rgba,
192 PixelType::U8x4,
193 )
194 .map_err(|error| ResizeError::ImageResize(error.to_string()))?;
195 let mut destination = FirImage::from_slice_u8(
196 self.plan.destination.width,
197 self.plan.destination.height,
198 &mut self.destination,
199 PixelType::U8x4,
200 )
201 .map_err(|error| ResizeError::ImageResize(error.to_string()))?;
202 let filter = match self.plan.filter {
203 ResizeFilter::Bilinear => FilterType::Bilinear,
204 ResizeFilter::CatmullRom => FilterType::CatmullRom,
205 ResizeFilter::Lanczos3 => FilterType::Lanczos3,
206 };
207 let options = FirResizeOptions::new()
208 .resize_alg(ResizeAlg::Convolution(filter))
209 .use_alpha(has_transparency);
210 self.resizer
211 .resize(&source, &mut destination, &options)
212 .map_err(|error| ResizeError::ImageResize(error.to_string()))?;
213 Ok(())
214 }
215
216 pub fn pixels(&self) -> &[u8] {
220 &self.destination
221 }
222}
223
224#[derive(Clone, Debug, Eq, PartialEq)]
226pub enum ResizeError {
227 InvalidCanvasSize {
229 which: &'static str,
231 size: CanvasSize,
233 },
234 SourceSizeOverflow,
236 OutputSizeOverflow,
238 OutputTooLarge {
240 actual: usize,
242 maximum: usize,
244 },
245 InvalidSourceBufferLength {
247 actual: usize,
249 expected: usize,
251 },
252 UnexpectedFrameCanvas {
254 actual: CanvasSize,
256 expected: CanvasSize,
258 },
259 ImageResize(String),
261}
262
263impl fmt::Display for ResizeError {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 match self {
266 Self::InvalidCanvasSize { which, size } => write!(
267 f,
268 "{which} canvas has invalid dimensions {}x{}",
269 size.width, size.height
270 ),
271 Self::SourceSizeOverflow => {
272 f.write_str("source RGBA size overflows the host address space")
273 }
274 Self::OutputSizeOverflow => {
275 f.write_str("output RGBA size overflows the host address space")
276 }
277 Self::OutputTooLarge { actual, maximum } => write!(
278 f,
279 "output is {actual} bytes, exceeding the {maximum}-byte limit"
280 ),
281 Self::InvalidSourceBufferLength { actual, expected } => {
282 write!(f, "source buffer is {actual} bytes; expected {expected}")
283 }
284 Self::UnexpectedFrameCanvas { actual, expected } => write!(
285 f,
286 "frame canvas {}x{} does not match plan source {}x{}",
287 actual.width, actual.height, expected.width, expected.height
288 ),
289 Self::ImageResize(message) => write!(f, "RGBA resize failed: {message}"),
290 }
291 }
292}
293
294impl Error for ResizeError {}
295
296fn validate_size(which: &'static str, size: CanvasSize) -> Result<(), ResizeError> {
297 if size.width == 0 || size.height == 0 {
298 return Err(ResizeError::InvalidCanvasSize { which, size });
299 }
300 Ok(())
301}
302
303fn destination_size(source: CanvasSize, maximum: CanvasSize, allow_upscale: bool) -> CanvasSize {
304 let width_limited = u64::from(maximum.width) * u64::from(source.height)
305 <= u64::from(maximum.height) * u64::from(source.width);
306 let (numerator, denominator) = if width_limited {
307 (maximum.width, source.width)
308 } else {
309 (maximum.height, source.height)
310 };
311 if !allow_upscale && numerator >= denominator {
312 return source;
313 }
314 CanvasSize {
315 width: u32::try_from(
316 (u64::from(source.width) * u64::from(numerator) / u64::from(denominator)).max(1),
317 )
318 .expect("scaled width fits u32"),
319 height: u32::try_from(
320 (u64::from(source.height) * u64::from(numerator) / u64::from(denominator)).max(1),
321 )
322 .expect("scaled height fits u32"),
323 }
324}