Skip to main content

laser_pdf/elements/
rectangle.rs

1use pdf_writer::Name;
2
3use crate::{utils::*, *};
4
5/// A rectangular shape element with optional fill and outline.
6/// 
7/// The rectangle is rendered with the specified width and height and can have
8/// both a fill color and an outline with configurable thickness and color.
9pub struct Rectangle {
10    /// Size as (width_mm, height_mm)
11    pub size: (f32, f32),
12    /// Optional fill color as RGBA (None for transparent)
13    pub fill: Option<u32>,
14    /// Optional outline as (thickness_mm, color_rgba)
15    pub outline: Option<(f32, u32)>,
16}
17
18impl Element for Rectangle {
19    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
20        let outline_thickness = outline_thickness(self);
21        if ctx.break_appropriate_for_min_height(self.size.1 + outline_thickness) {
22            FirstLocationUsage::WillSkip
23        } else {
24            FirstLocationUsage::WillUse
25        }
26    }
27
28    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
29        let outline_thickness = outline_thickness(self);
30        ctx.break_if_appropriate_for_min_height(self.size.1 + outline_thickness);
31
32        size(self)
33    }
34
35    fn draw(&self, mut ctx: DrawCtx) -> ElementSize {
36        let outline_thickness = outline_thickness(self);
37        ctx.break_if_appropriate_for_min_height(self.size.1 + outline_thickness);
38
39        let extra_outline_offset = outline_thickness as f32 / 2.0;
40
41        let resource_id;
42
43        let fill_alpha = self
44            .fill
45            .map(|c| u32_to_color_and_alpha(c).1)
46            .filter(|&a| a != 1.);
47
48        let outline_alpha = self
49            .outline
50            .map(|(_, c)| u32_to_color_and_alpha(c).1)
51            .filter(|&a| a != 1.);
52
53        if fill_alpha.is_some() || outline_alpha.is_some() {
54            let ext_graphics_ref = ctx.pdf.alloc();
55
56            let mut ext_graphics = ctx.pdf.pdf.ext_graphics(ext_graphics_ref);
57            fill_alpha.inspect(|&a| {
58                ext_graphics.non_stroking_alpha(a);
59            });
60            outline_alpha.inspect(|&a| {
61                ext_graphics.stroking_alpha(a);
62            });
63
64            resource_id =
65                Some(ctx.pdf.pages[ctx.location.page_idx].add_ext_g_state(ext_graphics_ref));
66        } else {
67            resource_id = None;
68        }
69
70        let layer = ctx.location.layer(ctx.pdf);
71
72        layer.save_state();
73
74        if let Some(color) = self.fill {
75            set_fill_color(layer, color);
76        }
77
78        if let Some((thickness, color)) = self.outline {
79            layer.set_line_width(mm_to_pt(thickness) as f32);
80
81            set_stroke_color(layer, color);
82        }
83
84        if let Some(ext_graphics) = resource_id {
85            layer.set_parameters(Name(format!("{}", ext_graphics).as_bytes()));
86        }
87
88        layer.rect(
89            mm_to_pt(ctx.location.pos.0 as f32 + extra_outline_offset),
90            mm_to_pt((ctx.location.pos.1 - self.size.1) as f32 - extra_outline_offset),
91            mm_to_pt(self.size.0 as f32),
92            mm_to_pt(self.size.1 as f32),
93        );
94
95        match (self.fill.is_some(), self.outline.is_some()) {
96            (true, true) => layer.fill_nonzero_and_stroke(),
97            (true, false) => layer.fill_nonzero(),
98            (false, true) => layer.stroke(),
99            (false, false) => layer,
100        };
101
102        layer.restore_state();
103
104        size(self)
105    }
106}
107
108fn outline_thickness(rectangle: &Rectangle) -> f32 {
109    rectangle.outline.map(|o| o.0).unwrap_or(0.0)
110}
111
112fn size(rectangle: &Rectangle) -> ElementSize {
113    let outline_thickness = outline_thickness(rectangle);
114
115    ElementSize {
116        width: Some(rectangle.size.0 + outline_thickness),
117        height: Some(rectangle.size.1 + outline_thickness),
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::test_utils::*;
125
126    #[test]
127    fn test_rectangle() {
128        for output in (ElementTestParams {
129            first_height: 12.,
130            ..Default::default()
131        })
132        .run(&Rectangle {
133            size: (11., 12.),
134            fill: None,
135            outline: Some((1., 0)),
136        }) {
137            output.assert_size(ElementSize {
138                width: Some(12.),
139                height: Some(13.),
140            });
141
142            if let Some(b) = output.breakable {
143                if output.first_height == 12. {
144                    b.assert_break_count(1);
145                } else {
146                    b.assert_break_count(0);
147                }
148
149                b.assert_extra_location_min_height(None);
150            }
151        }
152    }
153}