Skip to main content

laser_pdf/elements/
link.rs

1use crate::{
2    utils::{add_link_annotation, mm_to_pt},
3    *,
4};
5
6/// Wraps an element with a link annotation. If breaking happens, a separate annotation is added for
7/// each location that isn't collapsed.
8pub struct Link<'a, E: Element> {
9    pub element: E,
10    pub target: LinkTarget<'a>,
11}
12
13impl<'a, E: Element> Element for Link<'a, E> {
14    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
15        self.element.first_location_usage(ctx)
16    }
17
18    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
19        self.element.measure(ctx)
20    }
21
22    fn draw(&self, ctx: DrawCtx) -> ElementSize {
23        let size;
24
25        if let Some(breakable) = ctx.breakable {
26            let first_location = ctx.location.clone();
27
28            let mut max_location_idx = 0;
29
30            // We allocate here because for creating the annotations we need to know the width,
31            // which we only get at the end. Since this is only on draw it shouldn't be a big issue.
32            // We choose it over an additional measure pass first, which is definitely a bit of a
33            // tradeoff. The expectation is that in most cases where this element is used it won't
34            // break. This is an example where an arena allocator might be useful.
35            let mut heights = Vec::new();
36
37            size = self.element.draw(DrawCtx {
38                pdf: ctx.pdf,
39                breakable: Some(BreakableDraw {
40                    do_break: &mut |pdf: &mut Pdf, location_idx: u32, height: Option<f32>| {
41                        let location = (breakable.do_break)(pdf, location_idx, height);
42
43                        if location_idx >= max_location_idx {
44                            heights.resize(heights.len().max(location_idx as usize), None);
45                            heights.push(height);
46
47                            max_location_idx = location_idx + 1;
48                        }
49
50                        location
51                    },
52                    ..breakable
53                }),
54                ..ctx
55            });
56
57            if let Some(width) = size.width {
58                for (i, height) in heights
59                    .iter()
60                    .cloned()
61                    .chain(std::iter::once(size.height))
62                    .enumerate()
63                {
64                    if let Some(height) = height {
65                        let location = if i == 0 {
66                            &first_location
67                        } else {
68                            &(breakable.do_break)(ctx.pdf, i as u32 - 1, heights[i - 1])
69                        };
70                        let page_idx = location.page_idx;
71                        let pos = location.pos;
72
73                        add_link_annotation(
74                            ctx.pdf,
75                            page_idx,
76                            (mm_to_pt(pos.0), mm_to_pt(pos.1)),
77                            (mm_to_pt(width), mm_to_pt(height)),
78                            self.target,
79                        );
80                    }
81                }
82            }
83        } else {
84            let page_idx = ctx.location.page_idx;
85            let pos = ctx.location.pos;
86
87            size = self.element.draw(DrawCtx {
88                pdf: ctx.pdf,
89                ..ctx
90            });
91
92            if let ElementSize {
93                width: Some(width),
94                height: Some(height),
95            } = size
96            {
97                add_link_annotation(
98                    ctx.pdf,
99                    page_idx,
100                    (mm_to_pt(pos.0), mm_to_pt(pos.1)),
101                    (mm_to_pt(width), mm_to_pt(height)),
102                    self.target,
103                );
104            }
105        };
106
107        size
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use crate::{
114        elements::{column::Column, force_break::ForceBreak, text::Text},
115        fonts::builtin::BuiltinFont,
116        test_utils::FranticJumper,
117    };
118
119    use super::*;
120
121    #[test]
122    fn test_basic() {
123        use crate::test_utils::binary_snapshots::*;
124        use insta::*;
125
126        let bytes = test_element_bytes(TestElementParams::unbreakable(), |mut callback| {
127            let font = BuiltinFont::courier(callback.pdf());
128
129            let element = Text::new("test", &font, 11.);
130            let element = element.debug(1).show_max_width();
131
132            callback.call(
133                &Link {
134                    element,
135                    target: LinkTarget::Uri("https://github.com/laser-pdf/laser-pdf"),
136                }
137                .debug(0)
138                .show_max_width()
139                .show_last_location_max_height(),
140            );
141        });
142        assert_binary_snapshot!(".pdf", bytes);
143    }
144
145    #[test]
146    fn test_no_expand() {
147        use crate::test_utils::binary_snapshots::*;
148        use insta::*;
149
150        let bytes = test_element_bytes(
151            TestElementParams::breakable().no_expand(),
152            |mut callback| {
153                let font = BuiltinFont::courier(callback.pdf());
154
155                let element = Text::new("test", &font, 11.);
156                let element = element.debug(1).show_max_width();
157
158                callback.call(
159                    &Link {
160                        element,
161                        target: LinkTarget::Uri("https://github.com/laser-pdf/laser-pdf"),
162                    }
163                    .debug(0)
164                    .show_max_width()
165                    .show_last_location_max_height(),
166                );
167            },
168        );
169        assert_binary_snapshot!(".pdf", bytes);
170    }
171
172    #[test]
173    fn test_breaking() {
174        use crate::test_utils::binary_snapshots::*;
175        use insta::*;
176
177        let bytes = test_element_bytes(
178            TestElementParams::breakable().no_expand(),
179            |mut callback| {
180                let font = BuiltinFont::courier(callback.pdf());
181
182                let element = Column::new(|content| {
183                    content
184                        .add(&Text::new("test", &font, 11.))?
185                        .add(&ForceBreak)?
186                        .add(&Text::new("test", &font, 11.))?
187                        .add(&ForceBreak)?
188                        .add(&ForceBreak)?
189                        .add(&Text::new("test", &font, 11.))?;
190                    None
191                });
192                let element = element.debug(1).show_max_width();
193
194                callback.call(
195                    &Link {
196                        element,
197                        target: LinkTarget::Uri("https://github.com/laser-pdf/laser-pdf"),
198                    }
199                    .debug(0)
200                    .show_max_width()
201                    .show_last_location_max_height(),
202                );
203            },
204        );
205        assert_binary_snapshot!(".pdf", bytes);
206    }
207
208    #[test]
209    fn test_frantic_breaking() {
210        use crate::test_utils::binary_snapshots::*;
211        use insta::*;
212
213        let bytes = test_element_bytes(TestElementParams::breakable().no_expand(), |callback| {
214            let element = FranticJumper {
215                jumps: vec![(2, Some(8.)), (0, None)],
216                size: ElementSize {
217                    width: Some(12.),
218                    height: Some(4.),
219                },
220            };
221            let element = element.debug(1).show_max_width();
222
223            callback.call(
224                &Link {
225                    element,
226                    target: LinkTarget::Uri("https://github.com/laser-pdf/laser-pdf"),
227                }
228                .debug(0)
229                .show_max_width()
230                .show_last_location_max_height(),
231            );
232        });
233        assert_binary_snapshot!(".pdf", bytes);
234    }
235}