#![expect(clippy::mem_forget)]
#[cfg(feature = "color_fonts")]
use alloc::sync::Arc;
use ahash::HashMap;
use ecolor::Color32;
use emath::{GuiRounding as _, OrderedFloat, vec2};
use self_cell::self_cell;
use skrifa::{GlyphId, MetadataProvider as _};
use vello_cpu::{color, kurbo};
use crate::{
ColorImage, TextOptions,
text::{
FontTweak, GlyphBitmap, VariationCoords,
font_data::Blob,
glyph_atlas::SubpixelBin,
styled_metrics::{LocationHash, StyledMetrics},
unicode::invisible_char,
},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct GlyphInfo {
pub(crate) id: Option<GlyphId>,
pub advance_width_unscaled: OrderedFloat<f32>,
}
impl GlyphInfo {
pub const INVISIBLE: Self = Self {
id: None,
advance_width_unscaled: OrderedFloat(0.0),
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(super) enum GlyphIdResolution {
Glyph(GlyphId),
Invisible,
}
struct DependentFontData<'a> {
skrifa: skrifa::FontRef<'a>,
#[cfg(feature = "color_fonts")]
index: u32,
#[cfg(feature = "color_fonts")]
has_color_glyphs: bool,
charmap: skrifa::charmap::Charmap<'a>,
outline_glyphs: skrifa::outline::OutlineGlyphCollection<'a>,
metrics: skrifa::metrics::Metrics,
hinting_instance: Option<skrifa::outline::HintingInstance>,
}
self_cell! {
struct FontCell {
owner: Blob,
#[covariant]
dependent: DependentFontData,
}
}
impl FontCell {
fn px_scale_factor(&self, scale: f32) -> f32 {
let units_per_em = self.borrow_dependent().metrics.units_per_em as f32;
scale / units_per_em
}
fn rasterize_glyph(
&mut self,
metrics: &StyledMetrics,
glyph_id: GlyphId,
bin: SubpixelBin,
hinting_target: skrifa::outline::Target,
) -> Option<GlyphBitmap> {
let location: skrifa::instance::LocationRef<'_> = (&metrics.location).into();
core::cfg_select! {
feature = "color_fonts" => {
if let Some(bitmap) = self.rasterize_color_glyph(metrics, glyph_id, location) {
return Some(bitmap);
}
}
_ => {}
}
let mut path = kurbo::BezPath::new();
let mut pen = VelloPen {
path: &mut path,
x_offset: bin.as_float() as f64,
};
self.with_dependent_mut(|_, font_data| {
let outline = font_data.outline_glyphs.get(glyph_id)?;
if let Some(hinting_instance) = &mut font_data.hinting_instance {
let size = skrifa::instance::Size::new(metrics.scale);
if hinting_instance.size() != size
|| hinting_instance.location().coords() != location.coords()
|| hinting_instance.target() != hinting_target
{
hinting_instance
.reconfigure(&font_data.outline_glyphs, size, location, hinting_target)
.ok()?;
}
let draw_settings = skrifa::outline::DrawSettings::hinted(hinting_instance, false);
outline.draw(draw_settings, &mut pen).ok()?;
} else {
let draw_settings = skrifa::outline::DrawSettings::unhinted(
skrifa::instance::Size::new(metrics.scale),
location,
);
outline.draw(draw_settings, &mut pen).ok()?;
}
Some(())
})?;
let bounds = path.control_box().expand();
let width = bounds.width() as u16;
let height = bounds.height() as u16;
if width == 0 || height == 0 {
return None;
}
let mut ctx = vello_cpu::RenderContext::new(width, height);
ctx.set_transform(kurbo::Affine::translate((-bounds.x0, -bounds.y0)));
ctx.set_paint(color::OpaqueColor::<color::Srgb>::WHITE);
ctx.fill_path(&path);
let mut dest = vello_cpu::Pixmap::new(width, height);
let mut resources = vello_cpu::Resources::new();
ctx.render(&mut dest, &mut resources);
let pixels = dest
.data_as_u8_slice()
.chunks_exact(4)
.map(|px| Color32::from_rgba_premultiplied(px[0], px[1], px[2], px[3]))
.collect();
let image = ColorImage::new([width as usize, height as usize], pixels);
Some(GlyphBitmap {
image,
offset_px: vec2(bounds.x0 as f32, bounds.y0 as f32),
is_color: false,
})
}
#[cfg(feature = "color_fonts")]
fn has_color_glyph(&self, glyph_id: GlyphId, size: skrifa::instance::Size) -> bool {
let font_data = self.borrow_dependent();
font_data.has_color_glyphs
&& (font_data.skrifa.color_glyphs().get(glyph_id).is_some()
|| font_data
.skrifa
.bitmap_strikes()
.glyph_for_size(size, glyph_id)
.is_some())
}
#[cfg(feature = "color_fonts")]
fn rasterize_color_glyph(
&self,
metrics: &StyledMetrics,
glyph_id: GlyphId,
location: skrifa::instance::LocationRef<'_>,
) -> Option<GlyphBitmap> {
use vello_cpu::peniko;
let font_data = self.borrow_dependent();
let font_size_px = metrics.scale;
let size = skrifa::instance::Size::new(font_size_px);
if !self.has_color_glyph(glyph_id, size) || font_size_px == 0.0 || !font_size_px.is_finite()
{
return None;
}
let canvas_size = (3.0 * font_size_px)
.ceil()
.clamp(1.0, crate::text::MAX_GLYPH_SIZE as f32) as u16;
let origin_x = font_size_px as f64;
let origin_y = 2.0 * font_size_px as f64;
let font = peniko::FontData::new(
peniko::Blob::new(Arc::clone(self.borrow_owner())),
font_data.index,
);
let coords: Vec<i16> = location.coords().iter().map(|c| c.to_bits()).collect();
let mut ctx = vello_cpu::RenderContext::new(canvas_size, canvas_size);
let mut resources = vello_cpu::Resources::new();
ctx.set_paint(color::OpaqueColor::<color::Srgb>::WHITE);
ctx.glyph_run(&mut resources, &font)
.font_size(font_size_px)
.hint(false)
.normalized_coords(&coords)
.fill_glyphs(core::iter::once(vello_cpu::Glyph {
id: glyph_id.to_u32(),
x: origin_x as f32,
y: origin_y as f32,
}));
let mut pixmap = vello_cpu::Pixmap::new(canvas_size, canvas_size);
ctx.render(&mut pixmap, &mut resources);
let canvas_size = canvas_size as usize;
let pixels = pixmap.data_as_u8_slice();
let alpha_at = |x: usize, y: usize| pixels[4 * (y * canvas_size + x) + 3];
let mut min = [canvas_size, canvas_size];
let mut max = [0, 0];
for y in 0..canvas_size {
for x in 0..canvas_size {
if alpha_at(x, y) != 0 {
min = [min[0].min(x), min[1].min(y)];
max = [max[0].max(x + 1), max[1].max(y + 1)];
}
}
}
if max[0] <= min[0] || max[1] <= min[1] {
return None; }
let width = max[0] - min[0];
let height = max[1] - min[1];
let cropped: Vec<Color32> = (min[1]..max[1])
.flat_map(|y| (min[0]..max[0]).map(move |x| (x, y)))
.map(|(x, y)| {
let i = 4 * (y * canvas_size + x);
Color32::from_rgba_premultiplied(
pixels[i],
pixels[i + 1],
pixels[i + 2],
pixels[i + 3],
)
})
.collect();
Some(GlyphBitmap {
image: ColorImage::new([width, height], cropped),
offset_px: vec2(
min[0] as f32 - origin_x as f32,
min[1] as f32 - origin_y as f32,
),
is_color: true,
})
}
}
struct VelloPen<'a> {
path: &'a mut kurbo::BezPath,
x_offset: f64,
}
impl skrifa::outline::OutlinePen for VelloPen<'_> {
fn move_to(&mut self, x: f32, y: f32) {
self.path.move_to((x as f64 + self.x_offset, -y as f64));
}
fn line_to(&mut self, x: f32, y: f32) {
self.path.line_to((x as f64 + self.x_offset, -y as f64));
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.path.quad_to(
(cx0 as f64 + self.x_offset, -cy0 as f64),
(x as f64 + self.x_offset, -y as f64),
);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.path.curve_to(
(cx0 as f64 + self.x_offset, -cy0 as f64),
(cx1 as f64 + self.x_offset, -cy1 as f64),
(x as f64 + self.x_offset, -y as f64),
);
}
fn close(&mut self) {
self.path.close_path();
}
}
pub struct FontFace {
name: String,
font: FontCell,
tweak: FontTweak,
subpixel_binning: bool,
shaper_data: harfrust::ShaperData,
glyph_id_cache: HashMap<char, GlyphIdResolution>,
advance_width_cache: HashMap<(char, LocationHash), OrderedFloat<f32>>,
}
impl FontFace {
pub fn new(
options: TextOptions,
name: String,
font_data: Blob,
index: u32,
tweak: FontTweak,
) -> Result<Self, Box<dyn core::error::Error>> {
let font = FontCell::try_new(font_data, |font_data| {
let skrifa_font =
skrifa::FontRef::from_index(AsRef::<[u8]>::as_ref(font_data.as_ref()), index)?;
let charmap = skrifa_font.charmap();
let glyphs = skrifa_font.outline_glyphs();
let metrics = skrifa_font.metrics(
skrifa::instance::Size::unscaled(),
skrifa::instance::LocationRef::default(),
);
let hinting_enabled = tweak.hinting.unwrap_or(options.font_hinting);
let hinting_instance = hinting_enabled
.then(|| {
skrifa::outline::HintingInstance::new(
&glyphs,
skrifa::instance::Size::unscaled(),
skrifa::instance::LocationRef::default(),
skrifa::outline::Target::default(),
)
.ok()
})
.flatten();
#[cfg(feature = "color_fonts")]
let has_color_glyphs = {
use skrifa::raw::TableProvider as _;
skrifa_font.colr().is_ok()
|| skrifa_font.sbix().is_ok()
|| skrifa_font.cbdt().is_ok()
|| skrifa_font.ebdt().is_ok()
};
Ok::<DependentFontData<'_>, Box<dyn core::error::Error>>(DependentFontData {
skrifa: skrifa_font,
#[cfg(feature = "color_fonts")]
index,
#[cfg(feature = "color_fonts")]
has_color_glyphs,
charmap,
outline_glyphs: glyphs,
metrics,
hinting_instance,
})
})?;
let shaper_data = harfrust::ShaperData::new(&font.borrow_dependent().skrifa);
let subpixel_binning = tweak.subpixel_binning.unwrap_or(options.subpixel_binning);
Ok(Self {
name,
font,
tweak,
subpixel_binning,
shaper_data,
glyph_id_cache: Default::default(),
advance_width_cache: Default::default(),
})
}
pub(crate) fn set_options(&mut self, options: TextOptions) {
let Self {
font,
tweak,
subpixel_binning,
..
} = self;
*subpixel_binning = tweak.subpixel_binning.unwrap_or(options.subpixel_binning);
let hinting_enabled = tweak.hinting.unwrap_or(options.font_hinting);
font.with_dependent_mut(|_, font_data| {
font_data.hinting_instance = hinting_enabled
.then(|| {
skrifa::outline::HintingInstance::new(
&font_data.outline_glyphs,
skrifa::instance::Size::unscaled(),
skrifa::instance::LocationRef::default(),
skrifa::outline::Target::default(),
)
.ok()
})
.flatten();
});
}
fn ignore_character(&self, chr: char) -> bool {
use crate::text::FontDefinitions;
if !FontDefinitions::builtin_font_names().contains(&self.name.as_str()) {
return false;
}
matches!(
chr,
'\u{534d}' | '\u{5350}' |
'\u{E0FF}' | '\u{EFFD}' | '\u{F0FF}' | '\u{F200}'
)
}
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub(crate) fn px_scale_factor(&self, scale: f32) -> f32 {
self.font.px_scale_factor(scale)
}
pub fn characters(&self) -> impl Iterator<Item = char> + '_ {
self.font
.borrow_dependent()
.charmap
.mappings()
.filter_map(|(chr, _)| char::from_u32(chr).filter(|c| !self.ignore_character(*c)))
}
pub(super) fn glyph_id_resolution(&mut self, c: char) -> Option<GlyphIdResolution> {
if let Some(resolution) = self.glyph_id_cache.get(&c) {
return Some(*resolution);
}
if self.ignore_character(c) {
return None; }
let resolution = if c == '\t' || c == '\u{2009}' || c == '\u{202F}' {
self.glyph_id_resolution(' ')?
} else if invisible_char(c) {
GlyphIdResolution::Invisible
} else {
let glyph_id = self
.font
.borrow_dependent()
.charmap
.map(c)
.filter(|id| *id != GlyphId::NOTDEF)?;
GlyphIdResolution::Glyph(glyph_id)
};
self.glyph_id_cache.insert(c, resolution);
Some(resolution)
}
fn advance_width_unscaled(&mut self, c: char, metrics: &StyledMetrics) -> f32 {
let cache_key = (c, metrics.location_hash);
if let Some(advance) = self.advance_width_cache.get(&cache_key) {
return advance.0;
}
let advance = match c {
'\t' => self.tweak.tab_size * self.advance_width_unscaled(' ', metrics),
'\u{2009}' | '\u{202F}' => {
self.tweak.thin_space_width * self.advance_width_unscaled(' ', metrics)
}
_ => {
let Some(GlyphIdResolution::Glyph(glyph_id)) = self.glyph_id_resolution(c) else {
return 0.0;
};
let font_data = self.font.borrow_dependent();
let glyph_metrics = font_data
.skrifa
.glyph_metrics(skrifa::instance::Size::unscaled(), &metrics.location);
glyph_metrics.advance_width(glyph_id).unwrap_or_default()
}
};
self.advance_width_cache.insert(cache_key, advance.into());
advance
}
pub(super) fn glyph_info(&mut self, c: char, metrics: &StyledMetrics) -> Option<GlyphInfo> {
let resolution = self.glyph_id_resolution(c)?;
let glyph_info = match resolution {
GlyphIdResolution::Invisible => GlyphInfo::INVISIBLE,
GlyphIdResolution::Glyph(glyph_id) => GlyphInfo {
id: Some(glyph_id),
advance_width_unscaled: self.advance_width_unscaled(c, metrics).into(),
},
};
Some(glyph_info)
}
pub(super) fn notdef_glyph_info(&self, metrics: &StyledMetrics) -> GlyphInfo {
let font_data = self.font.borrow_dependent();
let glyph_metrics = font_data
.skrifa
.glyph_metrics(skrifa::instance::Size::unscaled(), &metrics.location);
GlyphInfo {
id: Some(GlyphId::NOTDEF),
advance_width_unscaled: glyph_metrics
.advance_width(GlyphId::NOTDEF)
.unwrap_or_default()
.into(),
}
}
#[inline(always)]
pub fn styled_metrics(
&self,
pixels_per_point: f32,
font_size: f32,
coords: &VariationCoords,
) -> StyledMetrics {
let pt_scale_factor = self.font.px_scale_factor(font_size * self.tweak.scale);
let font_data = self.font.borrow_dependent();
let ascent = (font_data.metrics.ascent * pt_scale_factor).round_ui();
let descent = (font_data.metrics.descent * pt_scale_factor).round_ui();
let line_gap = (font_data.metrics.leading * pt_scale_factor).round_ui();
let scale = font_size * self.tweak.scale * pixels_per_point;
let px_scale_factor = self.font.px_scale_factor(scale);
let y_offset_in_points = ((font_size * self.tweak.scale * self.tweak.y_offset_factor)
+ self.tweak.y_offset)
.round_ui();
let axes = font_data.skrifa.axes();
let settings = core::iter::chain(self.tweak.coords.as_ref(), coords.as_ref());
let location = axes.location(settings);
let location_hash = LocationHash::new(&location);
StyledMetrics {
pixels_per_point,
px_scale_factor,
scale,
y_offset_in_points,
ascent,
row_height: ascent - descent + line_gap,
location,
location_hash,
}
}
pub(crate) fn skrifa_font_ref(&self) -> &skrifa::FontRef<'_> {
&self.font.borrow_dependent().skrifa
}
pub(crate) fn tweak(&self) -> &FontTweak {
&self.tweak
}
pub(crate) fn shaper_data(&self) -> &harfrust::ShaperData {
&self.shaper_data
}
#[inline]
pub(crate) fn subpixel_binning(&self) -> bool {
self.subpixel_binning
}
pub(crate) fn is_color_glyph(&self, metrics: &StyledMetrics, glyph_id: GlyphId) -> bool {
core::cfg_select! {
feature = "color_fonts" => {
self.font
.has_color_glyph(glyph_id, skrifa::instance::Size::new(metrics.scale))
}
_ => {
let _ = (self, metrics, glyph_id);
false
}
}
}
pub(crate) fn rasterize_glyph(
&mut self,
metrics: &StyledMetrics,
glyph_id: GlyphId,
bin: SubpixelBin,
) -> Option<GlyphBitmap> {
let hinting_target = self.tweak.hinting_target.into();
self.font
.rasterize_glyph(metrics, glyph_id, bin, hinting_target)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ShapedGlyph {
pub glyph_id: GlyphId,
pub h_pos: f32,
pub is_cjk: bool,
}