Skip to main content

pretext_egui/
glyph_atlas.rs

1use ahash::{AHashMap, AHashSet};
2use egui::epaint::Mesh;
3use egui::{Color32, ColorImage, Context, Painter, Rect, Shape, TextureHandle, TextureOptions};
4use image::ImageFormat;
5use pretext::font_catalog::FontId;
6use pretext::{
7    PretextEngine, PretextGlyphRun as LayoutLineGlyphRun, PretextStyle as TextStyleSpec,
8};
9use resvg::usvg;
10
11const ATLAS_PAGE_SIZE: usize = 2048;
12const ATLAS_GLYPH_PADDING_PX: usize = 1;
13const ATLAS_TEXTURE_OPTIONS: TextureOptions = TextureOptions::LINEAR;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub struct GlyphAtlasStats {
17    pub pages: usize,
18    pub entries: usize,
19    pub hits: u64,
20    pub misses: u64,
21}
22
23#[derive(Default)]
24pub struct GlyphSceneBuilder {
25    meshes: AHashMap<egui::TextureId, Mesh>,
26    glyph_quads: u64,
27    painted: bool,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct GlyphSceneFlushStats {
32    pub mesh_flushes: u64,
33    pub glyph_quads: u64,
34    pub painted: bool,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum GlyphWarmResult {
39    Hit,
40    Miss,
41}
42
43#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
44struct GlyphRasterKey {
45    engine_revision: u64,
46    face_id: FontId,
47    glyph_id: u16,
48    size_px_q: u32,
49    pixels_per_point_q: u32,
50}
51
52#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
53struct FaceMetricKey {
54    engine_revision: u64,
55    face_id: FontId,
56    size_px_q: u32,
57}
58
59#[derive(Clone)]
60struct GlyphAtlasEntry {
61    page_index: usize,
62    uv_rect: Rect,
63    logical_size: egui::Vec2,
64    offset: egui::Vec2,
65    is_color: bool,
66}
67
68#[derive(Clone, Copy)]
69struct FaceMetrics {
70    ascent: f32,
71    descent: f32,
72}
73
74struct AtlasPage {
75    texture: TextureHandle,
76    next_x: usize,
77    next_y: usize,
78    row_height: usize,
79}
80
81pub struct GlyphAtlas {
82    pages: Vec<AtlasPage>,
83    entries: AHashMap<GlyphRasterKey, GlyphAtlasEntry>,
84    face_metrics: AHashMap<FaceMetricKey, FaceMetrics>,
85    hits: u64,
86    misses: u64,
87}
88
89impl Default for GlyphAtlas {
90    fn default() -> Self {
91        Self {
92            pages: Vec::new(),
93            entries: AHashMap::new(),
94            face_metrics: AHashMap::new(),
95            hits: 0,
96            misses: 0,
97        }
98    }
99}
100
101impl GlyphAtlas {
102    pub fn stats(&self) -> GlyphAtlasStats {
103        GlyphAtlasStats {
104            pages: self.pages.len(),
105            entries: self.entries.len(),
106            hits: self.hits,
107            misses: self.misses,
108        }
109    }
110
111    pub fn begin_scene(&self) -> GlyphSceneBuilder {
112        GlyphSceneBuilder::default()
113    }
114
115    #[allow(clippy::too_many_arguments)]
116    pub fn paint_line_glyph_runs(
117        &mut self,
118        painter: &Painter,
119        x: f32,
120        y: f32,
121        glyph_runs: &[LayoutLineGlyphRun],
122        style: &TextStyleSpec,
123        line_height: f32,
124        color: Color32,
125        ctx: &Context,
126        engine: &PretextEngine,
127        texture_uploads: &mut u64,
128        texture_upload_bytes: &mut u64,
129    ) -> bool {
130        let mut scene = self.begin_scene();
131        let painted = self.append_line_glyph_runs(
132            &mut scene,
133            x,
134            y,
135            glyph_runs,
136            style,
137            line_height,
138            color,
139            ctx,
140            engine,
141            texture_uploads,
142            texture_upload_bytes,
143        );
144        let _ = self.flush_scene(painter, &mut scene);
145        painted
146    }
147
148    #[allow(clippy::too_many_arguments)]
149    pub fn append_line_glyph_runs(
150        &mut self,
151        scene: &mut GlyphSceneBuilder,
152        x: f32,
153        y: f32,
154        glyph_runs: &[LayoutLineGlyphRun],
155        style: &TextStyleSpec,
156        line_height: f32,
157        color: Color32,
158        ctx: &Context,
159        engine: &PretextEngine,
160        texture_uploads: &mut u64,
161        texture_upload_bytes: &mut u64,
162    ) -> bool {
163        if glyph_runs.is_empty() {
164            return false;
165        }
166
167        let pixels_per_point = ctx.pixels_per_point().max(1.0);
168        for run in glyph_runs {
169            for glyph in &run.glyphs {
170                if self
171                    .glyph_entry(
172                        ctx,
173                        engine,
174                        glyph.face_id,
175                        glyph.glyph_id,
176                        style.size_px,
177                        pixels_per_point,
178                        texture_uploads,
179                        texture_upload_bytes,
180                    )
181                    .is_none()
182                {
183                    let Some(face) = engine.load_face(glyph.face_id) else {
184                        return false;
185                    };
186                    if glyph_has_visible_ink(&face, glyph.glyph_id) {
187                        return false;
188                    }
189                }
190            }
191        }
192
193        let baseline_y = y + self.baseline_px(glyph_runs, style, line_height, engine);
194
195        for run in glyph_runs {
196            for glyph in &run.glyphs {
197                let Some(lookup) = self.glyph_entry(
198                    ctx,
199                    engine,
200                    glyph.face_id,
201                    glyph.glyph_id,
202                    style.size_px,
203                    pixels_per_point,
204                    texture_uploads,
205                    texture_upload_bytes,
206                ) else {
207                    continue;
208                };
209
210                let rect = if engine
211                    .load_face(glyph.face_id)
212                    .as_ref()
213                    .is_some_and(|face| face_uses_emoji_baseline_defaults(face, glyph.glyph_id))
214                {
215                    centered_emoji_rect(
216                        x + glyph.x,
217                        y,
218                        glyph.advance,
219                        line_height,
220                        lookup.entry.logical_size,
221                        pixels_per_point,
222                    )
223                } else {
224                    let rect_min = egui::pos2(
225                        snap_to_pixel(
226                            x + glyph.x + glyph.x_offset + lookup.entry.offset.x,
227                            pixels_per_point,
228                        ),
229                        snap_to_pixel(
230                            baseline_y - glyph.y_offset + lookup.entry.offset.y,
231                            pixels_per_point,
232                        ),
233                    );
234                    Rect::from_min_size(rect_min, lookup.entry.logical_size)
235                };
236                let texture_id = self.pages[lookup.entry.page_index].texture.id();
237                let mesh = scene
238                    .meshes
239                    .entry(texture_id)
240                    .or_insert_with(|| Mesh::with_texture(texture_id));
241                mesh.add_rect_with_uv(
242                    rect,
243                    lookup.entry.uv_rect,
244                    if lookup.entry.is_color {
245                        Color32::WHITE
246                    } else {
247                        color
248                    },
249                );
250                scene.painted = true;
251                scene.glyph_quads += 1;
252            }
253        }
254
255        scene.painted
256    }
257
258    pub fn flush_scene(
259        &mut self,
260        painter: &Painter,
261        scene: &mut GlyphSceneBuilder,
262    ) -> GlyphSceneFlushStats {
263        let mut flush_stats = GlyphSceneFlushStats {
264            glyph_quads: scene.glyph_quads,
265            painted: scene.painted,
266            ..GlyphSceneFlushStats::default()
267        };
268
269        for mesh in std::mem::take(&mut scene.meshes).into_values() {
270            if mesh.is_empty() {
271                continue;
272            }
273            painter.add(Shape::mesh(mesh));
274            flush_stats.mesh_flushes += 1;
275        }
276
277        scene.glyph_quads = 0;
278        scene.painted = false;
279        flush_stats
280    }
281
282    #[allow(clippy::too_many_arguments)]
283    pub fn warm_glyph(
284        &mut self,
285        ctx: &Context,
286        engine: &PretextEngine,
287        face_id: FontId,
288        glyph_id: u16,
289        size_px: f32,
290        pixels_per_point: f32,
291        texture_uploads: &mut u64,
292        texture_upload_bytes: &mut u64,
293    ) -> Option<GlyphWarmResult> {
294        self.glyph_entry(
295            ctx,
296            engine,
297            face_id,
298            glyph_id,
299            size_px,
300            pixels_per_point,
301            texture_uploads,
302            texture_upload_bytes,
303        )
304        .map(|lookup| lookup.result)
305    }
306
307    fn baseline_px(
308        &mut self,
309        glyph_runs: &[LayoutLineGlyphRun],
310        style: &TextStyleSpec,
311        line_height: f32,
312        engine: &PretextEngine,
313    ) -> f32 {
314        let mut ascent = style.size_px * 0.8;
315        let mut descent = style.size_px * 0.2;
316        let mut seen = AHashSet::new();
317
318        for glyph in glyph_runs.iter().flat_map(|run| run.glyphs.iter()) {
319            if !seen.insert(glyph.face_id) {
320                continue;
321            }
322            let Some(face) = engine.load_face(glyph.face_id) else {
323                continue;
324            };
325            if face_uses_emoji_baseline_defaults(&face, glyph.glyph_id) {
326                continue;
327            }
328            let Some(metrics) = self.face_metrics(engine, glyph.face_id, style.size_px) else {
329                continue;
330            };
331            ascent = ascent.max(metrics.ascent);
332            descent = descent.max(metrics.descent);
333        }
334
335        let content_height = (ascent + descent).max(1.0);
336        ((line_height - content_height).max(0.0)) * 0.5 + ascent
337    }
338
339    fn face_metrics(
340        &mut self,
341        engine: &PretextEngine,
342        face_id: FontId,
343        size_px: f32,
344    ) -> Option<FaceMetrics> {
345        let key = FaceMetricKey {
346            engine_revision: engine.revision(),
347            face_id,
348            size_px_q: quantize(size_px),
349        };
350        if let Some(metrics) = self.face_metrics.get(&key).copied() {
351            return Some(metrics);
352        }
353
354        let face = engine.load_face(face_id)?;
355        let ttf = ttf_parser::Face::parse(face.data(), face.face_index()).ok()?;
356        let scale = size_px / face.units_per_em().max(1) as f32;
357        let metrics = FaceMetrics {
358            ascent: (ttf.ascender() as f32 * scale).max(1.0),
359            descent: (-(ttf.descender() as f32) * scale).max(0.0),
360        };
361        self.face_metrics.insert(key, metrics);
362        Some(metrics)
363    }
364
365    fn glyph_entry(
366        &mut self,
367        ctx: &Context,
368        engine: &PretextEngine,
369        face_id: FontId,
370        glyph_id: u16,
371        size_px: f32,
372        pixels_per_point: f32,
373        texture_uploads: &mut u64,
374        texture_upload_bytes: &mut u64,
375    ) -> Option<GlyphLookup> {
376        let key = GlyphRasterKey {
377            engine_revision: engine.revision(),
378            face_id,
379            glyph_id,
380            size_px_q: quantize(size_px),
381            pixels_per_point_q: quantize(pixels_per_point),
382        };
383        if let Some(entry) = self.entries.get(&key).cloned() {
384            self.hits += 1;
385            return Some(GlyphLookup {
386                entry,
387                result: GlyphWarmResult::Hit,
388            });
389        }
390        self.misses += 1;
391
392        let face = engine.load_face(face_id)?;
393        let raster = rasterize_glyph(&face, glyph_id, size_px, pixels_per_point)?;
394        let placement = self.allocate(raster.image.size, ctx)?;
395        let page = self.pages.get_mut(placement.page_index)?;
396        page.texture
397            .set_partial(placement.pos, raster.image, ATLAS_TEXTURE_OPTIONS);
398        *texture_uploads += 1;
399        *texture_upload_bytes += (placement.upload_size[0] * placement.upload_size[1] * 4) as u64;
400
401        let entry = GlyphAtlasEntry {
402            page_index: placement.page_index,
403            uv_rect: placement.uv_rect,
404            logical_size: raster.logical_size,
405            offset: raster.offset,
406            is_color: raster.is_color,
407        };
408        self.entries.insert(key, entry.clone());
409        Some(GlyphLookup {
410            entry,
411            result: GlyphWarmResult::Miss,
412        })
413    }
414
415    fn allocate(&mut self, size: [usize; 2], ctx: &Context) -> Option<Allocation> {
416        if size[0] > ATLAS_PAGE_SIZE || size[1] > ATLAS_PAGE_SIZE {
417            return None;
418        }
419
420        for (page_index, page) in self.pages.iter_mut().enumerate() {
421            if let Some(pos) = allocate_on_page(page, size) {
422                return Some(allocation(page_index, pos, size));
423            }
424        }
425
426        let image = ColorImage::filled([ATLAS_PAGE_SIZE, ATLAS_PAGE_SIZE], Color32::TRANSPARENT);
427        let texture = ctx.load_texture(
428            format!("pretext-egui/glyph-atlas/{}", self.pages.len()),
429            image,
430            ATLAS_TEXTURE_OPTIONS,
431        );
432        let mut page = AtlasPage {
433            texture,
434            next_x: 0,
435            next_y: 0,
436            row_height: 0,
437        };
438        let pos = allocate_on_page(&mut page, size)?;
439        let page_index = self.pages.len();
440        self.pages.push(page);
441        Some(allocation(page_index, pos, size))
442    }
443}
444
445fn glyph_has_visible_ink(face: &pretext::font_catalog::LoadedFace, glyph_id: u16) -> bool {
446    let Ok(ttf) = ttf_parser::Face::parse(face.data(), face.face_index()) else {
447        return true;
448    };
449    let glyph_id = ttf_parser::GlyphId(glyph_id);
450    ttf.is_color_glyph(glyph_id) || ttf.glyph_bounding_box(glyph_id).is_some()
451}
452
453fn face_uses_emoji_baseline_defaults(
454    face: &pretext::font_catalog::LoadedFace,
455    glyph_id: u16,
456) -> bool {
457    let family_name = face.family_name();
458    if family_name.contains("Emoji") {
459        return true;
460    }
461
462    let Ok(ttf) = ttf_parser::Face::parse(face.data(), face.face_index()) else {
463        return false;
464    };
465    ttf.is_color_glyph(ttf_parser::GlyphId(glyph_id))
466}
467
468fn centered_emoji_rect(
469    advance_left: f32,
470    slot_top: f32,
471    advance_width: f32,
472    line_height: f32,
473    logical_size: egui::Vec2,
474    pixels_per_point: f32,
475) -> Rect {
476    let target_height = logical_size.y.min(line_height.max(1.0));
477    let scale = if logical_size.y > 0.0 {
478        target_height / logical_size.y
479    } else {
480        1.0
481    };
482    let target_width = (logical_size.x * scale).max(1.0);
483    let x = advance_left + (advance_width - target_width).max(0.0) * 0.5;
484    let y = slot_top + (line_height - target_height).max(0.0) * 0.5;
485    Rect::from_min_size(
486        egui::pos2(
487            snap_to_pixel(x, pixels_per_point),
488            snap_to_pixel(y, pixels_per_point),
489        ),
490        egui::vec2(target_width, target_height),
491    )
492}
493
494struct Allocation {
495    page_index: usize,
496    pos: [usize; 2],
497    upload_size: [usize; 2],
498    uv_rect: Rect,
499}
500
501struct GlyphLookup {
502    entry: GlyphAtlasEntry,
503    result: GlyphWarmResult,
504}
505
506struct RasterizedGlyph {
507    image: ColorImage,
508    logical_size: egui::Vec2,
509    offset: egui::Vec2,
510    is_color: bool,
511}
512
513fn allocate_on_page(page: &mut AtlasPage, size: [usize; 2]) -> Option<[usize; 2]> {
514    if page.next_x + size[0] > ATLAS_PAGE_SIZE {
515        page.next_x = 0;
516        page.next_y += page.row_height;
517        page.row_height = 0;
518    }
519    if page.next_y + size[1] > ATLAS_PAGE_SIZE {
520        return None;
521    }
522
523    let pos = [page.next_x, page.next_y];
524    page.next_x += size[0];
525    page.row_height = page.row_height.max(size[1]);
526    Some(pos)
527}
528
529fn allocation(page_index: usize, pos: [usize; 2], size: [usize; 2]) -> Allocation {
530    let inv = 1.0 / ATLAS_PAGE_SIZE as f32;
531    Allocation {
532        page_index,
533        pos,
534        upload_size: size,
535        uv_rect: Rect::from_min_max(
536            egui::pos2(pos[0] as f32 * inv, pos[1] as f32 * inv),
537            egui::pos2(
538                (pos[0] + size[0]) as f32 * inv,
539                (pos[1] + size[1]) as f32 * inv,
540            ),
541        ),
542    }
543}
544
545fn rasterize_glyph(
546    face: &pretext::font_catalog::LoadedFace,
547    glyph_id: u16,
548    size_px: f32,
549    pixels_per_point: f32,
550) -> Option<RasterizedGlyph> {
551    let ttf = ttf_parser::Face::parse(face.data(), face.face_index()).ok()?;
552    let glyph_id = ttf_parser::GlyphId(glyph_id);
553
554    rasterize_bitmap_glyph(&ttf, glyph_id, size_px, pixels_per_point)
555        .or_else(|| rasterize_svg_glyph(&ttf, glyph_id, size_px, pixels_per_point))
556        .or_else(|| rasterize_outline_glyph(&ttf, glyph_id, size_px, pixels_per_point))
557}
558
559fn rasterize_outline_glyph(
560    face: &ttf_parser::Face<'_>,
561    glyph_id: ttf_parser::GlyphId,
562    size_px: f32,
563    pixels_per_point: f32,
564) -> Option<RasterizedGlyph> {
565    let bbox = face.glyph_bounding_box(glyph_id)?;
566    let units_per_em = face.units_per_em().max(1) as f32;
567    let scale = size_px * pixels_per_point / units_per_em;
568    let left_px = (bbox.x_min as f32 * scale).floor();
569    let right_px = (bbox.x_max as f32 * scale).ceil();
570    let top_px = (-bbox.y_max as f32 * scale).floor();
571    let bottom_px = (-bbox.y_min as f32 * scale).ceil();
572    let width = (right_px - left_px).max(1.0) as usize + ATLAS_GLYPH_PADDING_PX * 2;
573    let height = (bottom_px - top_px).max(1.0) as usize + ATLAS_GLYPH_PADDING_PX * 2;
574
575    let mut builder = GlyphPathBuilder::default();
576    face.outline_glyph(glyph_id, &mut builder)?;
577    let path = builder.finish()?;
578    let mut pixmap = tiny_skia::Pixmap::new(width as u32, height as u32)?;
579    let mut paint = tiny_skia::Paint::default();
580    paint.set_color_rgba8(255, 255, 255, 255);
581    paint.anti_alias = true;
582    let transform = tiny_skia::Transform::from_row(
583        scale,
584        0.0,
585        0.0,
586        -scale,
587        -left_px + ATLAS_GLYPH_PADDING_PX as f32,
588        -top_px + ATLAS_GLYPH_PADDING_PX as f32,
589    );
590    pixmap.fill_path(&path, &paint, tiny_skia::FillRule::Winding, transform, None);
591
592    Some(RasterizedGlyph {
593        image: ColorImage::from_rgba_premultiplied([width, height], pixmap.data()),
594        logical_size: egui::vec2(
595            width as f32 / pixels_per_point,
596            height as f32 / pixels_per_point,
597        ),
598        offset: egui::vec2(
599            left_px / pixels_per_point - ATLAS_GLYPH_PADDING_PX as f32 / pixels_per_point,
600            top_px / pixels_per_point - ATLAS_GLYPH_PADDING_PX as f32 / pixels_per_point,
601        ),
602        is_color: false,
603    })
604}
605
606fn rasterize_svg_glyph(
607    face: &ttf_parser::Face<'_>,
608    glyph_id: ttf_parser::GlyphId,
609    size_px: f32,
610    pixels_per_point: f32,
611) -> Option<RasterizedGlyph> {
612    let svg = face.glyph_svg_image(glyph_id)?;
613    let bbox = face.glyph_bounding_box(glyph_id)?;
614    let units_per_em = face.units_per_em().max(1) as f32;
615    let scale = size_px * pixels_per_point / units_per_em;
616    let left_px = (bbox.x_min as f32 * scale).floor();
617    let right_px = (bbox.x_max as f32 * scale).ceil();
618    let top_px = (-bbox.y_max as f32 * scale).floor();
619    let bottom_px = (-bbox.y_min as f32 * scale).ceil();
620    let width = (right_px - left_px).max(1.0) as usize;
621    let height = (bottom_px - top_px).max(1.0) as usize;
622    let upload_size = [
623        width + ATLAS_GLYPH_PADDING_PX * 2,
624        height + ATLAS_GLYPH_PADDING_PX * 2,
625    ];
626
627    let mut pixmap = tiny_skia::Pixmap::new(upload_size[0] as u32, upload_size[1] as u32)?;
628    let tree = usvg::Tree::from_data(svg.data, &usvg::Options::default()).ok()?;
629    let svg_size = tree.size();
630    let transform = tiny_skia::Transform::from_row(
631        width as f32 / svg_size.width().max(1.0),
632        0.0,
633        0.0,
634        height as f32 / svg_size.height().max(1.0),
635        ATLAS_GLYPH_PADDING_PX as f32,
636        ATLAS_GLYPH_PADDING_PX as f32,
637    );
638    resvg::render(&tree, transform, &mut pixmap.as_mut());
639
640    Some(RasterizedGlyph {
641        image: ColorImage::from_rgba_premultiplied(upload_size, pixmap.data()),
642        logical_size: egui::vec2(
643            upload_size[0] as f32 / pixels_per_point,
644            upload_size[1] as f32 / pixels_per_point,
645        ),
646        offset: egui::vec2(
647            left_px / pixels_per_point - ATLAS_GLYPH_PADDING_PX as f32 / pixels_per_point,
648            top_px / pixels_per_point - ATLAS_GLYPH_PADDING_PX as f32 / pixels_per_point,
649        ),
650        is_color: true,
651    })
652}
653
654fn rasterize_bitmap_glyph(
655    face: &ttf_parser::Face<'_>,
656    glyph_id: ttf_parser::GlyphId,
657    size_px: f32,
658    pixels_per_point: f32,
659) -> Option<RasterizedGlyph> {
660    let desired_ppem = (size_px * pixels_per_point)
661        .round()
662        .clamp(1.0, u16::MAX as f32) as u16;
663    let image = face.glyph_raster_image(glyph_id, desired_ppem)?;
664    let scale = desired_ppem as f32 / image.pixels_per_em.max(1) as f32;
665    let rgba = decode_raster_image(&image)?;
666    let upload_size = [
667        image.width as usize + ATLAS_GLYPH_PADDING_PX * 2,
668        image.height as usize + ATLAS_GLYPH_PADDING_PX * 2,
669    ];
670    let mut pixels = vec![Color32::TRANSPARENT; upload_size[0] * upload_size[1]];
671    for row in 0..image.height as usize {
672        let src_start = row * image.width as usize;
673        let dst_start = (row + ATLAS_GLYPH_PADDING_PX) * upload_size[0] + ATLAS_GLYPH_PADDING_PX;
674        pixels[dst_start..dst_start + image.width as usize]
675            .copy_from_slice(&rgba[src_start..src_start + image.width as usize]);
676    }
677
678    Some(RasterizedGlyph {
679        image: ColorImage::new(upload_size, pixels),
680        logical_size: egui::vec2(
681            upload_size[0] as f32 * scale / pixels_per_point,
682            upload_size[1] as f32 * scale / pixels_per_point,
683        ),
684        offset: egui::vec2(
685            image.x as f32 * scale / pixels_per_point
686                - ATLAS_GLYPH_PADDING_PX as f32 * scale / pixels_per_point,
687            -(image.y as f32) * scale / pixels_per_point
688                - ATLAS_GLYPH_PADDING_PX as f32 * scale / pixels_per_point,
689        ),
690        is_color: true,
691    })
692}
693
694fn decode_raster_image(image: &ttf_parser::RasterGlyphImage<'_>) -> Option<Vec<Color32>> {
695    match image.format {
696        ttf_parser::RasterImageFormat::PNG => {
697            let decoded = image::load_from_memory_with_format(image.data, ImageFormat::Png)
698                .ok()?
699                .to_rgba8();
700            Some(
701                decoded
702                    .pixels()
703                    .map(|pixel| {
704                        Color32::from_rgba_unmultiplied(pixel[0], pixel[1], pixel[2], pixel[3])
705                    })
706                    .collect(),
707            )
708        }
709        ttf_parser::RasterImageFormat::BitmapPremulBgra32 => Some(
710            image
711                .data
712                .chunks_exact(4)
713                .map(|pixel| {
714                    Color32::from_rgba_premultiplied(pixel[2], pixel[1], pixel[0], pixel[3])
715                })
716                .collect(),
717        ),
718        ttf_parser::RasterImageFormat::BitmapGray8 => Some(
719            image
720                .data
721                .iter()
722                .map(|alpha| Color32::from_white_alpha(*alpha))
723                .collect(),
724        ),
725        _ => None,
726    }
727}
728
729fn snap_to_pixel(value: f32, pixels_per_point: f32) -> f32 {
730    (value * pixels_per_point).round() / pixels_per_point
731}
732
733fn quantize(value: f32) -> u32 {
734    (value.max(0.0) * 64.0).round() as u32
735}
736
737#[derive(Default)]
738struct GlyphPathBuilder {
739    inner: tiny_skia::PathBuilder,
740}
741
742impl GlyphPathBuilder {
743    fn finish(self) -> Option<tiny_skia::Path> {
744        self.inner.finish()
745    }
746}
747
748impl ttf_parser::OutlineBuilder for GlyphPathBuilder {
749    fn move_to(&mut self, x: f32, y: f32) {
750        self.inner.move_to(x, y);
751    }
752
753    fn line_to(&mut self, x: f32, y: f32) {
754        self.inner.line_to(x, y);
755    }
756
757    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
758        self.inner.quad_to(x1, y1, x, y);
759    }
760
761    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
762        self.inner.cubic_to(x1, y1, x2, y2, x, y);
763    }
764
765    fn close(&mut self) {
766        self.inner.close();
767    }
768}