Skip to main content

text/
lcd.rs

1// lcd -- subpixel (LCD, ClearType-like) glyph smoothing
2//
3//   set_subpixel(SUBPIXEL_*) / subpixel()   the order of the display's subpixels; NONE (the
4//        default) keeps greyscale masks.  RGB / BGR: three columns per pixel, VRGB / VBGR:
5//        three rows (rotated panels)
6//
7// A glyph is rasterized at three times the resolution across the subpixels (x for RGB / BGR,
8// y for VRGB / VBGR), FreeType's default 5-tap FIR filter (08 4D 56 4D 08, sum 256) spreads
9// every subpixel's coverage over its neighbours (fewer colour fringes, the energy of each
10// pixel kept), and each colour channel is blended with its own coverage.  The cache entry
11// holds 3 bytes per pixel in the physical order of the subpixels (left to right, or top to
12// bottom), so RGB and BGR share it; the mask is one pixel wider (taller) on each side than
13// the greyscale one.
14//
15// Blending is gamma-correct-ish: target and text colour go to linear light (gamma 1.6, a
16// 256-entry table), each channel is mixed there and comes back through a 2 x 4096-entry
17// table.  Linear light makes dark text thinner and light text bolder, so the coverage is
18// adjusted first by the text colour's luminance (DirectWrite's "enhanced contrast",
19// c' = c (1 + k) / (1 + k c), k = LCD_DARK for black text down to -LCD_LIGHT for white):
20// black, white and grey text keep the weight of greyscale text within a few per cent
21// (fonttest --selftest measures it).
22//
23// Used only where it is safe: an opaque target (FMT_XRGB, the window framebuffer) and a
24// glyph from the cache.  A8 and PARGB targets (layers, images, anything composited later),
25// colour glyphs (COLR layers, CBDT bitmaps), the icon font, glyphs too large for the cache
26// (filled as paths) and black-and-white masks (set_antialias(false)) stay greyscale.  The
27// text colour's alpha and the global opacity scale the coverage (the target stays opaque).
28// Mirrored (RTL) windows mirror the command lists, not the pixels: text keeps the physical
29// subpixel order.
30
31use alloc::vec;
32use alloc::vec::Vec;
33
34use gfx::{Dc, Surface};
35
36use crate::glyphcache::GcEntry;
37use crate::Text;
38
39/// set_subpixel: greyscale smoothing (no subpixels)
40pub const SUBPIXEL_NONE: u8 = 0;
41/// set_subpixel: red, green, blue from left to right
42pub const SUBPIXEL_RGB: u8 = 1;
43/// set_subpixel: blue, green, red from left to right
44pub const SUBPIXEL_BGR: u8 = 2;
45/// set_subpixel: red, green, blue from top to bottom
46pub const SUBPIXEL_VRGB: u8 = 3;
47/// set_subpixel: blue, green, red from top to bottom
48pub const SUBPIXEL_VBGR: u8 = 4;
49
50/// FreeType's FT_LCD_FILTER_DEFAULT (the weights add up to 256)
51const FIR: [u32; 5] = [0x08, 0x4D, 0x56, 0x4D, 0x08];
52/// the gamma of the blend
53const LCD_GAMMA: f64 = 1.6;
54/// the coverage boost of black text (DirectWrite's enhanced contrast)
55const LCD_DARK: f64 = 1.0;
56/// ... and the reduction for white text
57const LCD_LIGHT: f64 = 0.35;
58
59/// the blend tables (built by set_subpixel)
60pub(crate) struct Lut {
61    /// sRGB byte -> linear 0..65535
62    to_lin: [u16; 256],
63    /// linear 0..4095 -> byte (exact near black)
64    from_lo: Vec<u8>,
65    /// linear >> 4 -> byte
66    from_hi: Vec<u8>,
67    /// coverage -> boosted coverage for the current text colour (adj_for)
68    adj: [u8; 256],
69    adj_for: u32,
70}
71
72impl Lut {
73    fn new() -> Lut {
74        let g = LCD_GAMMA;
75        let mut to_lin = [0u16; 256];
76        for (v, o) in to_lin.iter_mut().enumerate() {
77            *o = rt::fx::cvtd(rt::x87::pow(v as f64 / 255.0, g) * 65535.0).clamp(0, 65535) as u16;
78        }
79        let back = |l: f64| rt::fx::cvtd(rt::x87::pow(l / 65535.0, 1.0 / g) * 255.0).clamp(0, 255) as u8;
80        let from_lo = (0..4096).map(|i| back(i as f64)).collect();
81        let from_hi = (0..4096).map(|i| back(i as f64 * 16.0 + 7.5)).collect();
82        let mut l = Lut { to_lin, from_lo, from_hi, adj: [0; 256], adj_for: u32::MAX };
83        l.prepare(0);
84        l
85    }
86
87    /// the coverage adjustment of the colour prepared last (recorded glyphs keep a copy)
88    pub(crate) fn adj(&self) -> &[u8; 256] {
89        &self.adj
90    }
91
92    #[inline]
93    fn from_lin(&self, l: i32) -> u8 {
94        if l < 4096 { self.from_lo[l.max(0) as usize] } else { self.from_hi[(l.min(65535) >> 4) as usize] }
95    }
96
97    // the coverage adjustment for a text colour (0x??RRGGBB): k from LCD_DARK (black) to
98    // -LCD_LIGHT (white)
99    fn prepare(&mut self, rgb: u32) {
100        let rgb = rgb & 0xFF_FFFF;
101        if self.adj_for == rgb {
102            return;
103        }
104        self.adj_for = rgb;
105        let (r, g, b) = ((rgb >> 16) & 255, (rgb >> 8) & 255, rgb & 255);
106        let luma = (r * 54 + g * 183 + b * 19) as f64 / (255.0 * 256.0);
107        let k = LCD_DARK * (1.0 - luma) - LCD_LIGHT * luma;
108        for (c, o) in self.adj.iter_mut().enumerate() {
109            let x = c as f64 / 255.0;
110            *o = rt::fx::cvtd(x * (1.0 + k) / (1.0 + k * x) * 255.0).clamp(0, 255) as u8;
111        }
112    }
113
114    /// (tests) the byte that comes back from linear light: the tables are exact for every byte
115    pub(crate) fn round_trip(&self, v: u8) -> u8 {
116        self.from_lin(self.to_lin[v as usize] as i32)
117    }
118}
119
120/// one row (or column, with step) of subpixel coverage through the FIR filter
121fn fir(src: &[u8], n: usize, step: usize, out: &mut [u8]) {
122    for j in 0..n {
123        let mut s = 128u32;
124        for (k, w) in FIR.iter().enumerate() {
125            let t = j as isize + k as isize - 2;
126            if t >= 0 && (t as usize) < n {
127                s += w * src[t as usize * step] as u32;
128            }
129        }
130        out[j * step] = (s >> 8).min(255) as u8;
131    }
132}
133
134/// the filtered mask of a glyph rasterized at 3x across: horizontal (w3 = 3 w columns per
135/// row): the rows themselves; vertical (3 h rows of w): regrouped to 3 bytes per pixel
136pub(crate) fn filter(px: &[u8], w: usize, h: usize, vertical: bool) -> Vec<u8> {
137    let mut out = vec![0u8; w * h * 3];
138    if !vertical {
139        let w3 = w * 3;
140        for r in 0..h {
141            fir(&px[r * w3..r * w3 + w3], w3, 1, &mut out[r * w3..r * w3 + w3]);
142        }
143        return out;
144    }
145    let h3 = h * 3;
146    let mut col = vec![0u8; h3 * w];
147    for x in 0..w {
148        fir(&px[x..], h3, w, &mut col[x..]);
149    }
150    for y in 0..h {
151        for x in 0..w {
152            for i in 0..3 {
153                out[(y * w + x) * 3 + i] = col[(y * 3 + i) * w + x];
154            }
155        }
156    }
157    out
158}
159
160/// blend a subpixel mask (3 bytes per pixel in physical order; bgr: the first one is blue)
161/// with the colour of dc on an XRGB target at pen x / baseline (absolute), clipped; adj: the
162/// coverage adjustment for dc's colour (Lut::adj after lcd_prepare)
163#[allow(clippy::too_many_arguments)]
164pub(crate) fn blit(dc: &Dc, sf: &mut Surface, pen_x: i32, base: i32, e: &GcEntry, bgr: bool, lut: &Lut, adj: &[u8; 256]) {
165    let mut x0 = pen_x.wrapping_add(e.left);
166    let mut y0 = base.wrapping_add(e.top);
167    let mut sx = 0i32;
168    let mut sy = 0i32;
169    let x1 = x0.wrapping_add(e.w).min(dc.clip[2]);
170    let y1 = y0.wrapping_add(e.h).min(dc.clip[3]);
171    if dc.clip[0] > x0 {
172        sx = dc.clip[0] - x0;
173        x0 = dc.clip[0];
174    }
175    if dc.clip[1] > y0 {
176        sy = dc.clip[1] - y0;
177        y0 = dc.clip[1];
178    }
179    let w = x1 - x0;
180    let h = y1 - y0;
181    let alpha = dc.pcol[3];
182    if w <= 0 || h <= 0 || alpha == 0 {
183        return;
184    }
185    // the text colour in linear light: B, G, R (the byte order of the target)
186    let c = dc.color;
187    let src = [c & 255, (c >> 8) & 255, (c >> 16) & 255];
188    let src_lin = src.map(|v| lut.to_lin[v as usize] as i32);
189    // mask byte of each target byte B, G, R
190    let pick: [usize; 3] = if bgr { [0, 1, 2] } else { [2, 1, 0] };
191    let stride = sf.stride as usize;
192    for r in 0..h as usize {
193        let mo = ((sy as usize + r) * e.w as usize + sx as usize) * 3;
194        let o = ((y0 - dc.row0) as usize + r) * stride + x0 as usize * 4;
195        let row = &mut sf.px[o..o + w as usize * 4];
196        let mask = &e.px[mo..mo + w as usize * 3];
197        for (p, m) in row.chunks_exact_mut(4).zip(mask.chunks_exact(3)) {
198            if m[0] | m[1] | m[2] == 0 {
199                continue;
200            }
201            for ch in 0..3 {
202                let cov = adj[m[pick[ch]] as usize] as u32;
203                let a = (cov * alpha + 127) / 255;
204                if a == 0 {
205                    continue;
206                }
207                if a >= 255 {
208                    p[ch] = src[ch] as u8;
209                    continue;
210                }
211                let d = lut.to_lin[p[ch] as usize] as i32;
212                let l = d + ((src_lin[ch] - d) * a as i32 + if src_lin[ch] >= d { 127 } else { -127 }) / 255;
213                p[ch] = lut.from_lin(l);
214            }
215        }
216    }
217}
218
219impl Text {
220    /// the subpixel order of the display (SUBPIXEL_*): RGB / BGR / VRGB / VBGR draw glyphs on
221    /// opaque (FMT_XRGB) targets with subpixel smoothing, NONE (the default) in greyscale;
222    /// see lcd.rs for where it applies
223    pub fn set_subpixel(&mut self, order: u8) {
224        let order = if order <= SUBPIXEL_VBGR { order } else { SUBPIXEL_NONE };
225        self.lcd = order;
226        if order != SUBPIXEL_NONE && self.lcd_lut.is_none() {
227            self.lcd_lut = Some(alloc::boxed::Box::new(Lut::new()));
228        }
229    }
230
231    /// the subpixel order in use (SUBPIXEL_*)
232    pub fn subpixel(&self) -> u8 {
233        self.lcd
234    }
235
236    // subpixel smoothing for this glyph on this target?
237    pub(crate) fn lcd_for(&self, sf: &Surface, font: u32) -> bool {
238        self.lcd != SUBPIXEL_NONE && sf.fmt == gfx::FMT_XRGB && font != crate::FONT_ICON && self.antialias()
239    }
240
241    // set the coverage boost for the colour about to be drawn
242    pub(crate) fn lcd_prepare(&mut self, argb: u32) {
243        if let Some(l) = self.lcd_lut.as_mut() {
244            l.prepare(argb);
245        }
246    }
247
248    /// (tests) every byte survives the trip through linear light unchanged
249    pub fn lcd_tables_exact(&mut self) -> bool {
250        let l = Lut::new();
251        (0..=255u8).all(|v| l.round_trip(v) == v)
252    }
253}