typf-render-opixa 5.0.16

Pure Rust rendering backend using Opixa rasterizer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
//! The bridge between vectors and pixels: scanline conversion mastery
//!
//! Vector outlines describe curves and lines, but screens need pixels. This module
//! builds that bridge through the elegant scanline algorithm—organizing edges
//! into tables, tracking active ones, and filling horizontal spans where shapes
//! intersect the scanline. The result: perfect rasterization.

use crate::curves::{subdivide_cubic, subdivide_quadratic};
use crate::edge::{Edge, EdgeList};
use crate::fixed::F26Dot6;
use crate::{DropoutMode, FillRule};

use skrifa::outline::OutlinePen;

/// Your scanline conductor: orchestrating the rasterization symphony
///
/// This is the heart of our rasterization engine. It transforms mathematical
/// descriptions of shapes into actual pixels through careful bookkeeping.
#[derive(Debug)]
pub struct ScanConverter {
    /// Our edge memory: where each line segment first appears
    edge_table: Vec<EdgeList>,

    /// The current cast: edges crossing our scanline right now
    active_edges: EdgeList,

    /// Where our drawing pen currently sits
    current_x: F26Dot6,
    current_y: F26Dot6,

    /// Where we started this contour (needed for close operations)
    contour_start_x: F26Dot6,
    contour_start_y: F26Dot6,

    /// How we decide what's inside the shape
    fill_rule: FillRule,

    /// How we handle tiny details at small sizes
    dropout_mode: DropoutMode,

    /// The canvas dimensions we're working with
    width: usize,
    height: usize,
}

impl ScanConverter {
    /// Prepare your rasterization canvas
    ///
    /// We'll set up edge tables for every scanline and prepare active edge lists.
    /// This allocation happens once, then reuse makes every render fast.
    ///
    /// # What You Tell Us
    ///
    /// * `width` - How wide your canvas will be
    /// * `height` - How tall your canvas will be
    ///
    /// # What We Give You
    ///
    /// A ready-to-use converter with sensible defaults.
    pub fn new(width: usize, height: usize) -> Self {
        // Pre-allocate edge table with one list per scanline
        // Use with_capacity to avoid reallocations (see ALLOCATION.md)
        let mut edge_table = Vec::with_capacity(height);
        for _ in 0..height {
            edge_table.push(EdgeList::with_capacity(16)); // Estimate ~16 edges per scanline
        }

        Self {
            edge_table,
            active_edges: EdgeList::with_capacity(32), // Estimate ~32 active edges
            current_x: F26Dot6::ZERO,
            current_y: F26Dot6::ZERO,
            contour_start_x: F26Dot6::ZERO,
            contour_start_y: F26Dot6::ZERO,
            fill_rule: FillRule::NonZeroWinding,
            dropout_mode: DropoutMode::None,
            width,
            height,
        }
    }

    /// Set the fill rule.
    pub fn set_fill_rule(&mut self, rule: FillRule) {
        self.fill_rule = rule;
    }

    /// Set the dropout control mode.
    pub fn set_dropout_mode(&mut self, mode: DropoutMode) {
        self.dropout_mode = mode;
    }

    /// Get the current fill rule.
    pub fn fill_rule(&self) -> FillRule {
        self.fill_rule
    }

    /// Get the current dropout mode.
    pub fn dropout_mode(&self) -> DropoutMode {
        self.dropout_mode
    }

    /// Reset the scan converter for a new outline.
    ///
    /// Clears all edge tables and resets current position.
    pub fn reset(&mut self) {
        for list in &mut self.edge_table {
            list.clear();
        }
        self.active_edges.clear();
        self.current_x = F26Dot6::ZERO;
        self.current_y = F26Dot6::ZERO;
        self.contour_start_x = F26Dot6::ZERO;
        self.contour_start_y = F26Dot6::ZERO;
    }

    /// Move to a new position (start new contour).
    ///
    /// # Arguments
    ///
    /// * `x, y` - New position in 26.6 fixed-point format
    pub fn move_to(&mut self, x: F26Dot6, y: F26Dot6) {
        self.current_x = x;
        self.current_y = y;
        self.contour_start_x = x;
        self.contour_start_y = y;
    }

    /// Add a line from current position to (x, y).
    ///
    /// # Arguments
    ///
    /// * `x, y` - End point in 26.6 fixed-point format
    pub fn line_to(&mut self, x: F26Dot6, y: F26Dot6) {
        self.add_line(self.current_x, self.current_y, x, y);
        self.current_x = x;
        self.current_y = y;
    }

    /// Add a quadratic Bézier curve from current position.
    ///
    /// # Arguments
    ///
    /// * `x1, y1` - Control point in 26.6 fixed-point format
    /// * `x2, y2` - End point in 26.6 fixed-point format
    pub fn quadratic_to(&mut self, x1: F26Dot6, y1: F26Dot6, x2: F26Dot6, y2: F26Dot6) {
        let x0 = self.current_x;
        let y0 = self.current_y;

        // Subdivide curve and add line segments
        subdivide_quadratic(
            x0,
            y0,
            x1,
            y1,
            x2,
            y2,
            &mut |x, y| {
                self.add_line(self.current_x, self.current_y, x, y);
                self.current_x = x;
                self.current_y = y;
            },
            0,
        );
    }

    /// Add a cubic Bézier curve from current position.
    ///
    /// # Arguments
    ///
    /// * `x1, y1` - First control point in 26.6 fixed-point format
    /// * `x2, y2` - Second control point in 26.6 fixed-point format
    /// * `x3, y3` - End point in 26.6 fixed-point format
    pub fn cubic_to(
        &mut self,
        x1: F26Dot6,
        y1: F26Dot6,
        x2: F26Dot6,
        y2: F26Dot6,
        x3: F26Dot6,
        y3: F26Dot6,
    ) {
        let x0 = self.current_x;
        let y0 = self.current_y;

        // Subdivide curve and add line segments
        subdivide_cubic(
            x0,
            y0,
            x1,
            y1,
            x2,
            y2,
            x3,
            y3,
            &mut |x, y| {
                self.add_line(self.current_x, self.current_y, x, y);
                self.current_x = x;
                self.current_y = y;
            },
            0,
        );
    }

    /// Close current contour (line back to start).
    pub fn close(&mut self) {
        if self.current_x != self.contour_start_x || self.current_y != self.contour_start_y {
            self.add_line(
                self.current_x,
                self.current_y,
                self.contour_start_x,
                self.contour_start_y,
            );
        }
        self.current_x = self.contour_start_x;
        self.current_y = self.contour_start_y;
    }

    /// Add a line segment to the edge table.
    ///
    /// # Arguments
    ///
    /// * `x1, y1` - Start point in 26.6 fixed-point format
    /// * `x2, y2` - End point in 26.6 fixed-point format
    fn add_line(&mut self, x1: F26Dot6, y1: F26Dot6, x2: F26Dot6, y2: F26Dot6) {
        // Create edge (returns None for horizontal lines)
        if let Some(mut edge) = Edge::new(x1, y1, x2, y2) {
            let y_min = edge.y_min;

            // If edge starts below viewport, skip it
            if y_min >= self.height as i32 {
                return;
            }

            // If edge ends above viewport, skip it
            if edge.y_max < 0 {
                return;
            }

            // If edge starts above viewport, advance it to scanline 0
            let start_scanline = if y_min < 0 {
                let skip = -y_min;
                // Advance x by skip * x_increment
                // We can just call step() skip times, or multiply
                // Since x_increment is constant, multiplication is faster
                edge.x = edge.x + edge.x_increment.mul(F26Dot6::from_int(skip));
                0
            } else {
                y_min as usize
            };

            // Add edge to the edge table at its starting scanline (clamped)
            if start_scanline < self.edge_table.len() {
                self.edge_table[start_scanline].push(edge);
            }
        }
    }

    /// Rasterize outline to grayscale bitmap (allocating).
    ///
    /// Returns a new Vec<u8> containing grayscale pixels (0 = white, 255 = black).
    pub fn rasterize(&mut self) -> Vec<u8> {
        let mut mono = vec![0u8; self.width * self.height];
        self.render_mono(&mut mono);

        // Convert monochrome (0/1) to grayscale (0/255)
        mono.iter().map(|&p| if p == 1 { 255 } else { 0 }).collect()
    }

    /// Render outline to monochrome bitmap.
    ///
    /// # Arguments
    ///
    /// * `bitmap` - Output buffer (width * height bytes, 1 = black, 0 = white)
    ///
    /// # Panics
    ///
    /// Panics if bitmap.len() != width * height
    pub fn render_mono(&mut self, bitmap: &mut [u8]) {
        assert_eq!(
            bitmap.len(),
            self.width * self.height,
            "Bitmap size mismatch"
        );

        // Fill with white (0)
        bitmap.fill(0);

        // Scanline loop
        for y in 0..self.height {
            self.scan_line_mono(y as i32, bitmap);
        }
    }

    /// Process one scanline for monochrome rendering.
    fn scan_line_mono(&mut self, y: i32, bitmap: &mut [u8]) {
        if y < 0 || y >= self.height as i32 {
            return;
        }

        // Activate edges from edge table for this scanline
        if (y as usize) < self.edge_table.len() {
            self.active_edges.extend(&self.edge_table[y as usize]);
        }

        // Remove inactive edges (y > y_max)
        self.active_edges.remove_inactive(y);

        // Sort by X coordinate
        // Note: After step_all() at the end of previous scanline, edges may be nearly sorted,
        // but Rust's sort is adaptive (Tim sort) and handles nearly-sorted data efficiently.
        self.active_edges.sort_by_x();

        // Fill spans based on fill rule
        match self.fill_rule {
            FillRule::NonZeroWinding => self.fill_nonzero_winding(y, bitmap),
            FillRule::EvenOdd => self.fill_even_odd(y, bitmap),
        }

        // Step all edges to next scanline
        self.active_edges.step_all();
    }

    /// Fill spans using non-zero winding rule.
    fn fill_nonzero_winding(&self, y: i32, bitmap: &mut [u8]) {
        let mut winding = 0i32;
        let mut fill_start: Option<i32> = None;

        for edge in self.active_edges.iter() {
            let x = edge.x.to_int();
            let old_winding = winding;

            // Update winding number
            winding += edge.direction as i32;

            // Check for transitions
            if old_winding == 0 && winding != 0 {
                // Start fill span (entering filled region)
                fill_start = Some(x);
            } else if old_winding != 0 && winding == 0 {
                // End fill span (leaving filled region)
                if let Some(start) = fill_start {
                    self.fill_span(start, x, y, bitmap);
                    fill_start = None;
                }
            }
        }
    }

    /// Fill spans using even-odd rule.
    fn fill_even_odd(&self, y: i32, bitmap: &mut [u8]) {
        let mut inside = false;
        let mut fill_start = 0i32;

        for edge in self.active_edges.iter() {
            let x = edge.x.to_int();

            if inside {
                // End span
                self.fill_span(fill_start, x, y, bitmap);
                inside = false;
            } else {
                // Start span
                fill_start = x;
                inside = true;
            }
        }
    }

    /// Fill a horizontal span of pixels.
    fn fill_span(&self, x1: i32, x2: i32, y: i32, bitmap: &mut [u8]) {
        // Early return for invalid spans
        if y < 0 || y >= self.height as i32 || x1 >= x2 {
            return;
        }

        let x_start = x1.max(0).min(self.width as i32) as usize;
        let x_end = x2.max(0).min(self.width as i32) as usize;

        if x_start >= x_end {
            return;
        }

        let row_offset = y as usize * self.width;
        let span_start = row_offset + x_start;
        let span_end = row_offset + x_end;

        // Use slice::fill() which the compiler optimizes to memset
        if let Some(span) = bitmap.get_mut(span_start..span_end) {
            span.fill(1);
        }
    }
}

/// Implement skrifa OutlinePen for ScanConverter.
///
/// This allows direct rendering of font outlines from skrifa.
/// Coordinates are expected in pixels (already scaled from font units).
impl OutlinePen for ScanConverter {
    fn move_to(&mut self, x: f32, y: f32) {
        // Y-flip: font space (Y-up) → graphics space (Y-down)
        let y_flipped = self.height as f32 - y;
        self.move_to(F26Dot6::from_float(x), F26Dot6::from_float(y_flipped));
    }

    fn line_to(&mut self, x: f32, y: f32) {
        let y_flipped = self.height as f32 - y;
        self.line_to(F26Dot6::from_float(x), F26Dot6::from_float(y_flipped));
    }

    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
        let y1_flipped = self.height as f32 - y1;
        let y_flipped = self.height as f32 - y;
        self.quadratic_to(
            F26Dot6::from_float(x1),
            F26Dot6::from_float(y1_flipped),
            F26Dot6::from_float(x),
            F26Dot6::from_float(y_flipped),
        );
    }

    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
        let y1_flipped = self.height as f32 - y1;
        let y2_flipped = self.height as f32 - y2;
        let y_flipped = self.height as f32 - y;
        self.cubic_to(
            F26Dot6::from_float(x1),
            F26Dot6::from_float(y1_flipped),
            F26Dot6::from_float(x2),
            F26Dot6::from_float(y2_flipped),
            F26Dot6::from_float(x),
            F26Dot6::from_float(y_flipped),
        );
    }

    fn close(&mut self) {
        self.close();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_scan_converter_new() {
        let sc = ScanConverter::new(64, 64);
        assert_eq!(sc.width, 64);
        assert_eq!(sc.height, 64);
        assert_eq!(sc.fill_rule(), FillRule::NonZeroWinding);
        assert_eq!(sc.dropout_mode(), DropoutMode::None);
    }

    #[test]
    fn test_scan_converter_set_fill_rule() {
        let mut sc = ScanConverter::new(64, 64);
        sc.set_fill_rule(FillRule::EvenOdd);
        assert_eq!(sc.fill_rule(), FillRule::EvenOdd);
    }

    #[test]
    fn test_scan_converter_set_dropout_mode() {
        let mut sc = ScanConverter::new(64, 64);
        sc.set_dropout_mode(DropoutMode::Simple);
        assert_eq!(sc.dropout_mode(), DropoutMode::Simple);
    }

    #[test]
    fn test_scan_converter_move_to() {
        let mut sc = ScanConverter::new(64, 64);
        sc.move_to(F26Dot6::from_int(10), F26Dot6::from_int(20));
        assert_eq!(sc.current_x.to_int(), 10);
        assert_eq!(sc.current_y.to_int(), 20);
        assert_eq!(sc.contour_start_x.to_int(), 10);
        assert_eq!(sc.contour_start_y.to_int(), 20);
    }

    #[test]
    fn test_scan_converter_line_to() {
        let mut sc = ScanConverter::new(64, 64);
        sc.move_to(F26Dot6::from_int(0), F26Dot6::from_int(0));
        sc.line_to(F26Dot6::from_int(10), F26Dot6::from_int(10));
        assert_eq!(sc.current_x.to_int(), 10);
        assert_eq!(sc.current_y.to_int(), 10);
    }

    #[test]
    fn test_render_simple_rectangle() {
        let mut sc = ScanConverter::new(10, 10);

        // Draw rectangle from (2,2) to (8,8)
        sc.move_to(F26Dot6::from_int(2), F26Dot6::from_int(2));
        sc.line_to(F26Dot6::from_int(8), F26Dot6::from_int(2));
        sc.line_to(F26Dot6::from_int(8), F26Dot6::from_int(8));
        sc.line_to(F26Dot6::from_int(2), F26Dot6::from_int(8));
        sc.close();

        let mut bitmap = vec![0u8; 100];
        sc.render_mono(&mut bitmap);

        // Check that interior pixels are filled
        // Row 4 (middle), columns 2-7 should be black (1)
        for x in 2..8 {
            assert_eq!(bitmap[4 * 10 + x], 1, "Pixel ({}, 4) should be black", x);
        }

        // Check that exterior pixels are white (0)
        assert_eq!(bitmap[4 * 10], 0, "Pixel (0, 4) should be white");
        assert_eq!(bitmap[4 * 10 + 9], 0, "Pixel (9, 4) should be white");
    }

    #[test]
    fn test_render_triangle() {
        let mut sc = ScanConverter::new(20, 20);

        // Draw larger triangle: (5,5) -> (15,5) -> (10,15)
        sc.move_to(F26Dot6::from_int(5), F26Dot6::from_int(5));
        sc.line_to(F26Dot6::from_int(15), F26Dot6::from_int(5));
        sc.line_to(F26Dot6::from_int(10), F26Dot6::from_int(15));
        sc.close();

        let mut bitmap = vec![0u8; 400];
        sc.render_mono(&mut bitmap);

        // Count total filled pixels
        let filled_count: usize = bitmap.iter().filter(|&&p| p == 1).count();

        // Triangle should have filled pixels
        // Area ~ 0.5 * base * height = 0.5 * 10 * 10 = 50 pixels
        assert!(
            filled_count > 20,
            "Triangle should have filled pixels (got {})",
            filled_count
        );
    }

    #[test]
    fn test_even_odd_fill_rule() {
        let mut sc = ScanConverter::new(10, 10);
        sc.set_fill_rule(FillRule::EvenOdd);

        // Simple rectangle
        sc.move_to(F26Dot6::from_int(2), F26Dot6::from_int(2));
        sc.line_to(F26Dot6::from_int(8), F26Dot6::from_int(2));
        sc.line_to(F26Dot6::from_int(8), F26Dot6::from_int(8));
        sc.line_to(F26Dot6::from_int(2), F26Dot6::from_int(8));
        sc.close();

        let mut bitmap = vec![0u8; 100];
        sc.render_mono(&mut bitmap);

        // Should fill interior
        assert_eq!(bitmap[5 * 10 + 5], 1, "Center should be filled");
    }

    #[test]
    fn test_reset() {
        let mut sc = ScanConverter::new(10, 10);

        sc.move_to(F26Dot6::from_int(5), F26Dot6::from_int(5));
        sc.line_to(F26Dot6::from_int(10), F26Dot6::from_int(10));

        sc.reset();

        assert_eq!(sc.current_x.to_int(), 0);
        assert_eq!(sc.current_y.to_int(), 0);
    }
}