1#![allow(clippy::inline_always)]
3#![allow(clippy::cast_sign_loss)]
4#![allow(clippy::missing_errors_doc)]
5#![allow(clippy::missing_panics_doc)]
6#![allow(unsafe_code)]
7
8use rayon::prelude::*;
9
10#[derive(Copy, Clone)]
13pub struct ImageView<'a> {
14 pub data: &'a [u8],
16 pub width: usize,
18 pub height: usize,
20 pub stride: usize,
22}
23
24impl<'a> ImageView<'a> {
25 pub fn new(data: &'a [u8], width: usize, height: usize, stride: usize) -> Result<Self, String> {
27 if stride < width {
28 return Err(format!(
29 "Stride ({stride}) cannot be less than width ({width})"
30 ));
31 }
32 let required_size = if height > 0 {
33 (height - 1) * stride + width
34 } else {
35 0
36 };
37 if data.len() < required_size {
38 return Err(format!(
39 "Buffer size ({}) is too small for {}x{} image with stride {} (required: {})",
40 data.len(),
41 width,
42 height,
43 stride,
44 required_size
45 ));
46 }
47 Ok(Self {
48 data,
49 width,
50 height,
51 stride,
52 })
53 }
54
55 #[must_use]
60 pub fn has_simd_padding(&self) -> bool {
61 let required_size = if self.height > 0 {
62 (self.height - 1) * self.stride + self.width
63 } else {
64 0
65 };
66 self.data.len() >= required_size + 3
67 }
68
69 #[inline(always)]
71 #[must_use]
72 pub fn get_row(&self, y: usize) -> &[u8] {
73 assert!(y < self.height, "Row index {y} out of bounds");
74 let start = y * self.stride;
75 &self.data[start..start + self.width]
76 }
77
78 #[must_use]
80 pub fn get_pixel(&self, x: usize, y: usize) -> u8 {
81 let x = x.min(self.width - 1);
82 let y = y.min(self.height - 1);
83 unsafe { *self.data.get_unchecked(y * self.stride + x) }
85 }
86
87 #[inline(always)]
92 #[must_use]
93 pub unsafe fn get_pixel_unchecked(&self, x: usize, y: usize) -> u8 {
94 debug_assert!(x < self.width, "x {} out of bounds {}", x, self.width);
96 debug_assert!(y < self.height, "y {} out of bounds {}", y, self.height);
97 unsafe { *self.data.get_unchecked(y * self.stride + x) }
99 }
100
101 #[must_use]
103 pub fn sample_bilinear(&self, x: f64, y: f64) -> f64 {
104 let x = x - 0.5;
105 let y = y - 0.5;
106
107 if x < 0.0 || x >= (self.width - 1) as f64 || y < 0.0 || y >= (self.height - 1) as f64 {
108 return f64::from(
109 self.get_pixel(x.round().max(0.0) as usize, y.round().max(0.0) as usize),
110 );
111 }
112
113 let x0 = x.floor() as usize;
114 let y0 = y.floor() as usize;
115 let x1 = x0 + 1;
116 let y1 = y0 + 1;
117
118 let dx = x - x0 as f64;
119 let dy = y - y0 as f64;
120
121 let v00 = f64::from(self.get_pixel(x0, y0));
122 let v10 = f64::from(self.get_pixel(x1, y0));
123 let v01 = f64::from(self.get_pixel(x0, y1));
124 let v11 = f64::from(self.get_pixel(x1, y1));
125
126 let v0 = v00 * (1.0 - dx) + v10 * dx;
127 let v1 = v01 * (1.0 - dx) + v11 * dx;
128
129 v0 * (1.0 - dy) + v1 * dy
130 }
131
132 #[inline(always)]
138 #[must_use]
139 pub unsafe fn sample_bilinear_unchecked(&self, x: f64, y: f64) -> f64 {
140 let x = x - 0.5;
141 let y = y - 0.5;
142 let x0 = x as usize; let y0 = y as usize;
144 let x1 = x0 + 1;
145 let y1 = y0 + 1;
146
147 debug_assert!(x1 < self.width, "x1 {} out of bounds {}", x1, self.width);
148 debug_assert!(y1 < self.height, "y1 {} out of bounds {}", y1, self.height);
149
150 let dx = x - x0 as f64;
151 let dy = y - y0 as f64;
152
153 let row0 = unsafe { self.get_row_unchecked(y0) };
157 let row1 = unsafe { self.get_row_unchecked(y1) };
159
160 unsafe {
163 let v00 = f64::from(*row0.get_unchecked(x0));
164 let v10 = f64::from(*row0.get_unchecked(x1));
165 let v01 = f64::from(*row1.get_unchecked(x0));
166 let v11 = f64::from(*row1.get_unchecked(x1));
167
168 let v0 = v00 * (1.0 - dx) + v10 * dx;
169 let v1 = v01 * (1.0 - dx) + v11 * dx;
170
171 v0 * (1.0 - dy) + v1 * dy
172 }
173 }
174
175 #[must_use]
177 pub fn sample_gradient_bilinear(&self, x: f64, y: f64) -> [f64; 2] {
178 let x = x - 0.5;
179 let y = y - 0.5;
180
181 if x < 1.0 || x >= (self.width - 2) as f64 || y < 1.0 || y >= (self.height - 2) as f64 {
187 let gx = (self.sample_bilinear(x + 1.0, y) - self.sample_bilinear(x - 1.0, y)) * 0.5;
188 let gy = (self.sample_bilinear(x, y + 1.0) - self.sample_bilinear(x, y - 1.0)) * 0.5;
189 return [gx, gy];
190 }
191
192 let x0 = x.floor() as usize;
193 let y0 = y.floor() as usize;
194 let dx = x - x0 as f64;
195 let dy = y - y0 as f64;
196
197 let mut g00 = [0.0, 0.0];
201 let mut g10 = [0.0, 0.0];
202 let mut g01 = [0.0, 0.0];
203 let mut g11 = [0.0, 0.0];
204
205 unsafe {
207 for j in 0..2 {
208 for i in 0..2 {
209 let cx = x0 + i;
210 let cy = y0 + j;
211
212 let gx = (f64::from(self.get_pixel_unchecked(cx + 1, cy))
213 - f64::from(self.get_pixel_unchecked(cx - 1, cy)))
214 * 0.5;
215 let gy = (f64::from(self.get_pixel_unchecked(cx, cy + 1))
216 - f64::from(self.get_pixel_unchecked(cx, cy - 1)))
217 * 0.5;
218
219 match (i, j) {
220 (0, 0) => g00 = [gx, gy],
221 (1, 0) => g10 = [gx, gy],
222 (0, 1) => g01 = [gx, gy],
223 (1, 1) => g11 = [gx, gy],
224 _ => unreachable!(),
225 }
226 }
227 }
228 }
229
230 let gx = (g00[0] * (1.0 - dx) + g10[0] * dx) * (1.0 - dy)
231 + (g01[0] * (1.0 - dx) + g11[0] * dx) * dy;
232 let gy = (g00[1] * (1.0 - dx) + g10[1] * dx) * (1.0 - dy)
233 + (g01[1] * (1.0 - dx) + g11[1] * dx) * dy;
234
235 [gx, gy]
236 }
237
238 #[inline(always)]
240 pub(crate) unsafe fn get_row_unchecked(&self, y: usize) -> &[u8] {
241 let start = y * self.stride;
242 unsafe { &self.data.get_unchecked(start..start + self.width) }
244 }
245
246 pub fn decimate_to<'b>(
250 &self,
251 factor: usize,
252 output: &'b mut [u8],
253 ) -> Result<ImageView<'b>, String> {
254 let factor = factor.max(1);
255 if factor == 1 {
256 let len = self.data.len();
257 if output.len() < len {
258 return Err(format!(
259 "Output buffer too small: {} < {}",
260 output.len(),
261 len
262 ));
263 }
264 output[..len].copy_from_slice(self.data);
265 return ImageView::new(&output[..len], self.width, self.height, self.width);
266 }
267
268 let new_w = self.width / factor;
269 let new_h = self.height / factor;
270
271 if output.len() < new_w * new_h {
272 return Err(format!(
273 "Output buffer too small for decimation: {} < {}",
274 output.len(),
275 new_w * new_h
276 ));
277 }
278
279 output
280 .par_chunks_exact_mut(new_w)
281 .enumerate()
282 .take(new_h)
283 .for_each(|(y, out_row)| {
284 let src_y = y * factor;
285 let src_row = self.get_row(src_y);
286 for x in 0..new_w {
287 out_row[x] = src_row[x * factor];
288 }
289 });
290
291 ImageView::new(&output[..new_w * new_h], new_w, new_h, new_w)
292 }
293
294 pub fn upscale_to<'b>(
298 &self,
299 factor: usize,
300 output: &'b mut [u8],
301 ) -> Result<ImageView<'b>, String> {
302 let factor = factor.max(1);
303 if factor == 1 {
304 let len = self.data.len();
305 if output.len() < len {
306 return Err(format!(
307 "Output buffer too small: {} < {}",
308 output.len(),
309 len
310 ));
311 }
312 output[..len].copy_from_slice(self.data);
313 return ImageView::new(&output[..len], self.width, self.height, self.width);
314 }
315
316 let new_w = self.width * factor;
317 let new_h = self.height * factor;
318
319 if output.len() < new_w * new_h {
320 return Err(format!(
321 "Output buffer too small for upscaling: {} < {}",
322 output.len(),
323 new_w * new_h
324 ));
325 }
326
327 let scale = 1.0 / factor as f64;
328
329 output
330 .par_chunks_exact_mut(new_w)
331 .enumerate()
332 .take(new_h)
333 .for_each(|(y, out_row)| {
334 let src_y = y as f64 * scale;
335 for (x, val) in out_row.iter_mut().enumerate() {
336 let src_x = x as f64 * scale;
337 *val = self.sample_bilinear(src_x, src_y) as u8;
343 }
344 });
345
346 ImageView::new(&output[..new_w * new_h], new_w, new_h, new_w)
347 }
348}
349
350#[cfg(test)]
351#[allow(clippy::expect_used, clippy::unwrap_used)]
352mod tests {
353 use super::*;
354 use proptest::prelude::*;
355
356 #[test]
357 fn test_image_view_stride() {
358 let data = vec![
359 1, 2, 3, 0, 4, 5, 6, 0, ];
362 let view = ImageView::new(&data, 3, 2, 4).expect("Valid image creation");
363 assert_eq!(view.get_row(0), &[1, 2, 3]);
364 assert_eq!(view.get_row(1), &[4, 5, 6]);
365 assert_eq!(view.get_pixel(1, 1), 5);
366 }
367
368 #[test]
369 fn test_invalid_buffer_size() {
370 let data = vec![1, 2, 3];
371 let result = ImageView::new(&data, 2, 2, 2);
372 assert!(result.is_err());
373 }
374
375 proptest! {
376 #[test]
377 fn prop_image_view_creation(
378 width in 0..1000usize,
379 height in 0..1000usize,
380 stride_extra in 0..100usize,
381 has_enough_data in prop::bool::ANY
382 ) {
383 let stride = width + stride_extra;
384 let required_size = if height > 0 {
385 (height - 1) * stride + width
386 } else {
387 0
388 };
389
390 let data_len = if has_enough_data {
391 required_size
392 } else {
393 required_size.saturating_sub(1)
394 };
395
396 let data = vec![0u8; data_len];
397 let result = ImageView::new(&data, width, height, stride);
398
399 if height > 0 && !has_enough_data {
400 assert!(result.is_err());
401 } else {
402 assert!(result.is_ok());
403 }
404 }
405
406 #[test]
407 fn prop_get_pixel_clamping(
408 width in 1..100usize,
409 height in 1..100usize,
410 x in 0..200usize,
411 y in 0..200usize
412 ) {
413 let data = vec![0u8; height * width];
414 let view = ImageView::new(&data, width, height, width).expect("valid creation");
415 let p = view.get_pixel(x, y);
416 assert_eq!(p, 0);
418 }
419
420 #[test]
421 fn prop_sample_bilinear_invariants(
422 width in 2..20usize,
423 height in 2..20usize,
424 data in prop::collection::vec(0..=255u8, 20*20),
425 x in 0.0..20.0f64,
426 y in 0.0..20.0f64
427 ) {
428 let real_width = width.min(20);
429 let real_height = height.min(20);
430 let slice = &data[..real_width * real_height];
431 let view = ImageView::new(slice, real_width, real_height, real_width).expect("valid creation");
432
433 let x = x % real_width as f64;
434 let y = y % real_height as f64;
435
436 let val = view.sample_bilinear(x, y);
437
438 assert!((0.0..=255.0).contains(&val));
440
441 let x0 = (x - 0.5).max(0.0).floor() as usize;
445 let y0 = (y - 0.5).max(0.0).floor() as usize;
446 let x1 = x0 + 1;
447 let y1 = y0 + 1;
448
449 if x1 < real_width && y1 < real_height {
450 let v00 = view.get_pixel(x0, y0);
451 let v10 = view.get_pixel(x1, y0);
452 let v01 = view.get_pixel(x0, y1);
453 let v11 = view.get_pixel(x1, y1);
454
455 let min = f64::from(v00.min(v10).min(v01).min(v11));
456 let max = f64::from(v00.max(v10).max(v01).max(v11));
457
458 assert!(val >= min - 1e-9 && val <= max + 1e-9, "Value {val} not in [{min}, {max}] for x={x}, y={y}");
459 }
460 }
461 }
462}