use skia_safe::shaper::run_handler::{Buffer, RunInfo};
use skia_safe::shaper::{RunHandler, Shaper};
use skia_safe::shapers;
use skia_safe::{Font, GlyphId, Point, Typeface};
use thiserror::Error;
use crate::render::dimension::Pt;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ShapedGlyph {
pub id: GlyphId,
pub x: Pt,
pub y: Pt,
}
#[derive(Clone, Debug)]
pub struct ShapedRun {
pub glyphs: Vec<ShapedGlyph>,
pub total_advance: Pt,
}
#[derive(Debug, Error)]
pub enum ShapeError {
#[error("skia was built without a HarfBuzz shaper")]
ShaperUnavailable,
#[error("shaping produced no glyphs")]
NoGlyphs,
}
pub struct ClusterShaper {
shaper: Shaper,
}
impl ClusterShaper {
pub fn new() -> Result<Self, ShapeError> {
shapers::hb::shape_dont_wrap_or_reorder(None)
.map(|shaper| Self { shaper })
.ok_or(ShapeError::ShaperUnavailable)
}
pub fn shape(
&self,
typeface: &Typeface,
text: &str,
size_px: f32,
) -> Result<ShapedRun, ShapeError> {
let font = Font::from_typeface(typeface.clone(), size_px);
let mut collector = Collector::default();
self.shaper
.shape(text, &font, true, f32::MAX, &mut collector);
if collector.glyphs.is_empty() {
return Err(ShapeError::NoGlyphs);
}
let glyphs = collector
.glyphs
.iter()
.zip(collector.positions.iter())
.map(|(&id, p)| ShapedGlyph {
id,
x: Pt::new(p.x),
y: Pt::new(p.y),
})
.collect();
Ok(ShapedRun {
glyphs,
total_advance: Pt::new(collector.advance_x),
})
}
}
#[derive(Default)]
struct Collector {
glyphs: Vec<GlyphId>,
positions: Vec<Point>,
advance_x: f32,
}
impl RunHandler for Collector {
fn begin_line(&mut self) {}
fn run_info(&mut self, _info: &RunInfo) {}
fn commit_run_info(&mut self) {}
fn run_buffer<'a>(&'a mut self, info: &RunInfo) -> Buffer<'a> {
let base = self.glyphs.len();
self.glyphs.resize(base + info.glyph_count, 0);
self.positions
.resize(base + info.glyph_count, Point::new(0.0, 0.0));
self.advance_x += info.advance.x;
Buffer::new(&mut self.glyphs[base..], &mut self.positions[base..], None)
}
fn commit_run_buffer(&mut self, _info: &RunInfo) {}
fn commit_line(&mut self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::emoji::resolve::EmojiFamily;
use skia_safe::{FontMgr, FontStyle};
fn emoji_typeface() -> Option<Typeface> {
let mgr = FontMgr::new();
EmojiFamily::host_default().iter().find_map(|f| {
mgr.match_family_style(f.family_name(), FontStyle::normal())
.filter(|tf| tf.family_name().eq_ignore_ascii_case(f.family_name()))
})
}
fn any_typeface() -> Option<Typeface> {
FontMgr::new().legacy_make_typeface(None::<&str>, FontStyle::normal())
}
#[test]
fn shaper_constructs() {
assert!(
ClusterShaper::new().is_ok(),
"skia must expose a HarfBuzz shaper β the `textlayout` feature is \
what lets this module shape without serializing the font"
);
}
#[test]
fn ascii_shapes_one_glyph_per_char_advancing_rightwards() {
let Some(tf) = any_typeface() else { return };
let shaper = ClusterShaper::new().expect("shaper");
let run = shaper.shape(&tf, "abc", 20.0).expect("shape");
assert_eq!(run.glyphs.len(), 3, "ASCII must not ligate");
assert_eq!(run.glyphs[0].x, Pt::ZERO, "run origin is the first glyph");
assert!(
run.glyphs[1].x > run.glyphs[0].x && run.glyphs[2].x > run.glyphs[1].x,
"positions are absolute and strictly increasing, not per-glyph advances"
);
assert!(run.total_advance > Pt::ZERO);
}
#[test]
fn zwj_sequence_ligates_to_one_glyph() {
let Some(tf) = emoji_typeface() else { return };
let shaper = ClusterShaper::new().expect("shaper");
let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
assert_eq!(family.chars().count(), 5);
let run = shaper.shape(&tf, family, 44.0).expect("shape");
assert_eq!(
run.glyphs.len(),
1,
"GSUB must ligate the sequence; got {} glyphs",
run.glyphs.len()
);
assert!(run.total_advance > Pt::ZERO);
}
#[test]
fn modifier_and_keycap_sequences_ligate() {
let Some(tf) = emoji_typeface() else { return };
let shaper = ClusterShaper::new().expect("shaper");
for (label, text) in [
("skin-tone modifier", "\u{1F44D}\u{1F3FF}"),
("keycap", "1\u{FE0F}\u{20E3}"),
] {
let run = shaper.shape(&tf, text, 44.0).expect("shape");
assert_eq!(run.glyphs.len(), 1, "{label} must ligate to one glyph");
}
}
#[test]
fn empty_text_reports_no_glyphs() {
let Some(tf) = any_typeface() else { return };
let shaper = ClusterShaper::new().expect("shaper");
assert!(matches!(
shaper.shape(&tf, "", 20.0),
Err(ShapeError::NoGlyphs)
));
}
#[test]
fn glyph_ids_are_valid_for_the_same_typeface() {
let Some(tf) = any_typeface() else { return };
let shaper = ClusterShaper::new().expect("shaper");
let run = shaper.shape(&tf, "abc", 24.0).expect("shape");
let font = Font::from_typeface(tf, 24.0);
let ids: Vec<GlyphId> = run.glyphs.iter().map(|g| g.id).collect();
let mut widths = vec![0.0f32; ids.len()];
font.get_widths(&ids, &mut widths);
assert!(
widths.iter().all(|w| *w > 0.0),
"every shaped glyph id must have a width in the same font: {widths:?}"
);
}
#[test]
fn shaping_does_not_materialize_the_font() {
let Some(tf) = emoji_typeface() else { return };
let shaper = ClusterShaper::new().expect("shaper");
let Some(before) = resident_bytes() else {
return; };
for _ in 0..64 {
let _ = shaper.shape(&tf, "\u{1F44D}", 176.0);
}
let Some(after) = resident_bytes() else {
return;
};
let growth = after.saturating_sub(before);
assert!(
growth < 64 * 1024 * 1024,
"shaping grew RSS by {} MB β the font was probably serialized",
growth / 1024 / 1024
);
}
fn resident_bytes() -> Option<usize> {
std::process::Command::new("ps")
.args(["-o", "rss=", "-p", &std::process::id().to_string()])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse::<usize>().ok())
.map(|kb| kb * 1024)
}
}