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}