use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use agg_rust::basics::FillingRule;
use agg_rust::color::Gray8;
use agg_rust::conv_curve::ConvCurve;
use agg_rust::conv_transform::ConvTransform;
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::draw_ctx::FillRule;
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)>,
fill_rule: FillRule,
}
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,
fill_rule: FillRule::NonZero,
}
}
pub fn with_clip(mut self, clip: Option<(f64, f64, f64, f64)>) -> Self {
self.clip = clip;
self
}
pub fn with_fill_rule(mut self, fill_rule: FillRule) -> Self {
self.fill_rule = fill_rule;
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,
self.fill_rule,
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)>,
fill_rule: FillRule,
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();
ras.filling_rule(to_agg_fill_rule(fill_rule));
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 to_agg_fill_rule(rule: FillRule) -> FillingRule {
match rule {
FillRule::NonZero => FillingRule::NonZero,
FillRule::EvenOdd => FillingRule::EvenOdd,
}
}
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;
}
}
}