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 #[inline(always)]
57 #[must_use]
58 pub fn get_row(&self, y: usize) -> &[u8] {
59 assert!(y < self.height, "Row index {y} out of bounds");
60 let start = y * self.stride;
61 &self.data[start..start + self.width]
62 }
63
64 #[must_use]
66 pub fn get_pixel(&self, x: usize, y: usize) -> u8 {
67 let x = x.min(self.width - 1);
68 let y = y.min(self.height - 1);
69 unsafe { *self.data.get_unchecked(y * self.stride + x) }
71 }
72
73 #[inline(always)]
78 #[must_use]
79 pub unsafe fn get_pixel_unchecked(&self, x: usize, y: usize) -> u8 {
80 debug_assert!(x < self.width, "x {} out of bounds {}", x, self.width);
82 debug_assert!(y < self.height, "y {} out of bounds {}", y, self.height);
83 unsafe { *self.data.get_unchecked(y * self.stride + x) }
85 }
86
87 #[must_use]
89 pub fn sample_bilinear(&self, x: f64, y: f64) -> f64 {
90 let x = x - 0.5;
91 let y = y - 0.5;
92
93 if x < 0.0 || x >= (self.width - 1) as f64 || y < 0.0 || y >= (self.height - 1) as f64 {
94 return f64::from(
95 self.get_pixel(x.round().max(0.0) as usize, y.round().max(0.0) as usize),
96 );
97 }
98
99 let x0 = x.floor() as usize;
100 let y0 = y.floor() as usize;
101 let x1 = x0 + 1;
102 let y1 = y0 + 1;
103
104 let dx = x - x0 as f64;
105 let dy = y - y0 as f64;
106
107 let v00 = f64::from(self.get_pixel(x0, y0));
108 let v10 = f64::from(self.get_pixel(x1, y0));
109 let v01 = f64::from(self.get_pixel(x0, y1));
110 let v11 = f64::from(self.get_pixel(x1, y1));
111
112 let v0 = v00 * (1.0 - dx) + v10 * dx;
113 let v1 = v01 * (1.0 - dx) + v11 * dx;
114
115 v0 * (1.0 - dy) + v1 * dy
116 }
117
118 #[inline(always)]
124 #[must_use]
125 pub unsafe fn sample_bilinear_unchecked(&self, x: f64, y: f64) -> f64 {
126 let x = x - 0.5;
127 let y = y - 0.5;
128 let x0 = x as usize; let y0 = y as usize;
130 let x1 = x0 + 1;
131 let y1 = y0 + 1;
132
133 debug_assert!(x1 < self.width, "x1 {} out of bounds {}", x1, self.width);
134 debug_assert!(y1 < self.height, "y1 {} out of bounds {}", y1, self.height);
135
136 let dx = x - x0 as f64;
137 let dy = y - y0 as f64;
138
139 let row0 = unsafe { self.get_row_unchecked(y0) };
143 let row1 = unsafe { self.get_row_unchecked(y1) };
144
145 unsafe {
148 let v00 = f64::from(*row0.get_unchecked(x0));
149 let v10 = f64::from(*row0.get_unchecked(x1));
150 let v01 = f64::from(*row1.get_unchecked(x0));
151 let v11 = f64::from(*row1.get_unchecked(x1));
152
153 let v0 = v00 * (1.0 - dx) + v10 * dx;
154 let v1 = v01 * (1.0 - dx) + v11 * dx;
155
156 v0 * (1.0 - dy) + v1 * dy
157 }
158 }
159
160 #[must_use]
162 pub fn sample_gradient_bilinear(&self, x: f64, y: f64) -> [f64; 2] {
163 let x = x - 0.5;
164 let y = y - 0.5;
165
166 if x < 1.0 || x >= (self.width - 2) as f64 || y < 1.0 || y >= (self.height - 2) as f64 {
172 let gx = (self.sample_bilinear(x + 1.0, y) - self.sample_bilinear(x - 1.0, y)) * 0.5;
173 let gy = (self.sample_bilinear(x, y + 1.0) - self.sample_bilinear(x, y - 1.0)) * 0.5;
174 return [gx, gy];
175 }
176
177 let x0 = x.floor() as usize;
178 let y0 = y.floor() as usize;
179 let dx = x - x0 as f64;
180 let dy = y - y0 as f64;
181
182 let mut g00 = [0.0, 0.0];
186 let mut g10 = [0.0, 0.0];
187 let mut g01 = [0.0, 0.0];
188 let mut g11 = [0.0, 0.0];
189
190 unsafe {
191 for j in 0..2 {
192 for i in 0..2 {
193 let cx = x0 + i;
194 let cy = y0 + j;
195
196 let gx = (f64::from(self.get_pixel_unchecked(cx + 1, cy))
197 - f64::from(self.get_pixel_unchecked(cx - 1, cy)))
198 * 0.5;
199 let gy = (f64::from(self.get_pixel_unchecked(cx, cy + 1))
200 - f64::from(self.get_pixel_unchecked(cx, cy - 1)))
201 * 0.5;
202
203 match (i, j) {
204 (0, 0) => g00 = [gx, gy],
205 (1, 0) => g10 = [gx, gy],
206 (0, 1) => g01 = [gx, gy],
207 (1, 1) => g11 = [gx, gy],
208 _ => unreachable!(),
209 }
210 }
211 }
212 }
213
214 let gx = (g00[0] * (1.0 - dx) + g10[0] * dx) * (1.0 - dy)
215 + (g01[0] * (1.0 - dx) + g11[0] * dx) * dy;
216 let gy = (g00[1] * (1.0 - dx) + g10[1] * dx) * (1.0 - dy)
217 + (g01[1] * (1.0 - dx) + g11[1] * dx) * dy;
218
219 [gx, gy]
220 }
221
222 #[inline(always)]
224 pub(crate) unsafe fn get_row_unchecked(&self, y: usize) -> &[u8] {
225 let start = y * self.stride;
226 unsafe { &self.data.get_unchecked(start..start + self.width) }
228 }
229
230 pub fn decimate_to<'b>(
234 &self,
235 factor: usize,
236 output: &'b mut [u8],
237 ) -> Result<ImageView<'b>, String> {
238 let factor = factor.max(1);
239 if factor == 1 {
240 let len = self.data.len();
241 if output.len() < len {
242 return Err(format!(
243 "Output buffer too small: {} < {}",
244 output.len(),
245 len
246 ));
247 }
248 output[..len].copy_from_slice(self.data);
249 return ImageView::new(&output[..len], self.width, self.height, self.width);
250 }
251
252 let new_w = self.width / factor;
253 let new_h = self.height / factor;
254
255 if output.len() < new_w * new_h {
256 return Err(format!(
257 "Output buffer too small for decimation: {} < {}",
258 output.len(),
259 new_w * new_h
260 ));
261 }
262
263 output
264 .par_chunks_exact_mut(new_w)
265 .enumerate()
266 .take(new_h)
267 .for_each(|(y, out_row)| {
268 let src_y = y * factor;
269 let src_row = self.get_row(src_y);
270 for x in 0..new_w {
271 out_row[x] = src_row[x * factor];
272 }
273 });
274
275 ImageView::new(&output[..new_w * new_h], new_w, new_h, new_w)
276 }
277
278 pub fn upscale_to<'b>(
282 &self,
283 factor: usize,
284 output: &'b mut [u8],
285 ) -> Result<ImageView<'b>, String> {
286 let factor = factor.max(1);
287 if factor == 1 {
288 let len = self.data.len();
289 if output.len() < len {
290 return Err(format!(
291 "Output buffer too small: {} < {}",
292 output.len(),
293 len
294 ));
295 }
296 output[..len].copy_from_slice(self.data);
297 return ImageView::new(&output[..len], self.width, self.height, self.width);
298 }
299
300 let new_w = self.width * factor;
301 let new_h = self.height * factor;
302
303 if output.len() < new_w * new_h {
304 return Err(format!(
305 "Output buffer too small for upscaling: {} < {}",
306 output.len(),
307 new_w * new_h
308 ));
309 }
310
311 let scale = 1.0 / factor as f64;
312
313 output
314 .par_chunks_exact_mut(new_w)
315 .enumerate()
316 .take(new_h)
317 .for_each(|(y, out_row)| {
318 let src_y = y as f64 * scale;
319 for (x, val) in out_row.iter_mut().enumerate() {
320 let src_x = x as f64 * scale;
321 *val = self.sample_bilinear(src_x, src_y) as u8;
327 }
328 });
329
330 ImageView::new(&output[..new_w * new_h], new_w, new_h, new_w)
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use proptest::prelude::*;
338
339 #[test]
340 fn test_image_view_stride() {
341 let data = vec![
342 1, 2, 3, 0, 4, 5, 6, 0, ];
345 let view = ImageView::new(&data, 3, 2, 4).expect("Valid image creation");
346 assert_eq!(view.get_row(0), &[1, 2, 3]);
347 assert_eq!(view.get_row(1), &[4, 5, 6]);
348 assert_eq!(view.get_pixel(1, 1), 5);
349 }
350
351 #[test]
352 fn test_invalid_buffer_size() {
353 let data = vec![1, 2, 3];
354 let result = ImageView::new(&data, 2, 2, 2);
355 assert!(result.is_err());
356 }
357
358 proptest! {
359 #[test]
360 fn prop_image_view_creation(
361 width in 0..1000usize,
362 height in 0..1000usize,
363 stride_extra in 0..100usize,
364 has_enough_data in prop::bool::ANY
365 ) {
366 let stride = width + stride_extra;
367 let required_size = if height > 0 {
368 (height - 1) * stride + width
369 } else {
370 0
371 };
372
373 let data_len = if has_enough_data {
374 required_size
375 } else {
376 required_size.saturating_sub(1)
377 };
378
379 let data = vec![0u8; data_len];
380 let result = ImageView::new(&data, width, height, stride);
381
382 if height > 0 && !has_enough_data {
383 assert!(result.is_err());
384 } else {
385 assert!(result.is_ok());
386 }
387 }
388
389 #[test]
390 fn prop_get_pixel_clamping(
391 width in 1..100usize,
392 height in 1..100usize,
393 x in 0..200usize,
394 y in 0..200usize
395 ) {
396 let data = vec![0u8; height * width];
397 let view = ImageView::new(&data, width, height, width).expect("valid creation");
398 let p = view.get_pixel(x, y);
399 assert_eq!(p, 0);
401 }
402
403 #[test]
404 fn prop_sample_bilinear_invariants(
405 width in 2..20usize,
406 height in 2..20usize,
407 data in prop::collection::vec(0..=255u8, 20*20),
408 x in 0.0..20.0f64,
409 y in 0.0..20.0f64
410 ) {
411 let real_width = width.min(20);
412 let real_height = height.min(20);
413 let slice = &data[..real_width * real_height];
414 let view = ImageView::new(slice, real_width, real_height, real_width).expect("valid creation");
415
416 let x = x % real_width as f64;
417 let y = y % real_height as f64;
418
419 let val = view.sample_bilinear(x, y);
420
421 assert!((0.0..=255.0).contains(&val));
423
424 let x0 = (x - 0.5).max(0.0).floor() as usize;
428 let y0 = (y - 0.5).max(0.0).floor() as usize;
429 let x1 = x0 + 1;
430 let y1 = y0 + 1;
431
432 if x1 < real_width && y1 < real_height {
433 let v00 = view.get_pixel(x0, y0);
434 let v10 = view.get_pixel(x1, y0);
435 let v01 = view.get_pixel(x0, y1);
436 let v11 = view.get_pixel(x1, y1);
437
438 let min = f64::from(v00.min(v10).min(v01).min(v11));
439 let max = f64::from(v00.max(v10).max(v01).max(v11));
440
441 assert!(val >= min - 1e-9 && val <= max + 1e-9, "Value {val} not in [{min}, {max}] for x={x}, y={y}");
442 }
443 }
444 }
445}