kobo_core/rendering/text_render/
fonts.rs1use fontdue::Font;
4use rustybuzz::Face;
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::sync::RwLock;
7
8use super::Script;
9
10pub(crate) const FONT_ID_DEFAULT: u8 = 0;
11pub(crate) const FONT_ID_MONO: u8 = Script::SLOTS as u8 + 1;
13
14pub(crate) struct FontEntry {
15 pub face: Face<'static>,
16 pub body: Font,
17 pub id: u8,
18}
19
20static FONT_INSTALL_COUNTER: AtomicUsize = AtomicUsize::new(0);
21
22pub fn font_install_count() -> usize {
23 FONT_INSTALL_COUNTER.load(Ordering::Relaxed)
24}
25
26#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
40pub struct TextStyle {
41 pub bold: bool,
42 pub italic: bool,
43 pub mono: bool,
44 pub link: bool,
45}
46
47impl TextStyle {
48 pub const PLAIN: TextStyle = TextStyle {
49 bold: false,
50 italic: false,
51 mono: false,
52 link: false,
53 };
54
55 pub fn is_plain(self) -> bool {
56 self == Self::PLAIN
57 }
58
59 pub(crate) fn bits(self) -> u8 {
61 (self.bold as u8)
62 | ((self.italic as u8) << 1)
63 | ((self.mono as u8) << 2)
64 | ((self.link as u8) << 3)
65 }
66}
67
68pub(crate) struct FontRegistry {
69 pub default: FontEntry,
70 pub mono: FontEntry,
71 pub scripts: [RwLock<Option<FontEntry>>; Script::SLOTS],
75}
76
77static FONTS: std::sync::OnceLock<FontRegistry> = std::sync::OnceLock::new();
78
79type GlyphMap = std::collections::HashMap<(u8, u16, u32), (fontdue::Metrics, Vec<u8>)>;
80
81pub(crate) static GLYPH_CACHE: std::sync::OnceLock<std::sync::Mutex<GlyphMap>> =
82 std::sync::OnceLock::new();
83
84pub(crate) fn glyph_cache() -> &'static std::sync::Mutex<GlyphMap> {
85 GLYPH_CACHE.get_or_init(std::sync::Mutex::default)
86}
87
88fn load_font(data: &'static [u8], id: u8) -> FontEntry {
89 let face = Face::from_slice(data, 0).expect("font parse");
90 let body = Font::from_bytes(data, fontdue::FontSettings::default()).expect("fontdue load");
91 FontEntry { face, body, id }
92}
93
94pub(crate) fn fonts() -> &'static FontRegistry {
95 FONTS.get_or_init(|| FontRegistry {
96 default: load_font(super::FONT_LATIN, FONT_ID_DEFAULT),
97 mono: load_font(super::FONT_MONO, FONT_ID_MONO),
98 scripts: std::array::from_fn(|_| RwLock::new(None)),
99 })
100}
101
102pub fn install_font(script: Script, data: Vec<u8>) -> bool {
103 if matches!(script, Script::Latin | Script::Other) {
105 return false;
106 }
107 let entry = match load_font_owned(data, script.font_id()) {
108 Some(e) => e,
109 None => return false,
110 };
111 let reg = fonts();
112 if let Ok(mut guard) = reg.scripts[script.slot()].write() {
113 *guard = Some(entry);
114 }
115 super::clear_width_cache();
116 FONT_INSTALL_COUNTER.fetch_add(1, Ordering::Relaxed);
117 true
118}
119
120fn load_font_owned(data: Vec<u8>, id: u8) -> Option<FontEntry> {
121 let data_boxed: Box<[u8]> = data.into_boxed_slice();
122 let leaked: &'static [u8] = Box::leak(data_boxed);
123 let face = Face::from_slice(leaked, 0)?;
124 let body = Font::from_bytes(leaked, fontdue::FontSettings::default()).ok()?;
125 Some(FontEntry { face, body, id })
126}
127
128pub fn font_covers(data: &[u8], probe_chars: &str) -> bool {
129 let face = match Face::from_slice(data, 0) {
130 Some(f) => f,
131 None => return false,
132 };
133 probe_chars.chars().all(|c| face.glyph_index(c).is_some())
134}
135
136pub(crate) fn font_for(script: Script, style: TextStyle) -> &'static FontEntry {
140 if style.mono {
141 return &fonts().mono;
145 }
146 font_for_script(script)
147}
148
149pub(crate) fn font_for_script(script: Script) -> &'static FontEntry {
150 let reg = fonts();
151 if matches!(script, Script::Latin | Script::Other) {
152 return ®.default;
153 }
154 let slot = ®.scripts[script.slot()];
155 if let Ok(guard) = slot.read() {
156 if let Some(ref entry) = *guard {
157 unsafe {
161 return &*(entry as *const FontEntry);
162 }
163 }
164 }
165 ®.default
166}
167
168pub(crate) fn fallback_for_char(c: char) -> Option<(&'static FontEntry, u16)> {
169 let reg = fonts();
170 for slot in reg.scripts.iter() {
173 if let Ok(g) = slot.read() {
174 if let Some(entry) = g.as_ref() {
175 if let Some(gid) = entry.face.glyph_index(c) {
176 let e: &'static FontEntry = unsafe { &*(entry as *const FontEntry) };
179 return Some((e, gid.0));
180 }
181 }
182 }
183 }
184 reg.default
185 .face
186 .glyph_index(c)
187 .map(|gid| (®.default, gid.0))
188}
189
190pub fn has_font_for(script: Script) -> bool {
191 if matches!(script, Script::Latin | Script::Other) {
193 return true;
194 }
195 fonts().scripts[script.slot()]
196 .read()
197 .map(|g| g.is_some())
198 .unwrap_or(false)
199}
200
201pub fn line_height_for(script: Script, px_size: f32) -> usize {
202 let fd = font_for_script(script);
203 let lm = fd.body.horizontal_line_metrics(px_size);
204 let h = match lm {
205 Some(m) => m.ascent - m.descent + m.line_gap,
206 None => px_size * 1.2,
207 };
208 h.max(1.0) as usize
209}
210
211pub fn line_height(px_size: f32) -> usize {
212 line_height_for(Script::Latin, px_size)
213}
214
215pub fn word_width(word: &str, px_size: f32) -> f32 {
216 word_width_styled(word, px_size, TextStyle::PLAIN)
217}
218
219pub fn word_width_styled(word: &str, px_size: f32, style: TextStyle) -> f32 {
226 let key = (
227 format!("{}\u{1}{}", word, style.bits()),
228 (px_size * 100.0) as u32,
229 );
230 let cache = super::width_cache();
231 if let Ok(c) = cache.lock() {
232 if let Some(&w) = c.get(&key) {
233 return w;
234 }
235 }
236 let fd = font_for(super::detect_script(word), style);
237 let scale = px_size / fd.face.units_per_em() as f32;
238 let mut ub = rustybuzz::UnicodeBuffer::new();
239 ub.push_str(word);
240 let dir = if super::detect_script(word).is_rtl() {
241 rustybuzz::Direction::RightToLeft
242 } else {
243 rustybuzz::Direction::LeftToRight
244 };
245 ub.set_direction(dir);
246 let gb = rustybuzz::shape(&fd.face, &[], ub);
247 let w = gb
248 .glyph_positions()
249 .iter()
250 .map(|p| p.x_advance as f32)
251 .sum::<f32>()
252 * scale;
253 if let Ok(mut c) = cache.lock() {
254 c.insert(key, w);
255 }
256 w
257}
258
259pub fn char_widths_batch(text: &str, px_size: f32, style: TextStyle) -> Vec<f32> {
265 let fd = font_for(super::detect_script(text), style);
266 let scale = px_size / fd.face.units_per_em() as f32;
267 let mut ub = rustybuzz::UnicodeBuffer::new();
268 ub.push_str(text);
269 ub.set_direction(rustybuzz::Direction::LeftToRight);
270 let gb = rustybuzz::shape(&fd.face, &[], ub);
271 let clusters = gb.glyph_infos();
272 let positions = gb.glyph_positions();
273 let mut out = vec![0.0f32; text.chars().count()];
274 let mut ci = 0usize;
275 for (gi, pos) in positions.iter().enumerate() {
276 let adv = pos.x_advance as f32 * scale;
277 let cluster = clusters.get(gi).map(|c| c.cluster as usize).unwrap_or(0);
278 let char_idx = text[..cluster.min(text.len())].chars().count();
279 if char_idx < out.len() {
280 ci = char_idx;
281 }
282 out[ci] += adv;
283 }
284 out
285}