laser_pdf/elements/
link.rs1use crate::{
2 utils::{add_link_annotation, mm_to_pt},
3 *,
4};
5
6pub 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 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}