notedeck icon indicating copy to clipboard operation
notedeck copied to clipboard

egui support color emoji ttf fonts

Open kernelkind opened this issue 11 months ago • 2 comments

The following is a proposal for getting color emoji font support directly in egui. It's purpose would be to complete

  • #91

what we have now

What currently happens is in epaint, glyphs are rasterized here. This sets an opacity value for each glyph pixel. Then eventually the wgpu renderer takes the FontImage and converts it to sRGBA pixels here.

The COLR and CPAL tables are used in OpenType fonts to enable coloring of emojis. ttf-parser has a method for checking whether a glyph has coloring data called Face::is_color_glyph(GlyphId). It also has a method called Face::paint_color_glyph (here)

pub fn paint_color_glyph(
	&self,
	glyph_id: GlyphId,
	palette: u16,
	foreground_color: RgbaColor,
	painter: &mut dyn colr::Painter<'a>,
) -> Option<()> {
...
}

This tells the colr::Painter trait how to paint the current glyph.

I propose the following:

We need to make an implementation of Painter called ColorRasterizer. Calling paint_color_glyph using ColorRasterizer will store each method as a command, similar to VecPainter and will also compute the glyph's bounding box. It will support an additional function draw(FnMut(u32, u32, u8, u8, u8, u8)) which will play back the commands to rasterize the glyph within it's bounding box, using the closure to write the RGBa value to the pixel coordinate.

In egui add the following here:

let uv_rect = if self.ab_glyph_font.is_color_glyph(glyph_id) {
	let rasterizer = ColorRasterizer::new();
	let fg_color = RgbaColor::new(0, 0, 0, 255);
	self.ab_glyph_font.paint_color_glyph(glyph_id, 0, fg_color, &mut rasterizer);

	rasterizer.draw(|x, y, r, g, b, a| {
	    let px = glyph_pos.0 + x as usize;
	    let py = glyph_pos.1 + y as usize;
            image[(px, py)] = a;
	    image.with_colors()[(px, py)] = [r, g, b];
	});
	
	Some(rasterizer.get_uv_rect())
} else if self.ab_glyph_font.outline_glyph(glyph).map(|glyph| {
	...
}

Then in FontImage we can add the following:

pub struct FontImage {
    /// width, height
    pub size: [usize; 2],

    /// The coverage value.
    ///
    /// Often you want to use [`Self::srgba_pixels`] instead.
    pub pixels: Vec<f32>,
+ 
+   pub colors: Option<Vec<[u8; 3]>>,
}

This adds an optional colors Vec which holds the rgb values for each pixel in the FontImage. We exclude alpha because that is already captured in pixels.

Then FontImage::srgba_pixels would be modified to check if colors.is_some(), and if so, use them for Color32::from_rgba_premultiplied. Otherwise, use the previous implementation.

kernelkind avatar Dec 20 '24 16:12 kernelkind

curious what @emilk thinks of this plan

jb55 avatar Dec 20 '24 17:12 jb55

image

This is what an embedded cashu emoji looks like on notedeck

note1uatzcylh9ujcfxju7w8cz0hdaa8z9dmapwf6jsm2tn3r3q03t42qgec356

alltheseas avatar May 14 '25 01:05 alltheseas

Android fonts per kagi: Looking at the available information, here's how to render Android fonts in Rust egui:

Step-by-Step Approach

  1. Load font data from Android system
  2. Configure egui FontDefinitions
  3. Set fonts on egui Context

Implementation

use egui::{Context, FontDefinitions, FontData, FontFamily};
use std::collections::BTreeMap;
fn setup_android_fonts(ctx: &Context) {
    // Read Android system font (example: Roboto)
    let font_data = std::fs::read("/system/fonts/Roboto-Regular.ttf")
        .or_else(|_| std::fs::read("/system/fonts/DroidSans.ttf"))
        .expect("Failed to load Android system font");
    let mut fonts = FontDefinitions::default();
    
    // Insert the font data
    fonts.font_data.insert(
        "android_font".to_owned(),
        FontData::from_owned(font_data),
    );
    
    // Set as proportional font with fallback
    fonts.families.get_mut(&FontFamily::Proportional)
        .unwrap()
        .insert(0, "android_font".to_owned());
    
    // Apply fonts to context
    ctx.set_fonts(fonts);
}

Alternative Approach for Embedded Fonts

fn setup_embedded_android_font(ctx: &Context) {
    let mut fonts = FontDefinitions::default();
    
    // Embed font at compile time
    let font_data = include_bytes!("../assets/Roboto-Regular.ttf");
    
    fonts.font_data.insert(
        "roboto".to_owned(),
        FontData::from_static(font_data),
    );
    
    // Configure font family
    fonts.families.insert(
        FontFamily::Name("roboto".into()),
        vec!["roboto".to_owned()],
    );
    
    ctx.set_fonts(fonts);
}

Key Points

  • Use std::fs::read() to load fonts from Android's /system/fonts/ directory at runtime [^1][^2]
  • Common Android fonts: Roboto-Regular.ttf, DroidSans.ttf, NotoSansCJK.ttc
  • For TTC files (font collections), you may need additional handling [^3]
  • include_bytes! embeds fonts at compile time but increases binary size [^2]
  • Always provide fallback fonts for missing glyphs [^4]

The runtime loading approach is preferred for Android as it uses system fonts and keeps binary size smaller.

[^1]: How do we set custom fonts in Egui? : r/rust - Reddit

[^2]: How to use system installed fonts just like Windows? · emilk egui ...

[^3]: Allow specifying font index when loading a font file #853

[^4]: How I Setting Custom Fonts

alltheseas avatar Jul 18 '25 16:07 alltheseas

that looks reasonable!

jb55 avatar Jul 21 '25 00:07 jb55