Skip to main content

pdf_engine/
geometry.rs

1//! Page geometry: boxes (MediaBox, CropBox, TrimBox, BleedBox, ArtBox),
2//! rotation, and DPI-based pixel conversions.
3
4// Copyright (c) 2026 Innovation Trigger B.V.
5//
6// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
7// the PDFluent Commercial Licence. See the LICENSE file in this repository --
8// that file travels with the copy you received, which a URL does not.
9
10use pdf_render::pdf_syntax::object::dict::keys;
11use pdf_render::pdf_syntax::object::Rect;
12use pdf_render::pdf_syntax::page::Page;
13
14/// A rectangle in PDF user-space points.
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct PageBox {
17    /// Left edge.
18    pub x0: f64,
19    /// Bottom edge.
20    pub y0: f64,
21    /// Right edge.
22    pub x1: f64,
23    /// Top edge.
24    pub y1: f64,
25}
26
27impl PageBox {
28    /// Width in points.
29    pub fn width(&self) -> f64 {
30        (self.x1 - self.x0).abs()
31    }
32
33    /// Height in points.
34    pub fn height(&self) -> f64 {
35        (self.y1 - self.y0).abs()
36    }
37
38    /// Convert to pixel dimensions at the given DPI.
39    pub fn pixels(&self, dpi: f64) -> (u32, u32) {
40        let scale = dpi / 72.0;
41        (
42            (self.width() * scale).ceil() as u32,
43            (self.height() * scale).ceil() as u32,
44        )
45    }
46}
47
48impl From<Rect> for PageBox {
49    fn from(r: Rect) -> Self {
50        Self {
51            x0: r.x0,
52            y0: r.y0,
53            x1: r.x1,
54            y1: r.y1,
55        }
56    }
57}
58
59/// Page rotation.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum PageRotation {
62    /// No rotation (0 degrees).
63    None,
64    /// 90 degrees clockwise.
65    Rotate90,
66    /// 180 degrees.
67    Rotate180,
68    /// 270 degrees clockwise (90 counter-clockwise).
69    Rotate270,
70}
71
72impl PageRotation {
73    /// Rotation in degrees.
74    pub fn degrees(&self) -> u32 {
75        match self {
76            Self::None => 0,
77            Self::Rotate90 => 90,
78            Self::Rotate180 => 180,
79            Self::Rotate270 => 270,
80        }
81    }
82}
83
84/// Complete geometry for a single page.
85#[derive(Debug, Clone)]
86pub struct PageGeometry {
87    /// MediaBox (required, fallback to A4).
88    pub media_box: PageBox,
89    /// CropBox (defaults to MediaBox).
90    pub crop_box: PageBox,
91    /// TrimBox if present.
92    pub trim_box: Option<PageBox>,
93    /// BleedBox if present.
94    pub bleed_box: Option<PageBox>,
95    /// ArtBox if present.
96    pub art_box: Option<PageBox>,
97    /// Page rotation.
98    pub rotation: PageRotation,
99}
100
101impl PageGeometry {
102    /// Effective visible dimensions in points, accounting for rotation.
103    pub fn effective_dimensions(&self) -> (f64, f64) {
104        let w = self.crop_box.width();
105        let h = self.crop_box.height();
106        match self.rotation {
107            PageRotation::Rotate90 | PageRotation::Rotate270 => (h, w),
108            _ => (w, h),
109        }
110    }
111
112    /// Effective visible dimensions in pixels at the given DPI.
113    pub fn pixel_dimensions(&self, dpi: f64) -> (u32, u32) {
114        let (w, h) = self.effective_dimensions();
115        let scale = dpi / 72.0;
116        ((w * scale).ceil() as u32, (h * scale).ceil() as u32)
117    }
118}
119
120/// Extract full geometry from a pdf-syntax Page.
121pub(crate) fn extract_geometry(page: &Page<'_>) -> PageGeometry {
122    let media_box = PageBox::from(page.media_box());
123    let crop_box = PageBox::from(page.crop_box());
124
125    let rotation = match page.rotation() {
126        pdf_render::pdf_syntax::page::Rotation::None => PageRotation::None,
127        pdf_render::pdf_syntax::page::Rotation::Horizontal => PageRotation::Rotate90,
128        pdf_render::pdf_syntax::page::Rotation::Flipped => PageRotation::Rotate180,
129        pdf_render::pdf_syntax::page::Rotation::FlippedHorizontal => PageRotation::Rotate270,
130    };
131
132    let raw = page.raw();
133    let trim_box = raw.get::<Rect>(keys::TRIM_BOX).map(PageBox::from);
134    let bleed_box = raw.get::<Rect>(keys::BLEED_BOX).map(PageBox::from);
135    let art_box = raw.get::<Rect>(keys::ART_BOX).map(PageBox::from);
136
137    PageGeometry {
138        media_box,
139        crop_box,
140        trim_box,
141        bleed_box,
142        art_box,
143        rotation,
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn page_box_dimensions() {
153        let b = PageBox {
154            x0: 0.0,
155            y0: 0.0,
156            x1: 612.0,
157            y1: 792.0,
158        };
159        assert!((b.width() - 612.0).abs() < f64::EPSILON);
160        assert!((b.height() - 792.0).abs() < f64::EPSILON);
161    }
162
163    #[test]
164    fn page_box_pixels() {
165        let b = PageBox {
166            x0: 0.0,
167            y0: 0.0,
168            x1: 72.0,
169            y1: 72.0,
170        };
171        assert_eq!(b.pixels(72.0), (72, 72));
172        assert_eq!(b.pixels(144.0), (144, 144));
173    }
174
175    #[test]
176    fn rotation_degrees() {
177        assert_eq!(PageRotation::None.degrees(), 0);
178        assert_eq!(PageRotation::Rotate90.degrees(), 90);
179        assert_eq!(PageRotation::Rotate180.degrees(), 180);
180        assert_eq!(PageRotation::Rotate270.degrees(), 270);
181    }
182
183    #[test]
184    fn geometry_effective_dimensions() {
185        let g = PageGeometry {
186            media_box: PageBox {
187                x0: 0.0,
188                y0: 0.0,
189                x1: 612.0,
190                y1: 792.0,
191            },
192            crop_box: PageBox {
193                x0: 0.0,
194                y0: 0.0,
195                x1: 612.0,
196                y1: 792.0,
197            },
198            trim_box: None,
199            bleed_box: None,
200            art_box: None,
201            rotation: PageRotation::Rotate90,
202        };
203        let (w, h) = g.effective_dimensions();
204        assert!((w - 792.0).abs() < f64::EPSILON);
205        assert!((h - 612.0).abs() < f64::EPSILON);
206    }
207}