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}