Skip to main content

kobo_core/rendering/text_render/
blit.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Nayeem Bin Ahsan
3use rustybuzz::UnicodeBuffer;
4
5use super::detect_script;
6use super::fonts::{fallback_for_char, font_for, glyph_cache, TextStyle};
7
8pub fn blit_rgb565(
9    buf: &mut [u8],
10    buf_stride: usize,
11    text: &str,
12    px_size: f32,
13    ox: usize,
14    oy: usize,
15    max_w: usize,
16    max_h: usize,
17) {
18    blit_rgb565_color(buf, buf_stride, text, px_size, ox, oy, 0x0000, max_w, max_h);
19}
20
21/// Emphasis is synthesised from the regular face (see [`TextStyle`]).
22///
23/// Bold smears each glyph's coverage horizontally, italic shears it about the
24/// baseline. Both act on the rasterised bitmap rather than the outline, which
25/// means they compose with every script and with the glyph cache: the cache is
26/// keyed on the unstyled glyph, so a bold run reuses the same rasterisation as
27/// plain text.
28///
29/// Neither changes the pen advance. That is what keeps emphasis from reflowing
30/// a paragraph -- the same reason `word_width_styled` ignores them.
31const OBLIQUE_SHEAR: f32 = 0.21; // ~12 degrees
32
33/// Horizontal smear for synthetic bold, in px. Scaled with type size so it
34/// reads as the same weight at any font setting, and never less than 1.
35fn bold_smear(px_size: f32) -> usize {
36    ((px_size / 22.0).round() as usize).max(1)
37}
38
39pub fn blit_rgb565_color(
40    buf: &mut [u8],
41    buf_stride: usize,
42    text: &str,
43    px_size: f32,
44    ox: usize,
45    oy: usize,
46    color: u16,
47    max_w: usize,
48    max_h: usize,
49) {
50    blit_rgb565_styled(
51        buf,
52        buf_stride,
53        text,
54        px_size,
55        ox,
56        oy,
57        color,
58        TextStyle::PLAIN,
59        max_w,
60        max_h,
61    );
62}
63
64/// As [`blit_rgb565_color`], but sets the run in `style`.
65#[allow(clippy::too_many_arguments)]
66pub fn blit_rgb565_styled(
67    buf: &mut [u8],
68    buf_stride: usize,
69    text: &str,
70    px_size: f32,
71    ox: usize,
72    oy: usize,
73    color: u16,
74    style: TextStyle,
75    max_w: usize,
76    max_h: usize,
77) {
78    let cr = ((color >> 11) & 0x1f) as u32;
79    let cg = ((color >> 5) & 0x3f) as u32;
80    let cb = (color & 0x1f) as u32;
81    let script = detect_script(text);
82    let fd = font_for(script, style);
83    let smear = if style.bold { bold_smear(px_size) } else { 0 };
84    let scale = px_size / fd.face.units_per_em() as f32;
85    let lm = fd.body.horizontal_line_metrics(px_size);
86    let baseline = match lm {
87        Some(m) => m.ascent as i32,
88        None => px_size as i32,
89    };
90
91    let mut ub = UnicodeBuffer::new();
92    ub.push_str(text);
93    let dir = if script.is_rtl() {
94        rustybuzz::Direction::RightToLeft
95    } else {
96        rustybuzz::Direction::LeftToRight
97    };
98    ub.set_direction(dir);
99
100    let gb = rustybuzz::shape(&fd.face, &[], ub);
101    let glyphs = gb.glyph_infos();
102    let positions = gb.glyph_positions();
103
104    let mut cursor_x = 0.0;
105    let gc = glyph_cache();
106    for (info, pos) in glyphs.iter().zip(positions) {
107        let advance = pos.x_advance as f32 * scale;
108        let gid = info.glyph_id as u16;
109        let px_key = (px_size * 100.0) as u32;
110        let (rfont, rgid) = if gid != 0 {
111            (fd, gid)
112        } else {
113            let ch = text
114                .get(info.cluster as usize..)
115                .and_then(|s| s.chars().next());
116            match ch.and_then(fallback_for_char) {
117                Some(fb) => fb,
118                None => (fd, gid),
119            }
120        };
121        let key = (rfont.id, rgid, px_key);
122        let (metrics, bitmap) = if let Ok(cache) = gc.lock() {
123            cache.get(&key).cloned().unwrap_or_else(|| {
124                drop(cache);
125                let entry = rfont.body.rasterize_indexed(rgid, px_size);
126                // best-effort: a poisoned/contended cache lock just skips caching
127                let _ = gc.lock().map(|mut c| {
128                    c.insert(key, entry.clone());
129                });
130                entry
131            })
132        } else {
133            rfont.body.rasterize_indexed(rgid, px_size)
134        };
135        let gw = metrics.width;
136        let gh = metrics.height;
137        if gw > 0 && gh > 0 {
138            let pen_x = cursor_x + pos.x_offset as f32 * scale;
139            let gx0 = ox as i32 + pen_x.round() as i32 + metrics.xmin;
140            let gy0 = oy as i32 + baseline
141                - (pos.y_offset as f32 * scale).round() as i32
142                - metrics.ymin
143                - gh as i32;
144            for ry in 0..gh {
145                // Shear about the baseline so the glyph leans without drifting
146                // off the line: rows above the baseline move right, descenders
147                // move left, and the baseline itself does not move.
148                let shear_dx = if style.italic {
149                    let above_baseline = (gh as i32 - ry as i32) + metrics.ymin;
150                    (above_baseline as f32 * OBLIQUE_SHEAR).round() as i32
151                } else {
152                    0
153                };
154                for rx in 0..gw + smear {
155                    // Synthetic bold: a pixel is inked if any of the `smear`
156                    // columns to its left were, which thickens every stem by the
157                    // same amount regardless of direction.
158                    let lo = rx.saturating_sub(smear);
159                    let hi = rx.min(gw - 1);
160                    let cov = (lo..=hi)
161                        .map(|sx| bitmap[ry * gw + sx] as u32)
162                        .max()
163                        .unwrap_or(0);
164                    if cov == 0 {
165                        continue;
166                    }
167                    let px = gx0 + rx as i32 + shear_dx;
168                    let py = gy0 + ry as i32;
169                    if px < 0 || (px as usize) >= max_w || py < 0 || (py as usize) >= max_h {
170                        continue;
171                    }
172                    let off = (py as usize * buf_stride + px as usize) * 2;
173                    if off + 2 > buf.len() {
174                        continue;
175                    }
176                    let old = (buf[off] as u16) | ((buf[off + 1] as u16) << 8);
177                    let inv = 255 - cov;
178                    let nr = (cr * cov + (((old >> 11) & 0x1f) as u32) * inv) / 255;
179                    let ng = (cg * cov + (((old >> 5) & 0x3f) as u32) * inv) / 255;
180                    let nb = (cb * cov + ((old & 0x1f) as u32) * inv) / 255;
181                    let v = ((nr << 11) | (ng << 5) | nb) as u16;
182                    buf[off] = (v & 0xff) as u8;
183                    buf[off + 1] = (v >> 8) as u8;
184                }
185            }
186        }
187        cursor_x += advance;
188    }
189}