1use crate::{ImageDescriptor, PixelFormat, PixelsError, Region, Result};
16
17fn validate(region: Region, pixel: PixelFormat, stride: usize, data_len: usize) -> Result<usize> {
22 let row_bytes = (region.width as usize)
23 .checked_mul(pixel.bytes_per_pixel())
24 .ok_or_else(|| PixelsError::invalid_argument("region", "row byte length overflows"))?;
25 if stride < row_bytes {
26 return Err(PixelsError::invalid_argument(
27 "stride",
28 format!("stride {stride} is shorter than a {row_bytes}-byte row"),
29 ));
30 }
31 if region.is_empty() {
32 return Ok(row_bytes);
33 }
34 let needed = (region.height as usize - 1)
35 .checked_mul(stride)
36 .and_then(|full| full.checked_add(row_bytes))
37 .ok_or_else(|| PixelsError::invalid_argument("region", "tile byte length overflows"))?;
38 if data_len < needed {
39 return Err(PixelsError::invalid_argument(
40 "data",
41 format!("buffer of {data_len} bytes is too small for {region} (needs {needed})"),
42 ));
43 }
44 Ok(row_bytes)
45}
46
47fn row_offset(region: Region, stride: usize, y: u32) -> Option<usize> {
49 if y < region.y || u64::from(y) >= region.bottom() {
50 return None;
51 }
52 ((y - region.y) as usize).checked_mul(stride)
53}
54
55#[derive(Debug, Clone, Copy)]
57pub struct Tile<'a> {
58 region: Region,
59 pixel: PixelFormat,
60 stride: usize,
61 row_bytes: usize,
62 data: &'a [u8],
63}
64
65impl<'a> Tile<'a> {
66 pub fn new(region: Region, pixel: PixelFormat, stride: usize, data: &'a [u8]) -> Result<Self> {
76 let row_bytes = validate(region, pixel, stride, data.len())?;
77 Ok(Self {
78 region,
79 pixel,
80 stride,
81 row_bytes,
82 data,
83 })
84 }
85
86 #[must_use]
88 pub const fn region(&self) -> Region {
89 self.region
90 }
91
92 #[must_use]
94 pub const fn pixel(&self) -> PixelFormat {
95 self.pixel
96 }
97
98 #[must_use]
100 pub const fn stride(&self) -> usize {
101 self.stride
102 }
103
104 #[must_use]
106 pub const fn row_bytes(&self) -> usize {
107 self.row_bytes
108 }
109
110 #[must_use]
112 pub fn row(&self, y: u32) -> Option<&'a [u8]> {
113 let offset = row_offset(self.region, self.stride, y)?;
114 self.data.get(offset..offset.checked_add(self.row_bytes)?)
115 }
116
117 pub fn rows(&self) -> impl Iterator<Item = &'a [u8]> + '_ {
119 let first = self.region.y;
120 (0..self.region.height).filter_map(move |i| self.row(first + i))
121 }
122}
123
124#[derive(Debug)]
131pub struct TileMut<'a> {
132 region: Region,
133 pixel: PixelFormat,
134 stride: usize,
135 row_bytes: usize,
136 data: &'a mut [u8],
137}
138
139impl<'a> TileMut<'a> {
140 pub fn new(
146 region: Region,
147 pixel: PixelFormat,
148 stride: usize,
149 data: &'a mut [u8],
150 ) -> Result<Self> {
151 let row_bytes = validate(region, pixel, stride, data.len())?;
152 Ok(Self {
153 region,
154 pixel,
155 stride,
156 row_bytes,
157 data,
158 })
159 }
160
161 #[must_use]
163 pub const fn region(&self) -> Region {
164 self.region
165 }
166
167 #[must_use]
169 pub const fn pixel(&self) -> PixelFormat {
170 self.pixel
171 }
172
173 #[must_use]
175 pub const fn stride(&self) -> usize {
176 self.stride
177 }
178
179 #[must_use]
181 pub const fn row_bytes(&self) -> usize {
182 self.row_bytes
183 }
184
185 #[must_use]
187 pub fn row_mut(&mut self, y: u32) -> Option<&mut [u8]> {
188 let offset = row_offset(self.region, self.stride, y)?;
189 let end = offset.checked_add(self.row_bytes)?;
190 self.data.get_mut(offset..end)
191 }
192
193 pub fn rows_mut(&mut self) -> impl Iterator<Item = &mut [u8]> {
195 let (stride, row_bytes, height) =
196 (self.stride, self.row_bytes, self.region.height as usize);
197 self.data
198 .chunks_mut(stride)
199 .take(height)
200 .filter_map(move |chunk| chunk.get_mut(..row_bytes))
201 }
202
203 #[must_use]
205 pub fn as_ref(&self) -> Tile<'_> {
206 Tile {
207 region: self.region,
208 pixel: self.pixel,
209 stride: self.stride,
210 row_bytes: self.row_bytes,
211 data: self.data,
212 }
213 }
214}
215
216#[derive(Debug, Clone)]
223pub struct TileBuf {
224 region: Region,
225 pixel: PixelFormat,
226 stride: usize,
227 data: Vec<u8>,
228}
229
230impl TileBuf {
231 pub fn zeroed(region: Region, pixel: PixelFormat) -> Result<Self> {
238 let stride = (region.width as usize)
239 .checked_mul(pixel.bytes_per_pixel())
240 .ok_or_else(|| PixelsError::invalid_argument("region", "row length overflows"))?;
241 let len = (region.height as usize)
242 .checked_mul(stride)
243 .ok_or_else(|| PixelsError::invalid_argument("region", "tile length overflows"))?;
244 Ok(Self {
245 region,
246 pixel,
247 stride,
248 data: vec![0; len],
249 })
250 }
251
252 pub fn for_image(desc: &ImageDescriptor) -> Result<Self> {
258 Self::zeroed(desc.region(), desc.pixel)
259 }
260
261 pub fn from_vec(region: Region, pixel: PixelFormat, data: Vec<u8>) -> Result<Self> {
268 let stride = (region.width as usize)
269 .checked_mul(pixel.bytes_per_pixel())
270 .ok_or_else(|| PixelsError::invalid_argument("region", "row length overflows"))?;
271 let expected = (region.height as usize)
272 .checked_mul(stride)
273 .ok_or_else(|| PixelsError::invalid_argument("region", "tile length overflows"))?;
274 if data.len() != expected {
275 return Err(PixelsError::invalid_argument(
276 "data",
277 format!(
278 "expected exactly {expected} packed bytes, got {}",
279 data.len()
280 ),
281 ));
282 }
283 Ok(Self {
284 region,
285 pixel,
286 stride,
287 data,
288 })
289 }
290
291 #[must_use]
293 pub const fn region(&self) -> Region {
294 self.region
295 }
296
297 #[must_use]
299 pub const fn pixel(&self) -> PixelFormat {
300 self.pixel
301 }
302
303 #[must_use]
305 pub fn bytes(&self) -> &[u8] {
306 &self.data
307 }
308
309 #[must_use]
311 pub fn into_bytes(self) -> Vec<u8> {
312 self.data
313 }
314
315 pub fn as_tile(&self) -> Result<Tile<'_>> {
322 Tile::new(self.region, self.pixel, self.stride, &self.data)
323 }
324
325 pub fn as_tile_mut(&mut self) -> Result<TileMut<'_>> {
331 TileMut::new(self.region, self.pixel, self.stride, &mut self.data)
332 }
333}
334
335pub fn copy_region(src: &Tile<'_>, dst: &mut TileMut<'_>, region: Region) -> Result<()> {
347 if src.pixel() != dst.pixel() {
348 return Err(PixelsError::invalid_argument(
349 "pixel",
350 format!(
351 "cannot copy {} pixels into a {} tile",
352 src.pixel(),
353 dst.pixel()
354 ),
355 ));
356 }
357 if !src.region().contains(region) {
358 return Err(PixelsError::invalid_argument(
359 "region",
360 format!("source tile {} does not cover {region}", src.region()),
361 ));
362 }
363 if !dst.region().contains(region) {
364 return Err(PixelsError::invalid_argument(
365 "region",
366 format!("destination tile {} does not cover {region}", dst.region()),
367 ));
368 }
369 if region.is_empty() {
370 return Ok(());
371 }
372 let bpp = src.pixel().bytes_per_pixel();
373 let span = region.width as usize * bpp;
374 let src_offset = (region.x - src.region().x) as usize * bpp;
375 let dst_offset = (region.x - dst.region().x) as usize * bpp;
376 for y in region.y..region.y.saturating_add(region.height) {
377 let (Some(src_row), Some(dst_row)) = (src.row(y), dst.row_mut(y)) else {
378 return Err(PixelsError::invalid_argument(
379 "region",
380 format!("row {y} is out of range"),
381 ));
382 };
383 let (Some(from), Some(into)) = (
384 src_row.get(src_offset..src_offset + span),
385 dst_row.get_mut(dst_offset..dst_offset + span),
386 ) else {
387 return Err(PixelsError::invalid_argument(
388 "region",
389 format!("row {y} is too short for {region}"),
390 ));
391 };
392 into.copy_from_slice(from);
393 }
394 Ok(())
395}
396
397#[cfg(test)]
398#[allow(
399 clippy::unwrap_used,
400 clippy::indexing_slicing,
401 reason = "tests operate on known-good values and assert shapes directly"
402)]
403mod tests {
404 use super::*;
405
406 fn buf(width: u32, height: u32) -> TileBuf {
407 TileBuf::zeroed(Region::from_size(width, height), PixelFormat::Rgb8).unwrap()
408 }
409
410 #[test]
411 fn rows_are_addressed_in_image_coordinates() {
412 let region = Region::new(10, 20, 2, 3);
413 let data = vec![7_u8; 2 * 3 * 3];
414 let tile = Tile::new(region, PixelFormat::Rgb8, 6, &data).unwrap();
415 assert!(tile.row(19).is_none(), "above the tile");
416 assert!(tile.row(20).is_some(), "first row");
417 assert!(tile.row(22).is_some(), "last row");
418 assert!(tile.row(23).is_none(), "below the tile");
419 assert_eq!(tile.rows().count(), 3);
420 assert_eq!(tile.row(20).unwrap().len(), 6);
421 }
422
423 #[test]
424 fn stride_padding_is_skipped_by_row_views() {
425 let data: Vec<u8> = (0..16).collect();
427 let tile = Tile::new(Region::from_size(2, 2), PixelFormat::Rgb8, 8, &data).unwrap();
428 assert_eq!(tile.row(0).unwrap(), &[0, 1, 2, 3, 4, 5]);
429 assert_eq!(tile.row(1).unwrap(), &[8, 9, 10, 11, 12, 13]);
430 assert_eq!(tile.stride(), 8);
431 assert_eq!(tile.row_bytes(), 6);
432 }
433
434 #[test]
435 fn a_buffer_one_byte_short_is_an_error_not_a_panic() {
436 let data = vec![0_u8; 6 * 3 - 1];
437 let err = Tile::new(Region::from_size(6, 3), PixelFormat::Gray8, 6, &data).unwrap_err();
438 assert_eq!(err.code(), crate::ErrorCode::InvalidArgument);
439 }
440
441 #[test]
442 fn stride_shorter_than_a_row_is_rejected() {
443 let data = vec![0_u8; 64];
444 let err = Tile::new(Region::from_size(4, 2), PixelFormat::Rgb8, 11, &data).unwrap_err();
445 assert_eq!(err.code(), crate::ErrorCode::InvalidArgument);
446 }
447
448 #[test]
449 fn trailing_stride_padding_is_not_required() {
450 let data = vec![0_u8; 8 * 2 + 6];
452 assert!(Tile::new(Region::from_size(2, 3), PixelFormat::Rgb8, 8, &data).is_ok());
453 }
454
455 #[test]
456 fn mutable_rows_write_through_to_the_buffer() {
457 let mut b = buf(2, 2);
458 {
459 let mut tile = b.as_tile_mut().unwrap();
460 for (i, row) in tile.rows_mut().enumerate() {
461 row.fill(i as u8 + 1);
462 }
463 }
464 assert_eq!(b.bytes(), &[1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2]);
465 }
466
467 #[test]
468 fn row_mut_addresses_absolute_coordinates() {
469 let mut b = TileBuf::zeroed(Region::new(0, 5, 1, 2), PixelFormat::Gray8).unwrap();
470 let mut tile = b.as_tile_mut().unwrap();
471 assert!(tile.row_mut(4).is_none());
472 tile.row_mut(5).unwrap().fill(9);
473 tile.row_mut(6).unwrap().fill(8);
474 assert!(tile.row_mut(7).is_none());
475 assert_eq!(b.bytes(), &[9, 8]);
476 }
477
478 #[test]
479 fn from_vec_requires_an_exactly_packed_buffer() {
480 let region = Region::from_size(2, 2);
481 assert!(TileBuf::from_vec(region, PixelFormat::Gray8, vec![0; 4]).is_ok());
482 assert!(TileBuf::from_vec(region, PixelFormat::Gray8, vec![0; 3]).is_err());
483 assert!(TileBuf::from_vec(region, PixelFormat::Gray8, vec![0; 5]).is_err());
484 }
485
486 #[test]
487 fn empty_regions_are_representable() {
488 let tile = Tile::new(Region::EMPTY, PixelFormat::Rgb8, 0, &[]).unwrap();
489 assert_eq!(tile.rows().count(), 0);
490 assert!(tile.row(0).is_none());
491 assert_eq!(
492 TileBuf::zeroed(Region::EMPTY, PixelFormat::Rgb8)
493 .unwrap()
494 .bytes()
495 .len(),
496 0
497 );
498 }
499
500 #[test]
501 fn copy_region_moves_a_rectangle_between_differing_origins() {
502 let mut src_buf = TileBuf::zeroed(Region::from_size(4, 4), PixelFormat::Gray8).unwrap();
504 {
505 let mut tile = src_buf.as_tile_mut().unwrap();
506 for y in 0..4_u32 {
507 let row = tile.row_mut(y).unwrap();
508 for (x, cell) in row.iter_mut().enumerate() {
509 *cell = (y as u8) * 10 + x as u8;
510 }
511 }
512 }
513 let mut dst_buf = TileBuf::zeroed(Region::new(1, 1, 2, 2), PixelFormat::Gray8).unwrap();
515 let src = src_buf.as_tile().unwrap();
516 let mut dst = dst_buf.as_tile_mut().unwrap();
517 copy_region(&src, &mut dst, Region::new(1, 1, 2, 2)).unwrap();
518 assert_eq!(dst_buf.bytes(), &[11, 12, 21, 22]);
519 }
520
521 #[test]
522 fn copy_region_rejects_uncovered_or_mismatched_requests() {
523 let src_buf = TileBuf::zeroed(Region::from_size(4, 4), PixelFormat::Gray8).unwrap();
524 let mut dst_buf = TileBuf::zeroed(Region::from_size(4, 4), PixelFormat::Gray8).unwrap();
525 let src = src_buf.as_tile().unwrap();
526 {
527 let mut dst = dst_buf.as_tile_mut().unwrap();
528 let err = copy_region(&src, &mut dst, Region::new(2, 2, 4, 4)).unwrap_err();
530 assert_eq!(err.code(), crate::ErrorCode::InvalidArgument);
531 copy_region(&src, &mut dst, Region::EMPTY).unwrap();
533 }
534 let mut rgb = TileBuf::zeroed(Region::from_size(4, 4), PixelFormat::Rgb8).unwrap();
536 let src2 = src_buf.as_tile().unwrap();
537 let mut dst2 = rgb.as_tile_mut().unwrap();
538 let err = copy_region(&src2, &mut dst2, Region::from_size(4, 4)).unwrap_err();
539 assert_eq!(err.code(), crate::ErrorCode::InvalidArgument);
540 }
541
542 #[test]
543 fn copy_region_survives_stride_padding_on_both_sides() {
544 let src_data: Vec<u8> = (0..16).collect();
545 let src = Tile::new(Region::from_size(2, 4), PixelFormat::Gray8, 4, &src_data).unwrap();
546 let mut dst_data = vec![0_u8; 16];
547 let mut dst = TileMut::new(
548 Region::from_size(2, 4),
549 PixelFormat::Gray8,
550 4,
551 &mut dst_data,
552 )
553 .unwrap();
554 copy_region(&src, &mut dst, Region::from_size(2, 4)).unwrap();
555 assert_eq!(&dst_data[0..2], &[0, 1]);
557 assert_eq!(&dst_data[4..6], &[4, 5]);
558 assert_eq!(&dst_data[2..4], &[0, 0], "padding is not written");
559 }
560
561 #[test]
562 fn as_ref_preserves_the_view() {
563 let mut b = buf(2, 2);
564 let tile = b.as_tile_mut().unwrap();
565 let view = tile.as_ref();
566 assert_eq!(view.region(), Region::from_size(2, 2));
567 assert_eq!(view.pixel(), PixelFormat::Rgb8);
568 assert_eq!(view.stride(), 6);
569 }
570}