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) };
158
159 unsafe {
162 let v00 = f64::from(*row0.get_unchecked(x0));
163 let v10 = f64::from(*row0.get_unchecked(x1));
164 let v01 = f64::from(*row1.get_unchecked(x0));
165 let v11 = f64::from(*row1.get_unchecked(x1));
166
167 let v0 = v00 * (1.0 - dx) + v10 * dx;
168 let v1 = v01 * (1.0 - dx) + v11 * dx;
169
170 v0 * (1.0 - dy) + v1 * dy
171 }
172 }
173
174 #[must_use]
176 pub fn sample_gradient_bilinear(&self, x: f64, y: f64) -> [f64; 2] {
177 let x = x - 0.5;
178 let y = y - 0.5;
179
180 if x < 1.0 || x >= (self.width - 2) as f64 || y < 1.0 || y >= (self.height - 2) as f64 {
186 let gx = (self.sample_bilinear(x + 1.0, y) - self.sample_bilinear(x - 1.0, y)) * 0.5;
187 let gy = (self.sample_bilinear(x, y + 1.0) - self.sample_bilinear(x, y - 1.0)) * 0.5;
188 return [gx, gy];
189 }
190
191 let x0 = x.floor() as usize;
192 let y0 = y.floor() as usize;
193 let dx = x - x0 as f64;
194 let dy = y - y0 as f64;
195
196 let mut g00 = [0.0, 0.0];
200 let mut g10 = [0.0, 0.0];
201 let mut g01 = [0.0, 0.0];
202 let mut g11 = [0.0, 0.0];
203
204 unsafe {
205 for j in 0..2 {
206 for i in 0..2 {
207 let cx = x0 + i;
208 let cy = y0 + j;
209
210 let gx = (f64::from(self.get_pixel_unchecked(cx + 1, cy))
211 - f64::from(self.get_pixel_unchecked(cx - 1, cy)))
212 * 0.5;
213 let gy = (f64::from(self.get_pixel_unchecked(cx, cy + 1))
214 - f64::from(self.get_pixel_unchecked(cx, cy - 1)))
215 * 0.5;
216
217 match (i, j) {
218 (0, 0) => g00 = [gx, gy],
219 (1, 0) => g10 = [gx, gy],
220 (0, 1) => g01 = [gx, gy],
221 (1, 1) => g11 = [gx, gy],
222 _ => unreachable!(),
223 }
224 }
225 }
226 }
227
228 let gx = (g00[0] * (1.0 - dx) + g10[0] * dx) * (1.0 - dy)
229 + (g01[0] * (1.0 - dx) + g11[0] * dx) * dy;
230 let gy = (g00[1] * (1.0 - dx) + g10[1] * dx) * (1.0 - dy)
231 + (g01[1] * (1.0 - dx) + g11[1] * dx) * dy;
232
233 [gx, gy]
234 }
235
236 #[inline(always)]
238 pub(crate) unsafe fn get_row_unchecked(&self, y: usize) -> &[u8] {
239 let start = y * self.stride;
240 unsafe { &self.data.get_unchecked(start..start + self.width) }
242 }
243
244 pub fn decimate_to<'b>(
248 &self,
249 factor: usize,
250 output: &'b mut [u8],
251 ) -> Result<ImageView<'b>, String> {
252 let factor = factor.max(1);
253 if factor == 1 {
254 let len = self.data.len();
255 if output.len() < len {
256 return Err(format!(
257 "Output buffer too small: {} < {}",
258 output.len(),
259 len
260 ));
261 }
262 output[..len].copy_from_slice(self.data);
263 return ImageView::new(&output[..len], self.width, self.height, self.width);
264 }
265
266 let new_w = self.width / factor;
267 let new_h = self.height / factor;
268
269 if output.len() < new_w * new_h {
270 return Err(format!(
271 "Output buffer too small for decimation: {} < {}",
272 output.len(),
273 new_w * new_h
274 ));
275 }
276
277 output
278 .par_chunks_exact_mut(new_w)
279 .enumerate()
280 .take(new_h)
281 .for_each(|(y, out_row)| {
282 let src_y = y * factor;
283 let src_row = self.get_row(src_y);
284 for x in 0..new_w {
285 out_row[x] = src_row[x * factor];
286 }
287 });
288
289 ImageView::new(&output[..new_w * new_h], new_w, new_h, new_w)
290 }
291
292 pub fn upscale_to<'b>(
296 &self,
297 factor: usize,
298 output: &'b mut [u8],
299 ) -> Result<ImageView<'b>, String> {
300 let factor = factor.max(1);
301 if factor == 1 {
302 let len = self.data.len();
303 if output.len() < len {
304 return Err(format!(
305 "Output buffer too small: {} < {}",
306 output.len(),
307 len
308 ));
309 }
310 output[..len].copy_from_slice(self.data);
311 return ImageView::new(&output[..len], self.width, self.height, self.width);
312 }
313
314 let new_w = self.width * factor;
315 let new_h = self.height * factor;
316
317 if output.len() < new_w * new_h {
318 return Err(format!(
319 "Output buffer too small for upscaling: {} < {}",
320 output.len(),
321 new_w * new_h
322 ));
323 }
324
325 let scale = 1.0 / factor as f64;
326
327 output
328 .par_chunks_exact_mut(new_w)
329 .enumerate()
330 .take(new_h)
331 .for_each(|(y, out_row)| {
332 let src_y = y as f64 * scale;
333 for (x, val) in out_row.iter_mut().enumerate() {
334 let src_x = x as f64 * scale;
335 *val = self.sample_bilinear(src_x, src_y) as u8;
341 }
342 });
343
344 ImageView::new(&output[..new_w * new_h], new_w, new_h, new_w)
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use proptest::prelude::*;
352
353 #[test]
354 fn test_image_view_stride() {
355 let data = vec![
356 1, 2, 3, 0, 4, 5, 6, 0, ];
359 let view = ImageView::new(&data, 3, 2, 4).expect("Valid image creation");
360 assert_eq!(view.get_row(0), &[1, 2, 3]);
361 assert_eq!(view.get_row(1), &[4, 5, 6]);
362 assert_eq!(view.get_pixel(1, 1), 5);
363 }
364
365 #[test]
366 fn test_invalid_buffer_size() {
367 let data = vec![1, 2, 3];
368 let result = ImageView::new(&data, 2, 2, 2);
369 assert!(result.is_err());
370 }
371
372 proptest! {
373 #[test]
374 fn prop_image_view_creation(
375 width in 0..1000usize,
376 height in 0..1000usize,
377 stride_extra in 0..100usize,
378 has_enough_data in prop::bool::ANY
379 ) {
380 let stride = width + stride_extra;
381 let required_size = if height > 0 {
382 (height - 1) * stride + width
383 } else {
384 0
385 };
386
387 let data_len = if has_enough_data {
388 required_size
389 } else {
390 required_size.saturating_sub(1)
391 };
392
393 let data = vec![0u8; data_len];
394 let result = ImageView::new(&data, width, height, stride);
395
396 if height > 0 && !has_enough_data {
397 assert!(result.is_err());
398 } else {
399 assert!(result.is_ok());
400 }
401 }
402
403 #[test]
404 fn prop_get_pixel_clamping(
405 width in 1..100usize,
406 height in 1..100usize,
407 x in 0..200usize,
408 y in 0..200usize
409 ) {
410 let data = vec![0u8; height * width];
411 let view = ImageView::new(&data, width, height, width).expect("valid creation");
412 let p = view.get_pixel(x, y);
413 assert_eq!(p, 0);
415 }
416
417 #[test]
418 fn prop_sample_bilinear_invariants(
419 width in 2..20usize,
420 height in 2..20usize,
421 data in prop::collection::vec(0..=255u8, 20*20),
422 x in 0.0..20.0f64,
423 y in 0.0..20.0f64
424 ) {
425 let real_width = width.min(20);
426 let real_height = height.min(20);
427 let slice = &data[..real_width * real_height];
428 let view = ImageView::new(slice, real_width, real_height, real_width).expect("valid creation");
429
430 let x = x % real_width as f64;
431 let y = y % real_height as f64;
432
433 let val = view.sample_bilinear(x, y);
434
435 assert!((0.0..=255.0).contains(&val));
437
438 let x0 = (x - 0.5).max(0.0).floor() as usize;
442 let y0 = (y - 0.5).max(0.0).floor() as usize;
443 let x1 = x0 + 1;
444 let y1 = y0 + 1;
445
446 if x1 < real_width && y1 < real_height {
447 let v00 = view.get_pixel(x0, y0);
448 let v10 = view.get_pixel(x1, y0);
449 let v01 = view.get_pixel(x0, y1);
450 let v11 = view.get_pixel(x1, y1);
451
452 let min = f64::from(v00.min(v10).min(v01).min(v11));
453 let max = f64::from(v00.max(v10).max(v01).max(v11));
454
455 assert!(val >= min - 1e-9 && val <= max + 1e-9, "Value {val} not in [{min}, {max}] for x={x}, y={y}");
456 }
457 }
458 }
459}