Skip to main content

typf_render_opixa/
grayscale.rs

1//! Where beauty meets precision: the art of anti-aliased text
2//!
3//! Jagged edges betray amateur rendering. Professional text embraces
4//! grayscale—rendering at higher resolution then gracefully downscaling
5//! to create smooth edges that please the eye. This module transforms
6//! monochrome precision into 256 levels of visual perfection.
7
8use crate::scan_converter::ScanConverter;
9
10/// The quality spectrum: how smooth do you want your text?
11///
12/// More samples mean smoother edges but slower rendering. Choose your
13/// balance between speed and beauty based on your needs.
14#[derive(Debug, Copy, Clone, PartialEq, Eq)]
15pub enum GrayscaleLevel {
16    /// Good enough: 2x2 oversampling for fast, decent results
17    Level2x2 = 2,
18    /// Sweet spot: 4x4 oversampling that most users love
19    Level4x4 = 4,
20    /// Perfectionist: 8x8 oversampling for magazine-quality text
21    Level8x8 = 8,
22}
23
24impl GrayscaleLevel {
25    /// How many pixels wide and tall we render internally
26    pub const fn factor(self) -> usize {
27        self as usize
28    }
29
30    /// The total sample count that determines alpha precision
31    pub const fn samples_per_pixel(self) -> usize {
32        let f = self.factor();
33        f * f
34    }
35
36    /// The highest alpha value achievable at this quality level
37    pub const fn max_alpha(self) -> u8 {
38        self.samples_per_pixel() as u8
39    }
40}
41
42/// Transform crisp edges into smooth beauty
43///
44/// We take your perfectly outlined glyph and apply oversampling magic.
45/// The result is an alpha map where 255 means fully covered and 0 means
46/// completely transparent—all the values in between create the visual
47/// smoothness that makes text readable at any size.
48///
49/// # The Beauty Recipe
50///
51/// * `sc` - Your scan converter, loaded with glyph outlines
52/// * `width` - How wide the final beauty will be
53/// * `height` - How tall the final beauty will be
54/// * `level` - Your chosen quality setting
55///
56/// # Returns
57///
58/// A vector of alpha values, each begging to be blended into your canvas
59pub fn render_grayscale(
60    sc: &mut ScanConverter,
61    width: usize,
62    height: usize,
63    level: GrayscaleLevel,
64) -> Vec<u8> {
65    let factor = level.factor();
66    let _samples_per_pixel = level.samples_per_pixel();
67
68    // The scan converter is already at the correct resolution (oversampled)
69    // We just need to render it and downsample
70    // Note: The scan converter passed in should already be at the oversampled resolution
71    let mono_width = width * factor;
72    let mono_height = height * factor;
73
74    let mut mono_bitmap = vec![0u8; mono_width * mono_height];
75    sc.render_mono(&mut mono_bitmap);
76
77    // Downsample to grayscale
78    downsample_to_grayscale(&mono_bitmap, mono_width, mono_height, width, height, level)
79}
80
81/// SIMD-accelerated downsampling: when speed matters as much as beauty
82///
83/// Modern CPUs can process multiple pixels at once. This function leverages
84/// SIMD instructions to transform high-resolution monochrome into smooth
85/// grayscale with remarkable speed.
86#[cfg(target_feature = "simd128")]
87fn downsample_to_grayscale_simd(
88    mono: &[u8],
89    mono_width: usize,
90    _mono_height: usize,
91    out_width: usize,
92    out_height: usize,
93    level: GrayscaleLevel,
94) -> Vec<u8> {
95    let factor = level.factor();
96    let max_coverage = level.samples_per_pixel() as u32;
97    let normalization_factor = 255.0 / max_coverage as f32;
98
99    let mut output = vec![0u8; out_width * out_height];
100
101    for out_y in 0..out_height {
102        let src_y_base = out_y * factor;
103        let out_row_start = out_y * out_width;
104
105        for out_x in 0..out_width {
106            let src_x_base = out_x * factor;
107            let mut coverage = 0u32;
108
109            // Sum coverage in factor x factor block
110            // This loop structure allows LLVM to auto-vectorize
111            for dy in 0..factor {
112                let src_row_start = (src_y_base + dy) * mono_width;
113                let row_start = src_row_start + src_x_base;
114
115                if row_start + factor <= mono.len() {
116                    // Fast path: entire row is in bounds, LLVM can vectorize this
117                    for i in 0..factor {
118                        coverage += mono[row_start + i] as u32;
119                    }
120                } else {
121                    // Slow path: bounds checking required
122                    for i in 0..factor {
123                        let x = src_x_base + i;
124                        if x < mono_width {
125                            coverage += mono[src_row_start + x] as u32;
126                        }
127                    }
128                }
129            }
130
131            // Convert to 0-255 alpha
132            let alpha = (coverage as f32 * normalization_factor).round() as u8;
133            output[out_row_start + out_x] = alpha;
134        }
135    }
136    output
137}
138
139/// The reliable workhorse: pixel-by-pixel grayscale transformation
140///
141/// When SIMD isn't available, we fall back to careful scalar processing.
142/// Slower, but compatible with every CPU and equally precise.
143fn downsample_to_grayscale_scalar(
144    mono: &[u8],
145    mono_width: usize,
146    mono_height: usize,
147    out_width: usize,
148    out_height: usize,
149    level: GrayscaleLevel,
150) -> Vec<u8> {
151    let factor = level.factor();
152    let max_coverage = level.samples_per_pixel();
153
154    let mut output = vec![0u8; out_width * out_height];
155
156    for out_y in 0..out_height {
157        for out_x in 0..out_width {
158            // Accumulate coverage from factor x factor grid
159            let mut coverage = 0u32;
160
161            let src_x = out_x * factor;
162            let src_y = out_y * factor;
163
164            for dy in 0..factor {
165                for dx in 0..factor {
166                    let x = src_x + dx;
167                    let y = src_y + dy;
168
169                    if x < mono_width && y < mono_height && mono[y * mono_width + x] != 0 {
170                        coverage += 1;
171                    }
172                }
173            }
174
175            // Convert coverage to 0-255 alpha
176            let alpha = ((coverage * 255) / max_coverage as u32) as u8;
177            output[out_y * out_width + out_x] = alpha;
178        }
179    }
180
181    output
182}
183
184/// Choose your weapon: SIMD or scalar, automatically selected
185///
186/// We detect CPU capabilities at compile time and choose the fastest
187/// available implementation. No configuration needed—just performance.
188#[inline]
189fn downsample_to_grayscale(
190    mono: &[u8],
191    mono_width: usize,
192    mono_height: usize,
193    out_width: usize,
194    out_height: usize,
195    level: GrayscaleLevel,
196) -> Vec<u8> {
197    #[cfg(target_feature = "simd128")]
198    {
199        downsample_to_grayscale_simd(mono, mono_width, mono_height, out_width, out_height, level)
200    }
201    #[cfg(not(target_feature = "simd128"))]
202    {
203        downsample_to_grayscale_scalar(mono, mono_width, mono_height, out_width, out_height, level)
204    }
205}
206
207/// Build at high resolution, render at perfection
208///
209/// Instead of rendering twice (mono then downsample), we build the outline
210/// directly at oversampled resolution. Less memory, fewer operations,
211/// better performance—the holy trinity of optimization.
212pub fn render_grayscale_direct(
213    width: usize,
214    height: usize,
215    level: GrayscaleLevel,
216    build_outline: impl FnOnce(&mut ScanConverter),
217) -> Vec<u8> {
218    let factor = level.factor();
219    let _samples_per_pixel = level.samples_per_pixel();
220
221    // Create scan converter at oversampled resolution
222    let oversample_width = width * factor;
223    let oversample_height = height * factor;
224    let mut sc = ScanConverter::new(oversample_width, oversample_height);
225
226    // Build outline at oversampled resolution
227    build_outline(&mut sc);
228
229    // Render at high resolution
230    let mut mono_bitmap = vec![0u8; oversample_width * oversample_height];
231    sc.render_mono(&mut mono_bitmap);
232
233    // Downsample to grayscale
234    downsample_to_grayscale(
235        &mono_bitmap,
236        oversample_width,
237        oversample_height,
238        width,
239        height,
240        level,
241    )
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use crate::fixed::F26Dot6;
248
249    #[test]
250    fn test_grayscale_level_factor() {
251        assert_eq!(GrayscaleLevel::Level2x2.factor(), 2);
252        assert_eq!(GrayscaleLevel::Level4x4.factor(), 4);
253        assert_eq!(GrayscaleLevel::Level8x8.factor(), 8);
254    }
255
256    #[test]
257    fn test_grayscale_level_samples() {
258        assert_eq!(GrayscaleLevel::Level2x2.samples_per_pixel(), 4);
259        assert_eq!(GrayscaleLevel::Level4x4.samples_per_pixel(), 16);
260        assert_eq!(GrayscaleLevel::Level8x8.samples_per_pixel(), 64);
261    }
262
263    #[test]
264    fn test_downsample_all_black() {
265        // All pixels black (coverage = 1)
266        let mono = vec![1u8; 4 * 4]; // 4x4 mono
267        let gray = downsample_to_grayscale(&mono, 4, 4, 2, 2, GrayscaleLevel::Level2x2);
268
269        // Each 2x2 block has 4 samples, all black → alpha = 255
270        assert_eq!(gray.len(), 4);
271        for &alpha in &gray {
272            assert_eq!(alpha, 255);
273        }
274    }
275
276    #[test]
277    fn test_downsample_all_white() {
278        // All pixels white (coverage = 0)
279        let mono = vec![0u8; 4 * 4];
280        let gray = downsample_to_grayscale(&mono, 4, 4, 2, 2, GrayscaleLevel::Level2x2);
281
282        // All white → alpha = 0
283        for &alpha in &gray {
284            assert_eq!(alpha, 0);
285        }
286    }
287
288    #[test]
289    fn test_downsample_half_coverage() {
290        // Half coverage (2 out of 4 pixels black)
291        let mono = vec![1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1];
292        let gray = downsample_to_grayscale(&mono, 4, 4, 2, 2, GrayscaleLevel::Level2x2);
293
294        // Each 2x2 block has 2 black pixels → 2/4 coverage → alpha ~127
295        for &alpha in &gray {
296            assert!((120..=135).contains(&alpha), "Alpha = {}", alpha);
297        }
298    }
299
300    #[test]
301    fn test_render_grayscale_direct_rectangle() {
302        let gray = render_grayscale_direct(10, 10, GrayscaleLevel::Level2x2, |sc| {
303            // Draw rectangle at oversampled resolution (20x20)
304            sc.move_to(F26Dot6::from_int(4), F26Dot6::from_int(4));
305            sc.line_to(F26Dot6::from_int(16), F26Dot6::from_int(4));
306            sc.line_to(F26Dot6::from_int(16), F26Dot6::from_int(16));
307            sc.line_to(F26Dot6::from_int(4), F26Dot6::from_int(16));
308            sc.close();
309        });
310
311        assert_eq!(gray.len(), 100);
312
313        // Center should be filled (alpha ~255)
314        assert!(
315            gray[5 * 10 + 5] > 200,
316            "Center alpha = {}",
317            gray[5 * 10 + 5]
318        );
319
320        // Corners should be empty (alpha ~0)
321        assert!(gray[0] < 50, "Corner alpha = {}", gray[0]);
322    }
323
324    #[test]
325    fn test_render_grayscale_levels() {
326        // Test different oversampling levels
327        for level in [
328            GrayscaleLevel::Level2x2,
329            GrayscaleLevel::Level4x4,
330            GrayscaleLevel::Level8x8,
331        ] {
332            let factor = level.factor() as i32;
333            let gray = render_grayscale_direct(8, 8, level, |sc| {
334                // Coordinates are at oversampled resolution
335                let x1 = 2 * factor;
336                let y1 = 2 * factor;
337                let x2 = 6 * factor;
338                let y2 = 6 * factor;
339
340                sc.move_to(F26Dot6::from_int(x1), F26Dot6::from_int(y1));
341                sc.line_to(F26Dot6::from_int(x2), F26Dot6::from_int(y1));
342                sc.line_to(F26Dot6::from_int(x2), F26Dot6::from_int(y2));
343                sc.line_to(F26Dot6::from_int(x1), F26Dot6::from_int(y2));
344                sc.close();
345            });
346
347            assert_eq!(gray.len(), 64);
348            // Should have some filled pixels
349            let filled = gray.iter().filter(|&&a| a > 100).count();
350            assert!(filled > 0, "Level {:?} has no filled pixels", level);
351        }
352    }
353}