Skip to main content

typf_render_opixa/
rasterizer.rs

1//! Where fonts meet pixels: the final transformation
2//!
3//! Font files store mathematical curves, but screens need pixels. This module
4//! orchestrates the delicate dance between skrifa's outline extraction and
5//! our scan converter, turning vector curves into beautiful anti-aliased
6//! bitmaps that humans can read.
7
8use crate::fixed::F26Dot6;
9use crate::grayscale::GrayscaleLevel;
10use crate::scan_converter::ScanConverter;
11use crate::{DropoutMode, FillRule};
12
13use read_fonts::FontRef as ReadFontsRef;
14use skrifa::instance::Size;
15use skrifa::outline::DrawSettings;
16use skrifa::{GlyphId as SkrifaGlyphId, MetadataProvider};
17
18/// Your personal glyph artist: turning outlines into masterpieces
19///
20/// Every glyph starts as a mathematical blueprint in font files. This rasterizer
21/// brings them to life through three careful steps:
22/// - Extract perfect outlines from any font format
23/// - Convert curves to crisp monochrome pixels
24/// - Apply anti-aliasing magic for smooth edges
25///
26/// # Your First Rendering
27/// ```ignore
28/// let rasterizer = GlyphRasterizer::new(font_data, size)?;
29/// let bitmap = rasterizer.render_glyph(glyph_id, fill_rule, dropout_mode)?;
30/// // bitmap.data now contains pixels ready for your screen
31/// ```
32pub struct GlyphRasterizer<'a> {
33    /// The font we're bringing to life
34    font: ReadFontsRef<'a>,
35    /// How big to make the glyphs (in pixels, not font units)
36    size: f32,
37    /// Our smoothing level: 1=crisp, 4=balanced, 8=perfect
38    oversample: u8,
39    /// Variable font coordinates for infinite font variation
40    location: skrifa::instance::Location,
41}
42
43impl<'a> GlyphRasterizer<'a> {
44    /// Ready your glyph artist
45    ///
46    /// Give us font data and your desired size, we'll prepare everything
47    /// needed to transform those mathematical curves into beautiful pixels.
48    ///
49    /// # What You Need
50    ///
51    /// * `font_data` - Raw bytes from your TTF/OTF file
52    /// * `size` - Target pixel size (12 for body text, 48+ for headlines)
53    ///
54    /// # What You Get
55    ///
56    /// A ready-to-use rasterizer or a helpful error message
57    pub fn new(font_data: &'a [u8], size: f32) -> Result<Self, String> {
58        let font =
59            ReadFontsRef::new(font_data).map_err(|e| format!("Failed to parse font: {}", e))?;
60
61        Ok(Self {
62            font,
63            size,
64            oversample: 4, // 4x oversampling by default
65            location: skrifa::instance::Location::default(),
66        })
67    }
68
69    /// Shape your variable font: bend axes to your will
70    ///
71    /// Variable fonts contain infinite styles. This function lets you specify
72    /// exactly which variation you want—weight, width, slant, or custom axes.
73    /// All subsequent renderings will use this beautiful new shape.
74    pub fn set_variations(&mut self, variations: &[(String, f32)]) -> Result<(), String> {
75        if variations.is_empty() {
76            self.location = skrifa::instance::Location::default();
77            return Ok(());
78        }
79
80        // Use AxisCollection::location() which properly handles user-space
81        // coordinates including axis variation remapping (avar table).
82        // This is the recommended API instead of manually calling axis.normalize().
83        let axes = self.font.axes();
84
85        // Convert (String, f32) tuples to (&str, f32) for skrifa's location API
86        let settings: Vec<(&str, f32)> = variations
87            .iter()
88            .map(|(tag, value)| (tag.as_str(), *value))
89            .collect();
90
91        self.location = axes.location(settings);
92        Ok(())
93    }
94
95    /// Choose your smoothness: from razor-sharp to buttery-smooth
96    ///
97    /// Anti-aliasing is the art of compromise between speed and beauty.
98    /// Higher oversampling creates smoother edges but demands more memory
99    /// and processing time. Pick your sweet spot.
100    pub fn with_oversample(mut self, oversample: u8) -> Self {
101        self.oversample = oversample.max(1);
102        self
103    }
104
105    /// The moment of truth: curves become pixels
106    ///
107    /// This is where the magic happens. We take everything you've configured—
108    /// font, size, variations, smoothing—and transform a single glyph from
109    /// mathematical curves into actual pixels you can display.
110    ///
111    /// # The Recipe
112    ///
113    /// * `glyph_id` - Which character to bring to life
114    /// * `fill_rule` - How to decide what's inside vs outside
115    /// * `dropout_mode` - How to handle tiny details at small sizes
116    ///
117    /// # Your Reward
118    ///
119    /// A complete bitmap with alpha values, ready for blending into any surface
120    ///
121    /// # Before You Call
122    ///
123    /// Set up variable variations with `set_variations()` if needed
124    pub fn render_glyph(
125        &self,
126        glyph_id: u32,
127        fill_rule: FillRule,
128        dropout_mode: DropoutMode,
129    ) -> Result<GlyphBitmap, String> {
130        // Note: Scaling is handled by DrawSettings, which uses self.size directly
131
132        // Get glyph outlines - use GlyphId::new for full u32 range (>65k glyph IDs)
133        let skrifa_gid = SkrifaGlyphId::new(glyph_id);
134        let outline_glyphs = self.font.outline_glyphs();
135
136        let glyph = outline_glyphs
137            .get(skrifa_gid)
138            .ok_or_else(|| format!("Glyph {} not found", glyph_id))?;
139
140        // Bounds detection: how much canvas do we really need?
141        // We'll draw into a temporary pen to find the glyph's natural size
142        struct BoundsCalculator {
143            x_min: f32,
144            y_min: f32,
145            x_max: f32,
146            y_max: f32,
147            has_points: bool,
148        }
149
150        impl BoundsCalculator {
151            fn new() -> Self {
152                Self {
153                    x_min: f32::MAX,
154                    y_min: f32::MAX,
155                    x_max: f32::MIN,
156                    y_max: f32::MIN,
157                    has_points: false,
158                }
159            }
160
161            fn update(&mut self, x: f32, y: f32) {
162                self.x_min = self.x_min.min(x);
163                self.y_min = self.y_min.min(y);
164                self.x_max = self.x_max.max(x);
165                self.y_max = self.y_max.max(y);
166                self.has_points = true;
167            }
168        }
169
170        impl skrifa::outline::OutlinePen for BoundsCalculator {
171            fn move_to(&mut self, x: f32, y: f32) {
172                self.update(x, y);
173            }
174
175            fn line_to(&mut self, x: f32, y: f32) {
176                self.update(x, y);
177            }
178
179            fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
180                self.update(x1, y1);
181                self.update(x, y);
182            }
183
184            fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
185                self.update(x1, y1);
186                self.update(x2, y2);
187                self.update(x, y);
188            }
189
190            fn close(&mut self) {}
191        }
192
193        let size_setting = Size::new(self.size);
194
195        // Variable font location comes from our stored coordinates
196        let location_ref = self.location.coords();
197        let draw_settings = DrawSettings::unhinted(size_setting, location_ref);
198
199        let mut bounds_calc = BoundsCalculator::new();
200        glyph
201            .draw(draw_settings, &mut bounds_calc)
202            .map_err(|e| format!("Failed to calculate bounds: {:?}", e))?;
203
204        if !bounds_calc.has_points {
205            // Empty glyph (spaces, tabs, etc.) - perfectly valid, just needs no canvas
206            return Ok(GlyphBitmap {
207                width: 0,
208                height: 0,
209                left: 0,
210                top: 0,
211                data: Vec::new(),
212            });
213        }
214
215        // DrawSettings already scaled from font units to pixels
216        // Now we convert to integer pixel coordinates
217        let x_min = bounds_calc.x_min.floor() as i32;
218        let y_min = bounds_calc.y_min.floor() as i32;
219        let x_max = bounds_calc.x_max.ceil() as i32;
220        let y_max = bounds_calc.y_max.ceil() as i32;
221
222        // Calculate OUTPUT dimensions first (ensuring at least 1x1), then derive
223        // oversampled dimensions. This ensures render_grayscale's mono_bitmap size
224        // matches the scan_converter dimensions.
225        let out_width = ((x_max - x_min) as usize).max(1);
226        let out_height = ((y_max - y_min) as usize).max(1);
227        let width = out_width * self.oversample as usize;
228        let height = out_height * self.oversample as usize;
229
230        // Guard against memory bombs (malicious fonts or giant sizes)
231        if width > 4096 || height > 4096 {
232            return Err(format!(
233                "Glyph bitmap too large: {}x{} (max 4096x4096)",
234                width, height
235            ));
236        }
237
238        // Prepare our canvas, oversized for smooth anti-aliasing
239        let mut scan_converter = ScanConverter::new(width, height);
240        scan_converter.set_fill_rule(fill_rule);
241        scan_converter.set_dropout_mode(dropout_mode);
242
243        // Transform magic: where do pixels go in our oversized canvas?
244        // skrifa handled font units → pixels, now we apply oversampling
245        let oversample_scale = self.oversample as f32;
246        let x_offset = -x_min as f32 * self.oversample as f32;
247        // Y-flip: fonts go bottom-up, bitmaps go top-down
248        let y_offset = y_max as f32 * self.oversample as f32;
249
250        // Our coordinate transformer: shapes the canvas for the scan converter
251        struct TransformPen<'p> {
252            inner: &'p mut ScanConverter,
253            scale: f32,
254            x_offset: f32,
255            y_offset: f32,
256        }
257
258        impl<'p> skrifa::outline::OutlinePen for TransformPen<'p> {
259            fn move_to(&mut self, x: f32, y: f32) {
260                let tx = x * self.scale + self.x_offset;
261                let ty = -y * self.scale + self.y_offset; // Flip Y for bitmap coordinates
262                self.inner
263                    .move_to(F26Dot6::from_float(tx), F26Dot6::from_float(ty));
264            }
265
266            fn line_to(&mut self, x: f32, y: f32) {
267                let tx = x * self.scale + self.x_offset;
268                let ty = -y * self.scale + self.y_offset; // Flip Y
269                self.inner
270                    .line_to(F26Dot6::from_float(tx), F26Dot6::from_float(ty));
271            }
272
273            fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
274                let tx1 = x1 * self.scale + self.x_offset;
275                let ty1 = -y1 * self.scale + self.y_offset; // Flip Y
276                let tx = x * self.scale + self.x_offset;
277                let ty = -y * self.scale + self.y_offset; // Flip Y
278                self.inner.quadratic_to(
279                    F26Dot6::from_float(tx1),
280                    F26Dot6::from_float(ty1),
281                    F26Dot6::from_float(tx),
282                    F26Dot6::from_float(ty),
283                );
284            }
285
286            fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
287                let tx1 = x1 * self.scale + self.x_offset;
288                let ty1 = -y1 * self.scale + self.y_offset; // Flip Y
289                let tx2 = x2 * self.scale + self.x_offset;
290                let ty2 = -y2 * self.scale + self.y_offset; // Flip Y
291                let tx = x * self.scale + self.x_offset;
292                let ty = -y * self.scale + self.y_offset; // Flip Y
293                self.inner.cubic_to(
294                    F26Dot6::from_float(tx1),
295                    F26Dot6::from_float(ty1),
296                    F26Dot6::from_float(tx2),
297                    F26Dot6::from_float(ty2),
298                    F26Dot6::from_float(tx),
299                    F26Dot6::from_float(ty),
300                );
301            }
302
303            fn close(&mut self) {
304                self.inner.close();
305            }
306        }
307
308        let mut transform_pen = TransformPen {
309            inner: &mut scan_converter,
310            scale: oversample_scale,
311            x_offset,
312            y_offset,
313        };
314
315        // Draw the glyph outline
316        let size_setting = Size::new(self.size);
317        let location_ref = self.location.coords(); // Use stored variations
318        let draw_settings = DrawSettings::unhinted(size_setting, location_ref);
319
320        glyph
321            .draw(draw_settings, &mut transform_pen)
322            .map_err(|e| format!("Failed to draw outline: {:?}", e))?;
323
324        // The final touch: smooth those crisp pixels into beauty
325        // This downsampling creates the anti-aliased effect readers love
326        let grayscale_level = match self.oversample {
327            2 => GrayscaleLevel::Level2x2,
328            4 => GrayscaleLevel::Level4x4,
329            8 => GrayscaleLevel::Level8x8,
330            _ => GrayscaleLevel::Level4x4, // Default to 4x4
331        };
332
333        // Use the grayscale module's downsample function
334        // out_width and out_height were calculated at the top of this function
335        let gray_bitmap = crate::grayscale::render_grayscale(
336            &mut scan_converter,
337            out_width,
338            out_height,
339            grayscale_level,
340        );
341
342        Ok(GlyphBitmap {
343            width: out_width as u32,
344            height: out_height as u32,
345            left: x_min,
346            top: y_max, // TrueType origins are bottom-left, we prefer top-left
347            data: gray_bitmap,
348        })
349    }
350}
351
352/// Your rendered glyph: pixels, position, and purpose
353///
354/// This isn't just a bitmap—it's a complete rendering package. We give you
355/// the pixels plus exact positioning information so you can place each glyph
356/// perfectly in your text layout.
357#[derive(Debug, Clone)]
358pub struct GlyphBitmap {
359    /// How wide this glyph wants to be (in pixels)
360    pub width: u32,
361    /// How tall this glyph wants to be (in pixels)
362    pub height: u32,
363    /// How far from the origin to start drawing (left edge)
364    pub left: i32,
365    /// How far from the baseline to the top (for proper alignment)
366    pub top: i32,
367    /// The actual alpha values: 0=invisible air, 255=solid ink
368    pub data: Vec<u8>,
369}
370
371#[cfg(test)]
372mod tests {
373    // Real font testing happens in integration tests
374    // Unit tests would need embedded font data, which we avoid
375}