1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//! Text rendering using fontdue.
use fontdue::{Font, FontSettings};
use tiny_skia::Pixmap;
/// Embedded DejaVu Sans Mono font.
const FONT_DATA: &[u8] = include_bytes!("../../fonts/DejaVuSansMono.ttf");
/// Text renderer using fontdue for rasterization.
pub struct TextRenderer {
font: Font,
}
impl TextRenderer {
/// Creates a new text renderer with the embedded font.
pub fn new() -> Self {
let font = Font::from_bytes(FONT_DATA, FontSettings::default())
.expect("Failed to load embedded font");
Self { font }
}
/// Draws text onto a pixmap at the specified position.
///
/// # Arguments
/// * `pixmap` - The pixmap to draw onto
/// * `x` - X position (left edge of text)
/// * `y` - Y position (top edge of text)
/// * `text` - The text to render
/// * `size` - Font size in pixels
/// * `color` - RGB888 color (0xRRGGBB)
pub fn draw_text(
&self,
pixmap: &mut Pixmap,
x: i32,
y: i32,
text: &str,
size: f32,
color: u32,
) {
let r = ((color >> 16) & 0xFF) as u8;
let g = ((color >> 8) & 0xFF) as u8;
let b = (color & 0xFF) as u8;
let mut cursor_x = x;
for ch in text.chars() {
let (metrics, bitmap) = self.font.rasterize(ch, size);
// Draw the glyph bitmap
for glyph_y in 0..metrics.height {
for glyph_x in 0..metrics.width {
let coverage = bitmap[glyph_y * metrics.width + glyph_x];
if coverage > 0 {
let px = cursor_x + metrics.xmin + glyph_x as i32;
let py = y
+ (size as i32 - metrics.ymin - metrics.height as i32)
+ glyph_y as i32;
if px >= 0
&& py >= 0
&& (px as u32) < pixmap.width()
&& (py as u32) < pixmap.height()
{
let idx = (py as u32 * pixmap.width() + px as u32) as usize * 4;
let data = pixmap.data_mut();
// Alpha blend the glyph
let alpha = coverage as f32 / 255.0;
let inv_alpha = 1.0 - alpha;
data[idx] = (r as f32 * alpha + data[idx] as f32 * inv_alpha) as u8;
data[idx + 1] =
(g as f32 * alpha + data[idx + 1] as f32 * inv_alpha) as u8;
data[idx + 2] =
(b as f32 * alpha + data[idx + 2] as f32 * inv_alpha) as u8;
data[idx + 3] = 255; // Full opacity
}
}
}
}
cursor_x += metrics.advance_width as i32;
}
}
/// Returns the width of text when rendered at the specified size.
pub fn text_width(&self, text: &str, size: f32) -> i32 {
text.chars()
.map(|ch| {
let (metrics, _) = self.font.rasterize(ch, size);
metrics.advance_width as i32
})
.sum()
}
/// Returns the width of text when rendered with horizontal scaling.
pub fn text_width_scaled(&self, text: &str, size: f32, x_scale: f32) -> i32 {
(self.text_width(text, size) as f32 * x_scale) as i32
}
/// Draws text with horizontal scaling (x_scale < 1.0 makes text narrower/taller-looking).
#[allow(clippy::too_many_arguments)]
pub fn draw_text_scaled(
&self,
pixmap: &mut Pixmap,
x: i32,
y: i32,
text: &str,
size: f32,
color: u32,
x_scale: f32,
) {
let r = ((color >> 16) & 0xFF) as u8;
let g = ((color >> 8) & 0xFF) as u8;
let b = (color & 0xFF) as u8;
let mut cursor_x = x as f32;
for ch in text.chars() {
let (metrics, bitmap) = self.font.rasterize(ch, size);
// Draw the glyph bitmap with horizontal scaling
let scaled_width = (metrics.width as f32 * x_scale).ceil() as usize;
for glyph_y in 0..metrics.height {
for scaled_x in 0..scaled_width {
// Map scaled x back to source x
let src_x = (scaled_x as f32 / x_scale) as usize;
if src_x >= metrics.width {
continue;
}
let coverage = bitmap[glyph_y * metrics.width + src_x];
if coverage > 0 {
let px = cursor_x as i32
+ (metrics.xmin as f32 * x_scale) as i32
+ scaled_x as i32;
let py = y
+ (size as i32 - metrics.ymin - metrics.height as i32)
+ glyph_y as i32;
if px >= 0
&& py >= 0
&& (px as u32) < pixmap.width()
&& (py as u32) < pixmap.height()
{
let idx = (py as u32 * pixmap.width() + px as u32) as usize * 4;
let data = pixmap.data_mut();
// Alpha blend the glyph
let alpha = coverage as f32 / 255.0;
let inv_alpha = 1.0 - alpha;
data[idx] = (r as f32 * alpha + data[idx] as f32 * inv_alpha) as u8;
data[idx + 1] =
(g as f32 * alpha + data[idx + 1] as f32 * inv_alpha) as u8;
data[idx + 2] =
(b as f32 * alpha + data[idx + 2] as f32 * inv_alpha) as u8;
data[idx + 3] = 255; // Full opacity
}
}
}
}
cursor_x += metrics.advance_width * x_scale;
}
}
/// Returns the line height for the specified font size.
pub fn line_height(&self, size: f32) -> i32 {
// fontdue doesn't provide line metrics directly, approximate
(size * 1.2) as i32
}
}
impl Default for TextRenderer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_renderer_creation() {
let renderer = TextRenderer::new();
let width = renderer.text_width("Hello", 16.0);
assert!(width > 0);
}
#[test]
fn test_draw_text() {
let renderer = TextRenderer::new();
let mut pixmap = Pixmap::new(100, 50).unwrap();
renderer.draw_text(&mut pixmap, 10, 10, "Test", 14.0, 0xFFFFFF);
// Just verify no panic
}
}