#[derive(Debug, Clone, PartialEq)]
pub struct EmojiGlyph {
pub codepoint: u32,
pub atlas_x: u32,
pub atlas_y: u32,
pub atlas_w: u32,
pub atlas_h: u32,
pub bitmap: Vec<u8>,
}
impl EmojiGlyph {
pub fn new(
codepoint: u32,
atlas_x: u32,
atlas_y: u32,
w: u32,
h: u32,
bitmap: Vec<u8>,
) -> Self {
Self {
codepoint,
atlas_x,
atlas_y,
atlas_w: w,
atlas_h: h,
bitmap,
}
}
pub fn expected_bitmap_size(w: u32, h: u32) -> usize {
w as usize * h as usize * 4
}
pub fn bitmap_valid(&self) -> bool {
self.bitmap.len() == Self::expected_bitmap_size(self.atlas_w, self.atlas_h)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EmojiAtlas {
pub width: u32,
pub height: u32,
pub glyphs: Vec<EmojiGlyph>,
}
impl EmojiAtlas {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
glyphs: Vec::new(),
}
}
pub fn insert(&mut self, glyph: EmojiGlyph) {
self.glyphs.push(glyph);
}
pub fn get_glyph(&self, codepoint: u32) -> Option<&EmojiGlyph> {
self.glyphs.iter().find(|g| g.codepoint == codepoint)
}
pub fn len(&self) -> usize {
self.glyphs.len()
}
pub fn is_empty(&self) -> bool {
self.glyphs.is_empty()
}
pub fn pack(&mut self) -> Result<(), String> {
if self.glyphs.is_empty() {
return Ok(());
}
self.glyphs.sort_by_key(|b| std::cmp::Reverse(b.atlas_h));
let mut current_x: u32 = 0;
let mut current_y: u32 = 0;
let mut shelf_height: u32 = 0;
for glyph in self.glyphs.iter_mut() {
if glyph.atlas_w > self.width || glyph.atlas_h > self.height {
return Err(format!(
"Emoji U+{:04X} ({}x{}) exceeds atlas size {}x{}",
glyph.codepoint, glyph.atlas_w, glyph.atlas_h, self.width, self.height
));
}
if current_x + glyph.atlas_w > self.width {
current_y += shelf_height;
current_x = 0;
shelf_height = 0;
}
if current_y + glyph.atlas_h > self.height {
return Err(format!(
"Atlas {}x{} full, cannot pack emoji U+{:04X}",
self.width, self.height, glyph.codepoint
));
}
glyph.atlas_x = current_x;
glyph.atlas_y = current_y;
current_x += glyph.atlas_w;
shelf_height = shelf_height.max(glyph.atlas_h);
}
Ok(())
}
}
impl Default for EmojiAtlas {
fn default() -> Self {
Self::new(2048, 2048)
}
}
pub fn is_emoji(codepoint: u32) -> bool {
matches!(codepoint,
0x1F600..=0x1F64F | 0x1F300..=0x1F5FF | 0x1F680..=0x1F6FF | 0x1F1E0..=0x1F1FF | 0x2600..=0x26FF | 0x2700..=0x27BF | 0x1F900..=0x1F9FF | 0x1FA00..=0x1FA6F | 0x1FA70..=0x1FAFF | 0xFE0F | 0x200D )
}
pub fn is_emoji_base(codepoint: u32) -> bool {
matches!(codepoint,
0x1F600..=0x1F64F |
0x1F300..=0x1F5FF |
0x1F680..=0x1F6FF |
0x1F1E0..=0x1F1FF |
0x2600..=0x26FF |
0x2700..=0x27BF |
0x1F900..=0x1F9FF |
0x1FA00..=0x1FA6F |
0x1FA70..=0x1FAFF
)
}
pub fn render_emoji(codepoint: u32, size: u32) -> Option<Vec<u8>> {
if !is_emoji_base(codepoint) {
return None;
}
let total_pixels = (size * size) as usize;
let mut rgba = vec![0u8; total_pixels * 4];
let r = ((codepoint >> 16) & 0xFF) as u8;
let g = ((codepoint >> 8) & 0xFF) as u8;
let b = (codepoint & 0xFF) as u8;
let center = size as f32 / 2.0;
let radius = size as f32 * 0.4;
for y in 0..size {
for x in 0..size {
let dx = x as f32 + 0.5 - center;
let dy = y as f32 + 0.5 - center;
let dist_sq = dx * dx + dy * dy;
let idx = (y * size + x) as usize * 4;
if dist_sq <= radius * radius {
let dist = dist_sq.sqrt();
let alpha = if dist > radius - 2.0 {
((radius - dist) / 2.0 * 255.0) as u8
} else {
255
};
rgba[idx] = r;
rgba[idx + 1] = g;
rgba[idx + 2] = b;
rgba[idx + 3] = alpha;
}
}
}
Some(rgba)
}
pub fn find_emoji_runs(text: &str) -> Vec<(usize, usize, u32)> {
let mut runs = Vec::new();
let mut current_start: Option<usize> = None;
let mut current_codepoint: u32 = 0;
for (byte_idx, ch) in text.char_indices() {
let cp = ch as u32;
if is_emoji_base(cp) {
if current_start.is_none() {
current_start = Some(byte_idx);
current_codepoint = cp;
}
} else if is_emoji(cp) {
} else {
if let Some(start) = current_start {
runs.push((start, byte_idx, current_codepoint));
current_start = None;
}
}
}
if let Some(start) = current_start {
runs.push((start, text.len(), current_codepoint));
}
runs
}
pub fn pack_emoji_atlas(glyphs: &mut [EmojiGlyph], max_size: u32) -> Result<(u32, u32), String> {
if glyphs.is_empty() {
return Ok((0, 0));
}
glyphs.sort_by_key(|b| std::cmp::Reverse(b.atlas_h));
let mut current_x: u32 = 0;
let mut current_y: u32 = 0;
let mut shelf_height: u32 = 0;
let mut max_x: u32 = 0;
for glyph in glyphs.iter_mut() {
if glyph.atlas_w > max_size || glyph.atlas_h > max_size {
return Err(format!(
"Emoji U+{:04X} ({}x{}) exceeds max atlas size {}",
glyph.codepoint, glyph.atlas_w, glyph.atlas_h, max_size
));
}
if current_x + glyph.atlas_w > max_size {
current_y += shelf_height;
current_x = 0;
shelf_height = 0;
}
glyph.atlas_x = current_x;
glyph.atlas_y = current_y;
current_x += glyph.atlas_w;
shelf_height = shelf_height.max(glyph.atlas_h);
max_x = max_x.max(current_x);
}
let total_height = current_y + shelf_height;
Ok((max_x, total_height))
}
#[cfg(test)]
mod emoji_tests {
use super::*;
#[test]
fn test_is_emoji_emoticons() {
assert!(is_emoji(0x1F600)); assert!(is_emoji(0x1F64F)); assert!(!is_emoji(0x0041)); }
#[test]
fn test_is_emoji_misc_symbols() {
assert!(is_emoji(0x1F300)); assert!(is_emoji(0x1F5FF)); }
#[test]
fn test_is_emoji_transport() {
assert!(is_emoji(0x1F680)); assert!(is_emoji(0x1F6FF)); }
#[test]
fn test_is_emoji_flags() {
assert!(is_emoji(0x1F1FA)); assert!(is_emoji(0x1F1F8)); }
#[test]
fn test_is_emoji_dingbats() {
assert!(is_emoji(0x2764)); assert!(is_emoji(0x2728)); }
#[test]
fn test_is_emoji_variation_selector() {
assert!(is_emoji(0xFE0F));
}
#[test]
fn test_is_emoji_zwj() {
assert!(is_emoji(0x200D));
}
#[test]
fn test_is_emoji_base() {
assert!(is_emoji_base(0x1F600));
assert!(!is_emoji_base(0xFE0F));
assert!(!is_emoji_base(0x200D));
}
#[test]
fn test_render_emoji_smiley() {
let bitmap = render_emoji(0x1F600, 64);
assert!(bitmap.is_some());
let data = bitmap.unwrap();
assert_eq!(data.len(), 64 * 64 * 4);
}
#[test]
fn test_render_emoji_non_emoji() {
assert!(render_emoji(0x0041, 64).is_none()); }
#[test]
fn test_render_emoji_vs_is_none() {
assert!(render_emoji(0xFE0F, 64).is_none()); }
#[test]
fn test_emoji_glyph_valid() {
let bitmap = vec![0u8; 32 * 32 * 4];
let glyph = EmojiGlyph::new(0x1F600, 0, 0, 32, 32, bitmap);
assert!(glyph.bitmap_valid());
}
#[test]
fn test_emoji_glyph_invalid() {
let bitmap = vec![0u8; 100]; let glyph = EmojiGlyph::new(0x1F600, 0, 0, 32, 32, bitmap);
assert!(!glyph.bitmap_valid());
}
#[test]
fn test_emoji_atlas_insert() {
let mut atlas = EmojiAtlas::new(512, 512);
assert!(atlas.is_empty());
atlas.insert(EmojiGlyph::new(
0x1F600,
0,
0,
64,
64,
vec![0u8; 64 * 64 * 4],
));
assert_eq!(atlas.len(), 1);
assert!(atlas.get_glyph(0x1F600).is_some());
assert!(atlas.get_glyph(0x1F601).is_none());
}
#[test]
fn test_find_emoji_runs() {
let text = "hello 😀 world";
let runs = find_emoji_runs(text);
assert_eq!(runs.len(), 1);
let (start, end, cp) = runs[0];
assert_eq!(cp, 0x1F600);
assert!(start > 0);
assert_eq!(end - start, "😀".len());
}
#[test]
fn test_find_multiple_emoji_runs() {
let text = "😀 hello 😁 world 😂";
let runs = find_emoji_runs(text);
assert_eq!(runs.len(), 3);
assert_eq!(runs[0].2, 0x1F600);
assert_eq!(runs[1].2, 0x1F601);
assert_eq!(runs[2].2, 0x1F602);
}
#[test]
fn test_find_no_emoji() {
let text = "hello world";
let runs = find_emoji_runs(text);
assert!(runs.is_empty());
}
#[test]
fn test_pack_emoji_atlas_empty() {
let mut glyphs: Vec<EmojiGlyph> = vec![];
let result = pack_emoji_atlas(&mut glyphs, 1024);
assert_eq!(result, Ok((0, 0)));
}
#[test]
fn test_pack_emoji_atlas_single() {
let bitmap = vec![0u8; 64 * 64 * 4];
let mut glyphs = vec![EmojiGlyph::new(0x1F600, 0, 0, 64, 64, bitmap)];
let (w, h) = pack_emoji_atlas(&mut glyphs, 1024).unwrap();
assert_eq!(w, 64);
assert_eq!(h, 64);
assert_eq!(glyphs[0].atlas_x, 0);
assert_eq!(glyphs[0].atlas_y, 0);
}
#[test]
fn test_atlas_pack_method() {
let bitmap = vec![0u8; 64 * 64 * 4];
let mut atlas = EmojiAtlas::new(128, 128);
atlas.insert(EmojiGlyph::new(0x1F600, 0, 0, 64, 64, bitmap.clone()));
atlas.insert(EmojiGlyph::new(0x1F601, 0, 0, 64, 64, bitmap));
assert!(atlas.pack().is_ok());
assert_eq!(atlas.get_glyph(0x1F600).unwrap().atlas_x, 0);
assert_eq!(atlas.get_glyph(0x1F601).unwrap().atlas_x, 64);
}
#[test]
fn test_atlas_pack_overflow() {
let bitmap = vec![0u8; 64 * 64 * 4];
let mut atlas = EmojiAtlas::new(64, 64);
atlas.insert(EmojiGlyph::new(0x1F600, 0, 0, 64, 64, bitmap.clone()));
atlas.insert(EmojiGlyph::new(0x1F601, 0, 0, 64, 64, bitmap));
assert!(atlas.pack().is_err());
}
}