laser_pdf/elements/
shrink_to_fit.rs

1use crate::*;
2
3/// Shrinks the element to fit within the given `first_height`, as long as that is >= `min_height`.
4/// In a breakable context: if `first_height` is less than `min_height` a pre-break happens first,
5/// in which case the element will be shrunk to fit the `full_height`. In an unbreakable context it
6/// will simply overflow such that the element is never scaled smaller than the `min_height`.
7pub struct ShrinkToFit<E: Element> {
8    pub element: E,
9    pub min_height: f32,
10}
11
12struct Layout {
13    pre_break: bool,
14    scale_factor: f32,
15    size: ElementSize,
16    scaled_size: ElementSize,
17    height: f32,
18}
19
20impl<E: Element> ShrinkToFit<E> {
21    fn layout(
22        &self,
23        text_pieces_cache: &TextPiecesCache,
24        width: WidthConstraint,
25        first_height: f32,
26        full_height: Option<f32>,
27    ) -> Layout {
28        let pre_break;
29
30        let available_height = if first_height >= self.min_height {
31            pre_break = false;
32
33            first_height
34        } else {
35            pre_break = full_height.is_some();
36
37            // We prefer overflowing if min_height is not available. If available_height were to
38            // become negative it would lead to the element being flipped.
39            full_height.unwrap_or(first_height).max(self.min_height)
40        };
41
42        let size = self.element.measure(MeasureCtx {
43            text_pieces_cache,
44            width,
45            first_height: available_height,
46            breakable: None,
47        });
48
49        let height = size
50            .height
51            .map(|h| {
52                if h <= available_height {
53                    available_height
54                } else {
55                    h
56                }
57            })
58            .unwrap_or(available_height);
59
60        let scale_factor = size
61            .height
62            .map(|h| {
63                if h <= available_height {
64                    1.
65                } else {
66                    available_height / h
67                }
68            })
69            .unwrap_or(1.);
70
71        let scaled_size = ElementSize {
72            width: size.width.map(|w| w * scale_factor),
73            height: size.height.map(|h| h * scale_factor),
74        };
75
76        Layout {
77            pre_break,
78            scale_factor,
79            height,
80            size,
81            scaled_size,
82        }
83    }
84}
85
86impl<E: Element> Element for ShrinkToFit<E> {
87    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
88        let layout = self.layout(
89            ctx.text_pieces_cache,
90            ctx.width,
91            ctx.first_height,
92            Some(ctx.full_height),
93        );
94
95        if layout.pre_break {
96            FirstLocationUsage::WillSkip
97        } else if layout.size.height.is_some() {
98            FirstLocationUsage::WillUse
99        } else {
100            FirstLocationUsage::NoneHeight
101        }
102    }
103
104    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
105        let layout = self.layout(
106            ctx.text_pieces_cache,
107            ctx.width,
108            ctx.first_height,
109            ctx.breakable.as_ref().map(|b| b.full_height),
110        );
111
112        if layout.pre_break {
113            *ctx.breakable.unwrap().break_count = 1;
114        }
115
116        layout.scaled_size
117    }
118
119    fn draw(&self, ctx: DrawCtx) -> ElementSize {
120        let layout = self.layout(
121            ctx.text_pieces_cache,
122            ctx.width,
123            ctx.first_height,
124            ctx.breakable.as_ref().map(|b| b.full_height),
125        );
126
127        let location;
128
129        if layout.pre_break {
130            let breakable = ctx.breakable.unwrap();
131
132            location = (breakable.do_break)(ctx.pdf, 0, None);
133        } else {
134            location = ctx.location;
135        }
136
137        location
138            .layer(ctx.pdf)
139            .save_state()
140            .transform(utils::scale(layout.scale_factor));
141
142        self.element.draw(DrawCtx {
143            pdf: ctx.pdf,
144            text_pieces_cache: ctx.text_pieces_cache,
145            location: Location {
146                pos: (
147                    location.pos.0 / layout.scale_factor,
148                    location.pos.1 / layout.scale_factor,
149                ),
150                scale_factor: location.scale_factor * layout.scale_factor,
151                ..location.clone()
152            },
153            width: ctx.width,
154            first_height: layout.height,
155            preferred_height: None,
156            breakable: None,
157        });
158
159        location.layer(ctx.pdf).restore_state();
160
161        layout.scaled_size
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use elements::{align_location_bottom::AlignLocationBottom, styled_box::StyledBox};
168    use insta::assert_binary_snapshot;
169
170    use super::*;
171    use crate::{
172        elements::text::Text, fonts::builtin::BuiltinFont, test_utils::binary_snapshots::*,
173    };
174
175    #[test]
176    fn test_basic() {
177        let bytes = test_element_bytes(
178            TestElementParams {
179                first_height: 10.,
180                ..TestElementParams::breakable()
181            },
182            |mut callback| {
183                let font = BuiltinFont::courier(callback.pdf());
184                let text = Text::basic("TEST", &font, 100.);
185                let text = text
186                    .debug(1)
187                    .show_max_width()
188                    .show_last_location_max_height();
189
190                let shrink_to_fit = ShrinkToFit {
191                    element: text,
192                    min_height: 9.,
193                };
194                let shrink_to_fit = &shrink_to_fit
195                    .debug(0)
196                    .show_max_width()
197                    .show_last_location_max_height();
198
199                callback.call(shrink_to_fit);
200            },
201        );
202        assert_binary_snapshot!(".pdf", bytes);
203    }
204
205    #[test]
206    fn test_unbreakable_negative_first_height() {
207        let bytes = test_element_bytes(
208            TestElementParams {
209                first_height: -10.,
210                ..TestElementParams::unbreakable()
211            },
212            |mut callback| {
213                let font = BuiltinFont::courier(callback.pdf());
214                let text = Text::basic("TEST", &font, 100.);
215                let text = text
216                    .debug(1)
217                    .show_max_width()
218                    .show_last_location_max_height();
219
220                let shrink_to_fit = ShrinkToFit {
221                    element: text,
222                    min_height: 9.,
223                };
224                let shrink_to_fit = &shrink_to_fit
225                    .debug(0)
226                    .show_max_width()
227                    .show_last_location_max_height();
228
229                callback.call(shrink_to_fit);
230            },
231        );
232        assert_binary_snapshot!(".pdf", bytes);
233    }
234
235    #[test]
236    fn test_pre_break() {
237        let bytes = test_element_bytes(
238            TestElementParams {
239                first_height: 5.,
240                ..TestElementParams::breakable()
241            },
242            |mut callback| {
243                let font = BuiltinFont::courier(callback.pdf());
244                let text = Text::basic("T E S T", &font, 1024.);
245                let text = text
246                    .debug(1)
247                    .show_max_width()
248                    .show_last_location_max_height();
249
250                let shrink_to_fit = ShrinkToFit {
251                    element: text,
252                    min_height: 10.,
253                };
254                let shrink_to_fit = &shrink_to_fit
255                    .debug(0)
256                    .show_max_width()
257                    .show_last_location_max_height();
258
259                callback.call(shrink_to_fit);
260            },
261        );
262        assert_binary_snapshot!(".pdf", bytes);
263    }
264
265    #[test]
266    fn test_align_location_bottom() {
267        let bytes = test_element_bytes(
268            TestElementParams {
269                first_height: 20.,
270                ..TestElementParams::breakable()
271            },
272            |mut callback| {
273                let font = BuiltinFont::courier(callback.pdf());
274                let text = Text::basic("Test", &font, 20.);
275                let text = text
276                    .debug(1)
277                    .show_max_width()
278                    .show_last_location_max_height();
279
280                let bottom = AlignLocationBottom(text);
281                let bottom = bottom.debug(2);
282
283                let shrink_to_fit = ShrinkToFit {
284                    element: bottom,
285                    min_height: 10.,
286                };
287                let shrink_to_fit = &shrink_to_fit
288                    .debug(0)
289                    .show_max_width()
290                    .show_last_location_max_height();
291
292                callback.call(shrink_to_fit);
293            },
294        );
295        assert_binary_snapshot!(".pdf", bytes);
296    }
297
298    #[test]
299    fn test_layers() {
300        let bytes = test_element_bytes(
301            TestElementParams {
302                first_height: 20.,
303                ..TestElementParams::breakable()
304            },
305            |mut callback| {
306                let font = BuiltinFont::courier(callback.pdf());
307                let text = Text::basic("Test", &font, 100.);
308                let text = text
309                    .debug(1)
310                    .show_max_width()
311                    .show_last_location_max_height();
312
313                let wrapper = StyledBox {
314                    outline: Some(LineStyle {
315                        thickness: 12.,
316                        color: 0x00_00_00_FF,
317                        dash_pattern: None,
318                        cap_style: LineCapStyle::Round,
319                    }),
320                    ..StyledBox::new(text)
321                };
322                let wrapper = wrapper.debug(2);
323
324                let shrink_to_fit = ShrinkToFit {
325                    element: wrapper,
326                    min_height: 10.,
327                };
328                let shrink_to_fit = &shrink_to_fit
329                    .debug(0)
330                    .show_max_width()
331                    .show_last_location_max_height();
332
333                callback.call(shrink_to_fit);
334            },
335        );
336        assert_binary_snapshot!(".pdf", bytes);
337    }
338
339    #[test]
340    fn test_nested_layers() {
341        let bytes = test_element_bytes(
342            TestElementParams {
343                first_height: 30.,
344                ..TestElementParams::breakable()
345            },
346            |mut callback| {
347                let font = BuiltinFont::courier(callback.pdf());
348                let text = Text::basic("Test", &font, 100.);
349                let text = text
350                    .debug(1)
351                    .show_max_width()
352                    .show_last_location_max_height();
353
354                let wrapper = StyledBox {
355                    outline: Some(LineStyle {
356                        thickness: 10.,
357                        color: 0x00_00_00_FF,
358                        dash_pattern: None,
359                        cap_style: LineCapStyle::Round,
360                    }),
361                    ..StyledBox::new(text)
362                };
363                let wrapper = wrapper.debug(2);
364                let shrink_to_fit = ShrinkToFit {
365                    element: wrapper,
366                    min_height: 10.,
367                };
368
369                let wrapper_1 = StyledBox {
370                    outline: Some(LineStyle {
371                        thickness: 10.,
372                        color: 0xAA_00_00_FF,
373                        dash_pattern: None,
374                        cap_style: LineCapStyle::Round,
375                    }),
376                    ..StyledBox::new(shrink_to_fit)
377                };
378                let wrapper_1 = wrapper_1.debug(3);
379
380                let shrink_to_fit_1 = ShrinkToFit {
381                    element: wrapper_1,
382                    min_height: 10.,
383                };
384                let shrink_to_fit = &shrink_to_fit_1
385                    .debug(0)
386                    .show_max_width()
387                    .show_last_location_max_height();
388
389                callback.call(shrink_to_fit);
390            },
391        );
392        assert_binary_snapshot!(".pdf", bytes);
393    }
394}