Skip to main content

pdfrum_render/
region.rs

1//! Which part of the page a render covers: all of it, or one device-space
2//! tile.
3
4use crate::device::MAX_TARGET_DIMENSION;
5use crate::error::Error;
6
7/// A rectangle of whole device pixels, measured from the top-left corner of
8/// the page's full device box (the box the render transform maps the page
9/// to, whose size is the whole-page render's pixmap size).
10///
11/// Validated at construction: neither side is zero, neither exceeds
12/// [`MAX_TARGET_DIMENSION`] — the largest pixmap a backend allocates — and
13/// the far edges fit in a `u32`. Whether it lies inside a particular page's
14/// device box is the render's to check, since only the render knows the box.
15///
16/// ```
17/// use pdfrum_render::DeviceRect;
18///
19/// let tile = DeviceRect::new(512, 256, 256, 256)?;
20/// assert_eq!((tile.x(), tile.y(), tile.width(), tile.height()), (512, 256, 256, 256));
21/// assert!(DeviceRect::new(0, 0, 0, 10).is_err());
22/// # Ok::<(), pdfrum_render::Error>(())
23/// ```
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct DeviceRect {
26    x: u32,
27    y: u32,
28    width: u32,
29    height: u32,
30}
31
32impl DeviceRect {
33    /// The rectangle with its top-left corner at (`x`, `y`) and the given
34    /// size, in device pixels.
35    ///
36    /// # Errors
37    ///
38    /// [`Error::TargetEmpty`] when `width` or `height` is zero, and
39    /// [`Error::TargetTooLarge`] when either exceeds [`MAX_TARGET_DIMENSION`]
40    /// or when `x + width` or `y + height` overflows a `u32`.
41    pub fn new(x: u32, y: u32, width: u32, height: u32) -> Result<DeviceRect, Error> {
42        if width == 0 || height == 0 {
43            return Err(Error::TargetEmpty { width, height });
44        }
45        if width > MAX_TARGET_DIMENSION
46            || height > MAX_TARGET_DIMENSION
47            || x.checked_add(width).is_none()
48            || y.checked_add(height).is_none()
49        {
50            return Err(Error::TargetTooLarge {
51                width,
52                height,
53                limit: MAX_TARGET_DIMENSION,
54            });
55        }
56        Ok(DeviceRect {
57            x,
58            y,
59            width,
60            height,
61        })
62    }
63
64    /// The left edge, in pixels from the left of the page's device box.
65    #[must_use]
66    pub fn x(&self) -> u32 {
67        self.x
68    }
69
70    /// The top edge, in pixels from the top of the page's device box.
71    #[must_use]
72    pub fn y(&self) -> u32 {
73        self.y
74    }
75
76    /// The width in pixels; also the width of the pixmap a render of this
77    /// rectangle produces.
78    #[must_use]
79    pub fn width(&self) -> u32 {
80        self.width
81    }
82
83    /// The height in pixels; also the height of the pixmap a render of this
84    /// rectangle produces.
85    #[must_use]
86    pub fn height(&self) -> u32 {
87        self.height
88    }
89}
90
91/// How much of the page a render draws.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
93pub enum Region {
94    /// The page's whole device box, which must fit [`MAX_TARGET_DIMENSION`]
95    /// on both axes.
96    #[default]
97    Whole,
98    /// Only this rectangle of the device box, into a pixmap of exactly its
99    /// size. The page's full device size is not allocated and so is not
100    /// bound by [`MAX_TARGET_DIMENSION`]; the rectangle must lie inside it,
101    /// else [`Error::RegionOutOfBounds`].
102    ///
103    /// # Seams
104    ///
105    /// The walk is the whole page's: the page matrix, the pixel rounding of
106    /// every object's extent, the glyph snap and the culling are computed in
107    /// the whole render's frame, and only the target is the rectangle's size.
108    /// So a tile's pixels are the whole render's, except where the
109    /// rasterizer's own arithmetic depends on where the target starts:
110    ///
111    /// - `vello_cpu` flattens in `f32`, and a tile's geometry is the whole
112    ///   page's shifted by whole pixels, which can round differently. Tiles
113    ///   are byte-identical to the whole render in this crate's and the
114    ///   facade's tests, and otherwise differ by one count on an occasional
115    ///   antialiased edge pixel — never by a pixel of content.
116    /// - `tiny-skia` and the AGG port integrate coverage in a way that
117    ///   depends on where an edge enters the target, so a seam can differ by
118    ///   a few counts along an edge.
119    /// - A transparency group, soft mask or pattern cell that the rectangle's
120    ///   edge cuts is drawn in a buffer that starts at the edge, not at the
121    ///   group's own corner; the same integer shift, the same few counts.
122    ///
123    /// Tiles on a grid are rendered the same way every time: the output is a
124    /// function of the page, the options and the rectangle.
125    Rect(DeviceRect),
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn a_rect_is_non_empty_and_within_the_backend_limit() {
134        assert!(DeviceRect::new(0, 0, 1, 1).is_ok());
135        assert!(DeviceRect::new(u32::MAX - 65535, 0, 65535, 1).is_ok());
136        assert_eq!(
137            DeviceRect::new(0, 0, 0, 5),
138            Err(Error::TargetEmpty {
139                width: 0,
140                height: 5
141            })
142        );
143        assert!(matches!(
144            DeviceRect::new(0, 0, 65536, 5),
145            Err(Error::TargetTooLarge { .. })
146        ));
147        assert!(matches!(
148            DeviceRect::new(u32::MAX, 0, 1, 1),
149            Err(Error::TargetTooLarge { .. })
150        ));
151    }
152}