Skip to main content

laser_pdf/text/
pieces.rs

1use std::{
2    borrow::{Borrow, Cow},
3    cell::Cell,
4    iter::Peekable,
5    rc::Rc,
6};
7
8use elsa::FrozenMap;
9use icu_properties::LineBreak;
10use icu_segmenter::LineBreakIteratorUtf8;
11
12use crate::{
13    LinkTarget,
14    fonts::{Font, GeneralMetrics, ShapedGlyph},
15};
16
17struct TextPiecesCacheKey<'a> {
18    text: Cow<'a, str>,
19    font_index: usize,
20    size: f32,
21    color: u32,
22    extra_character_spacing: f32,
23    extra_word_spacing: f32,
24    extra_line_height: f32,
25}
26
27#[derive(Hash, PartialEq, Eq)]
28struct OwnedKey(TextPiecesCacheKey<'static>);
29
30impl<'a> Borrow<TextPiecesCacheKey<'a>> for OwnedKey {
31    fn borrow(&self) -> &TextPiecesCacheKey<'a> {
32        &self.0
33    }
34}
35
36impl<'a> PartialEq for TextPiecesCacheKey<'a> {
37    fn eq(&self, other: &Self) -> bool {
38        self.text == other.text
39            && self.font_index == other.font_index
40            && self.size.to_bits() == other.size.to_bits()
41            && self.color == other.color
42            && self.extra_character_spacing.to_bits() == other.extra_character_spacing.to_bits()
43            && self.extra_word_spacing.to_bits() == other.extra_word_spacing.to_bits()
44            && self.extra_line_height.to_bits() == other.extra_line_height.to_bits()
45    }
46}
47
48impl<'a> Eq for TextPiecesCacheKey<'a> {}
49
50impl<'a> std::hash::Hash for TextPiecesCacheKey<'a> {
51    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
52        self.text.hash(state);
53        self.font_index.hash(state);
54        self.size.to_bits().hash(state);
55        self.color.hash(state);
56        self.extra_character_spacing.to_bits().hash(state);
57        self.extra_word_spacing.to_bits().hash(state);
58        self.extra_line_height.to_bits().hash(state);
59    }
60}
61
62/// A data structure that holds cached text pieces (shaped and unicode-segmentend such that it is
63/// ready for line breaking). This type gets passed around in the contexts and is needed by the
64/// [crate::elements::text::Text] and [crate::elements::rich_text::RichText] elements. Currently
65/// only [Self::new] is public for API stability reasons.
66pub struct TextPiecesCache {
67    line_segmenter: icu_segmenter::LineSegmenter,
68    line_break_map:
69        icu_properties::maps::CodePointMapDataBorrowed<'static, icu_properties::LineBreak>,
70    cache: FrozenMap<OwnedKey, Vec<Piece>>,
71    shape_buffer: Cell<Vec<(Option<usize>, ShapedGlyph)>>,
72}
73
74impl TextPiecesCache {
75    pub fn new() -> Self {
76        TextPiecesCache {
77            line_segmenter: icu_segmenter::LineSegmenter::new_auto(),
78            line_break_map: icu_properties::maps::line_break(),
79            cache: FrozenMap::new(),
80            shape_buffer: Cell::new(Vec::new()),
81        }
82    }
83
84    pub(crate) fn pieces<'a, F: Font>(
85        &'a self,
86        text: &str,
87        font: &F,
88        size: f32,
89        color: u32,
90        extra_character_spacing: f32,
91        extra_word_spacing: f32,
92        extra_line_height: f32,
93        link: Option<LinkTarget<'a>>,
94    ) -> &'a [Piece] {
95        assert!(size.is_finite());
96        assert!(extra_character_spacing.is_finite());
97        assert!(extra_word_spacing.is_finite());
98        assert!(extra_line_height.is_finite());
99
100        let key = TextPiecesCacheKey {
101            text: Cow::Borrowed(text),
102            font_index: font.index(),
103            size,
104            color,
105            extra_character_spacing,
106            extra_word_spacing,
107            extra_line_height,
108        };
109
110        if let Some(value) = self.cache.get(&key) {
111            value
112        } else {
113            let shaped_hyphen = font.shape(super::HYPHEN, 0., 0.).next().unwrap();
114
115            let pieces: Vec<Piece> = text
116                .split("\n")
117                .enumerate()
118                .flat_map(|(i, text_line)| {
119                    let break_piece = if i > 0 {
120                        Some(Piece {
121                            text: String::new(),
122                            color: 0x00_00_00_FF,
123                            empty: true,
124                            glyph_count: 0,
125                            mandatory_break_after: true,
126                            height_above_baseline: 0.,
127                            height_below_baseline: 0.,
128                            shaped: Vec::new(),
129                            size: 0.,
130                            trailing_hyphen: None,
131                            trailing_whitespace_width: 0.,
132                            width: None,
133                            link: None,
134                        })
135                    } else {
136                        None
137                    };
138                    let mut shaped = self.shape_buffer.take();
139                    assert!(shaped.is_empty());
140
141                    super::shaping::shape(
142                        font,
143                        font.fallback_fonts(),
144                        None,
145                        text_line,
146                        extra_character_spacing / size,
147                        extra_word_spacing / size,
148                        &mut shaped,
149                        0,
150                    );
151
152                    let segments = self.line_segmenter.segment_str(text_line).peekable();
153
154                    let line_pieces: Vec<Piece> = Pieces {
155                        current: Some(0),
156                        text: text_line,
157                        shaped: shaped.iter(),
158                        segments,
159                        shaped_hyphen: shaped_hyphen.clone(),
160                        size,
161                        color,
162                        extra_line_height,
163                        main_font: font,
164                        main_font_metrics: font.general_metrics(),
165                        fallback_fonts: font.fallback_fonts(),
166                        line_break_map: &self.line_break_map,
167                        link,
168                    }
169                    .collect();
170
171                    shaped.clear();
172                    self.shape_buffer.set(shaped);
173
174                    break_piece.into_iter().chain(line_pieces)
175                })
176                .collect();
177
178            self.cache.insert(
179                OwnedKey(TextPiecesCacheKey {
180                    text: Cow::Owned(text.to_string()),
181                    ..key
182                }),
183                pieces,
184            )
185        }
186    }
187}
188
189pub struct Piece {
190    pub text: String,
191    pub shaped: Vec<(Option<usize>, ShapedGlyph)>,
192
193    /// The width of the main part of the piece. None means the piece consists only of whitespace.
194    /// This is needed for line breaking to determine how to treat the piece if placed at the end
195    /// of an overflowing line; a piece that consists only of whitespace can be placed there because
196    /// trailing whitespace does not count towards the width of the line. It's not clear whether
197    /// checking for zero would work for this as there might be fonts or specific shapings that
198    /// contain characters with a width of zero but with a visible glyph. The `None` indicates that
199    /// there are only glyphs in this piece that we count as whitespace.
200    pub width: Option<f32>,
201
202    pub height_above_baseline: f32,
203    pub height_below_baseline: f32,
204    pub trailing_whitespace_width: f32,
205
206    /// Only applies when the piece is at the end of the line. Otherwise, it will not be counted
207    /// towards the width and not displayed.
208    pub trailing_hyphen: Option<(Option<usize>, ShapedGlyph)>,
209    pub mandatory_break_after: bool,
210    pub glyph_count: usize,
211    pub empty: bool,
212    pub size: f32,
213    pub color: u32,
214
215    pub link: Option<CacheLinkTarget>,
216}
217
218pub enum CacheLinkTarget {
219    Uri(Rc<str>),
220}
221
222impl CacheLinkTarget {
223    pub fn as_link_target(&self) -> LinkTarget<'_> {
224        match self {
225            CacheLinkTarget::Uri(uri) => LinkTarget::Uri(uri),
226        }
227    }
228}
229
230impl<'a> From<LinkTarget<'a>> for CacheLinkTarget {
231    fn from(value: LinkTarget<'a>) -> Self {
232        match value {
233            LinkTarget::Uri(uri) => CacheLinkTarget::Uri(uri.into()),
234        }
235    }
236}
237
238pub struct Pieces<'a, 'b, 'c, F> {
239    current: Option<usize>,
240    text: &'a str,
241    shaped: std::slice::Iter<'c, (Option<usize>, ShapedGlyph)>,
242    segments: Peekable<LineBreakIteratorUtf8<'b, 'a>>,
243    main_font: &'a F,
244    main_font_metrics: GeneralMetrics,
245    fallback_fonts: &'a [F],
246    shaped_hyphen: ShapedGlyph,
247    size: f32,
248    color: u32,
249    extra_line_height: f32,
250    line_break_map: &'a icu_properties::maps::CodePointMapDataBorrowed<'static, LineBreak>,
251    link: Option<LinkTarget<'a>>,
252}
253
254impl<'a, 'b, 'c, F: Font> Iterator for Pieces<'a, 'b, 'c, F> {
255    type Item = Piece;
256
257    fn next(&mut self) -> Option<Self::Item> {
258        let mut shaped = self.shaped.clone();
259
260        let Some(current) = self.current else {
261            return None;
262        };
263
264        // TODO: Handle unsafe_to_break somewhere here. If unsafe_to_break is true when we would
265        // otherwise split pieces we should probably fuse them into one piece because that seems
266        // like the only reasonable thing to do.
267
268        let segment = self.segments.find(|&s| s != 0).unwrap_or_else(|| {
269            self.current = None;
270            self.text.len()
271        });
272
273        let mut iter = std::iter::from_fn({
274            let mut done = false;
275            let shaped = &mut shaped;
276            move || {
277                if done {
278                    return None;
279                }
280
281                let next = shaped.next()?;
282
283                if next.1.text_range.end >= segment {
284                    done = true;
285                }
286
287                Some(next)
288            }
289        })
290        .peekable();
291
292        let mut width = None;
293        let mut whitespace_width = 0.;
294        let mut glyph_count = 0;
295        let mut mandatory_break_after = false;
296
297        // A line and its the pieces is always at least as high as the main font. Otherwise empty
298        // lines pieces would have no height. We could special case the empty line case, but that
299        // would lead to the the possibility of an empty line being higher than a line that only has
300        // glyphs from a fallback font.
301        let mut height_above_baseline = self.main_font_metrics.height_above_baseline;
302        let mut height_below_baseline = self.main_font_metrics.height_below_baseline;
303
304        while let Some(glyph) = iter.next() {
305            glyph_count += 1;
306
307            // A space at the end of a line doesn't count towards the width.
308            if matches!(
309                &self.text[glyph.1.text_range.clone()],
310                " " | "\u{00A0}" | " "
311            ) {
312                whitespace_width += glyph.1.x_advance;
313            } else if matches!(
314                self.text[glyph.1.text_range.clone()]
315                    .chars()
316                    .next()
317                    .map(|c| self.line_break_map.get(c)),
318                Some(
319                    LineBreak::MandatoryBreak
320                        | LineBreak::CarriageReturn
321                        | LineBreak::LineFeed
322                        | LineBreak::NextLine,
323                )
324            ) {
325                // We probably can't break here because the font might generate two missing glyphs
326                // for a \r\n here.
327                mandatory_break_after = true;
328            } else {
329                *width.get_or_insert(0.) += whitespace_width + glyph.1.x_advance;
330                whitespace_width = 0.;
331            }
332
333            let font = glyph.0.map_or(self.main_font, |i| &self.fallback_fonts[i]);
334
335            let metrics = font.general_metrics();
336
337            height_above_baseline = height_above_baseline.max(metrics.height_above_baseline);
338            height_below_baseline = height_below_baseline.max(metrics.height_below_baseline);
339        }
340
341        let text = &self.text[current..segment];
342
343        // TODO: Handle the case of a soft hyphen followed by a space. Currently that just gets
344        // ignored.
345        let trailing_hyphen = text
346            .ends_with('\u{00AD}')
347            .then_some(self.shaped_hyphen.clone());
348
349        let piece = Piece {
350            text: text.to_string(),
351            shaped: self
352                .shaped
353                .by_ref()
354                .take(glyph_count)
355                .map(|&(f, ref g)| {
356                    (
357                        f,
358                        ShapedGlyph {
359                            text_range: (g.text_range.start - current)
360                                ..(g.text_range.end - current),
361                            ..g.clone()
362                        },
363                    )
364                })
365                .collect(),
366            width: width.map(|w| w * self.size),
367            height_above_baseline: height_above_baseline * self.size,
368            // TODO: Would it be better if this was only added to the below-baseline height of the
369            // main font?
370            height_below_baseline: height_below_baseline * self.size + self.extra_line_height,
371            trailing_whitespace_width: whitespace_width * self.size,
372            trailing_hyphen: trailing_hyphen.map(|glyph| (None, glyph)), // TODO: fallback if main font has no hyphen
373            mandatory_break_after,
374            glyph_count,
375
376            // TODO: This might not work for \r\n, but that depends on the shaping. We should
377            // proabably find a way to filter out newlines entirely so that they don't show up after
378            // line breaking (and maybe also don't get shaped?).
379            empty: glyph_count == 0 || (glyph_count == 1 && mandatory_break_after),
380
381            size: self.size,
382            color: self.color,
383            link: self.link.map(Into::into),
384        };
385
386        self.current = self.current.and(Some(segment));
387        self.shaped = shaped;
388
389        if self.segments.peek().is_none() && !mandatory_break_after {
390            self.current = None;
391        }
392
393        Some(piece)
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use crate::{fonts::ShapedGlyph, text::pieces::Piece};
400
401    use super::*;
402
403    #[derive(Debug)]
404    struct FakeFont;
405
406    #[derive(Clone, Debug)]
407    struct FakeShaped<'a> {
408        // last: usize,
409        inner: std::str::CharIndices<'a>,
410    }
411
412    impl<'a> Iterator for FakeShaped<'a> {
413        type Item = ShapedGlyph;
414
415        fn next(&mut self) -> Option<Self::Item> {
416            if let Some((i, c)) = self.inner.next() {
417                Some(ShapedGlyph {
418                    unsafe_to_break: false,
419                    glyph_id: c as u32,
420                    text_range: i..i + c.len_utf8(),
421                    // we don't match newlines here because they produce the missing glyph which has
422                    // a non-zero width.
423                    x_advance_font: if matches!(c, '\u{00ad}') { 0. } else { 1. },
424                    x_advance: if matches!(c, '\u{00ad}') { 0. } else { 1. },
425                    x_offset: 0.,
426                    y_offset: 0.,
427                    y_advance: 0.,
428                })
429            } else {
430                None
431            }
432        }
433    }
434
435    impl Font for FakeFont {
436        type Shaped<'a>
437            = FakeShaped<'a>
438        where
439            Self: 'a;
440
441        fn shape<'a>(&'a self, text: &'a str, _: f32, _: f32) -> Self::Shaped<'a> {
442            FakeShaped {
443                inner: text.char_indices(),
444            }
445        }
446
447        fn index(&self) -> usize {
448            0
449        }
450
451        fn encode(&self, _: &mut crate::Pdf, _: u32, _: &str) -> crate::fonts::EncodedGlyph {
452            unreachable!()
453        }
454
455        fn resource_name(&self) -> pdf_writer::Name<'_> {
456            unreachable!()
457        }
458
459        fn general_metrics(&self) -> crate::fonts::GeneralMetrics {
460            crate::fonts::GeneralMetrics {
461                height_above_baseline: 0.5,
462                height_below_baseline: 0.5,
463            }
464        }
465
466        fn fallback_fonts(&self) -> &[Self] {
467            &[]
468        }
469    }
470
471    fn collect_piece<'a>(piece: &'a Piece) -> (&'a str, Option<f32>, f32, bool) {
472        let mut text = String::new();
473
474        for glyph in &piece.shaped {
475            let character = glyph.1.glyph_id as u8 as char;
476
477            assert_eq!(
478                character.to_string(),
479                piece.text[glyph.1.text_range.clone()]
480            );
481
482            text.push(glyph.1.glyph_id as u8 as char);
483        }
484
485        assert_eq!(text, piece.text);
486
487        (
488            &piece.text,
489            piece.width,
490            piece.trailing_whitespace_width,
491            piece.mandatory_break_after,
492        )
493    }
494
495    #[test]
496    fn test_empty() {
497        let text = "";
498
499        let cache = TextPiecesCache::new();
500        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
501        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
502
503        assert_eq!(&pieces, &[("", None, 0., false)]);
504    }
505
506    #[test]
507    fn test_one() {
508        let text = "abcde";
509
510        let cache = TextPiecesCache::new();
511        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
512        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
513
514        assert_eq!(&pieces, &[("abcde", Some(5.), 0., false)]);
515    }
516
517    #[test]
518    fn test_two() {
519        let text = "deadbeef defaced";
520
521        let cache = TextPiecesCache::new();
522        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
523        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
524
525        assert_eq!(
526            &pieces,
527            &[
528                ("deadbeef ", Some(8.), 1., false),
529                ("defaced", Some(7.), 0., false),
530            ]
531        );
532    }
533
534    #[test]
535    fn test_three() {
536        let text = "deadbeef defaced fart";
537
538        let cache = TextPiecesCache::new();
539        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
540        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
541
542        assert_eq!(
543            &pieces,
544            &[
545                ("deadbeef ", Some(8.), 1., false),
546                ("defaced ", Some(7.), 1., false),
547                ("fart", Some(4.), 0., false)
548            ],
549        );
550    }
551
552    #[test]
553    fn test_just_newline() {
554        let text = "\n";
555
556        let cache = TextPiecesCache::new();
557        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
558        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
559
560        assert_eq!(
561            &pieces,
562            &[
563                ("", None, 0., false),
564                ("", None, 0., true),
565                ("", None, 0., false)
566            ]
567        );
568    }
569
570    #[test]
571    fn test_surrounded_newline() {
572        let text = "abc\ndef";
573
574        let cache = TextPiecesCache::new();
575        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
576        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
577
578        assert_eq!(
579            &pieces,
580            &[
581                ("abc", Some(3.), 0., false),
582                ("", None, 0., true),
583                ("def", Some(3.), 0., false)
584            ]
585        );
586    }
587
588    #[test]
589    fn test_newline_at_start() {
590        let text = "\nabc def";
591
592        let cache = TextPiecesCache::new();
593        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
594        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
595
596        assert_eq!(
597            &pieces,
598            &[
599                ("", None, 0.0, false),
600                ("", None, 0., true),
601                ("abc ", Some(3.), 1., false),
602                ("def", Some(3.), 0., false),
603            ]
604        );
605    }
606
607    #[test]
608    fn test_trailing_newline() {
609        let text = "abc def\n";
610
611        let cache = TextPiecesCache::new();
612        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
613        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
614
615        assert_eq!(
616            &pieces,
617            &[
618                ("abc ", Some(3.), 1., false),
619                ("def", Some(3.), 0., false),
620                ("", None, 0., true),
621                ("", None, 0., false),
622            ]
623        );
624    }
625
626    #[test]
627    fn test_newline_after_space() {
628        let text = "abc \ndef";
629
630        let cache = TextPiecesCache::new();
631        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
632        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
633
634        assert_eq!(
635            &pieces,
636            &[
637                ("abc ", Some(3.), 1., false),
638                ("", None, 0., true),
639                ("def", Some(3.), 0., false)
640            ],
641        );
642    }
643
644    #[test]
645    fn test_trailing_soft_hyphen() {
646        let text = "abc\u{ad}";
647
648        let cache = TextPiecesCache::new();
649        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
650        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
651
652        assert_eq!(&pieces, &[("abc\u{ad}", Some(3.), 0., false)]);
653    }
654
655    #[test]
656    fn test_trailing_soft_hyphen_and_space() {
657        let text = "abc\u{ad} ";
658
659        let cache = TextPiecesCache::new();
660        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
661
662        let pieces: Vec<_> = pieces
663            .iter()
664            .map(|p| {
665                let collected = collect_piece(p);
666
667                (
668                    collected.0,
669                    collected.1,
670                    collected.2,
671                    collected.3,
672                    p.trailing_hyphen.as_ref().map(|h| h.1.x_advance),
673                )
674            })
675            .collect();
676
677        assert_eq!(&pieces, &[("abc\u{ad} ", Some(3.), 1., false, None)]);
678    }
679
680    #[test]
681    fn test_soft_hyphen_after_space() {
682        let text = " \u{ad}abc";
683
684        let cache = TextPiecesCache::new();
685        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
686        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
687
688        assert_eq!(
689            &pieces,
690            &[
691                (" ", None, 1., false),
692                ("\u{ad}", Some(0.), 0., false),
693                ("abc", Some(3.), 0., false),
694            ],
695        );
696    }
697
698    #[test]
699    fn test_soft_hyphen_between_spaces() {
700        let text = " \u{ad} ";
701
702        let cache = TextPiecesCache::new();
703        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
704        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
705
706        assert_eq!(
707            &pieces,
708            &[(" ", None, 1., false), ("\u{ad} ", Some(0.), 1., false)],
709        );
710    }
711
712    #[test]
713    fn test_just_spaces() {
714        let text = "        ";
715
716        let cache = TextPiecesCache::new();
717        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
718        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
719
720        assert_eq!(&pieces, &[("        ", None, 8., false)]);
721    }
722
723    #[test]
724    fn test_mixed_whitespace() {
725        let text = "    abc    \ndef  the\tjflkdsa";
726
727        let cache = TextPiecesCache::new();
728        let pieces = cache.pieces(text, &FakeFont, 1., 0, 0., 0., 0., None);
729        let pieces: Vec<_> = pieces.iter().map(collect_piece).collect();
730
731        assert_eq!(
732            &pieces,
733            &[
734                ("    ", None, 4., false),
735                // It's somewhat unclear whether the trailing spaces should count toward the
736                // width here.
737                ("abc    ", Some(3.), 4., false),
738                ("", None, 0., true),
739                ("def  ", Some(3.), 2., false),
740                ("the\t", Some(4.), 0., false),
741                ("jflkdsa", Some(7.), 0., false),
742            ],
743        );
744    }
745}