Skip to main content

typf_render_opixa/
lib.rs

1//! Monochrome rasterizer for Typf.
2//!
3//! Opixa is the pure-Rust renderer that turns shaped glyph outlines into pixel
4//! coverage data. It is focused on predictable outline rasterization rather than
5//! color-glyph support. The submodules divide that work into fixed-point math,
6//! curve flattening, edge handling, scan conversion, and optional SIMD or
7//! parallel acceleration.
8
9use std::sync::Arc;
10
11pub mod curves;
12pub mod edge;
13pub mod fixed;
14pub mod glyph_cache;
15pub mod grayscale;
16pub mod rasterizer;
17pub mod scan_converter;
18
19/// Rule for deciding which parts of a path count as inside.
20#[derive(Debug, Copy, Clone, PartialEq, Eq)]
21pub enum FillRule {
22    /// Non-zero winding. Count edge crossings with direction.
23    NonZeroWinding,
24    /// Even-odd rule. An odd number of crossings means inside.
25    EvenOdd,
26}
27
28/// Strategy for preserving very thin strokes.
29#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub enum DropoutMode {
31    /// Do not apply dropout handling.
32    None,
33    /// Basic protection for thin strokes that might otherwise disappear.
34    Simple,
35    /// More expensive dropout handling for extreme small-size rendering.
36    Smart,
37}
38
39use typf_core::{
40    error::{RenderError, Result},
41    traits::{FontRef, Renderer},
42    types::{BitmapData, BitmapFormat, RenderOutput, ShapingResult},
43    Color, GlyphSource, RenderParams,
44};
45
46#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
47mod simd;
48
49#[cfg(feature = "parallel")]
50pub mod parallel;
51
52/// Renderer that rasterizes outline glyphs into bitmaps.
53///
54/// It applies scan conversion to glyph outlines and composites the resulting
55/// coverage onto the target bitmap. Caching is optional and can avoid repeated
56/// rasterization of the same glyph at the same size.
57pub struct OpixaRenderer {
58    max_width: u32,
59    max_height: u32,
60    max_pixels: u64,
61    cache: Option<Arc<glyph_cache::GlyphCache>>,
62}
63
64impl OpixaRenderer {
65    pub fn new() -> Self {
66        Self {
67            max_width: typf_core::get_max_bitmap_width(),
68            max_height: typf_core::get_max_bitmap_height(),
69            max_pixels: typf_core::get_max_bitmap_pixels(),
70            cache: None,
71        }
72    }
73
74    pub fn with_cache() -> Self {
75        Self::with_cache_capacity(1000)
76    }
77
78    pub fn with_cache_capacity(capacity: usize) -> Self {
79        Self {
80            max_width: typf_core::get_max_bitmap_width(),
81            max_height: typf_core::get_max_bitmap_height(),
82            max_pixels: typf_core::get_max_bitmap_pixels(),
83            cache: Some(Arc::new(glyph_cache::GlyphCache::new(capacity))),
84        }
85    }
86
87    pub fn cache_stats(&self) -> Option<glyph_cache::GlyphCacheStats> {
88        self.cache.as_ref().map(|c| c.stats())
89    }
90
91    pub fn cache_hit_rate(&self) -> Option<f64> {
92        self.cache.as_ref().map(|c| c.hit_rate())
93    }
94
95    pub fn clear_cache(&self) {
96        if let Some(ref cache) = self.cache {
97            cache.clear();
98        }
99    }
100
101    /// Create a parallel wrapper for larger rendering workloads.
102    #[cfg(feature = "parallel")]
103    pub fn with_parallel_rendering(&self) -> parallel::ParallelRenderer {
104        parallel::ParallelRenderer::new()
105    }
106
107    /// Blend one rasterized glyph bitmap onto the destination canvas.
108    fn composite_glyph(
109        &self,
110        canvas: &mut [u8],
111        canvas_width: u32,
112        glyph: &rasterizer::GlyphBitmap,
113        x: i32,
114        y: i32,
115        color: Color,
116    ) {
117        if glyph.width == 0 || glyph.height == 0 {
118            return;
119        }
120
121        let glyph_bitmap = &glyph.data;
122        let glyph_width = glyph.width;
123        let glyph_height = glyph.height;
124
125        let x = x + glyph.left;
126        let y = y - glyph.top;
127        let canvas_height = canvas.len() as u32 / (canvas_width * 4);
128
129        let mut colored_glyph = Vec::with_capacity((glyph_width * glyph_height * 4) as usize);
130
131        for coverage in glyph_bitmap.iter() {
132            let alpha = (*coverage as u16 * color.a as u16 / 255) as u8;
133            colored_glyph.push(color.r);
134            colored_glyph.push(color.g);
135            colored_glyph.push(color.b);
136            colored_glyph.push(alpha);
137        }
138
139        #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
140        {
141            for gy in 0..glyph_height {
142                let py = y + gy as i32;
143                if py < 0 || py >= canvas_height as i32 {
144                    continue;
145                }
146
147                let px_start = x.max(0);
148                let px_end = (x + glyph_width as i32).min(canvas_width as i32);
149                if px_start >= px_end {
150                    continue;
151                }
152
153                let glyph_x_start = (px_start - x) as u32;
154                let glyph_x_end = (px_end - x) as u32;
155                let row_width = (glyph_x_end - glyph_x_start) as usize * 4;
156
157                let canvas_row_start = ((py as u32 * canvas_width + px_start as u32) * 4) as usize;
158                let glyph_row_start = ((gy * glyph_width + glyph_x_start) * 4) as usize;
159
160                simd::blend_over(
161                    &mut canvas[canvas_row_start..canvas_row_start + row_width],
162                    &colored_glyph[glyph_row_start..glyph_row_start + row_width],
163                );
164            }
165        }
166
167        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
168        {
169            for gy in 0..glyph_height {
170                for gx in 0..glyph_width {
171                    let px = x + gx as i32;
172                    let py = y + gy as i32;
173
174                    if px < 0 || py < 0 || px >= canvas_width as i32 || py >= canvas_height as i32 {
175                        continue;
176                    }
177
178                    let coverage = glyph_bitmap[(gy * glyph_width + gx) as usize];
179                    if coverage == 0 {
180                        continue;
181                    }
182
183                    let canvas_idx = ((py as u32 * canvas_width + px as u32) * 4) as usize;
184
185                    let alpha = (coverage as f32 / 255.0) * (color.a as f32 / 255.0);
186                    let inv_alpha = 1.0 - alpha;
187
188                    canvas[canvas_idx] =
189                        (canvas[canvas_idx] as f32 * inv_alpha + color.r as f32 * alpha) as u8;
190                    canvas[canvas_idx + 1] =
191                        (canvas[canvas_idx + 1] as f32 * inv_alpha + color.g as f32 * alpha) as u8;
192                    canvas[canvas_idx + 2] =
193                        (canvas[canvas_idx + 2] as f32 * inv_alpha + color.b as f32 * alpha) as u8;
194                    canvas[canvas_idx + 3] = ((canvas[canvas_idx + 3] as f32 * inv_alpha
195                        + 255.0 * alpha)
196                        .min(255.0)) as u8;
197                }
198            }
199        }
200    }
201}
202
203impl Default for OpixaRenderer {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl Renderer for OpixaRenderer {
210    fn name(&self) -> &'static str {
211        "opixa"
212    }
213
214    fn render(
215        &self,
216        shaped: &ShapingResult,
217        font: Arc<dyn FontRef>,
218        params: &RenderParams,
219    ) -> Result<RenderOutput> {
220        log::debug!("OpixaRenderer: Rendering {} glyphs", shaped.glyphs.len());
221
222        let allows_outline = params
223            .glyph_sources
224            .effective_order()
225            .iter()
226            .any(|s| matches!(s, GlyphSource::Glyf | GlyphSource::Cff | GlyphSource::Cff2));
227        if !allows_outline {
228            return Err(RenderError::BackendError(
229                "opixa renderer requires outline glyph sources".to_string(),
230            )
231            .into());
232        }
233
234        let font_data = font.data();
235        let padding = params.padding as f32;
236        let glyph_size = shaped.advance_height;
237
238        let mut rendered_glyphs: Vec<RenderedGlyph> = Vec::new();
239        let mut min_y: f32 = 0.0;
240        let mut max_y: f32 = 0.0;
241
242        let mut rasterizer = if !shaped.glyphs.is_empty() {
243            match rasterizer::GlyphRasterizer::new(font_data, glyph_size) {
244                Ok(mut r) => {
245                    if !params.variations.is_empty() {
246                        if let Err(e) = r.set_variations(&params.variations) {
247                            log::warn!("Variable font setup failed: {}", e);
248                        }
249                    }
250                    Some(r)
251                },
252                Err(e) => {
253                    log::warn!("Failed to create rasterizer: {}", e);
254                    None
255                },
256            }
257        } else {
258            None
259        };
260
261        for glyph in &shaped.glyphs {
262            let glyph_bitmap = if let Some(ref cache) = self.cache {
263                let cache_key = glyph_cache::GlyphCacheKey::new(
264                    font_data,
265                    glyph.id,
266                    glyph_size,
267                    &params.variations,
268                );
269
270                if let Some(cached) = cache.get(&cache_key) {
271                    cached
272                } else {
273                    let Some(ref mut rast) = rasterizer else {
274                        log::warn!("Skipping glyph {} (no rasterizer available)", glyph.id);
275                        continue;
276                    };
277
278                    let bitmap = match rast.render_glyph(
279                        glyph.id,
280                        FillRule::NonZeroWinding,
281                        DropoutMode::None,
282                    ) {
283                        Ok(b) => b,
284                        Err(e) => {
285                            log::warn!("Glyph {} rasterization failed: {}", glyph.id, e);
286                            continue;
287                        },
288                    };
289
290                    cache.insert(cache_key, bitmap.clone());
291                    bitmap
292                }
293            } else {
294                let Some(ref mut rast) = rasterizer else {
295                    log::warn!("Skipping glyph {} (no rasterizer available)", glyph.id);
296                    continue;
297                };
298
299                match rast.render_glyph(glyph.id, FillRule::NonZeroWinding, DropoutMode::None) {
300                    Ok(bitmap) => bitmap,
301                    Err(e) => {
302                        log::warn!("Glyph {} rasterization failed: {}", glyph.id, e);
303                        continue;
304                    },
305                }
306            };
307
308            if glyph_bitmap.width == 0 || glyph_bitmap.height == 0 {
309                continue;
310            }
311
312            let glyph_top = glyph.y + glyph_bitmap.top as f32;
313            let glyph_bottom = glyph.y + glyph_bitmap.top as f32 - glyph_bitmap.height as f32;
314
315            max_y = max_y.max(glyph_top);
316            min_y = min_y.min(glyph_bottom);
317
318            rendered_glyphs.push(RenderedGlyph {
319                bitmap: glyph_bitmap,
320                glyph_x: glyph.x,
321                glyph_y: glyph.y,
322            });
323        }
324
325        let min_width = if shaped.glyphs.is_empty() && shaped.advance_width == 0.0 {
326            1
327        } else {
328            (shaped.advance_width + padding * 2.0).ceil() as u32
329        };
330        let width = min_width.max(1);
331
332        let (metrics_ascent, metrics_descent) = font
333            .metrics()
334            .filter(|m| m.units_per_em > 0 && (m.ascent != 0 || m.descent != 0))
335            .map(|m| {
336                let scale = glyph_size / (m.units_per_em as f32);
337                let ascent = (m.ascent as f32).max(0.0) * scale;
338                let descent = (m.descent as f32).abs() * scale;
339                (ascent, descent)
340            })
341            .unwrap_or((0.0, 0.0));
342
343        let glyph_top = max_y.max(0.0);
344        let glyph_bottom = (-min_y).max(0.0);
345        let top = glyph_top.max(metrics_ascent);
346        let bottom = glyph_bottom.max(metrics_descent);
347
348        let content_height = if rendered_glyphs.is_empty() {
349            16.0
350        } else {
351            top + bottom
352        };
353        let height = (content_height + padding * 2.0).ceil() as u32;
354
355        if width == 0 || height == 0 {
356            return Err(RenderError::ZeroDimensions { width, height }.into());
357        }
358
359        if width > self.max_width || height > self.max_height {
360            return Err(RenderError::DimensionsTooLarge {
361                width,
362                height,
363                max_width: self.max_width,
364                max_height: self.max_height,
365            }
366            .into());
367        }
368
369        let total_pixels = width as u64 * height as u64;
370        if total_pixels > self.max_pixels {
371            return Err(RenderError::TotalPixelsTooLarge {
372                width,
373                height,
374                total: total_pixels,
375                max: self.max_pixels,
376            }
377            .into());
378        }
379
380        let mut canvas = vec![0u8; (width * height * 4) as usize];
381
382        if let Some(bg) = params.background {
383            for pixel in canvas.chunks_exact_mut(4) {
384                pixel[0] = bg.r;
385                pixel[1] = bg.g;
386                pixel[2] = bg.b;
387                pixel[3] = bg.a;
388            }
389        }
390
391        let baseline_y = if rendered_glyphs.is_empty() {
392            padding
393        } else {
394            padding + top
395        };
396
397        for rg in rendered_glyphs {
398            let x = (rg.glyph_x + padding) as i32;
399            let y = (baseline_y + rg.glyph_y) as i32;
400
401            self.composite_glyph(&mut canvas, width, &rg.bitmap, x, y, params.foreground);
402        }
403
404        Ok(RenderOutput::Bitmap(BitmapData {
405            width,
406            height,
407            format: BitmapFormat::Rgba8,
408            data: canvas,
409        }))
410    }
411
412    fn supports_format(&self, format: &str) -> bool {
413        matches!(format, "bitmap" | "rgba" | "rgb" | "gray")
414    }
415}
416
417struct RenderedGlyph {
418    bitmap: rasterizer::GlyphBitmap,
419    glyph_x: f32,
420    glyph_y: f32,
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use typf_core::{
427        types::{Direction, PositionedGlyph},
428        GlyphSource, GlyphSourcePreference,
429    };
430
431    #[test]
432    fn test_basic_rendering() {
433        let renderer = OpixaRenderer::new();
434
435        let shaped = ShapingResult {
436            glyphs: vec![
437                PositionedGlyph {
438                    id: 72, // 'H'
439                    x: 0.0,
440                    y: 0.0,
441                    advance: 10.0,
442                    cluster: 0,
443                },
444                PositionedGlyph {
445                    id: 105, // 'i'
446                    x: 10.0,
447                    y: 0.0,
448                    advance: 5.0,
449                    cluster: 1,
450                },
451            ],
452            advance_width: 15.0,
453            advance_height: 16.0,
454            direction: Direction::LeftToRight,
455        };
456
457        struct MockFont;
458        impl FontRef for MockFont {
459            fn data(&self) -> &[u8] {
460                &[]
461            }
462            fn units_per_em(&self) -> u16 {
463                1000
464            }
465            fn glyph_id(&self, _ch: char) -> Option<u32> {
466                Some(0)
467            }
468            fn advance_width(&self, _glyph_id: u32) -> f32 {
469                500.0
470            }
471        }
472
473        let font = Arc::new(MockFont);
474        let params = RenderParams::default();
475
476        let result = renderer.render(&shaped, font, &params).unwrap();
477
478        match result {
479            RenderOutput::Bitmap(bitmap) => {
480                assert_eq!(bitmap.format, BitmapFormat::Rgba8);
481                assert!(bitmap.width > 0);
482                assert!(bitmap.height > 0);
483                assert_eq!(
484                    bitmap.data.len(),
485                    (bitmap.width * bitmap.height * 4) as usize
486                );
487            },
488            _ => panic!("Expected bitmap output"),
489        }
490    }
491
492    #[test]
493    fn errors_when_outlines_denied() {
494        let renderer = OpixaRenderer::new();
495
496        let shaped = ShapingResult {
497            glyphs: vec![PositionedGlyph {
498                id: 1,
499                x: 0.0,
500                y: 0.0,
501                advance: 10.0,
502                cluster: 0,
503            }],
504            advance_width: 10.0,
505            advance_height: 16.0,
506            direction: Direction::LeftToRight,
507        };
508
509        struct MockFont;
510        impl FontRef for MockFont {
511            fn data(&self) -> &[u8] {
512                &[]
513            }
514            fn units_per_em(&self) -> u16 {
515                1000
516            }
517            fn glyph_id(&self, _ch: char) -> Option<u32> {
518                Some(1)
519            }
520            fn advance_width(&self, _glyph_id: u32) -> f32 {
521                500.0
522            }
523        }
524
525        let font = Arc::new(MockFont);
526        let params = RenderParams {
527            glyph_sources: GlyphSourcePreference::from_parts(vec![GlyphSource::Colr1], []),
528            ..RenderParams::default()
529        };
530
531        let result = renderer.render(&shaped, font, &params);
532        assert!(result.is_err(), "outline denial should be an error");
533    }
534
535    #[test]
536    fn test_with_background() {
537        let renderer = OpixaRenderer::new();
538
539        let shaped = ShapingResult {
540            glyphs: vec![],
541            advance_width: 100.0,
542            advance_height: 20.0,
543            direction: Direction::LeftToRight,
544        };
545
546        struct MockFont;
547        impl FontRef for MockFont {
548            fn data(&self) -> &[u8] {
549                &[]
550            }
551            fn units_per_em(&self) -> u16 {
552                1000
553            }
554            fn glyph_id(&self, _ch: char) -> Option<u32> {
555                Some(0)
556            }
557            fn advance_width(&self, _glyph_id: u32) -> f32 {
558                500.0
559            }
560        }
561
562        let font = Arc::new(MockFont);
563        let params = RenderParams {
564            background: Some(Color::rgba(255, 0, 0, 255)),
565            ..Default::default()
566        };
567
568        let result = renderer.render(&shaped, font, &params).unwrap();
569
570        match result {
571            RenderOutput::Bitmap(bitmap) => {
572                // Check that background color was applied
573                assert_eq!(bitmap.data[0], 255); // R
574                assert_eq!(bitmap.data[1], 0); // G
575                assert_eq!(bitmap.data[2], 0); // B
576                assert_eq!(bitmap.data[3], 255); // A
577            },
578            _ => panic!("Expected bitmap output"),
579        }
580    }
581}