pdfrum_anyrender/
run_text.rs1use anyrender::Glyph;
10use peniko::FontData;
11use std::collections::HashMap;
12use std::ops::Range;
13
14#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct GlyphSource<'a> {
42 pub cluster: usize,
45 pub text: &'a str,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Default)]
54pub struct RunText {
55 text: String,
56 clusters: Vec<Range<usize>>,
57}
58
59impl RunText {
60 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 pub fn text(&self) -> &str {
82 &self.text
83 }
84
85 pub fn clusters(&self) -> &[Range<usize>] {
87 &self.clusters
88 }
89
90 pub fn len(&self) -> usize {
92 self.clusters.len()
93 }
94
95 pub fn is_empty(&self) -> bool {
97 self.clusters.is_empty()
98 }
99
100 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#[derive(Debug, Default)]
130pub struct RunTexts {
131 runs: HashMap<RunKey, RunText>,
132}
133
134impl RunTexts {
135 pub fn insert(&mut self, key: RunKey, text: RunText) {
137 self.runs.entry(key).or_insert(text);
138 }
139
140 pub fn get(&self, key: &RunKey) -> Option<&RunText> {
142 self.runs.get(key)
143 }
144
145 pub fn len(&self) -> usize {
147 self.runs.len()
148 }
149
150 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 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}