laser-pdf 0.5.0

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

pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut \
    labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco \
    laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in \
    voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat \
    non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

#[derive(Clone, Copy)]
pub struct TestElementParams {
    pub width: WidthConstraint,
    pub first_height: f32,
    pub preferred_height: Option<f32>,
    pub breakable: Option<TestElementParamsBreakable>,
    pub pos: (f32, f32),
    pub page_size: (f32, f32),
}

#[derive(Clone, Copy)]
pub struct TestElementParamsBreakable {
    pub preferred_height_break_count: u32,
    pub full_height: f32,
}

impl TestElementParams {
    pub const DEFAULT_MAX_WIDTH: f32 = 210. - 2. * 8.;
    pub const DEFAULT_FULL_HEIGHT: f32 = 297. - 2. * 16.;
    pub const DEFAULT_REDUCED_HEIGHT: f32 = 100.;

    pub fn unbreakable() -> Self {
        TestElementParams {
            width: WidthConstraint {
                max: Self::DEFAULT_MAX_WIDTH,
                expand: true,
            },
            first_height: Self::DEFAULT_FULL_HEIGHT,
            preferred_height: None,
            breakable: None,
            pos: (8., 297. - 16.),
            page_size: (210., 297.),
        }
    }

    pub fn breakable() -> Self {
        TestElementParams {
            width: WidthConstraint {
                max: Self::DEFAULT_MAX_WIDTH,
                expand: true,
            },
            first_height: Self::DEFAULT_REDUCED_HEIGHT,
            preferred_height: None,
            breakable: Some(TestElementParamsBreakable {
                preferred_height_break_count: 0,
                full_height: Self::DEFAULT_FULL_HEIGHT,
            }),
            pos: (8., 297. - 16.),
            page_size: (210., 297.),
        }
    }

    pub fn no_expand(mut self) -> Self {
        self.width.expand = false;
        self
    }
}

#[derive(Debug)]
struct MeasureStats {
    break_count: u32,
    extra_location_min_height: Option<f32>,
    size: ElementSize,
}

#[derive(Debug)]
pub struct DrawStats {
    break_count: u32,
    size: ElementSize,
}

struct Doc {
    params: TestElementParams,
    pdf: Pdf,
    text_pieces_cache: TextPiecesCache,
}

impl Doc {
    fn new(params: TestElementParams) -> Self {
        let mut pdf = Pdf::new(Metadata::fixed());
        pdf.add_page(params.page_size);

        Doc {
            params,
            pdf,
            text_pieces_cache: TextPiecesCache::new(),
        }
    }

    fn first_location_usage(&mut self, build: impl Fn(Callback)) -> FirstLocationUsage {
        let mut first_location_usage = None;

        let callback = Callback {
            doc: self,
            pass: CallbackPass::FirstLocationUsage {
                out: &mut first_location_usage,
            },
        };

        build(callback);

        first_location_usage.unwrap()
    }

    fn measure(&mut self, build: impl Fn(Callback)) -> MeasureStats {
        let mut stats = None;

        let callback = Callback {
            doc: self,
            pass: CallbackPass::Measure { out: &mut stats },
        };

        build(callback);

        stats.unwrap()
    }

    fn draw(&mut self, build: impl Fn(Callback)) -> DrawStats {
        let mut stats = None;

        let callback = Callback {
            doc: self,
            pass: CallbackPass::Draw { out: &mut stats },
        };

        build(callback);

        stats.unwrap()
    }
}

enum CallbackPass<'a> {
    FirstLocationUsage {
        out: &'a mut Option<FirstLocationUsage>,
    },
    Measure {
        out: &'a mut Option<MeasureStats>,
    },
    Draw {
        out: &'a mut Option<DrawStats>,
    },
}

pub struct Callback<'a> {
    doc: &'a mut Doc,
    pass: CallbackPass<'a>,
}

impl<'a> Callback<'a> {
    pub fn pdf(&mut self) -> &mut Pdf {
        &mut self.doc.pdf
    }

    pub fn call(self, element: &impl Element) {
        match self.pass {
            CallbackPass::FirstLocationUsage { out } => {
                let params = &self.doc.params;

                *out = Some(element.first_location_usage(FirstLocationUsageCtx {
                    text_pieces_cache: &self.doc.text_pieces_cache,
                    width: params.width,
                    first_height: params.first_height,
                    full_height: params.breakable.as_ref().unwrap().full_height,
                }));
            }
            CallbackPass::Measure { out } => {
                let mut break_count = 0;
                let mut extra_location_min_height = None;

                let ctx = MeasureCtx {
                    text_pieces_cache: &self.doc.text_pieces_cache,
                    width: self.doc.params.width,
                    first_height: self.doc.params.first_height,
                    breakable: self
                        .doc
                        .params
                        .breakable
                        .as_ref()
                        .map(|b| BreakableMeasure {
                            full_height: b.full_height,
                            break_count: &mut break_count,
                            extra_location_min_height: &mut extra_location_min_height,
                        }),
                };

                let size = element.measure(ctx);

                *out = Some(MeasureStats {
                    break_count,
                    extra_location_min_height,
                    size,
                })
            }
            CallbackPass::Draw { out } => {
                let params = &self.doc.params;
                let pdf = &mut self.doc.pdf;

                let mut page_idx = 0;

                let next_draw_pos = &mut |pdf: &mut Pdf, location_idx, _height| {
                    while page_idx <= location_idx {
                        pdf.add_page(self.doc.params.page_size);
                        page_idx += 1;
                    }

                    Location {
                        page_idx: location_idx as usize + 1,
                        layer_idx: 0,
                        pos: params.pos,
                        scale_factor: 1.,
                    }
                };

                let first_pos = (
                    params.pos.0,
                    params.breakable.as_ref().map_or(params.pos.1, |b| {
                        params.pos.1 - (b.full_height - params.first_height)
                    }),
                );

                let ctx = DrawCtx {
                    pdf,
                    text_pieces_cache: &self.doc.text_pieces_cache,
                    width: params.width,
                    location: Location {
                        page_idx: 0,
                        layer_idx: 0,
                        pos: first_pos,
                        scale_factor: 1.,
                    },

                    first_height: params.first_height,
                    preferred_height: params.preferred_height,

                    breakable: params.breakable.as_ref().map(|b| BreakableDraw {
                        full_height: b.full_height,
                        preferred_height_break_count: b.preferred_height_break_count,
                        do_break: next_draw_pos,
                    }),
                };

                let size = element.draw(ctx);

                *out = Some(DrawStats {
                    break_count: page_idx,
                    size,
                });
            }
        }
    }
}

pub fn test_element_bytes(params: TestElementParams, build_element: impl Fn(Callback)) -> Vec<u8> {
    let measure = Doc::new(params).measure(&build_element);

    let mut draw_doc = Doc::new(params);

    let draw = draw_doc.draw(&build_element);

    assert_eq!(measure.size.width, draw.size.width);

    let preferred_break_count = params
        .breakable
        .as_ref()
        .map(|b| b.preferred_height_break_count)
        .unwrap_or(0);

    if measure.extra_location_min_height.is_some() && preferred_break_count > measure.break_count {
        assert_eq!(draw.break_count, preferred_break_count);
        assert!(draw.size.height >= measure.extra_location_min_height);
        assert!(
            draw.size.height
                <= max_optional_size(measure.extra_location_min_height, params.preferred_height)
        );
    } else {
        let preferred = (preferred_break_count, params.preferred_height);
        let measured = (measure.break_count, measure.size.height);
        let drawn = (draw.break_count, draw.size.height);

        type Thing = (u32, Option<f32>);

        fn max(a: Thing, b: Thing) -> Thing {
            // Beware of wild NaNs, they bite!
            if a > b { a } else { b }
        }

        assert!(drawn >= measured);
        assert!(drawn <= max(preferred, measured));
    }

    let restricted_draw = Doc::new(TestElementParams {
        preferred_height: measure.size.height,
        breakable: params
            .breakable
            .as_ref()
            .map(|b| TestElementParamsBreakable {
                preferred_height_break_count: measure.break_count,
                ..*b
            }),
        ..params
    })
    .draw(&build_element);

    assert_eq!(measure.break_count, restricted_draw.break_count);
    assert_eq!(measure.size, restricted_draw.size);

    if let Some(breakable) = params.breakable {
        let full_height = breakable.full_height;
        let first_location_usage = Doc::new(params).first_location_usage(&build_element);

        match first_location_usage {
            FirstLocationUsage::NoneHeight => {
                assert!(measure.size.height.is_none());
                assert_eq!(measure.break_count, 0);
            }
            FirstLocationUsage::WillUse => {
                assert!(measure.size.height.is_some() || measure.break_count >= 1);
            }
            FirstLocationUsage::WillSkip => {
                assert!(measure.break_count >= 1);

                let _skipped_measure = Doc::new(TestElementParams {
                    first_height: full_height,
                    ..params
                })
                .measure(&build_element);

                // TODO: insert draw here

                // assert_eq!(skipped_measure.break_count + 1, measure.break_count);
                assert_ne!(params.first_height, full_height);
            }
        }
    }

    draw_doc.pdf.finish()
}