#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SubpixelOrder {
#[default]
Rgb,
Bgr,
RgbVertical,
None,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SubpixelGlyph {
pub glyph_id: u32,
pub x_subpixel: i32,
pub y_pixel: i32,
pub coverage_r: u8,
pub coverage_g: u8,
pub coverage_b: u8,
pub width: u32,
pub height: u32,
}
impl SubpixelGlyph {
pub fn new(
glyph_id: u32,
x_subpixel: i32,
y_pixel: i32,
coverage_r: u8,
coverage_g: u8,
coverage_b: u32,
width: u32,
height: u32,
) -> Self {
Self {
glyph_id,
x_subpixel,
y_pixel,
coverage_r,
coverage_g,
coverage_b: coverage_b as u8,
width,
height,
}
}
pub fn x_pixel(&self) -> i32 {
self.x_subpixel / 3
}
pub fn x_fraction(&self) -> i32 {
self.x_subpixel % 3
}
pub fn is_pixel_aligned(&self) -> bool {
self.x_subpixel % 3 == 0
}
}
pub fn subpixel_coverage(fraction: i32, order: SubpixelOrder) -> (u8, u8, u8) {
let f = fraction.rem_euclid(3);
let weights: [f32; 3] = match f {
0 => [1.0, 0.0, 0.0],
1 => [0.33, 0.67, 0.0],
2 => [0.0, 0.33, 0.67],
_ => [0.33, 0.34, 0.33],
};
let to_u8 = |w: f32| (w * 255.0).round() as u8;
match order {
SubpixelOrder::Rgb => (to_u8(weights[0]), to_u8(weights[1]), to_u8(weights[2])),
SubpixelOrder::Bgr => (to_u8(weights[2]), to_u8(weights[1]), to_u8(weights[0])),
SubpixelOrder::RgbVertical | SubpixelOrder::None => {
let avg = to_u8((weights[0] + weights[1] + weights[2]) / 3.0);
(avg, avg, avg)
}
}
}
pub fn layout_subpixel(
glyph_ids: &[u32],
advances: &[f32],
dpi_scale: f32,
order: SubpixelOrder,
) -> Vec<SubpixelGlyph> {
if glyph_ids.is_empty() || advances.is_empty() {
return Vec::new();
}
let mut result = Vec::new();
let mut x_accum: i32 = 0;
for (i, (&glyph_id, &advance)) in glyph_ids.iter().zip(advances.iter()).enumerate() {
let advance_subpx = (advance * dpi_scale * 3.0).round() as i32;
let x_subpixel = x_accum;
let fraction = x_subpixel.rem_euclid(3);
let (r, g, b) = subpixel_coverage(fraction, order);
let width = (advance * dpi_scale).max(1.0).ceil() as u32;
let height = (dpi_scale * 16.0).max(1.0).ceil() as u32;
result.push(SubpixelGlyph {
glyph_id,
x_subpixel,
y_pixel: 0,
coverage_r: r,
coverage_g: g,
coverage_b: b,
width,
height,
});
x_accum += advance_subpx;
if advance <= 0.0 && i < glyph_ids.len() - 1 {
x_accum += 3; }
}
result
}
pub fn render_lcd(
framebuffer: &mut [u8],
fb_width: u32,
fb_height: u32,
glyphs: &[SubpixelGlyph],
text_color: (u8, u8, u8, u8),
) {
if framebuffer.is_empty() || fb_width == 0 || fb_height == 0 {
return;
}
let (tr, tg, tb, ta) = text_color;
for glyph in glyphs {
let x_start = glyph.x_pixel().max(0) as u32;
let y_start = glyph.y_pixel.max(0) as u32;
let x_end = (glyph.x_pixel() + glyph.width as i32).min(fb_width as i32) as u32;
let y_end = (glyph.y_pixel + glyph.height as i32).min(fb_height as i32) as u32;
for y in y_start..y_end {
for x in x_start..x_end {
let idx = (y * fb_width + x) as usize * 4;
if idx + 3 >= framebuffer.len() {
continue;
}
let local_x = (x - x_start) as f32;
let subpixel_phase = (local_x * 3.0) as i32 % 3;
let (sr, sg, sb) = subpixel_coverage(subpixel_phase, SubpixelOrder::Rgb);
let blend = |text: u8, sub: u8| -> u8 {
let coverage = (sub as f32 / 255.0) * (ta as f32 / 255.0);
let val = text as f32 * coverage;
val.min(255.0) as u8
};
framebuffer[idx] = framebuffer[idx].saturating_add(blend(tr, sr));
framebuffer[idx + 1] = framebuffer[idx + 1].saturating_add(blend(tg, sg));
framebuffer[idx + 2] = framebuffer[idx + 2].saturating_add(blend(tb, sb));
}
}
}
}
pub fn subpixel_sharpness_factor(order: SubpixelOrder) -> f32 {
match order {
SubpixelOrder::Rgb | SubpixelOrder::Bgr => 3.0,
SubpixelOrder::RgbVertical => 1.0, SubpixelOrder::None => 1.0,
}
}
#[cfg(test)]
mod subpixel_tests {
use super::*;
#[test]
fn test_subpixel_coverage_aligned() {
let (r, g, b) = subpixel_coverage(0, SubpixelOrder::Rgb);
assert!(r > 200, "Aligned should have full R coverage");
assert!(g < 50, "Aligned should have minimal G coverage");
assert!(b < 50, "Aligned should have minimal B coverage");
}
#[test]
fn test_subpixel_coverage_shifted() {
let (r, g, b) = subpixel_coverage(1, SubpixelOrder::Rgb);
assert!(r > 0 && r < 200, "1/3 shift should blend R");
assert!(g > 50, "1/3 shift should have significant G");
assert!(b < 50, "1/3 shift should have minimal B");
}
#[test]
fn test_subpixel_coverage_bgr() {
let rgb = subpixel_coverage(0, SubpixelOrder::Rgb);
let bgr = subpixel_coverage(0, SubpixelOrder::Bgr);
assert_eq!(rgb.0, bgr.2, "BGR should swap R and B");
assert_eq!(rgb.2, bgr.0);
assert_eq!(rgb.1, bgr.1, "G should be the same");
}
#[test]
fn test_subpixel_coverage_none() {
let (r, g, b) = subpixel_coverage(0, SubpixelOrder::None);
assert_eq!(r, g, "None order should have equal coverage");
assert_eq!(g, b);
}
#[test]
fn test_layout_subpixel_empty() {
let glyphs = layout_subpixel(&[], &[], 1.0, SubpixelOrder::Rgb);
assert!(glyphs.is_empty());
}
#[test]
fn test_layout_subpixel_single() {
let glyphs = layout_subpixel(&[42], &[8.0], 1.0, SubpixelOrder::Rgb);
assert_eq!(glyphs.len(), 1);
assert_eq!(glyphs[0].glyph_id, 42);
assert_eq!(glyphs[0].x_subpixel, 0);
assert!(glyphs[0].is_pixel_aligned());
}
#[test]
fn test_layout_subpixel_fractional() {
let glyphs = layout_subpixel(&[1, 2], &[5.5, 5.5], 1.0, SubpixelOrder::Rgb);
assert_eq!(glyphs.len(), 2);
assert_eq!(glyphs[0].x_subpixel, 0);
assert_eq!(glyphs[1].x_subpixel, 17);
assert!(!glyphs[1].is_pixel_aligned());
}
#[test]
fn test_layout_subpixel_dpi_scale() {
let glyphs_1x = layout_subpixel(&[1], &[8.0], 1.0, SubpixelOrder::Rgb);
let glyphs_2x = layout_subpixel(&[1], &[8.0], 2.0, SubpixelOrder::Rgb);
assert_eq!(glyphs_1x[0].x_subpixel, 0);
assert_eq!(glyphs_2x[0].x_subpixel, 0);
assert!(glyphs_2x[0].width >= glyphs_1x[0].width * 2);
}
#[test]
fn test_subpixel_glyph_pixel_position() {
let glyph = SubpixelGlyph::new(1, 7, 0, 255, 128, 64, 10, 16);
assert_eq!(glyph.x_pixel(), 2); assert_eq!(glyph.x_fraction(), 1); assert!(!glyph.is_pixel_aligned());
}
#[test]
fn test_subpixel_glyph_aligned() {
let glyph = SubpixelGlyph::new(1, 9, 0, 255, 128, 64, 10, 16);
assert_eq!(glyph.x_pixel(), 3);
assert!(glyph.is_pixel_aligned());
}
#[test]
fn test_sharpness_factor() {
assert_eq!(subpixel_sharpness_factor(SubpixelOrder::Rgb), 3.0);
assert_eq!(subpixel_sharpness_factor(SubpixelOrder::Bgr), 3.0);
assert_eq!(subpixel_sharpness_factor(SubpixelOrder::RgbVertical), 1.0);
assert_eq!(subpixel_sharpness_factor(SubpixelOrder::None), 1.0);
}
#[test]
fn test_render_lcd_empty() {
let mut fb = vec![0u8; 100 * 100 * 4];
render_lcd(&mut fb, 100, 100, &[], (255, 255, 255, 255));
assert!(fb.iter().all(|&v| v == 0));
}
#[test]
fn test_render_lcd_single_glyph() {
let mut fb = vec![0u8; 100 * 100 * 4];
let glyphs = vec![SubpixelGlyph::new(1, 0, 0, 255, 0, 0, 10, 16)];
render_lcd(&mut fb, 100, 100, &glyphs, (255, 255, 255, 255));
let has_nonzero = fb.iter().any(|&v| v > 0);
assert!(has_nonzero, "LCD rendering should produce non-zero pixels");
}
#[test]
fn test_render_lcd_zero_size() {
let mut fb = vec![];
let glyphs = vec![SubpixelGlyph::new(1, 0, 0, 255, 0, 0, 10, 16)];
render_lcd(&mut fb, 0, 0, &glyphs, (255, 255, 255, 255));
}
}