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