Skip to main content

pdfrum_anyrender/
run_text.rs

1//! The text behind a glyph run, handed over by whoever shaped it. anyrender's `draw_glyphs`
2//! carries glyph ids and positions only; a layout engine that still has the run (parley keeps
3//! each cluster's byte range in its source text) can record it here under the key the painter
4//! will compute from the same call, so the PDF's text is the text that was shaped, not a guess
5//! from the font's cmap. That is what makes a ligature (one `fi` glyph) copy as `fi`, and a
6//! glyph two code points share (一 and the Kangxi radical ⼀ in CJK fonts) copy as the one the
7//! author typed.
8
9use anyrender::Glyph;
10use peniko::FontData;
11use std::collections::HashMap;
12use std::ops::Range;
13
14/// A glyph run as the painter sees it: the font face, the size and every glyph's id and
15/// position, bit for bit. Two runs with the same key draw the same thing.
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct RunKey {
18    face: (u64, u32),
19    size: u32,
20    glyphs: Vec<(u32, u32, u32)>,
21}
22
23impl RunKey {
24    /// The key of a run of `glyphs` in `font` at `font_size`, exactly as passed to
25    /// `draw_glyphs`.
26    pub fn new(font: &FontData, font_size: f32, glyphs: impl IntoIterator<Item = Glyph>) -> Self {
27        RunKey {
28            face: (font.data.id(), font.index),
29            size: font_size.to_bits(),
30            glyphs: glyphs
31                .into_iter()
32                .map(|glyph| (glyph.id, glyph.x.to_bits(), glyph.y.to_bits()))
33                .collect(),
34        }
35    }
36}
37
38/// One glyph's share of the source text: the cluster it belongs to (glyphs of one cluster
39/// share it) and that cluster's text.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct GlyphSource<'a> {
42    /// Any value that is the same for every glyph of one cluster and differs between
43    /// neighbouring clusters: the cluster's start in the source text is the natural one.
44    pub cluster: usize,
45    /// The cluster's text: one character usually, several for a ligature, a whole grapheme
46    /// for a base with its marks.
47    pub text: &'a str,
48}
49
50/// The text of one run and, for each glyph in drawing order, the byte range of its cluster in
51/// that text. Glyphs of one cluster share a range; pdfrum writes it once, as `/ActualText`
52/// when the glyphs cannot carry it in the font's ToUnicode map.
53#[derive(Debug, Clone, PartialEq, Eq, Default)]
54pub struct RunText {
55    text: String,
56    clusters: Vec<Range<usize>>,
57}
58
59impl RunText {
60    /// The run text for glyphs in drawing order.
61    pub fn from_glyphs<'a>(glyphs: impl IntoIterator<Item = GlyphSource<'a>>) -> Self {
62        let mut run = RunText::default();
63        let mut last: Option<usize> = None;
64        for glyph in glyphs {
65            match (last, run.clusters.last().cloned()) {
66                (Some(cluster), Some(range)) if cluster == glyph.cluster => {
67                    run.clusters.push(range);
68                }
69                _ => {
70                    let start = run.text.len();
71                    run.text.push_str(glyph.text);
72                    run.clusters.push(start..run.text.len());
73                }
74            }
75            last = Some(glyph.cluster);
76        }
77        run
78    }
79
80    /// The whole text.
81    pub fn text(&self) -> &str {
82        &self.text
83    }
84
85    /// Each glyph's cluster range in [`RunText::text`].
86    pub fn clusters(&self) -> &[Range<usize>] {
87        &self.clusters
88    }
89
90    /// How many glyphs the run has.
91    pub fn len(&self) -> usize {
92        self.clusters.len()
93    }
94
95    /// Whether the run has no glyphs.
96    pub fn is_empty(&self) -> bool {
97        self.clusters.is_empty()
98    }
99
100    /// The run cut down to the glyphs at `kept` (ascending indices): the ones a page keeps.
101    /// A cluster is kept whole if any of its glyphs is. Glyphs are of one cluster when their
102    /// ranges are equal, not merely when they start together: an empty range (a glyph with no
103    /// text) and the next glyph's share a start and are still two clusters.
104    pub(crate) fn select(&self, kept: &[usize]) -> RunText {
105        let mut run = RunText::default();
106        let mut last: Option<&Range<usize>> = None;
107        for &index in kept {
108            let Some(range) = self.clusters.get(index) else {
109                continue;
110            };
111            match (last, run.clusters.last().cloned()) {
112                (Some(previous), Some(held)) if previous == range => run.clusters.push(held),
113                _ => {
114                    let start = run.text.len();
115                    run.text
116                        .push_str(self.text.get(range.clone()).unwrap_or(""));
117                    run.clusters.push(start..run.text.len());
118                }
119            }
120            last = Some(range);
121        }
122        run
123    }
124}
125
126/// Run texts by key. When two runs share a key but not a text (only possible when two code
127/// points share a glyph and everything else about the runs matches), the first recorded wins:
128/// record in document order.
129#[derive(Debug, Default)]
130pub struct RunTexts {
131    runs: HashMap<RunKey, RunText>,
132}
133
134impl RunTexts {
135    /// Record `text` for runs keyed `key`, unless a run with that key was recorded first.
136    pub fn insert(&mut self, key: RunKey, text: RunText) {
137        self.runs.entry(key).or_insert(text);
138    }
139
140    /// The text recorded for `key`.
141    pub fn get(&self, key: &RunKey) -> Option<&RunText> {
142        self.runs.get(key)
143    }
144
145    /// How many distinct runs have text.
146    pub fn len(&self) -> usize {
147        self.runs.len()
148    }
149
150    /// Whether no run has text.
151    pub fn is_empty(&self) -> bool {
152        self.runs.is_empty()
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::{GlyphSource, RunText};
159
160    fn glyph(cluster: usize, text: &str) -> GlyphSource<'_> {
161        GlyphSource { cluster, text }
162    }
163
164    #[test]
165    fn a_ligature_glyph_covers_its_characters() {
166        let run = RunText::from_glyphs([glyph(0, "fi"), glyph(2, "n"), glyph(3, "d")]);
167        assert_eq!(run.text(), "find");
168        assert_eq!(run.clusters(), &[0..2, 2..3, 3..4]);
169    }
170
171    #[test]
172    fn glyphs_of_one_cluster_share_its_range() {
173        let run = RunText::from_glyphs([glyph(0, "é"), glyph(0, "é"), glyph(3, "t")]);
174        assert_eq!(run.text(), "ét");
175        assert_eq!(run.clusters(), &[0..2, 0..2, 2..3]);
176    }
177
178    #[test]
179    fn a_glyph_without_text_does_not_swallow_the_next() {
180        // A ligature the cmap cannot name (no text), then `c`: two clusters, though both
181        // ranges start at the same byte.
182        let run = RunText::from_glyphs([glyph(0, ""), glyph(1, "c")]);
183        let kept = run.select(&[0, 1]);
184        assert_eq!(kept.text(), "c");
185        assert_eq!(kept.clusters(), &[0..0, 0..1]);
186    }
187
188    #[test]
189    fn selecting_keeps_whole_clusters_in_order() {
190        let run = RunText::from_glyphs([glyph(0, "a"), glyph(1, "fi"), glyph(3, "b")]);
191        let kept = run.select(&[1, 2]);
192        assert_eq!(kept.text(), "fib");
193        assert_eq!(kept.clusters(), &[0..2, 2..3]);
194    }
195}