use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use agg_rust::color::Gray8;
use agg_rust::conv_curve::ConvCurve;
use agg_rust::conv_transform::ConvTransform;
pub struct LcdBuffer {
color: Vec<u8>,
alpha: Vec<u8>,
width: u32,
height: u32,
}
impl LcdBuffer {
pub fn new(width: u32, height: u32) -> Self {
let bytes = (width as usize) * (height as usize) * 3;
Self {
color: vec![0u8; bytes],
alpha: vec![0u8; bytes],
width,
height,
}
}
#[inline] pub fn width(&self) -> u32 { self.width }
#[inline] pub fn height(&self) -> u32 { self.height }
#[inline] pub fn color_plane(&self) -> &[u8] { &self.color }
#[inline] pub fn alpha_plane(&self) -> &[u8] { &self.alpha }
#[inline] pub fn color_plane_mut(&mut self) -> &mut [u8] { &mut self.color }
#[inline] pub fn alpha_plane_mut(&mut self) -> &mut [u8] { &mut self.alpha }
#[inline]
pub fn planes_mut(&mut self) -> (&mut [u8], &mut [u8]) {
(&mut self.color, &mut self.alpha)
}
pub fn into_planes(self) -> (Vec<u8>, Vec<u8>) { (self.color, self.alpha) }
pub fn color_plane_flipped(&self) -> Vec<u8> {
flip_plane(&self.color, self.width, self.height)
}
pub fn alpha_plane_flipped(&self) -> Vec<u8> {
flip_plane(&self.alpha, self.width, self.height)
}
pub fn to_rgba8_top_down_collapsed(&self) -> Vec<u8> {
let w = self.width as usize;
let h = self.height as usize;
let mut out = vec![0u8; w * h * 4];
for y in 0..h {
let src_y = h - 1 - y;
for x in 0..w {
let si = (src_y * w + x) * 3;
let di = (y * w + x) * 4;
let ra = self.alpha[si];
let ga = self.alpha[si + 1];
let ba = self.alpha[si + 2];
let a = ra.max(ga).max(ba);
if a == 0 { continue; } let af = a as f32 / 255.0;
let rc = self.color[si] as f32 / 255.0;
let gc = self.color[si + 1] as f32 / 255.0;
let bc = self.color[si + 2] as f32 / 255.0;
out[di] = ((rc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
out[di + 1] = ((gc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
out[di + 2] = ((bc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
out[di + 3] = a;
}
}
out
}
pub fn clear(&mut self, color: Color) {
let a = color.a.clamp(0.0, 1.0);
let r_c = ((color.r.clamp(0.0, 1.0) * a) * 255.0 + 0.5) as u8;
let g_c = ((color.g.clamp(0.0, 1.0) * a) * 255.0 + 0.5) as u8;
let b_c = ((color.b.clamp(0.0, 1.0) * a) * 255.0 + 0.5) as u8;
let a_byte = (a * 255.0 + 0.5) as u8;
for px in self.color.chunks_exact_mut(3) {
px[0] = r_c;
px[1] = g_c;
px[2] = b_c;
}
for px in self.alpha.chunks_exact_mut(3) {
px[0] = a_byte;
px[1] = a_byte;
px[2] = a_byte;
}
}
pub fn fill_path(
&mut self,
path: &mut PathStorage,
color: Color,
transform: &TransAffine,
clip: Option<(f64, f64, f64, f64)>,
) {
if self.width == 0 || self.height == 0 { return; }
let mut builder = LcdMaskBuilder::new(self.width, self.height).with_clip(clip);
builder.with_paths(transform, |add| { add(path); });
let mask = builder.finalize();
let clip_i = clip.map(rect_to_pixel_clip);
self.composite_mask(&mask, color, 0, 0, clip_i);
}
pub fn composite_mask(
&mut self,
mask: &LcdMask,
src: Color,
dst_x: i32,
dst_y: i32,
clip: Option<(i32, i32, i32, i32)>,
) {
if mask.width == 0 || mask.height == 0 { return; }
let sa = src.a.clamp(0.0, 1.0);
let sr = src.r.clamp(0.0, 1.0);
let sg = src.g.clamp(0.0, 1.0);
let sb = src.b.clamp(0.0, 1.0);
let dst_w_i = self.width as i32;
let dst_h_i = self.height as i32;
let dst_w_u = self.width as usize;
let mw = mask.width as i32;
let mh = mask.height as i32;
let (cx1, cy1, cx2, cy2) = match clip {
Some((cx1, cy1, cx2, cy2)) =>
(cx1.max(0), cy1.max(0), cx2.min(dst_w_i), cy2.min(dst_h_i)),
None => (0, 0, dst_w_i, dst_h_i),
};
if cx1 >= cx2 || cy1 >= cy2 { return; }
for my in 0..mh {
let dy = dst_y + my;
if dy < cy1 || dy >= cy2 { continue; }
let dy_u = dy as usize;
for mx in 0..mw {
let dx = dst_x + mx;
if dx < cx1 || dx >= cx2 { continue; }
let mi = ((my * mw + mx) * 3) as usize;
let ea_r = sa * (mask.data[mi] as f32 / 255.0);
let ea_g = sa * (mask.data[mi + 1] as f32 / 255.0);
let ea_b = sa * (mask.data[mi + 2] as f32 / 255.0);
if ea_r == 0.0 && ea_g == 0.0 && ea_b == 0.0 { continue; }
let di = (dy_u * dst_w_u + (dx as usize)) * 3;
let bc_r = self.color[di] as f32 / 255.0;
let bc_g = self.color[di + 1] as f32 / 255.0;
let bc_b = self.color[di + 2] as f32 / 255.0;
let ba_r = self.alpha[di] as f32 / 255.0;
let ba_g = self.alpha[di + 1] as f32 / 255.0;
let ba_b = self.alpha[di + 2] as f32 / 255.0;
let rc_r = sr * ea_r + bc_r * (1.0 - ea_r);
let rc_g = sg * ea_g + bc_g * (1.0 - ea_g);
let rc_b = sb * ea_b + bc_b * (1.0 - ea_b);
let ra_r = ea_r + ba_r * (1.0 - ea_r);
let ra_g = ea_g + ba_g * (1.0 - ea_g);
let ra_b = ea_b + ba_b * (1.0 - ea_b);
self.color[di] = (rc_r * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.color[di + 1] = (rc_g * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.color[di + 2] = (rc_b * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.alpha[di] = (ra_r * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.alpha[di + 1] = (ra_g * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.alpha[di + 2] = (ra_b * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
}
}
}
pub fn composite_buffer(
&mut self,
src: &LcdBuffer,
dst_x: i32,
dst_y: i32,
clip: Option<(i32, i32, i32, i32)>,
) {
if src.width == 0 || src.height == 0 { return; }
let dst_w_i = self.width as i32;
let dst_h_i = self.height as i32;
let dst_w_u = self.width as usize;
let src_w_u = src.width as usize;
let sw = src.width as i32;
let sh = src.height as i32;
let (cx1, cy1, cx2, cy2) = match clip {
Some((x1, y1, x2, y2)) =>
(x1.max(0), y1.max(0), x2.min(dst_w_i), y2.min(dst_h_i)),
None => (0, 0, dst_w_i, dst_h_i),
};
if cx1 >= cx2 || cy1 >= cy2 { return; }
for sy in 0..sh {
let dy = dst_y + sy;
if dy < cy1 || dy >= cy2 { continue; }
let dy_u = dy as usize;
let sy_u = sy as usize;
for sx in 0..sw {
let dx = dst_x + sx;
if dx < cx1 || dx >= cx2 { continue; }
let si = (sy_u * src_w_u + sx as usize) * 3;
let di = (dy_u * dst_w_u + dx as usize) * 3;
let sa_r = src.alpha[si] as f32 / 255.0;
let sa_g = src.alpha[si + 1] as f32 / 255.0;
let sa_b = src.alpha[si + 2] as f32 / 255.0;
if sa_r == 0.0 && sa_g == 0.0 && sa_b == 0.0 { continue; }
let sc_r = src.color[si] as f32 / 255.0;
let sc_g = src.color[si + 1] as f32 / 255.0;
let sc_b = src.color[si + 2] as f32 / 255.0;
let bc_r = self.color[di] as f32 / 255.0;
let bc_g = self.color[di + 1] as f32 / 255.0;
let bc_b = self.color[di + 2] as f32 / 255.0;
let ba_r = self.alpha[di] as f32 / 255.0;
let ba_g = self.alpha[di + 1] as f32 / 255.0;
let ba_b = self.alpha[di + 2] as f32 / 255.0;
let rc_r = sc_r + bc_r * (1.0 - sa_r);
let rc_g = sc_g + bc_g * (1.0 - sa_g);
let rc_b = sc_b + bc_b * (1.0 - sa_b);
let ra_r = sa_r + ba_r * (1.0 - sa_r);
let ra_g = sa_g + ba_g * (1.0 - sa_g);
let ra_b = sa_b + ba_b * (1.0 - sa_b);
self.color[di] = (rc_r * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.color[di + 1] = (rc_g * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.color[di + 2] = (rc_b * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.alpha[di] = (ra_r * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.alpha[di + 1] = (ra_g * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
self.alpha[di + 2] = (ra_b * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
}
}
}
}
fn flip_plane(src: &[u8], width: u32, height: u32) -> Vec<u8> {
let row_bytes = (width * 3) as usize;
let mut out = vec![0u8; src.len()];
for y in 0..height as usize {
let dst_y = height as usize - 1 - y;
out[dst_y * row_bytes .. (dst_y + 1) * row_bytes]
.copy_from_slice(&src[y * row_bytes .. (y + 1) * row_bytes]);
}
out
}
use agg_rust::path_storage::PathStorage;
use agg_rust::pixfmt_gray::PixfmtGray8;
use agg_rust::rasterizer_scanline_aa::RasterizerScanlineAa;
use agg_rust::renderer_base::RendererBase;
use agg_rust::renderer_scanline::render_scanlines_aa_solid;
use agg_rust::rendering_buffer::RowAccessor;
use agg_rust::scanline_u::ScanlineU8;
use agg_rust::trans_affine::TransAffine;
use crate::color::Color;
use crate::text::{measure_text_metrics, shape_text, Font};
pub fn identity_xform() -> TransAffine { TransAffine::new() }
pub struct CachedLcdText {
pub pixels: Arc<Vec<u8>>,
pub width: u32,
pub height: u32,
pub baseline_x_in_mask: f64,
pub baseline_y_in_mask: f64,
}
const MASK_PAD: f64 = 2.0;
#[derive(Clone, PartialEq, Eq, Hash)]
struct LcdMaskKey {
text: String,
font_ptr: usize,
size_bits: u64,
width_bits: u64,
italic_bits: u64,
interval_bits: u64,
hint_y: bool,
faux_weight_bits: u64,
primary_weight_bits: u64,
gamma_bits: u64,
}
struct LcdMaskEntry {
pixels: Arc<Vec<u8>>,
width: u32,
height: u32,
baseline_x_in_mask: f64,
baseline_y_in_mask: f64,
}
thread_local! {
static MASK_CACHE: RefCell<HashMap<LcdMaskKey, LcdMaskEntry>>
= RefCell::new(HashMap::new());
static MASK_LRU: RefCell<VecDeque<LcdMaskKey>>
= RefCell::new(VecDeque::new());
}
const MASK_CACHE_MAX: usize = 1024;
pub fn rasterize_text_lcd_cached(
font: &Arc<Font>,
text: &str,
size: f64,
) -> CachedLcdText {
let width_now = crate::font_settings::current_width();
let italic_now = crate::font_settings::current_faux_italic();
let interval_now = crate::font_settings::current_interval();
let hint_y_now = crate::font_settings::hinting_enabled();
let fweight_now = crate::font_settings::current_faux_weight();
let pweight_now = crate::font_settings::current_primary_weight();
let gamma_now = crate::font_settings::current_gamma();
let key = LcdMaskKey {
text: text.to_string(),
font_ptr: Arc::as_ptr(font) as *const () as usize,
size_bits: size.to_bits(),
width_bits: width_now.to_bits(),
italic_bits: italic_now.to_bits(),
interval_bits: interval_now.to_bits(),
hint_y: hint_y_now,
faux_weight_bits: fweight_now.to_bits(),
primary_weight_bits: pweight_now.to_bits(),
gamma_bits: gamma_now.to_bits(),
};
let hit = MASK_CACHE.with(|m| {
m.borrow().get(&key).map(|e| CachedLcdText {
pixels: Arc::clone(&e.pixels),
width: e.width,
height: e.height,
baseline_x_in_mask: e.baseline_x_in_mask,
baseline_y_in_mask: e.baseline_y_in_mask,
})
});
if let Some(got) = hit {
MASK_LRU.with(|lru| {
let mut lru = lru.borrow_mut();
if let Some(pos) = lru.iter().position(|k| k == &key) {
lru.remove(pos);
}
lru.push_back(key);
});
return got;
}
let m = measure_text_metrics(font, text, size);
let width_slack = (width_now - 1.0).abs() * size;
let italic_slack = (italic_now.abs() / 3.0) * (m.ascent + m.descent);
let extra_pad = (width_slack + italic_slack).ceil();
let pad_x = MASK_PAD + extra_pad;
let bw = (m.width + pad_x * 2.0).ceil().max(1.0) as u32;
let bh = (m.ascent + m.descent + MASK_PAD * 2.0).ceil().max(1.0) as u32;
let bx = pad_x;
let by_unhinted = MASK_PAD + m.descent;
let by = if hint_y_now { by_unhinted.round() } else { by_unhinted };
let mask = rasterize_lcd_mask(
font, text, size, bx, by, bw, bh, &TransAffine::new(),
);
let pixels = Arc::new(mask.data);
let entry = LcdMaskEntry {
pixels: Arc::clone(&pixels),
width: bw,
height: bh,
baseline_x_in_mask: bx,
baseline_y_in_mask: by,
};
MASK_CACHE.with(|m| m.borrow_mut().insert(key.clone(), entry));
MASK_LRU.with(|lru| {
let mut lru = lru.borrow_mut();
lru.push_back(key.clone());
while lru.len() > MASK_CACHE_MAX {
if let Some(old) = lru.pop_front() {
MASK_CACHE.with(|m| m.borrow_mut().remove(&old));
}
}
});
CachedLcdText {
pixels,
width: bw,
height: bh,
baseline_x_in_mask: bx,
baseline_y_in_mask: by,
}
}
pub struct LcdMask {
pub data: Vec<u8>, pub width: u32,
pub height: u32,
}
const FILTER_WEIGHTS: [u32; 5] = [1, 2, 3, 2, 1];
const FILTER_SUM: u32 = 9;
fn lcd_filter_weights() -> [f64; 5] {
let p_units = crate::font_settings::current_primary_weight() * 9.0;
let weights = [1.0, 2.0, p_units, 2.0, 1.0];
let sum = weights.iter().sum::<f64>().max(1e-9);
[
weights[0] / sum,
weights[1] / sum,
weights[2] / sum,
weights[3] / sum,
weights[4] / sum,
]
}
pub fn rasterize_lcd_mask(
font: &Font,
text: &str,
size: f64,
x: f64,
y: f64,
mask_w: u32,
mask_h: u32,
transform: &TransAffine,
) -> LcdMask {
rasterize_lcd_mask_multi(font, &[(text, x, y)], size, mask_w, mask_h, transform)
}
pub fn rasterize_lcd_mask_multi(
font: &Font,
spans: &[(&str, f64, f64)],
size: f64,
mask_w: u32,
mask_h: u32,
transform: &TransAffine,
) -> LcdMask {
let mut builder = LcdMaskBuilder::new(mask_w, mask_h);
builder.with_paths(transform, |add| {
for (text, x, y) in spans {
if text.is_empty() { continue; }
let (mut paths, _) = shape_text(font, text, size, *x, *y);
for path in paths.iter_mut() {
add(path);
}
}
});
builder.finalize()
}
pub fn rect_to_pixel_clip(rect: (f64, f64, f64, f64)) -> (i32, i32, i32, i32) {
let (x, y, w, h) = rect;
(
x.floor() as i32,
y.floor() as i32,
(x + w).ceil() as i32,
(y + h).ceil() as i32,
)
}
pub struct LcdMaskBuilder {
gray: Vec<u8>,
gray_w: u32,
gray_h: u32,
mask_w: u32,
mask_h: u32,
clip: Option<(f64, f64, f64, f64)>,
}
impl LcdMaskBuilder {
pub fn new(mask_w: u32, mask_h: u32) -> Self {
let gray_w = mask_w.saturating_mul(3);
let gray_h = mask_h;
let gray = vec![0u8; (gray_w as usize) * (gray_h as usize)];
Self { gray, gray_w, gray_h, mask_w, mask_h, clip: None }
}
pub fn with_clip(mut self, clip: Option<(f64, f64, f64, f64)>) -> Self {
self.clip = clip;
self
}
pub fn with_paths<F>(&mut self, transform: &TransAffine, f: F)
where F: FnOnce(&mut dyn FnMut(&mut PathStorage)),
{
rasterize_paths_into_gray(
&mut self.gray, self.gray_w, self.gray_h, transform, self.clip, f,
);
}
pub fn finalize(self) -> LcdMask {
if self.mask_w == 0 || self.mask_h == 0 {
return LcdMask { data: Vec::new(), width: self.mask_w, height: self.mask_h };
}
let data = apply_5_tap_filter(
&self.gray, self.gray_w, self.mask_w, self.mask_h,
);
LcdMask { data, width: self.mask_w, height: self.mask_h }
}
}
fn rasterize_paths_into_gray<F>(
gray: &mut [u8],
gray_w: u32,
gray_h: u32,
transform: &TransAffine,
clip: Option<(f64, f64, f64, f64)>,
f: F,
)
where F: FnOnce(&mut dyn FnMut(&mut PathStorage)),
{
if gray_w == 0 || gray_h == 0 { return; }
let stride = gray_w as i32;
let mut ra = RowAccessor::new();
unsafe { ra.attach(gray.as_mut_ptr(), gray_w, gray_h, stride); }
let pf = PixfmtGray8::new(&mut ra);
let mut rb = RendererBase::new(pf);
if let Some((cx, cy, cw, ch)) = clip {
let x1 = (cx.floor() as i32).saturating_mul(3);
let y1 = cy.floor() as i32;
let x2 = ((cx + cw).ceil() as i32).saturating_mul(3) - 1;
let y2 = (cy + ch).ceil() as i32 - 1;
rb.clip_box_i(x1, y1, x2, y2);
}
let mut ras = RasterizerScanlineAa::new();
let mut sl = ScanlineU8::new();
let cov_color = Gray8::new_opaque(255);
let mut xform = *transform;
xform.sx *= 3.0;
xform.shx *= 3.0;
xform.tx *= 3.0;
let mut add = |path: &mut PathStorage| {
let mut curves = ConvCurve::new(path);
let mut tx = ConvTransform::new(&mut curves, xform);
ras.reset();
ras.add_path(&mut tx, 0);
render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &cov_color);
};
f(&mut add);
}
fn apply_5_tap_filter(gray: &[u8], gray_w: u32, mask_w: u32, mask_h: u32) -> Vec<u8> {
let primary = crate::font_settings::current_primary_weight();
let gamma = crate::font_settings::current_gamma();
let is_default_primary = ((primary - 1.0 / 3.0).abs()) < 1e-6;
let is_default_gamma = ((gamma - 1.0).abs()) < 1e-6;
if is_default_primary && is_default_gamma {
return apply_5_tap_filter_legacy(gray, gray_w, mask_w, mask_h);
}
let mut data = vec![0u8; (mask_w as usize) * (mask_h as usize) * 3];
let gw = gray_w as i32;
let w = lcd_filter_weights();
let inv_g = 1.0 / gamma.max(1e-3);
let need_gamma = !is_default_gamma;
let apply_gamma = |c: f64| -> f64 {
if !need_gamma { return c; }
let t = (c / 255.0).clamp(0.0, 1.0);
t.powf(inv_g) * 255.0
};
for py in 0..mask_h {
let row_start = (py as usize) * (gray_w as usize);
let row = &gray[row_start .. row_start + gray_w as usize];
for px in 0..mask_w {
let base = (px as i32) * 3;
let sample = |off: i32| -> f64 {
let pos = base + off;
if pos < 0 || pos >= gw { 0.0 } else { row[pos as usize] as f64 }
};
let cov_r = w[0] * sample(-2) + w[1] * sample(-1)
+ w[2] * sample( 0) + w[3] * sample( 1)
+ w[4] * sample( 2);
let cov_g = w[0] * sample(-1) + w[1] * sample( 0)
+ w[2] * sample( 1) + w[3] * sample( 2)
+ w[4] * sample( 3);
let cov_b = w[0] * sample( 0) + w[1] * sample( 1)
+ w[2] * sample( 2) + w[3] * sample( 3)
+ w[4] * sample( 4);
let mi = ((py as usize) * (mask_w as usize) + (px as usize)) * 3;
data[mi] = apply_gamma(cov_r).round().clamp(0.0, 255.0) as u8;
data[mi + 1] = apply_gamma(cov_g).round().clamp(0.0, 255.0) as u8;
data[mi + 2] = apply_gamma(cov_b).round().clamp(0.0, 255.0) as u8;
}
}
data
}
fn apply_5_tap_filter_legacy(gray: &[u8], gray_w: u32, mask_w: u32, mask_h: u32) -> Vec<u8> {
let mut data = vec![0u8; (mask_w as usize) * (mask_h as usize) * 3];
let gw = gray_w as i32;
for py in 0..mask_h {
let row_start = (py as usize) * (gray_w as usize);
let row = &gray[row_start .. row_start + gray_w as usize];
for px in 0..mask_w {
let base = (px as i32) * 3;
let sample = |off: i32| -> u32 {
let pos = base + off;
if pos < 0 || pos >= gw { 0 } else { row[pos as usize] as u32 }
};
let cov_r = (FILTER_WEIGHTS[0] * sample(-2)
+ FILTER_WEIGHTS[1] * sample(-1)
+ FILTER_WEIGHTS[2] * sample(0)
+ FILTER_WEIGHTS[3] * sample(1)
+ FILTER_WEIGHTS[4] * sample(2)) / FILTER_SUM;
let cov_g = (FILTER_WEIGHTS[0] * sample(-1)
+ FILTER_WEIGHTS[1] * sample(0)
+ FILTER_WEIGHTS[2] * sample(1)
+ FILTER_WEIGHTS[3] * sample(2)
+ FILTER_WEIGHTS[4] * sample(3)) / FILTER_SUM;
let cov_b = (FILTER_WEIGHTS[0] * sample(0)
+ FILTER_WEIGHTS[1] * sample(1)
+ FILTER_WEIGHTS[2] * sample(2)
+ FILTER_WEIGHTS[3] * sample(3)
+ FILTER_WEIGHTS[4] * sample(4)) / FILTER_SUM;
let mi = ((py as usize) * (mask_w as usize) + (px as usize)) * 3;
data[mi] = cov_r.min(255) as u8;
data[mi + 1] = cov_g.min(255) as u8;
data[mi + 2] = cov_b.min(255) as u8;
}
}
data
}
pub fn composite_lcd_mask(
dst_rgba: &mut [u8],
dst_w: u32,
dst_h: u32,
mask: &LcdMask,
src: Color,
dst_x: i32,
dst_y: i32,
) {
if mask.width == 0 || mask.height == 0 { return; }
let sa = src.a.clamp(0.0, 1.0);
let sr = src.r.clamp(0.0, 1.0);
let sg = src.g.clamp(0.0, 1.0);
let sb = src.b.clamp(0.0, 1.0);
let dst_w_i = dst_w as i32;
let dst_h_i = dst_h as i32;
let mw = mask.width as i32;
let mh = mask.height as i32;
for my in 0..mh {
let dy = dst_y + my;
if dy < 0 || dy >= dst_h_i { continue; }
for mx in 0..mw {
let dx = dst_x + mx;
if dx < 0 || dx >= dst_w_i { continue; }
let mi = ((my * mw + mx) * 3) as usize;
let cr = (mask.data[mi] as f32 / 255.0) * sa;
let cg = (mask.data[mi + 1] as f32 / 255.0) * sa;
let cb = (mask.data[mi + 2] as f32 / 255.0) * sa;
if cr == 0.0 && cg == 0.0 && cb == 0.0 { continue; }
let di = ((dy * dst_w_i + dx) * 4) as usize;
let dr = dst_rgba[di] as f32 / 255.0;
let dg = dst_rgba[di + 1] as f32 / 255.0;
let db = dst_rgba[di + 2] as f32 / 255.0;
let rr = sr * cr + dr * (1.0 - cr);
let rg = sg * cg + dg * (1.0 - cg);
let rbb = sb * cb + db * (1.0 - cb);
dst_rgba[di] = (rr * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
dst_rgba[di + 1] = (rg * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
dst_rgba[di + 2] = (rbb * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
const FONT_BYTES: &[u8] =
include_bytes!("../../demo/assets/CascadiaCode.ttf");
fn font() -> Arc<Font> {
Arc::new(Font::from_slice(FONT_BYTES).expect("font"))
}
#[test]
fn test_lcd_mask_has_coverage() {
let mask = rasterize_lcd_mask(
&font(), "Hello", 16.0, 4.0, 12.0,
200, 40, &TransAffine::new(),
);
let total: u64 = mask.data.iter().map(|&b| b as u64).sum();
assert!(total > 0, "rasterize_lcd_mask produced all-zero coverage");
}
#[test]
fn test_lcd_mask_has_channel_variation() {
let mask = rasterize_lcd_mask(
&font(), "Wing", 24.0, 4.0, 16.0,
400, 40, &TransAffine::new(),
);
let mut saw = false;
for px in mask.data.chunks_exact(3) {
let r = px[0];
let g = px[1];
let b = px[2];
let mx = r.max(g).max(b);
let mn = r.min(g).min(b);
if mx > 20 && (mx - mn) > 10 {
saw = true;
break;
}
}
assert!(saw, "no per-channel variation at edges");
}
#[test]
fn test_composite_dark_on_light_and_light_on_dark() {
let mask = rasterize_lcd_mask(
&font(), "Hi", 20.0, 2.0, 14.0,
80, 24, &TransAffine::new(),
);
let mut fb_white = vec![255u8; 80 * 24 * 4];
composite_lcd_mask(&mut fb_white, 80, 24, &mask, Color::black(), 0, 0);
let sum_white: u64 = fb_white.chunks_exact(4)
.map(|p| (p[0] as u64 + p[1] as u64 + p[2] as u64))
.sum();
assert!(sum_white < 80 * 24 * 3 * 255,
"dark-on-white composite left every pixel white");
let mut fb_black = vec![0u8; 80 * 24 * 4];
for chunk in fb_black.chunks_exact_mut(4) { chunk[3] = 255; }
composite_lcd_mask(&mut fb_black, 80, 24, &mask, Color::white(), 0, 0);
let sum_black: u64 = fb_black.chunks_exact(4)
.map(|p| (p[0] as u64 + p[1] as u64 + p[2] as u64))
.sum();
assert!(sum_black > 0,
"light-on-black composite left every pixel black");
}
#[test]
fn test_composite_lcd_mask_honours_src_alpha() {
let mask = LcdMask { data: vec![255, 255, 255], width: 1, height: 1 };
let mut fb_full = vec![255u8, 255, 255, 255];
composite_lcd_mask(&mut fb_full, 1, 1, &mask, Color::rgba(0.0, 0.0, 0.0, 1.0), 0, 0);
assert_eq!(fb_full[0], 0, "alpha=1 black-on-white should fully cover → R=0");
let mut fb_half = vec![255u8, 255, 255, 255];
composite_lcd_mask(&mut fb_half, 1, 1, &mask, Color::rgba(0.0, 0.0, 0.0, 0.5), 0, 0);
assert!(fb_half[0] >= 120 && fb_half[0] <= 135,
"alpha=0.5 black-on-white should land near R=128, got {}", fb_half[0]);
let mut fb_zero = vec![255u8, 255, 255, 255];
composite_lcd_mask(&mut fb_zero, 1, 1, &mask, Color::rgba(0.0, 0.0, 0.0, 0.0), 0, 0);
assert_eq!(fb_zero[0], 255, "alpha=0 must leave destination untouched");
}
#[test]
fn test_lcd_buffer_clear_writes_solid_color() {
let mut buf = LcdBuffer::new(4, 3);
buf.clear(Color::rgba(1.0, 0.5, 0.25, 1.0));
for px in buf.color_plane().chunks_exact(3) {
assert_eq!(px[0], 255);
assert_eq!(px[1], 128);
assert_eq!(px[2], 64);
}
let mut buf2 = LcdBuffer::new(2, 2);
buf2.clear(Color::rgba(1.0, 1.0, 1.0, 0.5));
for px in buf2.color_plane().chunks_exact(3) {
assert_eq!(px[0], 128);
assert_eq!(px[1], 128);
assert_eq!(px[2], 128);
}
}
#[test]
fn test_lcd_buffer_fresh_is_fully_transparent() {
let buf = LcdBuffer::new(8, 4);
assert!(buf.color_plane().iter().all(|&b| b == 0),
"fresh buffer's color plane must be zero");
assert!(buf.alpha_plane().iter().all(|&b| b == 0),
"fresh buffer's alpha plane must be zero (= fully transparent)");
}
#[test]
fn test_lcd_buffer_transparent_plus_black_text_leaves_alpha_only() {
let f = font();
let mask = rasterize_lcd_mask(&f, "Hi", 20.0, 2.0, 14.0, 80, 24, &TransAffine::new());
let mut buf = LcdBuffer::new(80, 24);
buf.composite_mask(&mask, Color::black(), 0, 0, None);
assert!(buf.color_plane().iter().all(|&b| b == 0),
"black-text-on-transparent: premult colour is 0, so color_plane stays zero");
let alpha_nonzero = buf.alpha_plane().iter().filter(|&&b| b > 0).count();
assert!(alpha_nonzero > 0,
"alpha_plane must show coverage where text was rasterized");
let bottom_left_i = 0;
let bottom_right_i = (80 - 1) * 3;
let top_left_i = (23 * 80) * 3;
let top_right_i = (23 * 80 + 79) * 3;
for i in [bottom_left_i, bottom_right_i, top_left_i, top_right_i] {
assert_eq!(&buf.alpha_plane()[i .. i + 3], &[0u8, 0, 0],
"corner at byte offset {i} should be transparent");
}
}
#[test]
fn test_lcd_buffer_red_text_writes_premultiplied_color() {
let f = font();
let w = 80u32; let h = 24u32;
let mask = rasterize_lcd_mask(&f, "I", 24.0, 4.0, 18.0, w, h, &TransAffine::new());
let mut buf = LcdBuffer::new(w, h);
buf.composite_mask(&mask, Color::rgba(1.0, 0.0, 0.0, 1.0), 0, 0, None);
let mut saw_full_red = false;
for i in (0..(w * h) as usize).map(|p| p * 3) {
if buf.alpha_plane()[i] == 255
&& buf.color_plane()[i] == 255
&& buf.color_plane()[i + 1] == 0
&& buf.color_plane()[i + 2] == 0
{
saw_full_red = true;
break;
}
}
assert!(saw_full_red, "expected at least one fully-covered pure-red pixel");
}
#[test]
fn test_lcd_buffer_composite_buffer_leaves_dst_untouched_where_src_is_transparent() {
let src = LcdBuffer::new(4, 4);
let mut dst = LcdBuffer::new(4, 4);
dst.clear(Color::white());
for px in dst.color_plane().chunks_exact(3) { assert_eq!(px, [255, 255, 255]); }
for px in dst.alpha_plane().chunks_exact(3) { assert_eq!(px, [255, 255, 255]); }
dst.composite_buffer(&src, 0, 0, None);
for px in dst.color_plane().chunks_exact(3) {
assert_eq!(px, [255, 255, 255], "dst colour must survive transparent src composite");
}
for px in dst.alpha_plane().chunks_exact(3) {
assert_eq!(px, [255, 255, 255], "dst alpha must survive transparent src composite");
}
}
#[test]
fn test_lcd_buffer_composite_buffer_opaque_pixel_replaces_dst() {
let mut src = LcdBuffer::new(3, 3);
let i = (1 * 3 + 1) * 3;
src.color_plane_mut()[i] = 255; src.color_plane_mut()[i + 1] = 0;
src.color_plane_mut()[i + 2] = 0;
src.alpha_plane_mut()[i] = 255;
src.alpha_plane_mut()[i + 1] = 255;
src.alpha_plane_mut()[i + 2] = 255;
let mut dst = LcdBuffer::new(3, 3);
dst.clear(Color::white());
dst.composite_buffer(&src, 0, 0, None);
assert_eq!(&dst.color_plane()[i .. i + 3], &[255, 0, 0],
"opaque src pixel must fully replace dst pixel's colour");
assert_eq!(&dst.alpha_plane()[i .. i + 3], &[255, 255, 255],
"alpha stays full opacity after opaque-src overwrite");
assert_eq!(&dst.color_plane()[0 .. 3], &[255, 255, 255],
"corner should retain dst white (src was transparent there)");
}
#[test]
fn test_lcd_buffer_composite_mask_deposits_coverage() {
let mask = rasterize_lcd_mask(
&font(), "Hi", 20.0, 2.0, 14.0,
80, 24, &TransAffine::new(),
);
let mut buf = LcdBuffer::new(80, 24);
buf.clear(Color::white()); let before: u64 = buf.color_plane().iter().map(|&b| b as u64).sum();
buf.composite_mask(&mask, Color::black(), 0, 0, None); let after: u64 = buf.color_plane().iter().map(|&b| b as u64).sum();
assert!(after < before,
"compositing dark text onto white bg should reduce summed brightness");
}
#[test]
fn test_lcd_mask_builder_matches_legacy_text_path() {
let f = font();
let w: u32 = 120;
let h: u32 = 30;
let xform = TransAffine::new();
let legacy = rasterize_lcd_mask_multi(
&f, &[("Equiv", 4.0, 18.0)], 22.0, w, h, &xform,
);
let mut builder = LcdMaskBuilder::new(w, h);
builder.with_paths(&xform, |add| {
let (mut paths, _) = crate::text::shape_text(&f, "Equiv", 22.0, 4.0, 18.0);
for p in paths.iter_mut() { add(p); }
});
let built = builder.finalize();
assert_eq!(legacy.width, built.width);
assert_eq!(legacy.height, built.height);
assert_eq!(legacy.data, built.data,
"LcdMaskBuilder must reproduce rasterize_lcd_mask_multi byte-for-byte");
}
#[test]
fn test_lcd_buffer_fill_path_solid_rect() {
use agg_rust::basics::PATH_FLAGS_NONE;
let mut buf = LcdBuffer::new(20, 10);
buf.clear(Color::white());
let mut path = PathStorage::new();
path.move_to( 5.0, 3.0);
path.line_to(15.0, 3.0);
path.line_to(15.0, 7.0);
path.line_to( 5.0, 7.0);
path.close_polygon(PATH_FLAGS_NONE);
buf.fill_path(&mut path, Color::black(), &TransAffine::new(), None);
let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
let i = (y * 20 + x) * 3;
(buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2])
};
assert_eq!(pixel(10, 5), (0, 0, 0),
"interior pixel of solid rect should be fully covered black");
assert_eq!(pixel(1, 1), (255, 255, 255),
"pixel outside rect should be untouched");
assert_eq!(pixel(18, 8), (255, 255, 255),
"pixel outside rect should be untouched");
}
#[test]
fn test_lcd_buffer_fill_path_matches_text_pipeline_for_glyphs() {
let f = font();
let w: u32 = 80;
let h: u32 = 24;
let size = 18.0;
let baseline = (4.0_f64, 14.0_f64);
let legacy_mask = rasterize_lcd_mask_multi(
&f, &[("ag", baseline.0, baseline.1)], size, w, h, &TransAffine::new(),
);
let mut buf_a = LcdBuffer::new(w, h);
buf_a.clear(Color::white());
buf_a.composite_mask(&legacy_mask, Color::black(), 0, 0, None);
let (mut paths, _) = crate::text::shape_text(&f, "ag", size, baseline.0, baseline.1);
let mut buf_b = LcdBuffer::new(w, h);
buf_b.clear(Color::white());
let mut builder = LcdMaskBuilder::new(w, h);
builder.with_paths(&TransAffine::new(), |add| {
for p in paths.iter_mut() { add(p); }
});
let mask_b = builder.finalize();
buf_b.composite_mask(&mask_b, Color::black(), 0, 0, None);
assert_eq!(buf_a.color_plane(), buf_b.color_plane(),
"fill_path-via-builder must match legacy text mask pipeline byte-for-byte");
}
#[test]
fn test_lcd_buffer_composite_matches_composite_lcd_mask() {
let w: u32 = 100;
let h: u32 = 28;
let mask = rasterize_lcd_mask(
&font(), "Equiv", 22.0, 4.0, 18.0, w, h, &TransAffine::new(),
);
let mut rgba = vec![255u8; (w * h * 4) as usize];
composite_lcd_mask(&mut rgba, w, h, &mask, Color::black(), 0, 0);
let mut buf = LcdBuffer::new(w, h);
buf.clear(Color::white());
buf.composite_mask(&mask, Color::black(), 0, 0, None);
for y in 0..h as usize {
for x in 0..w as usize {
let ai = (y * w as usize + x) * 4;
let bi = (y * w as usize + x) * 3;
let a_rgb = (rgba[ai], rgba[ai + 1], rgba[ai + 2]);
let b_rgb = (buf.color_plane()[bi], buf.color_plane()[bi + 1], buf.color_plane()[bi + 2]);
assert_eq!(a_rgb, b_rgb,
"RGB mismatch at ({x},{y}): RGBA-path={a_rgb:?} LcdBuffer-path={b_rgb:?}");
}
}
}
}