⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/desktop/desktop_ext/
font_render.rs

1//! TrueType Font Rendering
2//!
3//! TrueType parser with integer Bezier rasterization and glyph caching.
4//! All math is integer-only (no floating point).
5
6#[cfg(feature = "alloc")]
7use alloc::{vec, vec::Vec};
8
9/// Errors during font parsing or rendering.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FontError {
12    /// Invalid or unrecognized font data.
13    InvalidFont,
14    /// Required table not found.
15    TableNotFound,
16    /// Glyph index out of range.
17    GlyphNotFound,
18    /// Unsupported format version.
19    UnsupportedFormat,
20    /// Data truncated or corrupt.
21    DataTruncated,
22    /// Buffer too small for rendered glyph.
23    BufferTooSmall,
24}
25
26impl core::fmt::Display for FontError {
27    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28        match self {
29            Self::InvalidFont => write!(f, "invalid font"),
30            Self::TableNotFound => write!(f, "table not found"),
31            Self::GlyphNotFound => write!(f, "glyph not found"),
32            Self::UnsupportedFormat => write!(f, "unsupported format"),
33            Self::DataTruncated => write!(f, "data truncated"),
34            Self::BufferTooSmall => write!(f, "buffer too small"),
35        }
36    }
37}
38
39/// Subpixel rendering mode.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum SubpixelMode {
42    /// No subpixel rendering (grayscale AA).
43    #[default]
44    None,
45    /// RGB subpixel order (most common LCD).
46    Rgb,
47    /// BGR subpixel order.
48    Bgr,
49    /// Vertical RGB (rotated display).
50    VerticalRgb,
51    /// Vertical BGR.
52    VerticalBgr,
53}
54
55/// A point in a glyph outline.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct OutlinePoint {
58    /// X coordinate in font units.
59    pub x: i16,
60    /// Y coordinate in font units.
61    pub y: i16,
62    /// Whether this is an on-curve control point.
63    pub on_curve: bool,
64}
65
66/// A contour in a glyph outline (sequence of points).
67#[derive(Debug, Clone, PartialEq, Eq)]
68#[cfg(feature = "alloc")]
69pub struct GlyphContour {
70    /// Points forming this contour.
71    pub points: Vec<OutlinePoint>,
72}
73
74/// A parsed glyph outline.
75#[derive(Debug, Clone, PartialEq, Eq)]
76#[cfg(feature = "alloc")]
77pub struct GlyphOutline {
78    /// Contours forming this glyph.
79    pub contours: Vec<GlyphContour>,
80    /// Bounding box: min x.
81    pub x_min: i16,
82    /// Bounding box: min y.
83    pub y_min: i16,
84    /// Bounding box: max x.
85    pub x_max: i16,
86    /// Bounding box: max y.
87    pub y_max: i16,
88    /// Advance width in font units.
89    pub advance_width: u16,
90    /// Left side bearing.
91    pub lsb: i16,
92}
93
94/// TrueType table tag (4-byte ASCII).
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct TableTag(pub [u8; 4]);
97
98impl TableTag {
99    pub const CMAP: Self = Self(*b"cmap");
100    pub const GLYF: Self = Self(*b"glyf");
101    pub const HEAD: Self = Self(*b"head");
102    pub const HHEA: Self = Self(*b"hhea");
103    pub const HMTX: Self = Self(*b"hmtx");
104    pub const LOCA: Self = Self(*b"loca");
105    pub const MAXP: Self = Self(*b"maxp");
106}
107
108/// A table directory entry.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct TableEntry {
111    pub tag: TableTag,
112    pub offset: u32,
113    pub length: u32,
114}
115
116/// Parsed `head` table fields.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct HeadTable {
119    /// Units per em (typically 1000 or 2048).
120    pub units_per_em: u16,
121    /// Index-to-loc format: 0=short, 1=long.
122    pub index_to_loc_format: i16,
123    /// Font bounding box.
124    pub x_min: i16,
125    pub y_min: i16,
126    pub x_max: i16,
127    pub y_max: i16,
128}
129
130/// Parsed `hhea` table fields.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct HheaTable {
133    /// Ascent.
134    pub ascent: i16,
135    /// Descent (negative).
136    pub descent: i16,
137    /// Line gap.
138    pub line_gap: i16,
139    /// Number of horizontal metrics in hmtx.
140    pub num_h_metrics: u16,
141}
142
143/// Parsed `maxp` table fields.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct MaxpTable {
146    pub num_glyphs: u16,
147}
148
149/// Helper: read u16 big-endian from a byte slice.
150pub(crate) fn read_u16_be(data: &[u8], offset: usize) -> Option<u16> {
151    if offset + 2 > data.len() {
152        return None;
153    }
154    Some(u16::from_be_bytes([data[offset], data[offset + 1]]))
155}
156
157/// Helper: read i16 big-endian.
158pub(crate) fn read_i16_be(data: &[u8], offset: usize) -> Option<i16> {
159    read_u16_be(data, offset).map(|v| v as i16)
160}
161
162/// Helper: read u32 big-endian.
163pub(crate) fn read_u32_be(data: &[u8], offset: usize) -> Option<u32> {
164    if offset + 4 > data.len() {
165        return None;
166    }
167    Some(u32::from_be_bytes([
168        data[offset],
169        data[offset + 1],
170        data[offset + 2],
171        data[offset + 3],
172    ]))
173}
174
175/// TrueType font parser.
176///
177/// Parses the font directory and individual tables from raw TTF data.
178/// Does not own the font data; operates on borrowed slices.
179#[derive(Debug)]
180pub struct TtfParser<'a> {
181    /// Raw font file data.
182    data: &'a [u8],
183    /// Number of tables.
184    num_tables: u16,
185}
186
187impl<'a> TtfParser<'a> {
188    /// Create a new parser from raw TTF data.
189    pub fn new(data: &'a [u8]) -> Result<Self, FontError> {
190        if data.len() < 12 {
191            return Err(FontError::InvalidFont);
192        }
193
194        // Check sfVersion (0x00010000 for TrueType, 'OTTO' for CFF).
195        let version = read_u32_be(data, 0).ok_or(FontError::DataTruncated)?;
196        if version != 0x00010000 && version != 0x4F54544F {
197            return Err(FontError::InvalidFont);
198        }
199
200        let num_tables = read_u16_be(data, 4).ok_or(FontError::DataTruncated)?;
201
202        Ok(Self { data, num_tables })
203    }
204
205    /// Find a table by tag.
206    pub fn find_table(&self, tag: TableTag) -> Option<TableEntry> {
207        let header_size = 12;
208        let entry_size = 16;
209
210        for i in 0..self.num_tables as usize {
211            let offset = header_size + i * entry_size;
212            if offset + entry_size > self.data.len() {
213                break;
214            }
215
216            let t = [
217                self.data[offset],
218                self.data[offset + 1],
219                self.data[offset + 2],
220                self.data[offset + 3],
221            ];
222
223            if t == tag.0 {
224                let table_offset = read_u32_be(self.data, offset + 8)?;
225                let length = read_u32_be(self.data, offset + 12)?;
226                return Some(TableEntry {
227                    tag,
228                    offset: table_offset,
229                    length,
230                });
231            }
232        }
233        None
234    }
235
236    /// Get the raw bytes for a table.
237    pub fn table_data(&self, entry: &TableEntry) -> Result<&'a [u8], FontError> {
238        let start = entry.offset as usize;
239        let end = start + entry.length as usize;
240        if end > self.data.len() {
241            return Err(FontError::DataTruncated);
242        }
243        Ok(&self.data[start..end])
244    }
245
246    /// Parse the `head` table.
247    pub fn parse_head(&self) -> Result<HeadTable, FontError> {
248        let entry = self
249            .find_table(TableTag::HEAD)
250            .ok_or(FontError::TableNotFound)?;
251        let d = self.table_data(&entry)?;
252        if d.len() < 54 {
253            return Err(FontError::DataTruncated);
254        }
255
256        Ok(HeadTable {
257            units_per_em: read_u16_be(d, 18).ok_or(FontError::DataTruncated)?,
258            x_min: read_i16_be(d, 36).ok_or(FontError::DataTruncated)?,
259            y_min: read_i16_be(d, 38).ok_or(FontError::DataTruncated)?,
260            x_max: read_i16_be(d, 40).ok_or(FontError::DataTruncated)?,
261            y_max: read_i16_be(d, 42).ok_or(FontError::DataTruncated)?,
262            index_to_loc_format: read_i16_be(d, 50).ok_or(FontError::DataTruncated)?,
263        })
264    }
265
266    /// Parse the `hhea` table.
267    pub fn parse_hhea(&self) -> Result<HheaTable, FontError> {
268        let entry = self
269            .find_table(TableTag::HHEA)
270            .ok_or(FontError::TableNotFound)?;
271        let d = self.table_data(&entry)?;
272        if d.len() < 36 {
273            return Err(FontError::DataTruncated);
274        }
275
276        Ok(HheaTable {
277            ascent: read_i16_be(d, 4).ok_or(FontError::DataTruncated)?,
278            descent: read_i16_be(d, 6).ok_or(FontError::DataTruncated)?,
279            line_gap: read_i16_be(d, 8).ok_or(FontError::DataTruncated)?,
280            num_h_metrics: read_u16_be(d, 34).ok_or(FontError::DataTruncated)?,
281        })
282    }
283
284    /// Parse the `maxp` table.
285    pub fn parse_maxp(&self) -> Result<MaxpTable, FontError> {
286        let entry = self
287            .find_table(TableTag::MAXP)
288            .ok_or(FontError::TableNotFound)?;
289        let d = self.table_data(&entry)?;
290        if d.len() < 6 {
291            return Err(FontError::DataTruncated);
292        }
293
294        Ok(MaxpTable {
295            num_glyphs: read_u16_be(d, 4).ok_or(FontError::DataTruncated)?,
296        })
297    }
298
299    /// Look up a glyph index from a character code using `cmap` table.
300    /// Supports format 4 (BMP) cmap subtable.
301    pub fn char_to_glyph(&self, ch: u32) -> Result<u16, FontError> {
302        let entry = self
303            .find_table(TableTag::CMAP)
304            .ok_or(FontError::TableNotFound)?;
305        let d = self.table_data(&entry)?;
306        if d.len() < 4 {
307            return Err(FontError::DataTruncated);
308        }
309
310        let num_subtables = read_u16_be(d, 2).ok_or(FontError::DataTruncated)?;
311
312        // Find a Unicode (platform 0 or 3) subtable.
313        for i in 0..num_subtables as usize {
314            let rec_off = 4 + i * 8;
315            if rec_off + 8 > d.len() {
316                break;
317            }
318            let platform = read_u16_be(d, rec_off).ok_or(FontError::DataTruncated)?;
319            let sub_offset = read_u32_be(d, rec_off + 4).ok_or(FontError::DataTruncated)? as usize;
320
321            if platform != 0 && platform != 3 {
322                continue;
323            }
324
325            if sub_offset + 6 > d.len() {
326                continue;
327            }
328
329            let format = read_u16_be(d, sub_offset).ok_or(FontError::DataTruncated)?;
330
331            if format == 4 {
332                return self.cmap_format4_lookup(d, sub_offset, ch);
333            }
334        }
335
336        Err(FontError::GlyphNotFound)
337    }
338
339    /// Format 4 cmap lookup (segmented mapping for BMP).
340    fn cmap_format4_lookup(
341        &self,
342        cmap_data: &[u8],
343        offset: usize,
344        ch: u32,
345    ) -> Result<u16, FontError> {
346        if ch > 0xFFFF {
347            return Err(FontError::GlyphNotFound);
348        }
349        let ch = ch as u16;
350
351        let seg_count_x2 =
352            read_u16_be(cmap_data, offset + 6).ok_or(FontError::DataTruncated)? as usize;
353        let seg_count = seg_count_x2 / 2;
354
355        let end_codes_off = offset + 14;
356        // +2 for reserved pad
357        let start_codes_off = end_codes_off + seg_count_x2 + 2;
358        let id_delta_off = start_codes_off + seg_count_x2;
359        let id_range_off = id_delta_off + seg_count_x2;
360
361        for seg in 0..seg_count {
362            let end_code =
363                read_u16_be(cmap_data, end_codes_off + seg * 2).ok_or(FontError::DataTruncated)?;
364
365            if ch > end_code {
366                continue;
367            }
368
369            let start_code = read_u16_be(cmap_data, start_codes_off + seg * 2)
370                .ok_or(FontError::DataTruncated)?;
371
372            if ch < start_code {
373                return Err(FontError::GlyphNotFound);
374            }
375
376            let id_delta =
377                read_i16_be(cmap_data, id_delta_off + seg * 2).ok_or(FontError::DataTruncated)?;
378            let id_range =
379                read_u16_be(cmap_data, id_range_off + seg * 2).ok_or(FontError::DataTruncated)?;
380
381            if id_range == 0 {
382                return Ok((ch as i16).wrapping_add(id_delta) as u16);
383            }
384
385            let glyph_offset =
386                id_range_off + seg * 2 + id_range as usize + (ch - start_code) as usize * 2;
387            let glyph_id = read_u16_be(cmap_data, glyph_offset).ok_or(FontError::DataTruncated)?;
388
389            if glyph_id == 0 {
390                return Err(FontError::GlyphNotFound);
391            }
392
393            return Ok((glyph_id as i16).wrapping_add(id_delta) as u16);
394        }
395
396        Err(FontError::GlyphNotFound)
397    }
398
399    /// Get glyph offset from `loca` table.
400    pub fn glyph_offset(&self, glyph_id: u16, head: &HeadTable) -> Result<(u32, u32), FontError> {
401        let entry = self
402            .find_table(TableTag::LOCA)
403            .ok_or(FontError::TableNotFound)?;
404        let d = self.table_data(&entry)?;
405
406        if head.index_to_loc_format == 0 {
407            // Short format: offset/2 stored as u16.
408            let idx = glyph_id as usize * 2;
409            let off1 = read_u16_be(d, idx).ok_or(FontError::DataTruncated)? as u32 * 2;
410            let off2 = read_u16_be(d, idx + 2).ok_or(FontError::DataTruncated)? as u32 * 2;
411            Ok((off1, off2))
412        } else {
413            // Long format: offsets stored as u32.
414            let idx = glyph_id as usize * 4;
415            let off1 = read_u32_be(d, idx).ok_or(FontError::DataTruncated)?;
416            let off2 = read_u32_be(d, idx + 4).ok_or(FontError::DataTruncated)?;
417            Ok((off1, off2))
418        }
419    }
420
421    /// Parse a simple glyph outline from the `glyf` table.
422    #[cfg(feature = "alloc")]
423    pub fn parse_glyph(&self, glyph_id: u16) -> Result<GlyphOutline, FontError> {
424        let head = self.parse_head()?;
425        let (off1, off2) = self.glyph_offset(glyph_id, &head)?;
426
427        if off1 == off2 {
428            // Empty glyph (e.g., space).
429            return Ok(GlyphOutline {
430                contours: Vec::new(),
431                x_min: 0,
432                y_min: 0,
433                x_max: 0,
434                y_max: 0,
435                advance_width: 0,
436                lsb: 0,
437            });
438        }
439
440        let glyf_entry = self
441            .find_table(TableTag::GLYF)
442            .ok_or(FontError::TableNotFound)?;
443        let glyf_data = self.table_data(&glyf_entry)?;
444
445        let glyph_start = off1 as usize;
446        if glyph_start + 10 > glyf_data.len() {
447            return Err(FontError::DataTruncated);
448        }
449
450        let num_contours = read_i16_be(glyf_data, glyph_start).ok_or(FontError::DataTruncated)?;
451        let x_min = read_i16_be(glyf_data, glyph_start + 2).ok_or(FontError::DataTruncated)?;
452        let y_min = read_i16_be(glyf_data, glyph_start + 4).ok_or(FontError::DataTruncated)?;
453        let x_max = read_i16_be(glyf_data, glyph_start + 6).ok_or(FontError::DataTruncated)?;
454        let y_max = read_i16_be(glyf_data, glyph_start + 8).ok_or(FontError::DataTruncated)?;
455
456        if num_contours < 0 {
457            // Compound glyph -- not parsed, return bounding box only.
458            return Ok(GlyphOutline {
459                contours: Vec::new(),
460                x_min,
461                y_min,
462                x_max,
463                y_max,
464                advance_width: 0,
465                lsb: 0,
466            });
467        }
468
469        let num_contours = num_contours as usize;
470        let mut cursor = glyph_start + 10;
471
472        // Read end-points of each contour.
473        let mut end_pts = Vec::with_capacity(num_contours);
474        for _ in 0..num_contours {
475            let ep = read_u16_be(glyf_data, cursor).ok_or(FontError::DataTruncated)?;
476            end_pts.push(ep);
477            cursor += 2;
478        }
479
480        let num_points = if let Some(&last) = end_pts.last() {
481            last as usize + 1
482        } else {
483            return Ok(GlyphOutline {
484                contours: Vec::new(),
485                x_min,
486                y_min,
487                x_max,
488                y_max,
489                advance_width: 0,
490                lsb: 0,
491            });
492        };
493
494        // Skip instructions.
495        let instruction_length =
496            read_u16_be(glyf_data, cursor).ok_or(FontError::DataTruncated)? as usize;
497        cursor += 2 + instruction_length;
498
499        // Parse flags.
500        let mut flags = Vec::with_capacity(num_points);
501        while flags.len() < num_points {
502            if cursor >= glyf_data.len() {
503                return Err(FontError::DataTruncated);
504            }
505            let flag = glyf_data[cursor];
506            cursor += 1;
507            flags.push(flag);
508
509            // Bit 3: repeat.
510            if flag & 0x08 != 0 {
511                if cursor >= glyf_data.len() {
512                    return Err(FontError::DataTruncated);
513                }
514                let repeat = glyf_data[cursor] as usize;
515                cursor += 1;
516                for _ in 0..repeat {
517                    if flags.len() < num_points {
518                        flags.push(flag);
519                    }
520                }
521            }
522        }
523
524        // Parse X coordinates.
525        let mut x_coords = Vec::with_capacity(num_points);
526        let mut x: i16 = 0;
527        for flag in &flags {
528            let short = flag & 0x02 != 0;
529            let same_or_positive = flag & 0x10 != 0;
530
531            if short {
532                if cursor >= glyf_data.len() {
533                    return Err(FontError::DataTruncated);
534                }
535                let dx = glyf_data[cursor] as i16;
536                cursor += 1;
537                x += if same_or_positive { dx } else { -dx };
538            } else if !same_or_positive {
539                let dx = read_i16_be(glyf_data, cursor).ok_or(FontError::DataTruncated)?;
540                cursor += 2;
541                x += dx;
542            }
543            // else: same_or_positive && !short => x unchanged.
544            x_coords.push(x);
545        }
546
547        // Parse Y coordinates.
548        let mut y_coords = Vec::with_capacity(num_points);
549        let mut y: i16 = 0;
550        for flag in &flags {
551            let short = flag & 0x04 != 0;
552            let same_or_positive = flag & 0x20 != 0;
553
554            if short {
555                if cursor >= glyf_data.len() {
556                    return Err(FontError::DataTruncated);
557                }
558                let dy = glyf_data[cursor] as i16;
559                cursor += 1;
560                y += if same_or_positive { dy } else { -dy };
561            } else if !same_or_positive {
562                let dy = read_i16_be(glyf_data, cursor).ok_or(FontError::DataTruncated)?;
563                cursor += 2;
564                y += dy;
565            }
566            y_coords.push(y);
567        }
568
569        // Build contours.
570        let mut contours = Vec::with_capacity(num_contours);
571        let mut start = 0usize;
572        for &end in &end_pts {
573            let end = end as usize;
574            let mut points = Vec::new();
575            for idx in start..=end {
576                if idx < num_points {
577                    points.push(OutlinePoint {
578                        x: x_coords[idx],
579                        y: y_coords[idx],
580                        on_curve: flags[idx] & 0x01 != 0,
581                    });
582                }
583            }
584            contours.push(GlyphContour { points });
585            start = end + 1;
586        }
587
588        Ok(GlyphOutline {
589            contours,
590            x_min,
591            y_min,
592            x_max,
593            y_max,
594            advance_width: 0,
595            lsb: 0,
596        })
597    }
598}
599
600/// Rendered glyph bitmap.
601#[derive(Debug, Clone, PartialEq, Eq)]
602#[cfg(feature = "alloc")]
603pub struct GlyphBitmap {
604    /// Grayscale pixel data (0 = transparent, 255 = opaque).
605    pub data: Vec<u8>,
606    /// Bitmap width in pixels.
607    pub width: u32,
608    /// Bitmap height in pixels.
609    pub height: u32,
610    /// Left bearing in pixels.
611    pub bearing_x: i32,
612    /// Top bearing in pixels.
613    pub bearing_y: i32,
614    /// Advance width in pixels.
615    pub advance: u32,
616}
617
618/// Glyph cache entry.
619#[derive(Debug, Clone)]
620#[cfg(feature = "alloc")]
621struct GlyphCacheEntry {
622    /// Character code.
623    ch: u32,
624    /// Rendered size in pixels.
625    size_px: u16,
626    /// Cached bitmap.
627    bitmap: GlyphBitmap,
628    /// Access count for LRU eviction.
629    access_count: u32,
630}
631
632/// Maximum glyph cache entries.
633pub(crate) const GLYPH_CACHE_SIZE: usize = 256;
634
635/// Glyph cache with LRU eviction.
636#[derive(Debug)]
637#[cfg(feature = "alloc")]
638pub struct GlyphCache {
639    entries: Vec<GlyphCacheEntry>,
640    total_lookups: u64,
641    cache_hits: u64,
642}
643
644#[cfg(feature = "alloc")]
645impl Default for GlyphCache {
646    fn default() -> Self {
647        Self::new()
648    }
649}
650
651#[cfg(feature = "alloc")]
652impl GlyphCache {
653    /// Create a new empty glyph cache.
654    pub fn new() -> Self {
655        Self {
656            entries: Vec::new(),
657            total_lookups: 0,
658            cache_hits: 0,
659        }
660    }
661
662    /// Look up a cached glyph.
663    pub fn get(&mut self, ch: u32, size_px: u16) -> Option<&GlyphBitmap> {
664        self.total_lookups += 1;
665
666        let idx = self
667            .entries
668            .iter()
669            .position(|e| e.ch == ch && e.size_px == size_px);
670
671        if let Some(i) = idx {
672            self.cache_hits += 1;
673            self.entries[i].access_count += 1;
674            Some(&self.entries[i].bitmap)
675        } else {
676            None
677        }
678    }
679
680    /// Insert a glyph bitmap into the cache.
681    pub fn insert(&mut self, ch: u32, size_px: u16, bitmap: GlyphBitmap) {
682        // Evict LRU if at capacity.
683        if self.entries.len() >= GLYPH_CACHE_SIZE {
684            // Find the entry with the lowest access count.
685            let min_idx = self
686                .entries
687                .iter()
688                .enumerate()
689                .min_by_key(|(_, e)| e.access_count)
690                .map(|(i, _)| i)
691                .unwrap_or(0);
692            self.entries.swap_remove(min_idx);
693        }
694
695        self.entries.push(GlyphCacheEntry {
696            ch,
697            size_px,
698            bitmap,
699            access_count: 1,
700        });
701    }
702
703    /// Get cache hit rate as a percentage (0-100).
704    pub fn hit_rate_percent(&self) -> u32 {
705        if self.total_lookups == 0 {
706            return 0;
707        }
708        ((self.cache_hits * 100) / self.total_lookups) as u32
709    }
710
711    /// Clear the cache.
712    pub fn clear(&mut self) {
713        self.entries.clear();
714        self.total_lookups = 0;
715        self.cache_hits = 0;
716    }
717
718    /// Number of entries in the cache.
719    pub fn len(&self) -> usize {
720        self.entries.len()
721    }
722
723    /// Check if cache is empty.
724    pub fn is_empty(&self) -> bool {
725        self.entries.is_empty()
726    }
727}
728
729/// Rasterize a glyph outline to a grayscale bitmap using integer math.
730///
731/// `scale_num` / `scale_den` is the scaling factor (e.g., pixel_size /
732/// units_per_em). Uses midpoint line drawing for on-curve segments and
733/// quadratic Bezier subdivision for off-curve control points.
734#[cfg(feature = "alloc")]
735pub fn rasterize_outline(outline: &GlyphOutline, scale_num: u32, scale_den: u32) -> GlyphBitmap {
736    if outline.contours.is_empty() || scale_den == 0 {
737        return GlyphBitmap {
738            data: Vec::new(),
739            width: 0,
740            height: 0,
741            bearing_x: 0,
742            bearing_y: 0,
743            advance: 0,
744        };
745    }
746
747    // Compute scaled bounding box.
748    let scale = |v: i16| -> i32 { (v as i32 * scale_num as i32) / scale_den as i32 };
749
750    let x_min = scale(outline.x_min);
751    let y_min = scale(outline.y_min);
752    let x_max = scale(outline.x_max);
753    let y_max = scale(outline.y_max);
754
755    let width = (x_max - x_min + 1).max(1) as u32;
756    let height = (y_max - y_min + 1).max(1) as u32;
757
758    // Clamp to reasonable size.
759    let width = width.min(512);
760    let height = height.min(512);
761
762    let mut data = vec![0u8; (width * height) as usize];
763
764    // Rasterize each contour using scanline edge tracking.
765    for contour in &outline.contours {
766        let points = &contour.points;
767        if points.len() < 2 {
768            continue;
769        }
770
771        let num = points.len();
772        for i in 0..num {
773            let p0 = &points[i];
774            let p1 = &points[(i + 1) % num];
775
776            let x0 = scale(p0.x) - x_min;
777            let y0 = y_max - scale(p0.y);
778            let x1 = scale(p1.x) - x_min;
779            let y1 = y_max - scale(p1.y);
780
781            if p0.on_curve && p1.on_curve {
782                // Straight line segment.
783                draw_line(&mut data, width, height, x0, y0, x1, y1);
784            } else if !p1.on_curve && (i + 2) <= num {
785                // Quadratic bezier: p0 on-curve, p1 off-curve, p2 on-curve.
786                let p2 = &points[(i + 2) % num];
787                let x2 = scale(p2.x) - x_min;
788                let y2 = y_max - scale(p2.y);
789                draw_quadratic_bezier(&mut data, width, height, x0, y0, x1, y1, x2, y2);
790            }
791        }
792    }
793
794    GlyphBitmap {
795        data,
796        width,
797        height,
798        bearing_x: x_min,
799        bearing_y: y_max,
800        advance: width,
801    }
802}
803
804/// Draw a line using Bresenham's midpoint algorithm (integer only).
805#[cfg(feature = "alloc")]
806fn draw_line(buf: &mut [u8], w: u32, h: u32, x0: i32, y0: i32, x1: i32, y1: i32) {
807    let mut x0 = x0;
808    let mut y0 = y0;
809
810    let dx = (x1 - x0).abs();
811    let dy = -(y1 - y0).abs();
812    let sx: i32 = if x0 < x1 { 1 } else { -1 };
813    let sy: i32 = if y0 < y1 { 1 } else { -1 };
814    let mut err = dx + dy;
815
816    loop {
817        // Plot pixel.
818        if x0 >= 0 && (x0 as u32) < w && y0 >= 0 && (y0 as u32) < h {
819            let idx = y0 as u32 * w + x0 as u32;
820            if (idx as usize) < buf.len() {
821                buf[idx as usize] = 255;
822            }
823        }
824
825        if x0 == x1 && y0 == y1 {
826            break;
827        }
828
829        let e2 = 2 * err;
830        if e2 >= dy {
831            err += dy;
832            x0 += sx;
833        }
834        if e2 <= dx {
835            err += dx;
836            y0 += sy;
837        }
838    }
839}
840
841/// Draw a quadratic Bezier curve using recursive subdivision (integer only).
842#[cfg(feature = "alloc")]
843#[allow(clippy::too_many_arguments)]
844fn draw_quadratic_bezier(
845    buf: &mut [u8],
846    w: u32,
847    h: u32,
848    x0: i32,
849    y0: i32,
850    cx: i32,
851    cy: i32,
852    x2: i32,
853    y2: i32,
854) {
855    // Subdivision: if the control point is close to the midpoint of the
856    // line p0-p2, just draw a line.
857    let mx = (x0 + x2) / 2;
858    let my = (y0 + y2) / 2;
859    let dist = (cx - mx).abs() + (cy - my).abs();
860
861    if dist <= 1 {
862        draw_line(buf, w, h, x0, y0, x2, y2);
863        return;
864    }
865
866    // Subdivide at midpoint.
867    let ax = (x0 + cx) / 2;
868    let ay = (y0 + cy) / 2;
869    let bx = (cx + x2) / 2;
870    let by = (cy + y2) / 2;
871    let midx = (ax + bx) / 2;
872    let midy = (ay + by) / 2;
873
874    draw_quadratic_bezier(buf, w, h, x0, y0, ax, ay, midx, midy);
875    draw_quadratic_bezier(buf, w, h, midx, midy, bx, by, x2, y2);
876}
877
878/// Hinting mode stub.
879#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
880pub enum HintingMode {
881    /// No hinting.
882    #[default]
883    None,
884    /// Light hinting (vertical only).
885    Light,
886    /// Full hinting.
887    Full,
888    /// Auto-hinting (algorithmic).
889    Auto,
890}
891
892/// Apply hinting to a glyph outline (stub -- returns outline unchanged).
893#[cfg(feature = "alloc")]
894pub fn apply_hinting(outline: &GlyphOutline, _mode: HintingMode) -> GlyphOutline {
895    // Hinting is complex and typically requires bytecode interpretation.
896    // This is a stub that returns the outline unchanged.
897    outline.clone()
898}
899
900/// Render a character to a grayscale bitmap at the given pixel size.
901///
902/// This is the main entry point for glyph rendering. It:
903/// 1. Looks up the glyph ID from the character code via `cmap`.
904/// 2. Parses the glyph outline from `glyf`.
905/// 3. Rasterizes the outline to a bitmap.
906#[cfg(feature = "alloc")]
907pub fn render_glyph(
908    parser: &TtfParser<'_>,
909    ch: char,
910    pixel_size: u16,
911) -> Result<GlyphBitmap, FontError> {
912    let head = parser.parse_head()?;
913    let glyph_id = parser.char_to_glyph(ch as u32)?;
914    let outline = parser.parse_glyph(glyph_id)?;
915    let bitmap = rasterize_outline(&outline, pixel_size as u32, head.units_per_em as u32);
916    Ok(bitmap)
917}