Skip to main content

laser_pdf/elements/
text.rs

1use crate::{
2    elements::rich_text::{RichText, Span},
3    fonts::Font,
4    *,
5};
6
7pub use elements::rich_text::TextAlign;
8
9/// A text element that renders text content with various styling options.
10pub struct Text<'a, F: Font> {
11    /// The text content to render
12    pub text: &'a str,
13    /// Font reference
14    pub font: &'a F,
15    /// Font size in points
16    pub size: f32,
17    /// Text color as RGBA (default: black 0x00_00_00_FF)
18    pub color: u32,
19    /// Whether to underline the text
20    pub underline: bool,
21    /// Additional spacing between characters
22    pub extra_character_spacing: f32,
23    /// Additional spacing between words
24    pub extra_word_spacing: f32,
25    /// Additional line height
26    pub extra_line_height: f32,
27    /// Text alignment
28    pub align: TextAlign,
29    /// Link to be added as a link annotation
30    pub link: Option<LinkTarget<'a>>,
31}
32
33impl<'a, F: Font> Text<'a, F> {
34    pub fn new(text: &'a str, font: &'a F, size: f32) -> Self {
35        Text {
36            text,
37            font,
38            size,
39            color: 0x00_00_00_FF,
40            underline: false,
41            extra_character_spacing: 0.,
42            extra_word_spacing: 0.,
43            extra_line_height: 0.,
44            align: TextAlign::Left,
45            link: None,
46        }
47    }
48
49    fn as_rich_text(&self) -> RichText<std::iter::Once<Span<'a, F>>> {
50        RichText {
51            spans: std::iter::once(Span {
52                text: self.text,
53                font: self.font,
54                size: self.size,
55                color: self.color,
56                underline: self.underline,
57                extra_character_spacing: self.extra_character_spacing,
58                extra_word_spacing: self.extra_word_spacing,
59                extra_line_height: self.extra_line_height, // TODO: thread this through to the pieces
60                link: self.link,
61            }),
62            align: self.align,
63        }
64    }
65}
66
67impl<'a, F: Font + 'a> Element for Text<'a, F> {
68    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
69        self.as_rich_text().first_location_usage(ctx)
70    }
71
72    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
73        self.as_rich_text().measure(ctx)
74    }
75
76    fn draw(&self, ctx: DrawCtx) -> ElementSize {
77        self.as_rich_text().draw(ctx)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use elements::column::Column;
84    use fonts::truetype::TruetypeFont;
85    use insta::*;
86
87    use crate::fonts::builtin::BuiltinFont;
88    use crate::test_utils::binary_snapshots::*;
89
90    use super::*;
91
92    const FONT: &[u8] = include_bytes!("../../assets/fonts/Kenney Bold.ttf");
93
94    #[test]
95    fn test_multi_page() {
96        let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
97            let font = BuiltinFont::courier(callback.pdf());
98
99            let content = Text::new(LOREM_IPSUM, &font, 32.);
100            let content = content.debug(0);
101
102            callback.call(&content);
103        });
104        assert_binary_snapshot!(".pdf", bytes);
105    }
106
107    #[test]
108    fn test_truetype() {
109        let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
110            let font = TruetypeFont::new(callback.pdf(), FONT);
111
112            let content = Text::new(LOREM_IPSUM, &font, 32.);
113            let content = content.debug(0);
114
115            callback.call(&content);
116        });
117        assert_binary_snapshot!(".pdf", bytes);
118    }
119
120    #[test]
121    fn test_truetype_trailing_whitespace() {
122        let mut params = TestElementParams::breakable();
123        params.width.expand = false;
124
125        let bytes = test_element_bytes(params, |mut callback| {
126            let font = TruetypeFont::new(callback.pdf(), FONT);
127
128            let content = Text::new("Whitespace ", &font, 32.);
129            let content = content.debug(0);
130
131            callback.call(&content);
132        });
133        assert_binary_snapshot!(".pdf", bytes);
134    }
135
136    #[test]
137    fn test_truetype_extra_spacing() {
138        let mut params = TestElementParams::breakable();
139        params.width.expand = false;
140
141        let bytes = test_element_bytes(params, |mut callback| {
142            let font = TruetypeFont::new(callback.pdf(), FONT);
143
144            callback.call(&Column {
145                gap: 12.,
146                collapse: false,
147                content: |content| {
148                    let normal = Text::new("Hello, World", &font, 32.);
149
150                    let character_spacing = Text {
151                        extra_character_spacing: 16.,
152                        ..Text::new("Hello, World", &font, 32.)
153                    };
154
155                    let word_spacing = Text {
156                        extra_word_spacing: 16.,
157                        ..Text::new("Hello, World", &font, 32.)
158                    };
159
160                    let both = Text {
161                        extra_character_spacing: 16.,
162                        extra_word_spacing: 16.,
163                        ..Text::new("Hello, World", &font, 32.)
164                    };
165
166                    content
167                        .add(&normal.debug(0).show_max_width())?
168                        .add(&character_spacing.debug(1).show_max_width())?
169                        .add(&word_spacing.debug(2).show_max_width())?
170                        .add(&both.debug(3).show_max_width())?;
171
172                    None
173                },
174            });
175        });
176        assert_binary_snapshot!(".pdf", bytes);
177    }
178
179    #[test]
180    fn test_truetype_soft_hyphen() {
181        let mut params = TestElementParams::breakable();
182        params.width.expand = false;
183
184        let bytes = test_element_bytes(params, |mut callback| {
185            let font = TruetypeFont::new(callback.pdf(), FONT);
186
187            callback.call(&Column {
188                gap: 12.,
189                collapse: false,
190                content: |content| {
191                    let a = Text::new("Hello\u{00AD}Wrld", &font, 32.);
192                    let b = Text::new("A Hello\u{00AD}Wrld", &font, 32.);
193                    let c = Text::new("A\u{00A0}Hello\u{00AD}Wrld", &font, 32.);
194                    let d = Text::new("Hello\u{00AD}Wrld\u{00AD}", &font, 32.);
195
196                    content
197                        .add(&Padding::right(100., a.debug(0).show_max_width()))?
198                        .add(&Padding::right(120., b.debug(0).show_max_width()))?
199                        .add(&Padding::right(120., c.debug(0).show_max_width()))?
200                        .add(&Padding::right(20., d.debug(0).show_max_width()))?;
201
202                    None
203                },
204            });
205        });
206        assert_binary_snapshot!(".pdf", bytes);
207    }
208}