1use crate::{ColorModel, Limit, PixelFormat, PixelsError, Result};
4use core::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct Region {
16 pub x: u32,
18 pub y: u32,
20 pub width: u32,
22 pub height: u32,
24}
25
26impl Region {
27 pub const EMPTY: Self = Self {
29 x: 0,
30 y: 0,
31 width: 0,
32 height: 0,
33 };
34
35 #[must_use]
37 pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
38 Self {
39 x,
40 y,
41 width,
42 height,
43 }
44 }
45
46 #[must_use]
48 pub const fn from_size(width: u32, height: u32) -> Self {
49 Self {
50 x: 0,
51 y: 0,
52 width,
53 height,
54 }
55 }
56
57 #[must_use]
59 pub const fn right(self) -> u64 {
60 self.x as u64 + self.width as u64
61 }
62
63 #[must_use]
65 pub const fn bottom(self) -> u64 {
66 self.y as u64 + self.height as u64
67 }
68
69 #[must_use]
71 pub const fn is_empty(self) -> bool {
72 self.width == 0 || self.height == 0
73 }
74
75 #[must_use]
77 pub const fn pixel_count(self) -> u64 {
78 self.width as u64 * self.height as u64
79 }
80
81 #[must_use]
85 pub const fn contains(self, other: Self) -> bool {
86 if other.is_empty() {
87 return true;
88 }
89 if self.is_empty() {
90 return false;
91 }
92 other.x >= self.x
93 && other.y >= self.y
94 && other.right() <= self.right()
95 && other.bottom() <= self.bottom()
96 }
97
98 #[must_use]
101 pub fn intersect(self, other: Self) -> Self {
102 let x = self.x.max(other.x);
103 let y = self.y.max(other.y);
104 let right = self.right().min(other.right());
105 let bottom = self.bottom().min(other.bottom());
106 if u64::from(x) >= right || u64::from(y) >= bottom {
107 return Self::EMPTY;
108 }
109 Self {
112 x,
113 y,
114 width: (right - u64::from(x)) as u32,
115 height: (bottom - u64::from(y)) as u32,
116 }
117 }
118
119 #[must_use]
121 pub fn translate(self, dx: i64, dy: i64) -> Self {
122 let shift = |v: u32, d: i64| -> u32 {
123 let moved = i64::from(v).saturating_add(d);
124 moved.clamp(0, i64::from(u32::MAX)) as u32
125 };
126 Self {
127 x: shift(self.x, dx),
128 y: shift(self.y, dy),
129 ..self
130 }
131 }
132}
133
134impl fmt::Display for Region {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146#[non_exhaustive]
147pub struct Limits {
148 pub max_pixels: u64,
152}
153
154impl Limits {
155 pub const DEFAULT_MAX_PIXELS: u64 = 268_402_689;
157
158 #[must_use]
162 pub const fn unlimited() -> Self {
163 Self {
164 max_pixels: u64::MAX,
165 }
166 }
167
168 #[must_use]
174 pub const fn with_max_pixels(mut self, max_pixels: u64) -> Self {
175 self.max_pixels = max_pixels;
176 self
177 }
178
179 pub fn check(&self, width: u32, height: u32) -> Result<()> {
187 if width == 0 {
188 return Err(PixelsError::invalid_argument("width", "must be non-zero"));
189 }
190 if height == 0 {
191 return Err(PixelsError::invalid_argument("height", "must be non-zero"));
192 }
193 let pixels = u64::from(width) * u64::from(height);
194 if pixels > self.max_pixels {
195 return Err(PixelsError::limit_exceeded(
196 Limit::MaxPixels,
197 pixels,
198 self.max_pixels,
199 ));
200 }
201 Ok(())
202 }
203}
204
205impl Default for Limits {
206 fn default() -> Self {
207 Self {
208 max_pixels: Self::DEFAULT_MAX_PIXELS,
209 }
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
222#[non_exhaustive]
223pub struct ImageDescriptor {
224 pub width: u32,
226 pub height: u32,
228 pub pixel: PixelFormat,
230 pub color: ColorModel,
232}
233
234impl ImageDescriptor {
235 pub fn new(width: u32, height: u32, pixel: PixelFormat) -> Result<Self> {
241 Self::with_limits(width, height, pixel, &Limits::default())
242 }
243
244 pub fn with_limits(
250 width: u32,
251 height: u32,
252 pixel: PixelFormat,
253 limits: &Limits,
254 ) -> Result<Self> {
255 limits.check(width, height)?;
256 Ok(Self {
257 width,
258 height,
259 pixel,
260 color: ColorModel::Srgb,
261 })
262 }
263
264 #[must_use]
266 pub const fn region(&self) -> Region {
267 Region::from_size(self.width, self.height)
268 }
269
270 #[must_use]
272 pub const fn row_bytes(&self) -> usize {
273 self.width as usize * self.pixel.bytes_per_pixel()
274 }
275
276 #[must_use]
282 pub fn byte_len(&self) -> Option<usize> {
283 (self.height as usize).checked_mul(self.row_bytes())
284 }
285
286 pub fn resized(&self, width: u32, height: u32) -> Result<Self> {
292 Limits::default().check(width, height)?;
293 Ok(Self {
294 width,
295 height,
296 ..*self
297 })
298 }
299}
300
301impl fmt::Display for ImageDescriptor {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 write!(f, "{}x{} {}", self.width, self.height, self.pixel)
304 }
305}
306
307#[cfg(test)]
308#[allow(
309 clippy::unwrap_used,
310 clippy::indexing_slicing,
311 reason = "tests operate on known-good values and assert shapes directly"
312)]
313mod tests {
314 use super::*;
315 use crate::ErrorCode;
316
317 #[test]
318 fn intersect_of_overlapping_regions() {
319 let a = Region::new(0, 0, 10, 10);
320 let b = Region::new(5, 5, 10, 10);
321 assert_eq!(a.intersect(b), Region::new(5, 5, 5, 5));
322 assert_eq!(
323 a.intersect(b),
324 b.intersect(a),
325 "intersection is commutative"
326 );
327 }
328
329 #[test]
330 fn intersect_of_disjoint_regions_is_empty() {
331 let a = Region::new(0, 0, 4, 4);
332 let b = Region::new(10, 10, 4, 4);
333 assert!(a.intersect(b).is_empty());
334 assert!(
336 Region::new(0, 0, 4, 4)
337 .intersect(Region::new(4, 0, 4, 4))
338 .is_empty()
339 );
340 }
341
342 #[test]
343 fn intersect_at_the_coordinate_limit_does_not_overflow() {
344 let a = Region::new(u32::MAX - 4, 0, 4, 4);
345 let b = Region::new(u32::MAX - 2, 0, 2, 4);
346 assert_eq!(a.intersect(b), Region::new(u32::MAX - 2, 0, 2, 4));
347 assert_eq!(
348 Region::new(u32::MAX, u32::MAX, u32::MAX, u32::MAX).right(),
349 8_589_934_590
350 );
351 }
352
353 #[test]
354 fn contains_handles_empty_regions() {
355 let full = Region::from_size(10, 10);
356 assert!(full.contains(Region::new(2, 2, 3, 3)));
357 assert!(full.contains(full));
358 assert!(!full.contains(Region::new(8, 8, 4, 4)));
359 assert!(full.contains(Region::EMPTY), "empty fits anywhere");
360 assert!(!Region::EMPTY.contains(full));
361 assert!(Region::EMPTY.contains(Region::EMPTY));
362 }
363
364 #[test]
365 fn translate_clamps_instead_of_wrapping() {
366 assert_eq!(
367 Region::new(5, 5, 2, 2).translate(-10, -10),
368 Region::new(0, 0, 2, 2)
369 );
370 assert_eq!(
371 Region::new(5, 5, 2, 2).translate(3, 4),
372 Region::new(8, 9, 2, 2)
373 );
374 assert_eq!(
375 Region::new(0, 0, 2, 2).translate(i64::MAX, i64::MAX).x,
376 u32::MAX
377 );
378 }
379
380 #[test]
381 fn max_pixels_is_checked_before_allocation() {
382 let limits = Limits { max_pixels: 100 };
383 assert!(limits.check(10, 10).is_ok());
384 let err = limits.check(10, 11).unwrap_err();
385 assert_eq!(err.code(), ErrorCode::LimitExceeded);
386 }
387
388 #[test]
389 fn zero_dimensions_are_rejected() {
390 let limits = Limits::default();
391 assert_eq!(
392 limits.check(0, 10).unwrap_err().code(),
393 ErrorCode::InvalidArgument
394 );
395 assert_eq!(
396 limits.check(10, 0).unwrap_err().code(),
397 ErrorCode::InvalidArgument
398 );
399 }
400
401 #[test]
402 fn hostile_dimensions_cannot_overflow_the_limit_check() {
403 let err = Limits::default().check(u32::MAX, u32::MAX).unwrap_err();
405 assert_eq!(err.code(), ErrorCode::LimitExceeded);
406 }
407
408 #[test]
409 fn default_max_pixels_matches_spec() {
410 assert_eq!(Limits::default().max_pixels, 268_402_689);
411 assert!(Limits::unlimited().check(u32::MAX, u32::MAX).is_ok());
412 }
413
414 #[test]
415 fn max_pixels_is_configurable_without_a_struct_literal() {
416 let limits = Limits::default().with_max_pixels(16);
419 assert_eq!(limits.max_pixels, 16);
420 assert!(limits.check(4, 4).is_ok());
421 assert_eq!(
422 limits.check(4, 5).unwrap_err().code(),
423 ErrorCode::LimitExceeded
424 );
425 }
426
427 #[test]
428 fn descriptor_reports_packed_sizes() {
429 let desc = ImageDescriptor::new(4, 3, PixelFormat::Rgb8).unwrap();
430 assert_eq!(desc.row_bytes(), 12);
431 assert_eq!(desc.byte_len(), Some(36));
432 assert_eq!(desc.region(), Region::from_size(4, 3));
433 assert_eq!(desc.color, ColorModel::Srgb);
434 }
435
436 #[test]
437 fn descriptor_construction_enforces_limits() {
438 assert_eq!(
439 ImageDescriptor::new(0, 4, PixelFormat::Rgb8)
440 .unwrap_err()
441 .code(),
442 ErrorCode::InvalidArgument
443 );
444 let desc = ImageDescriptor::new(4, 4, PixelFormat::Rgb8).unwrap();
445 assert_eq!(desc.resized(2, 2).unwrap().width, 2);
446 assert!(desc.resized(0, 2).is_err());
447 }
448}