1pub mod blit;
4pub mod fonts;
5
6pub use blit::*;
7pub use fonts::*;
8
9const FONT_LATIN: &[u8] = include_bytes!("../../fonts/NotoSansLatin.ttf");
10const FONT_MONO: &[u8] = include_bytes!("../../fonts/DejaVuSansMono.ttf");
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Script {
17 Latin,
18 Bengali,
19 Devanagari,
20 Arabic,
21 Hebrew,
22 Greek,
23 Cyrillic,
24 Georgian,
25 Armenian,
26 Ethiopic,
27 Gujarati,
28 Gurmukhi,
29 Tamil,
30 Telugu,
31 Kannada,
32 Malayalam,
33 Sinhala,
34 Thai,
35 Lao,
36 Khmer,
37 Myanmar,
38 Japanese,
39 Korean,
40 Chinese,
44 Other,
45}
46
47pub const LOADABLE_SCRIPTS: &[Script] = &[
50 Script::Bengali,
51 Script::Devanagari,
52 Script::Arabic,
53 Script::Hebrew,
54 Script::Greek,
55 Script::Cyrillic,
56 Script::Georgian,
57 Script::Armenian,
58 Script::Ethiopic,
59 Script::Gujarati,
60 Script::Gurmukhi,
61 Script::Tamil,
62 Script::Telugu,
63 Script::Kannada,
64 Script::Malayalam,
65 Script::Sinhala,
66 Script::Thai,
67 Script::Lao,
68 Script::Khmer,
69 Script::Myanmar,
70 Script::Japanese,
71 Script::Korean,
72 Script::Chinese,
73];
74
75impl Script {
76 pub const SLOTS: usize = 24;
79
80 pub fn slot(self) -> usize {
82 self as usize
83 }
84
85 pub fn font_id(self) -> u8 {
88 self as u8 + 1
89 }
90
91 pub fn is_rtl(self) -> bool {
92 matches!(self, Script::Arabic | Script::Hebrew)
93 }
94
95 pub fn uses_word_spacing(self) -> bool {
98 !matches!(
99 self,
100 Script::Japanese
101 | Script::Chinese
102 | Script::Korean
103 | Script::Thai
104 | Script::Lao
105 | Script::Khmer
106 | Script::Myanmar
107 )
108 }
109
110 pub fn lang_tag(self) -> &'static str {
113 match self {
114 Script::Latin => "en-US",
115 Script::Bengali => "bn-BD",
116 Script::Devanagari => "hi-IN",
117 Script::Arabic => "ar-SA",
118 Script::Hebrew => "he-IL",
119 Script::Greek => "el-GR",
120 Script::Cyrillic => "ru-RU",
121 Script::Georgian => "ka-GE",
122 Script::Armenian => "hy-AM",
123 Script::Ethiopic => "am-ET",
124 Script::Gujarati => "gu-IN",
125 Script::Gurmukhi => "pa-IN",
126 Script::Tamil => "ta-IN",
127 Script::Telugu => "te-IN",
128 Script::Kannada => "kn-IN",
129 Script::Malayalam => "ml-IN",
130 Script::Sinhala => "si-LK",
131 Script::Thai => "th-TH",
132 Script::Lao => "lo-LA",
133 Script::Khmer => "km-KH",
134 Script::Myanmar => "my-MM",
135 Script::Japanese => "ja-JP",
136 Script::Korean => "ko-KR",
137 Script::Chinese => "zh-CN",
138 Script::Other => "en-US",
139 }
140 }
141}
142
143fn script_of(code: u32) -> Script {
145 match code {
146 0x0980..=0x09FF => Script::Bengali,
147 0x0900..=0x097F => Script::Devanagari,
148 0x0600..=0x06FF | 0x0750..=0x077F | 0xFB50..=0xFDFF | 0xFE70..=0xFEFF => Script::Arabic,
149 0x0590..=0x05FF => Script::Hebrew,
150 0x0370..=0x03FF | 0x1F00..=0x1FFF => Script::Greek,
151 0x0400..=0x052F | 0x2DE0..=0x2DFF | 0xA640..=0xA69F => Script::Cyrillic,
152 0x10A0..=0x10FF | 0x1C90..=0x1CBF | 0x2D00..=0x2D2F => Script::Georgian,
153 0x0530..=0x058F => Script::Armenian,
154 0x1200..=0x137F | 0x1380..=0x139F | 0x2D80..=0x2DDF => Script::Ethiopic,
155 0x0A80..=0x0AFF => Script::Gujarati,
156 0x0A00..=0x0A7F => Script::Gurmukhi,
157 0x0B80..=0x0BFF => Script::Tamil,
158 0x0C00..=0x0C7F => Script::Telugu,
159 0x0C80..=0x0CFF => Script::Kannada,
160 0x0D00..=0x0D7F => Script::Malayalam,
161 0x0D80..=0x0DFF => Script::Sinhala,
162 0x0E00..=0x0E7F => Script::Thai,
163 0x0E80..=0x0EFF => Script::Lao,
164 0x1780..=0x17FF | 0x19E0..=0x19FF => Script::Khmer,
165 0x1000..=0x109F | 0xA9E0..=0xA9FF | 0xAA60..=0xAA7F => Script::Myanmar,
166 0x0000..=0x024F | 0x1E00..=0x1EFF | 0x2C60..=0x2C7F => Script::Latin,
169 _ => Script::Other,
170 }
171}
172
173pub fn detect_script(text: &str) -> Script {
184 let mut counts = [0usize; Script::SLOTS];
185 let mut kana = 0usize;
186 let mut hangul = 0usize;
187 let mut han = 0usize;
188
189 for c in text.chars() {
190 if !c.is_alphabetic() {
191 continue;
192 }
193 match c as u32 {
194 0x3040..=0x30FF | 0x31F0..=0x31FF => kana += 1,
195 0xAC00..=0xD7AF | 0x1100..=0x11FF | 0x3130..=0x318F => hangul += 1,
196 0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0xF900..=0xFAFF | 0x20000..=0x2A6DF => han += 1,
197 code => counts[script_of(code).slot()] += 1,
198 }
199 }
200
201 if kana > 0 {
204 return Script::Japanese;
205 }
206 if hangul > 0 {
207 return Script::Korean;
208 }
209 counts[Script::Chinese.slot()] += han;
210
211 let (best, best_count) = counts
212 .iter()
213 .enumerate()
214 .max_by_key(|&(_, n)| *n)
215 .map(|(i, n)| (i, *n))
216 .unwrap_or((Script::Latin.slot(), 0));
217
218 if best_count == 0 {
219 return Script::Latin;
220 }
221 LOADABLE_SCRIPTS
222 .iter()
223 .copied()
224 .chain(std::iter::once(Script::Latin))
225 .find(|s| s.slot() == best)
226 .unwrap_or(Script::Latin)
227}
228
229static WIDTH_CACHE: std::sync::OnceLock<
230 std::sync::Mutex<std::collections::HashMap<(String, u32), f32>>,
231> = std::sync::OnceLock::new();
232
233pub(crate) fn width_cache(
234) -> &'static std::sync::Mutex<std::collections::HashMap<(String, u32), f32>> {
235 WIDTH_CACHE.get_or_init(std::sync::Mutex::default)
236}
237
238#[allow(dead_code)]
239pub fn clear_width_cache() {
240 if let Ok(mut cache) = width_cache().lock() {
241 cache.clear();
242 }
243}
244
245pub struct DecodedImage {
246 pub rgb: Vec<u8>,
247 pub width: usize,
248 pub height: usize,
249}
250
251pub struct DecodedRgba {
252 pub rgba: Vec<u8>,
253 pub width: usize,
254 pub height: usize,
255}
256
257pub fn decode_image(raw: &[u8], max_w: usize, max_h: usize) -> Option<DecodedImage> {
258 let img = image::load_from_memory(raw).ok()?;
259 let (ow, oh) = (img.width() as usize, img.height() as usize);
260 if ow == 0 || oh == 0 {
261 return None;
262 }
263 let scale = max_w as f32 / ow as f32;
264 let mut nw = max_w;
265 let mut nh = (oh as f32 * scale).round() as usize;
266 if nh == 0 {
267 return None;
268 }
269 if nh > max_h {
270 let hscale = max_h as f32 / nh as f32;
271 nh = max_h;
272 nw = (nw as f32 * hscale).round() as usize;
273 if nw == 0 {
274 return None;
275 }
276 }
277 let resized = img.resize(nw as u32, nh as u32, image::imageops::FilterType::Triangle);
278 let rgb = resized.to_rgb8();
279 let (rw, rh) = (rgb.width() as usize, rgb.height() as usize);
280 Some(DecodedImage {
281 rgb: rgb.into_raw(),
282 width: rw,
283 height: rh,
284 })
285}
286
287pub fn decode_image_rgba(raw: &[u8], max_w: usize, max_h: usize) -> Option<DecodedRgba> {
288 let img = image::load_from_memory(raw).ok()?;
289 let (ow, oh) = (img.width() as usize, img.height() as usize);
290 if ow == 0 || oh == 0 {
291 return None;
292 }
293 let scale = max_w as f32 / ow as f32;
294 let mut nw = max_w;
295 let mut nh = (oh as f32 * scale).round() as usize;
296 if nh == 0 {
297 return None;
298 }
299 if nh > max_h {
300 let hscale = max_h as f32 / nh as f32;
301 nh = max_h;
302 nw = (nw as f32 * hscale).round() as usize;
303 if nw == 0 {
304 return None;
305 }
306 }
307 let resized = img.resize(nw as u32, nh as u32, image::imageops::FilterType::Triangle);
308 let rgba = resized.to_rgba8();
309 let (rw, rh) = (rgba.width() as usize, rgba.height() as usize);
310 Some(DecodedRgba {
311 rgba: rgba.into_raw(),
312 width: rw,
313 height: rh,
314 })
315}
316
317pub fn blit_rgb565_image(
318 buf: &mut [u8],
319 buf_stride: usize,
320 rgb: &[u8],
321 iw: usize,
322 ih: usize,
323 ox: usize,
324 oy: usize,
325 max_w: usize,
326 max_h: usize,
327) {
328 for ry in 0..ih {
329 let py = oy + ry;
330 if py >= max_h {
331 break;
332 }
333 for rx in 0..iw {
334 let px = ox + rx;
335 if px >= max_w {
336 break;
337 }
338 let idx = (ry * iw + rx) * 3;
339 let r = rgb[idx] as u16;
340 let g = rgb[idx + 1] as u16;
341 let b = rgb[idx + 2] as u16;
342 let r5 = (r >> 3) & 0x1f;
343 let g6 = (g >> 2) & 0x3f;
344 let b5 = (b >> 3) & 0x1f;
345 let v = (r5 << 11) | (g6 << 5) | b5;
346 let off = (py * buf_stride + px) * 2;
347 if off + 2 > buf.len() {
348 continue;
349 }
350 buf[off] = (v & 0xff) as u8;
351 buf[off + 1] = (v >> 8) as u8;
352 }
353 }
354}
355
356pub fn blit_rgb565_image_alpha(
357 buf: &mut [u8],
358 buf_stride: usize,
359 rgba: &[u8],
360 iw: usize,
361 ih: usize,
362 ox: usize,
363 oy: usize,
364 max_w: usize,
365 max_h: usize,
366) {
367 for ry in 0..ih {
368 let py = oy + ry;
369 if py >= max_h {
370 break;
371 }
372 for rx in 0..iw {
373 let px = ox + rx;
374 if px >= max_w {
375 break;
376 }
377 let idx = (ry * iw + rx) * 4;
378 let a = rgba[idx + 3];
379 if a == 0 {
380 continue;
381 }
382 let r = rgba[idx] as u16;
383 let g = rgba[idx + 1] as u16;
384 let b = rgba[idx + 2] as u16;
385 let r5 = (r >> 3) & 0x1f;
386 let g6 = (g >> 2) & 0x3f;
387 let b5 = (b >> 3) & 0x1f;
388 let v = (r5 << 11) | (g6 << 5) | b5;
389 let off = (py * buf_stride + px) * 2;
390 if off + 2 > buf.len() {
391 continue;
392 }
393 buf[off] = (v & 0xff) as u8;
394 buf[off + 1] = (v >> 8) as u8;
395 }
396 }
397}
398
399pub fn style_at(runs: &[crate::html_text::StyleRun], off: usize) -> TextStyle {
400 for r in runs {
401 if off >= r.start && off < r.end {
402 return TextStyle {
403 bold: r.bold,
404 italic: r.italic,
405 mono: false,
406 };
407 }
408 if r.start > off {
409 break;
410 }
411 }
412 TextStyle::PLAIN
413}
414
415pub fn style_for(runs: &[crate::html_text::StyleRun], off: usize, base: TextStyle) -> TextStyle {
416 let s = style_at(runs, off);
417 TextStyle {
418 bold: s.bold,
419 italic: s.italic,
420 mono: base.mono,
421 }
422}
423
424#[cfg(test)]
425mod tests;