laser-pdf 0.5.0

A Rust library for programmatic PDF generation with precise, predictable layout control.
Documentation
use crate::*;

pub struct MinFirstHeight<E: Element> {
    pub element: E,
    pub min_first_height: f32,
}

impl<E: Element> Element for MinFirstHeight<E> {
    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
        use FirstLocationUsage::*;

        let layout = self.layout(
            ctx.text_pieces_cache,
            ctx.width,
            ctx.first_height,
            ctx.full_height,
        );

        if layout.pre_break {
            match self.element.first_location_usage(FirstLocationUsageCtx {
                text_pieces_cache: ctx.text_pieces_cache,
                width: ctx.width,
                first_height: ctx.full_height,
                full_height: ctx.full_height,
            }) {
                NoneHeight => NoneHeight, // collapse
                _ => WillSkip,
            }
        } else {
            match layout.measured {
                Some(measure_output) if measure_output.break_count == 0 => {
                    if measure_output.size.height.is_none() {
                        NoneHeight
                    } else {
                        WillUse
                    }
                }
                _ => self.element.first_location_usage(ctx),
            }
        }
    }

    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
        if let Some(breakable) = ctx.breakable {
            let location_offset;
            let first_height;

            let layout = self.layout(
                ctx.text_pieces_cache,
                ctx.width,
                ctx.first_height,
                breakable.full_height,
            );

            if layout.pre_break {
                first_height = breakable.full_height;
                location_offset = 1;
            } else {
                first_height = ctx.first_height;
                location_offset = 0;
            }

            let size = if let Some(measure_output) = layout.measured {
                *breakable.break_count = measure_output.break_count;
                *breakable.extra_location_min_height = measure_output.extra_location_min_height;
                measure_output.size
            } else {
                self.element.measure(MeasureCtx {
                    text_pieces_cache: ctx.text_pieces_cache,
                    width: ctx.width,
                    first_height,
                    breakable: Some(BreakableMeasure {
                        full_height: breakable.full_height,
                        break_count: breakable.break_count,
                        extra_location_min_height: breakable.extra_location_min_height,
                    }),
                })
            };

            *breakable.break_count += location_offset;
            size
        } else {
            self.element.measure(ctx)
        }
    }

    fn draw(&self, ctx: DrawCtx) -> ElementSize {
        if let Some(breakable) = ctx.breakable {
            let location;
            let first_height;
            let preferred_height;
            let location_offset;

            if self
                .layout(
                    ctx.text_pieces_cache,
                    ctx.width,
                    ctx.first_height,
                    breakable.full_height,
                )
                .pre_break
            {
                location = (breakable.do_break)(ctx.pdf, 0, None);
                location_offset = 1;
                first_height = breakable.full_height;
                preferred_height = if breakable.preferred_height_break_count == 0 {
                    None
                } else {
                    ctx.preferred_height
                };
            } else {
                location = ctx.location;
                location_offset = 0;
                first_height = ctx.first_height;
                preferred_height = ctx.preferred_height;
            }

            self.element.draw(DrawCtx {
                pdf: ctx.pdf,
                text_pieces_cache: ctx.text_pieces_cache,
                location,
                width: ctx.width,
                first_height,
                preferred_height,
                breakable: Some(BreakableDraw {
                    full_height: breakable.full_height,
                    preferred_height_break_count: breakable
                        .preferred_height_break_count
                        .saturating_sub(location_offset),

                    do_break: &mut |pdf, location_idx, height| {
                        (breakable.do_break)(pdf, location_idx + location_offset, height)
                    },
                }),
            })
        } else {
            self.element.draw(ctx)
        }
    }
}

struct MeasureOutput {
    size: ElementSize,
    break_count: u32,
    extra_location_min_height: Option<f32>,
}

struct Layout {
    pre_break: bool,
    measured: Option<MeasureOutput>,
}

impl<E: Element> MinFirstHeight<E> {
    #[inline(always)]
    fn layout(
        &self,
        text_pieces_cache: &TextPiecesCache,
        width: WidthConstraint,
        first_height: f32,
        full_height: f32,
    ) -> Layout {
        let mut measured = None;
        let pre_break = first_height < full_height && first_height < self.min_first_height && {
            let mut break_count = 0;
            let mut extra_location_min_height = None;

            let size = self.element.measure(MeasureCtx {
                text_pieces_cache,
                width,
                first_height,
                breakable: Some(BreakableMeasure {
                    full_height,
                    break_count: &mut break_count,
                    extra_location_min_height: &mut extra_location_min_height,
                }),
            });

            if break_count > 0 {
                true
            } else {
                measured = Some(MeasureOutput {
                    size,
                    break_count,
                    extra_location_min_height,
                });
                false
            }
        };

        Layout {
            pre_break,
            measured,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        elements::{none::NoneElement, ref_element::RefElement, text::Text},
        fonts::builtin::BuiltinFont,
        test_utils::{record_passes::RecordPasses, *},
    };
    use insta::assert_debug_snapshot;

    #[test]
    fn test_unbreakable() {
        let output = test_element(
            TestElementParams {
                width: WidthConstraint {
                    max: 12.,
                    expand: false,
                },
                first_height: 12.,
                breakable: None,
                pos: (7., 20.0),
                ..Default::default()
            },
            |assert, callback| {
                let content = RecordPasses::new(FakeText {
                    lines: 1,
                    line_height: 5.,
                    width: 3.,
                });

                let element = MinFirstHeight {
                    element: RefElement(&content),
                    min_first_height: 10.,
                };

                let ret = callback.call(element);

                if assert {
                    assert_debug_snapshot!(content.into_passes());
                }

                ret
            },
        );

        assert_debug_snapshot!(output);
    }

    #[test]
    fn test_breakable() {
        let output = test_element(
            TestElementParams {
                width: WidthConstraint {
                    max: 12.,
                    expand: false,
                },
                first_height: 12.,
                breakable: Some(TestElementParamsBreakable {
                    preferred_height_break_count: 0,
                    full_height: 15.,
                }),
                pos: (7., 20.0),
                ..Default::default()
            },
            |assert, callback| {
                let content = RecordPasses::new(FakeText {
                    lines: 1,
                    line_height: 5.,
                    width: 3.,
                });

                let element = MinFirstHeight {
                    element: RefElement(&content),
                    min_first_height: 10.,
                };

                let ret = callback.call(element);

                if assert {
                    assert_debug_snapshot!(content.into_passes());
                }

                ret
            },
        );

        assert_debug_snapshot!(output);
    }

    #[test]
    fn test_pre_break() {
        use crate::test_utils::binary_snapshots::*;
        use insta::*;

        let bytes = test_element_bytes(
            TestElementParams {
                first_height: 9.,
                ..TestElementParams::breakable()
            },
            |mut callback| {
                let font = BuiltinFont::courier(callback.pdf());

                let content = Text::new(LOREM_IPSUM, &font, 12.);
                let content = content.debug(1).show_max_width();

                callback.call(
                    &MinFirstHeight {
                        element: content,
                        min_first_height: 10.,
                    }
                    .debug(0)
                    .show_max_width()
                    .show_last_location_max_height(),
                );
            },
        );
        assert_binary_snapshot!(".pdf", bytes);
    }

    #[test]
    fn test_collapse() {
        let output = test_element(
            TestElementParams {
                width: WidthConstraint {
                    max: 12.,
                    expand: false,
                },
                first_height: 9.,
                breakable: Some(TestElementParamsBreakable {
                    preferred_height_break_count: 3,
                    full_height: 15.,
                }),
                pos: (7., 20.0),
                preferred_height: Some(4.),
                ..Default::default()
            },
            |assert, callback| {
                let content = RecordPasses::new(NoneElement);

                let element = MinFirstHeight {
                    element: RefElement(&content),
                    min_first_height: 10.,
                };

                let ret = callback.call(element);

                if assert {
                    assert_debug_snapshot!(content.into_passes());
                }

                ret
            },
        );

        assert_debug_snapshot!(output);
    }
}