Skip to main content

laser_pdf/test_utils/
binary_snapshots.rs

1use crate::{utils::max_optional_size, *};
2
3pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut \
4    labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco \
5    laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in \
6    voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat \
7    non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
8
9#[derive(Clone, Copy)]
10pub struct TestElementParams {
11    pub width: WidthConstraint,
12    pub first_height: f32,
13    pub preferred_height: Option<f32>,
14    pub breakable: Option<TestElementParamsBreakable>,
15    pub pos: (f32, f32),
16    pub page_size: (f32, f32),
17}
18
19#[derive(Clone, Copy)]
20pub struct TestElementParamsBreakable {
21    pub preferred_height_break_count: u32,
22    pub full_height: f32,
23}
24
25impl TestElementParams {
26    pub const DEFAULT_MAX_WIDTH: f32 = 210. - 2. * 8.;
27    pub const DEFAULT_FULL_HEIGHT: f32 = 297. - 2. * 16.;
28    pub const DEFAULT_REDUCED_HEIGHT: f32 = 100.;
29
30    pub fn unbreakable() -> Self {
31        TestElementParams {
32            width: WidthConstraint {
33                max: Self::DEFAULT_MAX_WIDTH,
34                expand: true,
35            },
36            first_height: Self::DEFAULT_FULL_HEIGHT,
37            preferred_height: None,
38            breakable: None,
39            pos: (8., 297. - 16.),
40            page_size: (210., 297.),
41        }
42    }
43
44    pub fn breakable() -> Self {
45        TestElementParams {
46            width: WidthConstraint {
47                max: Self::DEFAULT_MAX_WIDTH,
48                expand: true,
49            },
50            first_height: Self::DEFAULT_REDUCED_HEIGHT,
51            preferred_height: None,
52            breakable: Some(TestElementParamsBreakable {
53                preferred_height_break_count: 0,
54                full_height: Self::DEFAULT_FULL_HEIGHT,
55            }),
56            pos: (8., 297. - 16.),
57            page_size: (210., 297.),
58        }
59    }
60
61    pub fn no_expand(mut self) -> Self {
62        self.width.expand = false;
63        self
64    }
65}
66
67#[derive(Debug)]
68struct MeasureStats {
69    break_count: u32,
70    extra_location_min_height: Option<f32>,
71    size: ElementSize,
72}
73
74#[derive(Debug)]
75pub struct DrawStats {
76    break_count: u32,
77    size: ElementSize,
78}
79
80struct Doc {
81    params: TestElementParams,
82    pdf: Pdf,
83    text_pieces_cache: TextPiecesCache,
84}
85
86impl Doc {
87    fn new(params: TestElementParams) -> Self {
88        let mut pdf = Pdf::new();
89        pdf.add_page(params.page_size);
90
91        Doc {
92            params,
93            pdf,
94            text_pieces_cache: TextPiecesCache::new(),
95        }
96    }
97
98    fn first_location_usage(&mut self, build: impl Fn(Callback)) -> FirstLocationUsage {
99        let mut first_location_usage = None;
100
101        let callback = Callback {
102            doc: self,
103            pass: CallbackPass::FirstLocationUsage {
104                out: &mut first_location_usage,
105            },
106        };
107
108        build(callback);
109
110        first_location_usage.unwrap()
111    }
112
113    fn measure(&mut self, build: impl Fn(Callback)) -> MeasureStats {
114        let mut stats = None;
115
116        let callback = Callback {
117            doc: self,
118            pass: CallbackPass::Measure { out: &mut stats },
119        };
120
121        build(callback);
122
123        stats.unwrap()
124    }
125
126    fn draw(&mut self, build: impl Fn(Callback)) -> DrawStats {
127        let mut stats = None;
128
129        let callback = Callback {
130            doc: self,
131            pass: CallbackPass::Draw { out: &mut stats },
132        };
133
134        build(callback);
135
136        stats.unwrap()
137    }
138}
139
140enum CallbackPass<'a> {
141    FirstLocationUsage {
142        out: &'a mut Option<FirstLocationUsage>,
143    },
144    Measure {
145        out: &'a mut Option<MeasureStats>,
146    },
147    Draw {
148        out: &'a mut Option<DrawStats>,
149    },
150}
151
152pub struct Callback<'a> {
153    doc: &'a mut Doc,
154    pass: CallbackPass<'a>,
155}
156
157impl<'a> Callback<'a> {
158    pub fn pdf(&mut self) -> &mut Pdf {
159        &mut self.doc.pdf
160    }
161
162    pub fn call(self, element: &impl Element) {
163        match self.pass {
164            CallbackPass::FirstLocationUsage { out } => {
165                let params = &self.doc.params;
166
167                *out = Some(element.first_location_usage(FirstLocationUsageCtx {
168                    text_pieces_cache: &self.doc.text_pieces_cache,
169                    width: params.width,
170                    first_height: params.first_height,
171                    full_height: params.breakable.as_ref().unwrap().full_height,
172                }));
173            }
174            CallbackPass::Measure { out } => {
175                let mut break_count = 0;
176                let mut extra_location_min_height = None;
177
178                let ctx = MeasureCtx {
179                    text_pieces_cache: &self.doc.text_pieces_cache,
180                    width: self.doc.params.width,
181                    first_height: self.doc.params.first_height,
182                    breakable: self
183                        .doc
184                        .params
185                        .breakable
186                        .as_ref()
187                        .map(|b| BreakableMeasure {
188                            full_height: b.full_height,
189                            break_count: &mut break_count,
190                            extra_location_min_height: &mut extra_location_min_height,
191                        }),
192                };
193
194                let size = element.measure(ctx);
195
196                *out = Some(MeasureStats {
197                    break_count,
198                    extra_location_min_height,
199                    size,
200                })
201            }
202            CallbackPass::Draw { out } => {
203                let params = &self.doc.params;
204                let pdf = &mut self.doc.pdf;
205
206                let mut page_idx = 0;
207
208                let next_draw_pos = &mut |pdf: &mut Pdf, location_idx, _height| {
209                    while page_idx <= location_idx {
210                        pdf.add_page(self.doc.params.page_size);
211                        page_idx += 1;
212                    }
213
214                    Location {
215                        page_idx: location_idx as usize + 1,
216                        layer_idx: 0,
217                        pos: params.pos,
218                        scale_factor: 1.,
219                    }
220                };
221
222                let first_pos = (
223                    params.pos.0,
224                    params.breakable.as_ref().map_or(params.pos.1, |b| {
225                        params.pos.1 - (b.full_height - params.first_height)
226                    }),
227                );
228
229                let ctx = DrawCtx {
230                    pdf,
231                    text_pieces_cache: &self.doc.text_pieces_cache,
232                    width: params.width,
233                    location: Location {
234                        page_idx: 0,
235                        layer_idx: 0,
236                        pos: first_pos,
237                        scale_factor: 1.,
238                    },
239
240                    first_height: params.first_height,
241                    preferred_height: params.preferred_height,
242
243                    breakable: params.breakable.as_ref().map(|b| BreakableDraw {
244                        full_height: b.full_height,
245                        preferred_height_break_count: b.preferred_height_break_count,
246                        do_break: next_draw_pos,
247                    }),
248                };
249
250                let size = element.draw(ctx);
251
252                *out = Some(DrawStats {
253                    break_count: page_idx,
254                    size,
255                });
256            }
257        }
258    }
259}
260
261pub fn test_element_bytes(params: TestElementParams, build_element: impl Fn(Callback)) -> Vec<u8> {
262    let measure = Doc::new(params).measure(&build_element);
263
264    let mut draw_doc = Doc::new(params);
265
266    let draw = draw_doc.draw(&build_element);
267
268    assert_eq!(measure.size.width, draw.size.width);
269
270    let preferred_break_count = params
271        .breakable
272        .as_ref()
273        .map(|b| b.preferred_height_break_count)
274        .unwrap_or(0);
275
276    if measure.extra_location_min_height.is_some() && preferred_break_count > measure.break_count {
277        assert_eq!(draw.break_count, preferred_break_count);
278        assert!(draw.size.height >= measure.extra_location_min_height);
279        assert!(
280            draw.size.height
281                <= max_optional_size(measure.extra_location_min_height, params.preferred_height)
282        );
283    } else {
284        let preferred = (preferred_break_count, params.preferred_height);
285        let measured = (measure.break_count, measure.size.height);
286        let drawn = (draw.break_count, draw.size.height);
287
288        type Thing = (u32, Option<f32>);
289
290        fn max(a: Thing, b: Thing) -> Thing {
291            // Beware of wild NaNs, they bite!
292            if a > b { a } else { b }
293        }
294
295        assert!(drawn >= measured);
296        assert!(drawn <= max(preferred, measured));
297    }
298
299    let restricted_draw = Doc::new(TestElementParams {
300        preferred_height: measure.size.height,
301        breakable: params
302            .breakable
303            .as_ref()
304            .map(|b| TestElementParamsBreakable {
305                preferred_height_break_count: measure.break_count,
306                ..*b
307            }),
308        ..params
309    })
310    .draw(&build_element);
311
312    assert_eq!(measure.break_count, restricted_draw.break_count);
313    assert_eq!(measure.size, restricted_draw.size);
314
315    if let Some(breakable) = params.breakable {
316        let full_height = breakable.full_height;
317        let first_location_usage = Doc::new(params).first_location_usage(&build_element);
318
319        match first_location_usage {
320            FirstLocationUsage::NoneHeight => {
321                assert!(measure.size.height.is_none());
322                assert_eq!(measure.break_count, 0);
323            }
324            FirstLocationUsage::WillUse => {
325                assert!(measure.size.height.is_some() || measure.break_count >= 1);
326            }
327            FirstLocationUsage::WillSkip => {
328                assert!(measure.break_count >= 1);
329
330                let _skipped_measure = Doc::new(TestElementParams {
331                    first_height: full_height,
332                    ..params
333                })
334                .measure(&build_element);
335
336                // TODO: insert draw here
337
338                // assert_eq!(skipped_measure.break_count + 1, measure.break_count);
339                assert_ne!(params.first_height, full_height);
340            }
341        }
342    }
343
344    draw_doc.pdf.finish()
345}