Skip to main content

otf_pixels_core/
geometry.rs

1//! Regions, image descriptors, and the safety limits checked against them.
2
3use crate::{ColorModel, Limit, PixelFormat, PixelsError, Result};
4use core::fmt;
5
6/// An axis-aligned rectangle of pixels, in image coordinates.
7///
8/// The origin is the top-left corner; `x` grows right and `y` grows down. A
9/// region with zero width or height is *empty* and legal to represent — it is
10/// what demand propagation produces when an op needs nothing from an input.
11///
12/// Edge accessors return [`u64`] so that a region touching the far edge of the
13/// coordinate space cannot overflow during intersection math.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct Region {
16    /// Distance from the left edge of the image, in pixels.
17    pub x: u32,
18    /// Distance from the top edge of the image, in pixels.
19    pub y: u32,
20    /// Width in pixels; may be zero.
21    pub width: u32,
22    /// Height in pixels; may be zero.
23    pub height: u32,
24}
25
26impl Region {
27    /// The empty region at the origin.
28    pub const EMPTY: Self = Self {
29        x: 0,
30        y: 0,
31        width: 0,
32        height: 0,
33    };
34
35    /// Construct a region from its origin and size.
36    #[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    /// A region covering `width` × `height` pixels at the origin.
47    #[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    /// The x coordinate one past the right edge.
58    #[must_use]
59    pub const fn right(self) -> u64 {
60        self.x as u64 + self.width as u64
61    }
62
63    /// The y coordinate one past the bottom edge.
64    #[must_use]
65    pub const fn bottom(self) -> u64 {
66        self.y as u64 + self.height as u64
67    }
68
69    /// Whether the region contains no pixels.
70    #[must_use]
71    pub const fn is_empty(self) -> bool {
72        self.width == 0 || self.height == 0
73    }
74
75    /// The number of pixels in the region.
76    #[must_use]
77    pub const fn pixel_count(self) -> u64 {
78        self.width as u64 * self.height as u64
79    }
80
81    /// Whether `other` lies entirely within `self`.
82    ///
83    /// An empty region is contained by any region, including another empty one.
84    #[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    /// The overlap between two regions, or [`Region::EMPTY`] if they are
99    /// disjoint.
100    #[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        // Both differences are positive and bounded by a u32 edge, so the
110        // truncating casts below cannot lose information.
111        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    /// Move the region by a signed offset, clamping at the coordinate origin.
120    #[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/// Safety limits applied before any pixel memory is allocated.
141///
142/// See SPEC §Safety. Limits are checked at header parse time, so a hostile
143/// header that claims enormous dimensions is rejected before the engine
144/// commits memory to it.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146#[non_exhaustive]
147pub struct Limits {
148    /// Maximum total pixels (width × height) in a single image.
149    ///
150    /// Defaults to [`Limits::DEFAULT_MAX_PIXELS`].
151    pub max_pixels: u64,
152}
153
154impl Limits {
155    /// The default `max_pixels`: 268 megapixels, matching Sharp.
156    pub const DEFAULT_MAX_PIXELS: u64 = 268_402_689;
157
158    /// Limits with every check disabled.
159    ///
160    /// Only appropriate for fully trusted input.
161    #[must_use]
162    pub const fn unlimited() -> Self {
163        Self {
164            max_pixels: u64::MAX,
165        }
166    }
167
168    /// The default limits with `max_pixels` replaced.
169    ///
170    /// This exists because [`Limits`] is `#[non_exhaustive]`: downstream crates
171    /// cannot write `Limits { max_pixels, .. }`, so without a setter the field
172    /// would be readable but unconfigurable outside this crate.
173    #[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    /// Check a dimension pair against these limits.
180    ///
181    /// # Errors
182    ///
183    /// Returns [`PixelsError::LimitExceeded`] if the pixel count exceeds
184    /// [`Limits::max_pixels`], or [`PixelsError::InvalidArgument`] if either
185    /// dimension is zero.
186    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/// The shape of an image at a point in the graph.
214///
215/// Descriptors flow **forward at graph-build time**: every node computes its
216/// output descriptor from its inputs' descriptors when it is constructed. That
217/// is what makes [`metadata()`] free — no pixels are touched to answer it
218/// (ARCHITECTURE §Layer 3).
219///
220/// [`metadata()`]: crate::Image::metadata
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
222#[non_exhaustive]
223pub struct ImageDescriptor {
224    /// Width in pixels; always non-zero.
225    pub width: u32,
226    /// Height in pixels; always non-zero.
227    pub height: u32,
228    /// Interleaved pixel format of the samples.
229    pub pixel: PixelFormat,
230    /// Color model the samples are interpreted in (sRGB-assumed in v1).
231    pub color: ColorModel,
232}
233
234impl ImageDescriptor {
235    /// Construct a descriptor, validating it against the default [`Limits`].
236    ///
237    /// # Errors
238    ///
239    /// See [`Limits::check`].
240    pub fn new(width: u32, height: u32, pixel: PixelFormat) -> Result<Self> {
241        Self::with_limits(width, height, pixel, &Limits::default())
242    }
243
244    /// Construct a descriptor, validating it against explicit `limits`.
245    ///
246    /// # Errors
247    ///
248    /// See [`Limits::check`].
249    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    /// The region covering the whole image.
265    #[must_use]
266    pub const fn region(&self) -> Region {
267        Region::from_size(self.width, self.height)
268    }
269
270    /// Bytes in one densely packed row of this image.
271    #[must_use]
272    pub const fn row_bytes(&self) -> usize {
273        self.width as usize * self.pixel.bytes_per_pixel()
274    }
275
276    /// Bytes in the whole image when densely packed.
277    ///
278    /// Returns [`None`] on overflow, which on a 32-bit target is reachable for
279    /// large-but-legal dimensions. Callers sizing an allocation must treat
280    /// [`None`] as "too large for this platform" rather than unwrapping.
281    #[must_use]
282    pub fn byte_len(&self) -> Option<usize> {
283        (self.height as usize).checked_mul(self.row_bytes())
284    }
285
286    /// The same descriptor with different dimensions, revalidated.
287    ///
288    /// # Errors
289    ///
290    /// See [`Limits::check`].
291    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        // Touching edges do not overlap.
335        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        // u32::MAX * u32::MAX would wrap in 32-bit math; the check is u64.
404        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        // `Limits` is non_exhaustive, so downstream crates need this setter to
417        // configure the limit at all.
418        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}