kobo_core/rendering/
text_render.rs1pub mod blit;
2pub mod fonts;
3
4pub use blit::*;
5pub use fonts::*;
6
7const FONT_LATIN: &[u8] = include_bytes!("../../fonts/NotoSansLatin.ttf");
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Script {
11 Latin,
12 Bengali,
13 Devanagari,
14 Arabic,
15 Hebrew,
16 Cjk,
17 Thai,
18 Other,
19}
20
21impl Script {
22 pub fn is_rtl(self) -> bool {
23 matches!(self, Script::Arabic | Script::Hebrew)
24 }
25
26 pub fn uses_word_spacing(self) -> bool {
27 !matches!(self, Script::Cjk | Script::Thai)
28 }
29
30 pub fn lang_tag(self) -> &'static str {
31 match self {
32 Script::Latin => "en-US",
33 Script::Bengali => "bn-BD",
34 Script::Devanagari => "hi-IN",
35 Script::Arabic => "ar-SA",
36 Script::Hebrew => "he-IL",
37 Script::Cjk => "ja-JP",
38 Script::Thai => "th-TH",
39 Script::Other => "en-US",
40 }
41 }
42}
43
44pub fn detect_script(text: &str) -> Script {
45 for c in text.chars() {
46 if !c.is_alphabetic() {
47 continue;
48 }
49 let code = c as u32;
50 return match code {
51 0x0980..=0x09FF => Script::Bengali,
52 0x0900..=0x097F => Script::Devanagari,
53 0x0600..=0x06FF | 0x0750..=0x077F | 0xFB50..=0xFDFF | 0xFE70..=0xFEFF => Script::Arabic,
54 0x0590..=0x05FF => Script::Hebrew,
55 0x3040..=0x30FF | 0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0xAC00..=0xD7AF => Script::Cjk,
56 0x0E00..=0x0E7F => Script::Thai,
57 0x0000..=0x024F => Script::Latin,
58 _ => Script::Other,
59 };
60 }
61 Script::Latin
62}
63
64static WIDTH_CACHE: std::sync::OnceLock<
65 std::sync::Mutex<std::collections::HashMap<(String, u32), f32>>,
66> = std::sync::OnceLock::new();
67
68pub(crate) fn width_cache(
69) -> &'static std::sync::Mutex<std::collections::HashMap<(String, u32), f32>> {
70 WIDTH_CACHE.get_or_init(std::sync::Mutex::default)
71}
72
73#[allow(dead_code)]
74pub fn clear_width_cache() {
75 if let Ok(mut cache) = width_cache().lock() {
76 cache.clear();
77 }
78}
79
80pub struct DecodedImage {
81 pub rgb: Vec<u8>,
82 pub width: usize,
83 pub height: usize,
84}
85
86pub fn decode_image(raw: &[u8], max_w: usize, max_h: usize) -> Option<DecodedImage> {
87 let img = image::load_from_memory(raw).ok()?;
88 let (ow, oh) = (img.width() as usize, img.height() as usize);
89 if ow == 0 || oh == 0 {
90 return None;
91 }
92 let scale = max_w as f32 / ow as f32;
93 let mut nw = max_w;
94 let mut nh = (oh as f32 * scale).round() as usize;
95 if nh == 0 {
96 return None;
97 }
98 if nh > max_h {
99 let hscale = max_h as f32 / nh as f32;
100 nh = max_h;
101 nw = (nw as f32 * hscale).round() as usize;
102 if nw == 0 {
103 return None;
104 }
105 }
106 let resized = img.resize(nw as u32, nh as u32, image::imageops::FilterType::Triangle);
107 let rgb = resized.to_rgb8();
108 let (rw, rh) = (rgb.width() as usize, rgb.height() as usize);
109 Some(DecodedImage {
110 rgb: rgb.into_raw(),
111 width: rw,
112 height: rh,
113 })
114}
115
116pub fn blit_rgb565_image(
117 buf: &mut [u8],
118 buf_stride: usize,
119 rgb: &[u8],
120 iw: usize,
121 ih: usize,
122 ox: usize,
123 oy: usize,
124 max_w: usize,
125 max_h: usize,
126) {
127 for ry in 0..ih {
128 let py = oy + ry;
129 if py >= max_h {
130 break;
131 }
132 for rx in 0..iw {
133 let px = ox + rx;
134 if px >= max_w {
135 break;
136 }
137 let idx = (ry * iw + rx) * 3;
138 let r = rgb[idx] as u16;
139 let g = rgb[idx + 1] as u16;
140 let b = rgb[idx + 2] as u16;
141 let r5 = (r >> 3) & 0x1f;
142 let g6 = (g >> 2) & 0x3f;
143 let b5 = (b >> 3) & 0x1f;
144 let v = (r5 << 11) | (g6 << 5) | b5;
145 let off = (py * buf_stride + px) * 2;
146 if off + 2 > buf.len() {
147 continue;
148 }
149 buf[off] = (v & 0xff) as u8;
150 buf[off + 1] = (v >> 8) as u8;
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests;