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