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}