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