use std::collections::HashMap;
use std::rc::Rc;
use skia_safe::{surfaces, Color, Font, Image, Paint, PaintStyle, Point};
use unicode_normalization::UnicodeNormalization;
use crate::render::dimension::Pt;
use crate::render::emoji::cluster::EmojiCluster;
use crate::render::emoji::shape::shape_text;
use crate::render::fonts::{TypefaceEntry, TypefaceId};
use crate::render::geometry::PtSize;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SuperSample {
OnePerPt,
TwoPerPt,
ThreePerPt,
FourPerPt,
SixPerPt,
}
impl SuperSample {
pub const fn factor(self) -> f32 {
match self {
SuperSample::OnePerPt => 1.0,
SuperSample::TwoPerPt => 2.0,
SuperSample::ThreePerPt => 3.0,
SuperSample::FourPerPt => 4.0,
SuperSample::SixPerPt => 6.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RasterConfig {
pub super_sample: SuperSample,
}
impl Default for RasterConfig {
fn default() -> Self {
Self {
super_sample: SuperSample::FourPerPt,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct EmojiKey {
pub cluster: String,
pub typeface_id: TypefaceId,
pub size_bits: u32,
pub scale_bits: u32,
pub target_w_bits: u32,
pub target_h_bits: u32,
}
impl EmojiKey {
pub fn new(
text: &str,
typeface: &TypefaceEntry,
size: Pt,
scale: SuperSample,
target: PtSize,
) -> Self {
Self {
cluster: text.nfc().collect(),
typeface_id: TypefaceId::from(&typeface.typeface),
size_bits: f32::from(size).to_bits(),
scale_bits: scale.factor().to_bits(),
target_w_bits: target.width.raw().to_bits(),
target_h_bits: target.height.raw().to_bits(),
}
}
}
#[derive(Clone, Debug)]
pub struct EmojiImage {
pub image: Image,
pub pixels: (i32, i32),
pub draw_size: PtSize,
pub baseline_offset: Pt,
}
pub struct EmojiRasterizer {
config: RasterConfig,
cache: HashMap<EmojiKey, EmojiImage>,
font_bytes: HashMap<TypefaceId, Rc<Vec<u8>>>,
}
impl Default for EmojiRasterizer {
fn default() -> Self {
Self::new(RasterConfig::default())
}
}
impl EmojiRasterizer {
pub fn new(config: RasterConfig) -> Self {
Self {
config,
cache: HashMap::new(),
font_bytes: HashMap::new(),
}
}
pub fn config(&self) -> RasterConfig {
self.config
}
pub fn cached_count(&self) -> usize {
self.cache.len()
}
pub fn rasterize(
&mut self,
cluster: &EmojiCluster,
typeface: &TypefaceEntry,
size: Pt,
target: PtSize,
) -> &EmojiImage {
let scale = self.config.super_sample;
let key = EmojiKey::new(cluster.text, typeface, size, scale, target);
if !self.cache.contains_key(&key) {
let bytes = self.font_bytes_for(typeface);
let image = rasterize_uncached(
cluster.text,
typeface,
size,
scale,
target,
bytes.as_deref().map(|v| v.as_slice()),
);
self.cache.insert(key.clone(), image);
}
self.cache.get(&key).expect("just inserted")
}
fn font_bytes_for(&mut self, typeface: &TypefaceEntry) -> Option<Rc<Vec<u8>>> {
let id = TypefaceId::from(&typeface.typeface);
if let Some(bytes) = self.font_bytes.get(&id) {
return Some(bytes.clone());
}
let bytes = typeface.typeface.to_font_data().map(|(b, _)| Rc::new(b))?;
self.font_bytes.insert(id, bytes.clone());
Some(bytes)
}
}
fn rasterize_uncached(
text: &str,
typeface: &TypefaceEntry,
size: Pt,
scale: SuperSample,
target: PtSize,
font_bytes: Option<&[u8]>,
) -> EmojiImage {
let factor = scale.factor();
let scaled_size = f32::from(size) * factor;
let font = Font::from_typeface(typeface.typeface.clone(), scaled_size);
let width_px = (target.width.raw() * factor).ceil().max(1.0) as i32;
let height_px = (target.height.raw() * factor).ceil().max(1.0) as i32;
let original_font = Font::from_typeface(typeface.typeface.clone(), f32::from(size));
let (_, original_metrics) = original_font.metrics();
let baseline_y_px = -original_metrics.ascent * factor;
let shaped = font_bytes.and_then(|b| shape_text(b, text, scaled_size).ok());
let mut surface = surfaces::raster_n32_premul((width_px, height_px))
.expect("raster_n32_premul returned None for non-degenerate dimensions");
let canvas = surface.canvas();
let mut paint = Paint::default();
paint.set_anti_alias(true);
paint.set_style(PaintStyle::Fill);
paint.set_color(Color::BLACK);
match shaped {
Some(run) => {
let mut ids = Vec::with_capacity(run.glyphs.len());
let mut positions = Vec::with_capacity(run.glyphs.len());
let mut pen_x = 0.0f32;
for g in &run.glyphs {
ids.push(g.id);
positions.push(Point::new(
pen_x + g.x_offset.raw(),
baseline_y_px - g.y_offset.raw(),
));
pen_x += g.advance.raw();
}
canvas.draw_glyphs_at(&ids, &*positions, (0.0, 0.0), &font, &paint);
}
None => {
let (_, bounds) = font.measure_str(text, None);
canvas.translate((-bounds.left(), -bounds.top()));
canvas.draw_str(text, (0.0, 0.0), &font, &paint);
}
}
let image = surface.image_snapshot();
EmojiImage {
image,
pixels: (width_px, height_px),
draw_size: target,
baseline_offset: Pt::new(baseline_y_px / factor),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::emoji::cluster::{EmojiPresentation, EmojiStructure};
use crate::render::emoji::resolve::{resolve, EmojiTypeface, RegistryLookup};
use crate::render::fonts::{FontRegistry, TypefaceOrigin};
use skia_safe::{FontMgr, FontStyle};
fn any_typeface() -> TypefaceEntry {
let mgr = FontMgr::new();
let tf = mgr
.legacy_make_typeface(None::<&str>, FontStyle::normal())
.expect("system has no default typeface — cannot run test");
let id = TypefaceId::from(&tf);
TypefaceEntry {
typeface: tf,
origin: TypefaceOrigin::System { typeface_id: id },
}
}
fn single_emoji(text: &'static str) -> EmojiCluster<'static> {
EmojiCluster {
text,
presentation: EmojiPresentation::Emoji,
structure: EmojiStructure::Single,
}
}
fn default_target() -> PtSize {
PtSize::new(Pt::new(12.0), Pt::new(18.0))
}
#[test]
fn x1_same_input_dedupes_in_cache() {
let mut r = EmojiRasterizer::default();
let tf = any_typeface();
let c = single_emoji("\u{1F4DE}");
let _ = r.rasterize(&c, &tf, Pt::new(12.0), default_target());
let _ = r.rasterize(&c, &tf, Pt::new(12.0), default_target());
assert_eq!(r.cached_count(), 1, "identical key must reuse cache slot");
}
#[test]
fn x2_distinct_clusters_cache_independently() {
let mut r = EmojiRasterizer::default();
let tf = any_typeface();
let _ = r.rasterize(
&single_emoji("\u{1F4DE}"),
&tf,
Pt::new(12.0),
default_target(),
);
let _ = r.rasterize(
&single_emoji("\u{1F4E7}"),
&tf,
Pt::new(12.0),
default_target(),
);
assert_eq!(r.cached_count(), 2);
}
#[test]
fn x3_distinct_sizes_cache_independently() {
let mut r = EmojiRasterizer::default();
let tf = any_typeface();
let c = single_emoji("\u{1F4DE}");
let _ = r.rasterize(&c, &tf, Pt::new(12.0), default_target());
let _ = r.rasterize(&c, &tf, Pt::new(24.0), default_target());
assert_eq!(r.cached_count(), 2);
}
#[test]
fn x4_pixel_dimensions_non_degenerate() {
let mut r = EmojiRasterizer::default();
let tf = any_typeface();
let img = r
.rasterize(
&single_emoji("\u{1F4DE}"),
&tf,
Pt::new(12.0),
default_target(),
)
.clone();
assert!(
img.pixels.0 >= 1,
"width must be >= 1 px, got {}",
img.pixels.0
);
assert!(
img.pixels.1 >= 1,
"height must be >= 1 px, got {}",
img.pixels.1
);
assert!(img.draw_size.width.raw() > 0.0);
assert!(img.draw_size.height.raw() > 0.0);
}
#[test]
fn x4b_zero_width_input_yields_non_degenerate_surface() {
let mut r = EmojiRasterizer::default();
let tf = any_typeface();
let img = r
.rasterize(&single_emoji(""), &tf, Pt::new(12.0), default_target())
.clone();
assert!(img.pixels.0 >= 1);
assert!(img.pixels.1 >= 1);
}
#[test]
fn y_aspect_image_matches_target() {
let mut r = EmojiRasterizer::default();
let tf = any_typeface();
let target = PtSize::new(Pt::new(11.0), Pt::new(18.0));
let img = r
.rasterize(&single_emoji("A"), &tf, Pt::new(11.0), target)
.clone();
let img_aspect = img.pixels.0 as f32 / img.pixels.1 as f32;
let target_aspect = target.width.raw() / target.height.raw();
let rel_err = (img_aspect - target_aspect).abs() / target_aspect;
assert!(
rel_err < 0.05,
"image aspect {img_aspect:.4} must match target aspect {target_aspect:.4} \
within rounding (rel err {rel_err:.4})"
);
}
#[test]
fn x5_rasterized_image_has_visible_pixels() {
let registry = FontRegistry::new(FontMgr::new());
let lookup = RegistryLookup {
registry: ®istry,
};
let resolved = resolve(&lookup, None);
let entry = match resolved {
EmojiTypeface::Resolved { entry, .. } => entry,
EmojiTypeface::Unavailable { .. } => {
eprintln!("skipping X5: no color emoji typeface on this host");
return;
}
};
let mut r = EmojiRasterizer::default();
let img = r
.rasterize(
&single_emoji("\u{1F4DE}"),
&entry,
Pt::new(24.0),
PtSize::new(Pt::new(24.0), Pt::new(36.0)),
)
.clone();
let peek = img.image.peek_pixels();
let pixels = peek.expect("raster image must expose pixel data");
let bytes = pixels.bytes().expect("RGBA pixel data must be readable");
assert!(
bytes.iter().any(|&b| b != 0),
"rendered emoji must contain at least one non-zero pixel"
);
}
#[test]
fn x6_canonically_equivalent_inputs_share_cache() {
let mut r = EmojiRasterizer::default();
let tf = any_typeface();
let precomposed = EmojiCluster {
text: "\u{00E9}",
presentation: EmojiPresentation::Emoji,
structure: EmojiStructure::Single,
};
let decomposed = EmojiCluster {
text: "e\u{0301}",
presentation: EmojiPresentation::Emoji,
structure: EmojiStructure::Single,
};
let _ = r.rasterize(&precomposed, &tf, Pt::new(12.0), default_target());
let _ = r.rasterize(&decomposed, &tf, Pt::new(12.0), default_target());
assert_eq!(
r.cached_count(),
1,
"NFC-equivalent inputs must share a cache slot"
);
}
#[test]
fn super_sample_factors_monotonic() {
assert!(SuperSample::OnePerPt.factor() < SuperSample::TwoPerPt.factor());
assert!(SuperSample::TwoPerPt.factor() < SuperSample::ThreePerPt.factor());
}
#[test]
fn pixel_dimensions_scale_with_super_sample_for_outline_glyphs() {
let tf = any_typeface();
let c = single_emoji("A");
let size = Pt::new(12.0);
let mut r1 = EmojiRasterizer::new(RasterConfig {
super_sample: SuperSample::OnePerPt,
});
let img1 = r1.rasterize(&c, &tf, size, default_target()).clone();
let mut r3 = EmojiRasterizer::new(RasterConfig {
super_sample: SuperSample::ThreePerPt,
});
let img3 = r3.rasterize(&c, &tf, size, default_target()).clone();
assert!(
img3.pixels.0 >= img1.pixels.0 * 2,
"3× super-sample width must be at least 2× the 1× width"
);
assert!(
img3.pixels.1 >= img1.pixels.1 * 2,
"3× super-sample height must be at least 2× the 1× height"
);
let dw1 = img1.draw_size.width.raw();
let dw3 = img3.draw_size.width.raw();
assert!(
(dw1 - dw3).abs() <= 2.0,
"outline glyph draw widths must match within rounding, got {dw1} vs {dw3}"
);
}
}