use crate::options::LcdFilterKernel;
use crate::stem_darken::{apply_stem_darkening, stem_darkening_amount};
use oxitext_core::LcdBitmap;
fn fir_kernel(kind: LcdFilterKernel) -> &'static [f32] {
match kind {
LcdFilterKernel::Box => &[1.0_f32 / 3.0, 1.0 / 3.0, 1.0 / 3.0],
LcdFilterKernel::Triangle => &[1.0_f32 / 4.0, 1.0 / 2.0, 1.0 / 4.0],
LcdFilterKernel::FreeType5Tap => {
&[1.0_f32 / 9.0, 2.0 / 9.0, 3.0 / 9.0, 2.0 / 9.0, 1.0 / 9.0]
}
}
}
fn fir_horizontal(buf: &mut [f32], width: usize, height: usize, kernel: &[f32]) {
let half = kernel.len() / 2;
let mut tmp = vec![0.0f32; buf.len()];
for y in 0..height {
for x in 0..width {
let mut sum = 0.0f32;
for (k, &kv) in kernel.iter().enumerate() {
let xi = x as isize + k as isize - half as isize;
if xi >= 0 && (xi as usize) < width {
sum += buf[y * width + xi as usize] * kv;
}
}
tmp[y * width + x] = sum;
}
}
buf.copy_from_slice(&tmp);
}
pub fn rasterize_lcd(
face_data: &[u8],
glyph_id: u16,
px_size: f32,
filter: LcdFilterKernel,
stem_darkening_enabled: bool,
) -> Option<LcdBitmap> {
use ab_glyph::{Font, FontRef, GlyphId as AbGlyphId, PxScale};
let font = FontRef::try_from_slice(face_data).ok()?;
let ab_gid = AbGlyphId(glyph_id);
let scale = PxScale {
x: px_size * 3.0,
y: px_size,
};
let glyph = ab_gid.with_scale(scale);
let outlined = font.outline_glyph(glyph)?;
let bounds = outlined.px_bounds();
let hi_width = bounds.width().ceil() as usize;
let height = bounds.height().ceil() as usize;
if hi_width == 0 || height == 0 {
return None;
}
let mut coverage = vec![0.0f32; hi_width * height];
outlined.draw(|x, y, c| {
let idx = y as usize * hi_width + x as usize;
if idx < coverage.len() {
coverage[idx] = c;
}
});
if stem_darkening_enabled {
let amount = stem_darkening_amount(px_size);
apply_stem_darkening(&mut coverage, amount);
}
let kernel = fir_kernel(filter);
fir_horizontal(&mut coverage, hi_width, height, kernel);
let out_width = hi_width / 3;
if out_width == 0 {
return None;
}
let mut rgb = vec![0u8; out_width * height * 3];
for y in 0..height {
for x in 0..out_width {
let r = (coverage[y * hi_width + x * 3].clamp(0.0, 1.0) * 255.0).round() as u8;
let g = (coverage[y * hi_width + x * 3 + 1].clamp(0.0, 1.0) * 255.0).round() as u8;
let b = (coverage[y * hi_width + x * 3 + 2].clamp(0.0, 1.0) * 255.0).round() as u8;
let base = (y * out_width + x) * 3;
rgb[base] = r;
rgb[base + 1] = g;
rgb[base + 2] = b;
}
}
Some(LcdBitmap::new(out_width as u32, height as u32, rgb))
}
pub fn kernel_weight_sum(kind: LcdFilterKernel) -> f32 {
fir_kernel(kind).iter().sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fir_kernel_sums_to_one() {
let kinds = [
LcdFilterKernel::Box,
LcdFilterKernel::Triangle,
LcdFilterKernel::FreeType5Tap,
];
for kind in kinds {
let sum = kernel_weight_sum(kind);
assert!(
(sum - 1.0).abs() < 1e-5,
"kernel {:?} does not sum to 1.0: sum={sum}",
kind
);
}
}
#[test]
fn fir_horizontal_preserves_flat_signal() {
let width = 20usize;
let height = 2usize;
let kernel = fir_kernel(LcdFilterKernel::FreeType5Tap);
let half = kernel.len() / 2;
let mut buf = vec![0.5f32; width * height];
fir_horizontal(&mut buf, width, height, kernel);
for y in 0..height {
for x in half..(width - half) {
let v = buf[y * width + x];
assert!(
(v - 0.5).abs() < 1e-4,
"uniform signal changed after FIR at ({x},{y}): got {v}"
);
}
}
}
}