Skip to main content

normordis_pdf/elements/
fixed_image.rs

1use super::{Element, LayoutMode, RenderContext};
2use crate::layout::FixedBox;
3
4/// Scaling / fitting mode for an image inside a `FixedImageBox`.
5#[derive(Debug, Clone, Copy, Default, PartialEq)]
6pub enum ImageFit {
7    /// Scale to fit entirely within the box, preserving aspect ratio.
8    #[default]
9    Contain,
10    /// Scale to fill the box, preserving aspect ratio (may crop).
11    Cover,
12    /// Stretch to fill the box exactly (ignores aspect ratio).
13    Stretch,
14    /// Render at original pixel size; apply `OverflowPolicy` if it exceeds the box.
15    Original,
16}
17
18/// An image element placed at a fixed position on the page.
19///
20/// Does not participate in `PageFlow`.
21#[derive(Debug, Clone)]
22pub struct FixedImageBox {
23    pub image_box: FixedBox,
24    /// Raw PNG or JPEG bytes.
25    pub data: Vec<u8>,
26    pub fit: ImageFit,
27}
28
29impl FixedImageBox {
30    pub fn new(image_box: FixedBox, data: Vec<u8>) -> Self {
31        Self {
32            image_box,
33            data,
34            fit: ImageFit::Contain,
35        }
36    }
37
38    pub fn fit(mut self, fit: ImageFit) -> Self {
39        self.fit = fit;
40        self
41    }
42}
43
44impl Element for FixedImageBox {
45    fn layout_mode(&self) -> LayoutMode {
46        LayoutMode::Fixed(self.image_box.clone())
47    }
48
49    fn estimated_height_mm(&self) -> f64 {
50        0.0
51    }
52
53    fn render(&self, ctx: &mut RenderContext) -> crate::Result<super::RenderResult> {
54        let ua = ctx.ua_config.enabled;
55        if ua {
56            match &self.image_box.ua_role {
57                Some(tag) => {
58                    let mcid = ctx.ua_tag_element(tag.clone(), self.image_box.ua_alt.clone());
59                    ctx.backend.begin_tagged_content(tag.pdf_name().as_bytes(), mcid);
60                }
61                None => {
62                    ctx.backend.begin_artifact_content();
63                }
64            }
65        }
66
67        if !self.data.is_empty() {
68            if let Ok(img) = image::load_from_memory(&self.data) {
69                let (px_w, px_h) = (img.width() as f64, img.height() as f64);
70                let aspect = if px_w > 0.0 { px_h / px_w } else { 1.0 };
71                let box_w = self.image_box.width_mm;
72                let box_h = self.image_box.height_mm;
73
74                let (render_w, render_h, x_off, y_off) = match self.fit {
75                    ImageFit::Contain => {
76                        let h_by_w = box_w * aspect;
77                        if h_by_w <= box_h {
78                            (box_w, h_by_w, 0.0, (box_h - h_by_w) / 2.0)
79                        } else {
80                            let rw = box_h / aspect;
81                            (rw, box_h, (box_w - rw) / 2.0, 0.0)
82                        }
83                    }
84                    ImageFit::Cover => {
85                        let h_by_w = box_w * aspect;
86                        if h_by_w >= box_h {
87                            (box_w, h_by_w, 0.0, 0.0)
88                        } else {
89                            (box_h / aspect, box_h, 0.0, 0.0)
90                        }
91                    }
92                    ImageFit::Stretch => (box_w, box_h, 0.0, 0.0),
93                    ImageFit::Original => {
94                        let w = (px_w * 25.4 / 96.0).min(box_w);
95                        let h = (px_h * 25.4 / 96.0).min(box_h);
96                        (w, h, 0.0, 0.0)
97                    }
98                };
99
100                if render_w > 0.0 && render_h > 0.0 {
101                    let img_ref = ctx.backend.embed_image(&self.data)?;
102                    ctx.backend.draw_image(
103                        img_ref,
104                        self.image_box.x_mm + x_off,
105                        self.image_box.y_mm + y_off,
106                        render_w,
107                        render_h,
108                    );
109                }
110            }
111        }
112
113        if ua { ctx.backend.end_tagged_content(); }
114        Ok(super::RenderResult::done())
115    }
116}