Skip to main content

laser_pdf/elements/
svg.rs

1use utils::mm_to_pt;
2
3use crate::{utils::pt_to_mm, *};
4
5/// An SVG graphics element that renders vector graphics from a usvg tree.
6///
7/// The SVG is automatically scaled to fit the available width while maintaining its aspect ratio.
8/// Uses the `svg2pdf` crate for rendering the parsed `usvg` tree.
9pub struct Svg<'a> {
10    /// Reference to the parsed SVG tree
11    pub data: &'a usvg::Tree,
12}
13
14impl<'a> Element for Svg<'a> {
15    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
16        let (height, _) = calculate_size(self.data, ctx.width);
17
18        if ctx.break_appropriate_for_min_height(height) {
19            FirstLocationUsage::WillSkip
20        } else {
21            FirstLocationUsage::WillUse
22        }
23    }
24
25    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
26        let (height, element_size) = calculate_size(self.data, ctx.width);
27
28        ctx.break_if_appropriate_for_min_height(height);
29
30        element_size
31    }
32
33    fn draw(&self, mut ctx: DrawCtx) -> ElementSize {
34        let (height, element_size) = calculate_size(self.data, ctx.width);
35
36        ctx.break_if_appropriate_for_min_height(height);
37
38        let pos = ctx.location.pos;
39
40        let (svg_chunk, svg_id) = svg2pdf::to_chunk(
41            self.data,
42            svg2pdf::ConversionOptions {
43                compress: false,
44                raster_scale: 1.5,
45                embed_text: false,
46                pdfa: true,
47            },
48        )
49        .unwrap();
50
51        let offset = ctx.pdf.alloc.get();
52        let mut max = offset;
53
54        // We want to avoid using a hashmap here for the mapping. This assumes, of course, that
55        // there aren't any huge gaps in the id space of the chunk. They'd have to be so huge that
56        // we'd be very close to the end of the i32 space though, so this being an actual problem
57        // is extremely unlikely. Also, as far as I can tell, svg2pdf seems to just use a bumping id
58        // allocator internally as well, so this should not be a problem at all.
59        svg_chunk.renumber_into(&mut ctx.pdf.pdf, |old| {
60            let val = offset + old.get();
61            max = max.max(val);
62            pdf_writer::Ref::new(val)
63        });
64
65        let svg_id = pdf_writer::Ref::new(offset + svg_id.get());
66        ctx.pdf.alloc = pdf_writer::Ref::new(max + 1);
67
68        let x_object = ctx.pdf.pages[ctx.location.page_idx].add_x_object(svg_id);
69
70        let layer = ctx.location.layer(ctx.pdf);
71
72        layer
73            .save_state()
74            .transform([
75                mm_to_pt(element_size.width.unwrap()),
76                0.,
77                0.,
78                mm_to_pt(element_size.height.unwrap()),
79                mm_to_pt(pos.0),
80                mm_to_pt(pos.1 - element_size.height.unwrap()),
81            ])
82            .x_object(Name(x_object.as_bytes()))
83            .restore_state();
84
85        element_size
86    }
87}
88
89#[inline]
90fn calculate_size(data: &usvg::Tree, width: WidthConstraint) -> (f32, ElementSize) {
91    let svg = data;
92    let svg_size = svg.size();
93    let svg_width = pt_to_mm(svg_size.width() as f32);
94    let svg_height = pt_to_mm(svg_size.height() as f32);
95
96    let width = width.constrain(svg_width);
97    let scale_factor = width / svg_width;
98    let height = svg_height * scale_factor;
99
100    (
101        height,
102        ElementSize {
103            width: Some(width),
104            height: Some(height),
105        },
106    )
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use insta::*;
113    use test_utils::binary_snapshots::*;
114
115    #[test]
116    fn test() {
117        const SVG: &str = "\
118            <svg
119               width=\"512\"
120               height=\"512\"
121               viewBox=\"0 0 135.46666 135.46667\"
122               version=\"1.1\"
123               id=\"svg1\"
124               xmlns=\"http://www.w3.org/2000/svg\"
125               xmlns:svg=\"http://www.w3.org/2000/svg\">
126              <defs
127                 id=\"defs1\" />
128              <g
129                 id=\"layer1\">
130                <rect
131                   style=\"fill:#000f80;fill-opacity:1;stroke:none;stroke-width:1.3386\"
132                   id=\"rect1\"
133                   width=\"108.92857\"
134                   height=\"72.85714\"
135                   x=\"18.571426\"
136                   y=\"11.428572\" />
137                <ellipse
138                   style=\"fill:#008080;fill-opacity:1;stroke:none;stroke-width:2.07092\"
139                   id=\"path1\"
140                   cx=\"84.107147\"
141                   cy=\"84.107132\"
142                   rx=\"51.964283\"
143                   ry=\"46.250004\" />
144              </g>
145            </svg>
146        ";
147
148        let tree = usvg::Tree::from_str(
149            SVG,
150            &usvg::Options {
151                ..Default::default()
152            },
153        )
154        .unwrap();
155
156        let bytes = test_element_bytes(TestElementParams::breakable(), |callback| {
157            callback.call(
158                &Svg { data: &tree }
159                    .debug(0)
160                    .show_max_width()
161                    .show_last_location_max_height(),
162            );
163        });
164        assert_binary_snapshot!(".pdf", bytes);
165    }
166}