pub mod constants;
pub mod fonts;
pub use rio_graphics::glyph::{glyf_decode, glyph_registry};
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))]
pub mod linux;
#[cfg(not(target_arch = "wasm32"))]
pub mod loader;
#[cfg(target_os = "macos")]
pub mod macos;
pub mod metrics;
pub use rio_fonts::nerd_font as nerd_font_attributes;
pub mod text_run_cache;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(test)]
mod cjk_metrics_tests;
pub const FONT_ID_REGULAR: usize = 0;
pub const FONT_ID_ITALIC: usize = 1;
pub const FONT_ID_BOLD: usize = 2;
pub const FONT_ID_BOLD_ITALIC: usize = 3;
use crate::font::constants::*;
use crate::font::fonts::{parse_unicode, FontStyle};
use crate::font::metrics::{FaceMetrics, Metrics};
use crate::layout::SpanStyle;
use crate::SugarloafErrors;
use dashmap::DashMap;
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use swash::text::cluster::Parser;
use swash::text::cluster::Token;
use swash::text::cluster::{CharCluster, Status};
use swash::text::Codepoint;
use swash::text::Script;
use swash::{tag_from_bytes, CacheKey, FontRef, Synthesis};
pub use swash::{Style, Weight};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Slot {
Regular,
Bold,
Italic,
BoldItalic,
}
impl Slot {
#[inline]
pub fn is_bold(self) -> bool {
matches!(self, Slot::Bold | Slot::BoldItalic)
}
#[inline]
pub fn is_italic(self) -> bool {
matches!(self, Slot::Italic | Slot::BoldItalic)
}
}
type FontDataCache = Arc<DashMap<PathBuf, SharedData>>;
static FONT_DATA_CACHE: OnceLock<FontDataCache> = OnceLock::new();
fn get_font_data_cache() -> &'static FontDataCache {
FONT_DATA_CACHE.get_or_init(|| Arc::new(DashMap::default()))
}
pub fn clear_font_data_cache() {
if let Some(cache) = FONT_DATA_CACHE.get() {
cache.clear();
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LookupAttrs {
pub italic: bool,
pub bold: bool,
}
fn cluster_covered(
cluster: &mut CharCluster,
library: &FontLibraryData,
font_id: usize,
font: &FontData,
) -> bool {
#[cfg(target_os = "macos")]
{
let _ = (library, font_id);
let handle_opt = if let Some(path) = &font.path {
crate::font::macos::FontHandle::from_path(path)
} else if let Some(bytes) = &font.data {
crate::font::macos::FontHandle::from_bytes(bytes.as_ref())
} else {
None
};
if let Some(handle) = handle_opt {
let status = cluster.map(|ch| {
if crate::font::macos::font_has_char(&handle, ch) {
1
} else {
0
}
});
status != Status::Discard
} else {
false
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = font;
if let Some((shared_data, offset, key)) = library.get_data(&font_id) {
let font_ref = FontRef {
data: shared_data.as_ref(),
offset,
key,
};
let charmap = font_ref.charmap();
let status = cluster.map(|ch| charmap.map(ch));
status != Status::Discard
} else {
false
}
}
}
pub fn lookup_for_font_match(
cluster: &mut CharCluster,
synth: &mut Synthesis,
library: &FontLibraryData,
spec: Option<LookupAttrs>,
) -> Option<(usize, bool)> {
if let Some(spec) = spec {
let slot = match (spec.bold, spec.italic) {
(false, true) => FONT_ID_ITALIC,
(true, false) => FONT_ID_BOLD,
(true, true) => FONT_ID_BOLD_ITALIC,
(false, false) => FONT_ID_REGULAR,
};
let slot = library.resolve_id(slot);
if let Some(FontEntry::Owned(font)) = library.inner.get(&slot) {
if cluster_covered(cluster, library, slot, font) {
*synth = font.synth;
return Some((slot, font.is_emoji));
}
}
}
let mut search_result = None;
let fonts_len: usize = library.inner.len();
for font_id in 0..fonts_len {
let font = match library.inner.get(&font_id) {
Some(FontEntry::Owned(d)) => d,
Some(FontEntry::Alias(_)) | None => continue,
};
let is_emoji = font.is_emoji;
let font_synth = font.synth;
if let Some(spec) = spec {
if spec.italic && !font.is_italic() && !font.should_italicize {
continue;
}
if spec.bold && !font.is_bold() && !font.should_embolden {
continue;
}
}
if cluster_covered(cluster, library, font_id, font) {
*synth = font_synth;
search_result = Some((font_id, is_emoji));
break;
}
}
if search_result.is_none() && spec.is_some() {
return lookup_for_font_match(cluster, synth, library, None);
}
search_result
}
#[derive(Clone)]
pub struct FontLibrary {
pub inner: Arc<RwLock<FontLibraryData>>,
}
impl FontLibrary {
pub fn new(spec: SugarloafFonts) -> (Self, Option<SugarloafErrors>) {
let mut font_library = FontLibraryData::default();
let mut sugarloaf_errors = None;
let fonts_not_found = font_library.load(spec);
if !fonts_not_found.is_empty() {
sugarloaf_errors = Some(SugarloafErrors { fonts_not_found });
}
(
Self {
inner: Arc::new(RwLock::new(font_library)),
},
sugarloaf_errors,
)
}
#[cfg(target_os = "macos")]
pub fn ct_font(&self, font_id: usize) -> Option<crate::font::macos::FontHandle> {
self.inner
.read()
.try_get(&font_id)
.and_then(|f| f.handle().cloned())
}
pub fn font_id_for_postscript_name(&self, name: &str) -> Option<usize> {
self.inner.read().font_id_for_postscript_name(name)
}
pub fn resolve_font_for_char(
&self,
ch: char,
fragment_style: &SpanStyle,
route_id: Option<usize>,
) -> (usize, bool) {
if let Some(found) =
self.inner
.read()
.find_best_font_match_strict(ch, fragment_style, route_id)
{
return found;
}
self.cascade_discover(ch, fragment_style)
.unwrap_or((0, false))
}
#[cfg(target_os = "macos")]
fn cascade_discover(
&self,
ch: char,
_fragment_style: &SpanStyle,
) -> Option<(usize, bool)> {
let primary = self.ct_font(FONT_ID_REGULAR)?;
let discovered = crate::font::macos::discover_fallback(&primary, ch)?;
let ps_name = discovered.postscript_name();
if let Some(found) = self.dedupe_existing(&ps_name) {
return Some(found);
}
let mut lib = self.inner.write();
if let Some(existing) = lib.font_id_for_postscript_name(&ps_name) {
let is_emoji = lib
.try_get(&existing)
.map(|fd| fd.is_emoji)
.unwrap_or(false);
return Some((existing, is_emoji));
}
let font_data = FontData::from_ctfont_macos(discovered);
let is_emoji = font_data.is_emoji;
let new_id = lib.inner.len();
lib.insert(font_data);
tracing::debug!(
"CoreText cascade discovered {} for U+{:04X}, registered as font_id {}",
ps_name,
ch as u32,
new_id
);
Some((new_id, is_emoji))
}
#[cfg(any(
all(unix, not(target_os = "macos"), not(target_os = "android")),
target_os = "windows"
))]
fn cascade_discover(
&self,
ch: char,
fragment_style: &SpanStyle,
) -> Option<(usize, bool)> {
let primary_family = self.primary_family_name()?;
let want_bold = fragment_style.font_attrs.weight() == swash::Weight::BOLD;
let want_italic = fragment_style.font_attrs.style() == swash::Style::Italic;
let want_mono = true;
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))]
let discovered = crate::font::linux::discover_fallback(
&primary_family,
ch,
want_mono,
want_bold,
want_italic,
)?;
#[cfg(target_os = "windows")]
let discovered = crate::font::windows::discover_fallback(
&primary_family,
ch,
want_mono,
want_bold,
want_italic,
)?;
let (path, face_index) = discovered;
let font_data = FontData::from_discovered_path(path, face_index).ok()?;
let ps_name = font_data.postscript_name()?.to_string();
if let Some(found) = self.dedupe_existing(&ps_name) {
return Some(found);
}
let mut lib = self.inner.write();
if let Some(existing) = lib.font_id_for_postscript_name(&ps_name) {
let is_emoji = lib
.try_get(&existing)
.map(|fd| fd.is_emoji)
.unwrap_or(false);
return Some((existing, is_emoji));
}
let is_emoji = font_data.is_emoji;
let new_id = lib.inner.len();
lib.insert(font_data);
tracing::debug!(
"system cascade discovered {} for U+{:04X}, registered as font_id {}",
ps_name,
ch as u32,
new_id
);
Some((new_id, is_emoji))
}
fn dedupe_existing(&self, ps_name: &str) -> Option<(usize, bool)> {
let lib = self.inner.read();
let existing = lib.font_id_for_postscript_name(ps_name)?;
let is_emoji = lib
.try_get(&existing)
.map(|fd| fd.is_emoji)
.unwrap_or(false);
Some((existing, is_emoji))
}
#[cfg(any(
all(unix, not(target_os = "macos"), not(target_os = "android")),
target_os = "windows"
))]
fn primary_family_name(&self) -> Option<String> {
let lib = self.inner.read();
let primary = lib.try_get(&FONT_ID_REGULAR)?;
primary
.postscript_name()
.map(|s| s.to_string())
.or_else(|| Some(String::from("monospace")))
}
#[cfg(target_os = "macos")]
pub fn family_names(&self) -> Vec<String> {
crate::font::macos::all_families()
}
#[cfg(all(not(target_os = "macos"), not(target_arch = "wasm32")))]
pub fn family_names(&self) -> Vec<String> {
let source = font_kit::source::SystemSource::new();
let mut families = source.all_families().unwrap_or_default();
families.sort_unstable();
families.dedup();
families
}
#[cfg(target_arch = "wasm32")]
pub fn family_names(&self) -> Vec<String> {
Vec::new()
}
pub fn install_glyph_registry(
&self,
route_id: usize,
registry: glyph_registry::GlyphRegistry,
) {
self.inner
.write()
.glyph_registries
.insert(route_id, registry);
}
pub fn remove_glyph_registry(&self, route_id: usize) {
self.inner.write().glyph_registries.remove(&route_id);
}
#[inline]
pub fn glyph_registry_for(
&self,
route_id: usize,
) -> Option<glyph_registry::GlyphRegistry> {
self.inner.read().glyph_registries.get(&route_id).cloned()
}
pub fn covers_codepoint(&self, cp: u32) -> bool {
let Some(ch) = char::from_u32(cp) else {
return false;
};
self.inner
.read()
.find_best_font_match_strict(ch, &SpanStyle::default(), None)
.is_some_and(|(font_id, _)| font_id != glyph_registry::CUSTOM_GLYPH_FONT_ID)
}
}
impl Default for FontLibrary {
fn default() -> Self {
let mut font_library = FontLibraryData::default();
let _fonts_not_found = font_library.load(SugarloafFonts::default());
Self {
inner: Arc::new(RwLock::new(font_library)),
}
}
}
pub struct SymbolMap {
pub font_index: usize,
pub range: Range<char>,
}
#[derive(Clone)]
pub enum FontEntry {
Owned(FontData),
Alias(usize),
}
impl FontEntry {
#[inline]
pub fn as_owned(&self) -> Option<&FontData> {
match self {
FontEntry::Owned(d) => Some(d),
FontEntry::Alias(_) => None,
}
}
}
pub struct FontLibraryData {
pub inner: FxHashMap<usize, FontEntry>,
pub symbol_maps: Option<Vec<SymbolMap>>,
pub hinting: bool,
pub features: Arc<Vec<swash::Setting<u16>>>,
primary_metrics_cache: FxHashMap<u32, Metrics>,
postscript_to_id: FxHashMap<String, usize>,
glyph_registries: FxHashMap<usize, glyph_registry::GlyphRegistry>,
}
impl Default for FontLibraryData {
fn default() -> Self {
Self {
inner: FxHashMap::default(),
hinting: true,
features: Arc::new(Vec::new()),
symbol_maps: None,
primary_metrics_cache: FxHashMap::default(),
postscript_to_id: FxHashMap::default(),
glyph_registries: FxHashMap::default(),
}
}
}
impl FontLibraryData {
#[inline]
pub fn find_best_font_match(
&self,
ch: char,
fragment_style: &SpanStyle,
route_id: Option<usize>,
) -> Option<(usize, bool)> {
if let Some(route_id) = route_id {
if let Some(registry) = self.glyph_registries.get(&route_id) {
if registry.contains(ch as u32) {
return Some((glyph_registry::CUSTOM_GLYPH_FONT_ID, false));
}
}
}
let mut synth = Synthesis::default();
let mut char_cluster = CharCluster::new();
let mut parser = Parser::new(
Script::Latin,
std::iter::once(Token {
ch,
offset: 0,
len: ch.len_utf8() as u8,
info: ch.properties().into(),
data: 0,
}),
);
if !parser.next(&mut char_cluster) {
return Some((0, false));
}
if let Some(symbol_maps) = &self.symbol_maps {
for symbol_map in symbol_maps {
if symbol_map.range.contains(&ch) {
return Some((symbol_map.font_index, false));
}
}
}
let italic = fragment_style.font_attrs.style() == Style::Italic;
let bold = fragment_style.font_attrs.weight() == Weight::BOLD;
let spec = (italic || bold).then_some(LookupAttrs { italic, bold });
if let Some(result) =
lookup_for_font_match(&mut char_cluster, &mut synth, self, spec)
{
return Some(result);
}
Some((0, false))
}
#[inline]
pub fn find_best_font_match_strict(
&self,
ch: char,
fragment_style: &SpanStyle,
route_id: Option<usize>,
) -> Option<(usize, bool)> {
if let Some(route_id) = route_id {
if let Some(registry) = self.glyph_registries.get(&route_id) {
if registry.contains(ch as u32) {
return Some((glyph_registry::CUSTOM_GLYPH_FONT_ID, false));
}
}
}
let mut synth = Synthesis::default();
let mut char_cluster = CharCluster::new();
let mut parser = Parser::new(
Script::Latin,
std::iter::once(Token {
ch,
offset: 0,
len: ch.len_utf8() as u8,
info: ch.properties().into(),
data: 0,
}),
);
if !parser.next(&mut char_cluster) {
return None;
}
if let Some(symbol_maps) = &self.symbol_maps {
for symbol_map in symbol_maps {
if symbol_map.range.contains(&ch) {
return Some((symbol_map.font_index, false));
}
}
}
let italic = fragment_style.font_attrs.style() == Style::Italic;
let bold = fragment_style.font_attrs.weight() == Weight::BOLD;
let spec = (italic || bold).then_some(LookupAttrs { italic, bold });
lookup_for_font_match(&mut char_cluster, &mut synth, self, spec)
}
#[inline]
pub fn insert(&mut self, font_data: FontData) {
let id = self.inner.len();
if let Some(ps_name) = font_data.postscript_name() {
self.postscript_to_id
.entry(ps_name.to_string())
.or_insert(id);
}
self.inner.insert(id, FontEntry::Owned(font_data));
}
#[inline]
pub fn insert_alias(&mut self, target: usize) {
let id = self.inner.len();
let target = self.resolve_id(target);
self.inner.insert(id, FontEntry::Alias(target));
}
#[inline]
pub fn resolve_id(&self, font_id: usize) -> usize {
match self.inner.get(&font_id) {
Some(FontEntry::Alias(target)) => *target,
_ => font_id,
}
}
pub fn font_id_for_postscript_name(&self, name: &str) -> Option<usize> {
self.postscript_to_id.get(name).copied()
}
#[inline]
pub fn get(&self, font_id: &usize) -> &FontData {
let id = self.resolve_id(*font_id);
match &self.inner[&id] {
FontEntry::Owned(d) => d,
FontEntry::Alias(_) => {
unreachable!("alias must resolve to Owned in single hop")
}
}
}
#[inline]
pub fn try_get(&self, font_id: &usize) -> Option<&FontData> {
let id = self.resolve_id(*font_id);
self.inner.get(&id).and_then(FontEntry::as_owned)
}
pub fn get_data(&self, font_id: &usize) -> Option<(SharedData, u32, CacheKey)> {
if let Some(font) = self.try_get(font_id) {
if let Some(data) = &font.data {
return Some((data.clone(), font.offset, font.key));
} else if let Some(path) = &font.path {
if let Some(raw_data) = load_from_font_source(path) {
return Some((raw_data, font.offset, font.key));
}
}
}
None
}
#[inline]
pub fn get_mut(&mut self, font_id: &usize) -> Option<&mut FontData> {
let id = self.resolve_id(*font_id);
match self.inner.get_mut(&id)? {
FontEntry::Owned(d) => Some(d),
FontEntry::Alias(_) => None,
}
}
pub fn get_font_metrics(
&mut self,
font_id: &usize,
font_size: f32,
) -> Option<(f32, f32, f32)> {
let size_key = (font_size * 100.0) as u32;
let primary_metrics =
if let Some(cached) = self.primary_metrics_cache.get(&size_key) {
*cached
} else {
let primary_font = self.get_mut(&FONT_ID_REGULAR)?;
let primary_metrics = primary_font.get_metrics(font_size, None)?;
self.primary_metrics_cache.insert(size_key, primary_metrics);
primary_metrics
};
let resolved = self.resolve_id(*font_id);
match resolved {
FONT_ID_REGULAR => {
Some(primary_metrics.for_rich_text())
}
_ => {
let font = self.get_mut(&resolved)?;
font.get_rich_text_metrics(font_size, Some(&primary_metrics))
}
}
}
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load(&mut self, mut spec: SugarloafFonts) -> Vec<SugarloafFont> {
self.hinting = spec.hinting;
self.features = Arc::new(
spec.features
.as_deref()
.map(parse_font_features)
.unwrap_or_default(),
);
let mut fonts_not_fount: Vec<SugarloafFont> = vec![];
if let Some(font_family_overwrite) = spec.family {
font_family_overwrite.clone_into(&mut spec.regular.family);
font_family_overwrite.clone_into(&mut spec.bold.family);
font_family_overwrite.clone_into(&mut spec.bold_italic.family);
font_family_overwrite.clone_into(&mut spec.italic.family);
}
#[cfg(not(target_os = "macos"))]
let mut db = loader::Database::new();
let additional_dirs = spec.additional_dirs.unwrap_or_default();
for dir in additional_dirs.into_iter().map(PathBuf::from) {
#[cfg(target_os = "macos")]
crate::font::macos::register_fonts_in_dir(&dir);
#[cfg(not(target_os = "macos"))]
db.load_fonts_dir(dir);
}
#[cfg(target_os = "macos")]
let resolve = |spec: SugarloafFont, slot: Slot, evictable: bool| {
find_font(spec, slot, evictable)
};
#[cfg(not(target_os = "macos"))]
let resolve = |spec: SugarloafFont, slot: Slot, evictable: bool| {
find_font(&db, spec, slot, evictable)
};
let regular_index = self.len();
match resolve(spec.regular, Slot::Regular, false) {
FindResult::Found(data) => {
self.insert(data);
}
FindResult::NotFound(spec) => {
if !spec.is_default_family() {
fonts_not_fount.push(spec.to_owned());
}
self.insert(load_fallback_from_memory(Slot::Regular));
}
}
for (slot, slot_spec, evictable) in [
(Slot::Italic, spec.italic, false),
(Slot::Bold, spec.bold, false),
(Slot::BoldItalic, spec.bold_italic, true),
] {
if slot_spec.style.is_disabled() {
self.insert_alias(regular_index);
continue;
}
match resolve(slot_spec, slot, evictable) {
FindResult::Found(data) => {
self.insert(data);
}
FindResult::NotFound(spec) => {
if spec.is_default_family() {
self.insert(load_fallback_from_memory(slot));
} else {
warn!(
"Font family '{}' has no {:?} variant; falling back to regular",
spec.family, slot
);
self.insert_alias(regular_index);
}
}
}
}
#[cfg(target_os = "macos")]
{
let primary_handle = self.try_get(&FONT_ID_REGULAR).and_then(|f| {
if let Some(path) = &f.path {
crate::font::macos::FontHandle::from_path(path)
} else if let Some(bytes) = &f.data {
crate::font::macos::FontHandle::from_bytes(bytes.as_ref())
} else {
None
}
});
if let Some(primary_handle) = primary_handle {
let default_spec = SugarloafFont::default();
for path in crate::font::macos::default_cascade_list(&primary_handle) {
if let Ok(font_data) =
FontData::from_path_macos(path, Slot::Regular, &default_spec)
{
self.insert(font_data);
}
}
}
}
if let Some(symbol_map) = spec.symbol_map {
let mut symbol_maps = Vec::default();
for extra_font_from_symbol_map in symbol_map {
match resolve(
SugarloafFont {
family: extra_font_from_symbol_map.font_family,
..SugarloafFont::default()
},
Slot::Regular,
true,
) {
FindResult::Found(data) => {
if let Some(start) =
parse_unicode(&extra_font_from_symbol_map.start)
{
if let Some(end) =
parse_unicode(&extra_font_from_symbol_map.end)
{
self.insert(data);
symbol_maps.push(SymbolMap {
range: start..end,
font_index: self.len() - 1,
});
continue;
}
}
warn!("symbol-map: Failed to parse start and end values");
}
FindResult::NotFound(spec) => {
fonts_not_fount.push(spec);
}
}
}
self.symbol_maps = Some(symbol_maps);
}
if spec.disable_warnings_not_found {
vec![]
} else {
fonts_not_fount
}
}
#[cfg(target_arch = "wasm32")]
pub fn load(&mut self, _font_spec: SugarloafFonts) -> Vec<SugarloafFont> {
self.insert(FontData::from_slice(FONT_CASCADIA_CODE_NF).unwrap());
vec![]
}
}
#[derive(Clone, Debug)]
pub enum SharedData {
Heap(Arc<[u8]>),
Static(&'static [u8]),
#[cfg(not(target_arch = "wasm32"))]
Mmap(Arc<memmap2::Mmap>),
}
impl SharedData {
pub fn new(data: Vec<u8>) -> Self {
Self::Heap(Arc::from(data))
}
pub const fn from_static(data: &'static [u8]) -> Self {
Self::Static(data)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_mmap(mmap: memmap2::Mmap) -> Self {
Self::Mmap(Arc::new(mmap))
}
pub const fn is_static(&self) -> bool {
matches!(self, Self::Static(_))
}
}
impl std::ops::Deref for SharedData {
type Target = [u8];
fn deref(&self) -> &Self::Target {
match self {
Self::Heap(a) => a,
Self::Static(s) => s,
#[cfg(not(target_arch = "wasm32"))]
Self::Mmap(m) => m.as_ref(),
}
}
}
impl AsRef<[u8]> for SharedData {
fn as_ref(&self) -> &[u8] {
match self {
Self::Heap(a) => a,
Self::Static(s) => s,
#[cfg(not(target_arch = "wasm32"))]
Self::Mmap(m) => m.as_ref(),
}
}
}
#[derive(Clone)]
pub struct FontData {
data: Option<SharedData>,
path: Option<PathBuf>,
offset: u32,
pub key: CacheKey,
pub weight: swash::Weight,
pub style: swash::Style,
pub stretch: swash::Stretch,
pub synth: Synthesis,
pub should_embolden: bool,
pub should_italicize: bool,
pub wght_variation: Option<f32>,
pub is_emoji: bool,
metrics_cache: FxHashMap<u32, Metrics>,
#[cfg(target_os = "macos")]
handle: Option<crate::font::macos::FontHandle>,
postscript_name: Option<String>,
}
impl PartialEq for FontData {
fn eq(&self, other: &Self) -> bool {
self.key == other.key
}
}
impl FontData {
#[inline]
pub fn is_bold(&self) -> bool {
self.weight >= Weight(700)
}
#[inline]
pub fn is_italic(&self) -> bool {
self.style == Style::Italic
}
pub fn data(&self) -> &Option<SharedData> {
&self.data
}
pub fn path(&self) -> Option<&PathBuf> {
self.path.as_ref()
}
#[cfg(target_os = "macos")]
pub fn handle(&self) -> Option<&crate::font::macos::FontHandle> {
self.handle.as_ref()
}
pub fn postscript_name(&self) -> Option<&str> {
self.postscript_name.as_deref()
}
pub fn offset(&self) -> u32 {
self.offset
}
pub fn get_metrics(
&mut self,
font_size: f32,
primary_metrics: Option<&Metrics>,
) -> Option<Metrics> {
let size_key = (font_size * 100.0) as u32;
if let Some(cached) = self.metrics_cache.get(&size_key) {
return Some(*cached);
}
#[cfg(target_os = "macos")]
if self.data.is_none() {
let handle = if let Some(h) = self.handle.as_ref() {
h.clone()
} else {
self.path
.as_ref()
.and_then(|p| crate::font::macos::FontHandle::from_path(p))?
};
let font_metrics = crate::font::macos::design_unit_metrics(&handle);
let scaled_metrics = font_metrics.scale(font_size);
let face_metrics = FaceMetrics {
cell_width: scaled_metrics.max_width as f64,
ascent: scaled_metrics.ascent as f64,
descent: scaled_metrics.descent as f64,
line_gap: scaled_metrics.leading as f64,
underline_position: Some(scaled_metrics.underline_offset as f64),
underline_thickness: Some(scaled_metrics.stroke_size as f64),
strikethrough_position: Some(scaled_metrics.strikeout_offset as f64),
strikethrough_thickness: Some(scaled_metrics.stroke_size as f64),
cap_height: Some(scaled_metrics.cap_height as f64),
ex_height: Some(scaled_metrics.x_height as f64),
ic_width: crate::font::macos::cjk_ic_width(&handle).map(|u| {
u * font_size as f64 / scaled_metrics.units_per_em as f64
}),
};
let metrics = if let Some(primary) = primary_metrics {
Metrics::calc_with_primary_cell_dimensions(face_metrics, primary)
} else {
Metrics::calc(face_metrics)
};
self.metrics_cache.insert(size_key, metrics);
return Some(metrics);
}
if let Some(ref data) = self.data {
let font_ref = swash::FontRef {
data: data.as_ref(),
offset: self.offset,
key: self.key,
};
let scaled_metrics = font_ref.metrics(&[]).scale(font_size);
let face_metrics = FaceMetrics::from_font(&font_ref, &scaled_metrics);
let metrics = if let Some(primary) = primary_metrics {
Metrics::calc_with_primary_cell_dimensions(face_metrics, primary)
} else {
Metrics::calc(face_metrics)
};
self.metrics_cache.insert(size_key, metrics);
Some(metrics)
} else {
None
}
}
pub fn get_rich_text_metrics(
&mut self,
font_size: f32,
primary_metrics: Option<&Metrics>,
) -> Option<(f32, f32, f32)> {
self.get_metrics(font_size, primary_metrics)
.map(|m| m.for_rich_text())
}
#[inline]
pub fn from_data(
data: SharedData,
path: PathBuf,
evictable: bool,
slot: Slot,
font_spec: &SugarloafFont,
face_index: u32,
) -> Result<Self, Box<dyn std::error::Error>> {
let font = FontRef::from_index(&data, face_index as usize)
.ok_or_else(|| format!("Failed to load font from path: {:?}", path))?;
let (offset, key) = (font.offset, font.key);
let attributes = font.attributes();
let style = attributes.style();
let weight = match font_spec.weight {
Some(w) => Weight(w),
None => attributes.weight(),
};
let (should_embolden, should_italicize) = synth_decisions(
slot,
font_spec,
font_spec.weight.is_some() || weight >= Weight(600),
style == Style::Italic,
);
let stretch = attributes.stretch();
let synth = attributes.synthesize(attributes);
let is_emoji = has_color_tables(&font);
let postscript_name = parse_postscript_name(&data, face_index);
let data = (!evictable).then_some(data);
Ok(Self {
data,
offset,
should_italicize,
should_embolden,
wght_variation: font_spec.weight.map(f32::from),
key,
synth,
style,
weight,
stretch,
path: Some(path),
is_emoji,
metrics_cache: FxHashMap::default(),
#[cfg(target_os = "macos")]
handle: None,
postscript_name,
})
}
#[cfg(target_os = "macos")]
pub fn from_ctfont_macos(handle: crate::font::macos::FontHandle) -> Self {
let attrs = crate::font::macos::font_attributes(&handle);
let style = if attrs.is_italic {
swash::Style::Italic
} else {
swash::Style::Normal
};
let weight = swash::Weight(attrs.weight);
let postscript_name = Some(handle.postscript_name());
Self {
data: None,
path: None,
offset: 0,
key: CacheKey::new(),
weight,
style,
stretch: swash::Stretch::NORMAL,
synth: Synthesis::default(),
should_embolden: false,
should_italicize: false,
wght_variation: None,
is_emoji: attrs.is_color,
metrics_cache: FxHashMap::default(),
handle: Some(handle),
postscript_name,
}
}
#[cfg(target_os = "macos")]
pub fn from_path_macos(
path: PathBuf,
slot: Slot,
font_spec: &SugarloafFont,
) -> Result<Self, Box<dyn std::error::Error>> {
let handle = crate::font::macos::FontHandle::from_path(&path)
.ok_or_else(|| format!("CoreText refused {}", path.display()))?;
let handle = match font_spec.weight {
Some(w) => handle
.clone()
.with_wght_variation(f32::from(w))
.unwrap_or(handle),
None => handle,
};
let attrs = crate::font::macos::font_attributes(&handle);
let style = if attrs.is_italic {
swash::Style::Italic
} else {
swash::Style::Normal
};
let weight = match font_spec.weight {
Some(w) => swash::Weight(w),
None => swash::Weight(attrs.weight),
};
let (should_embolden, should_italicize) = synth_decisions(
slot,
font_spec,
font_spec.weight.is_some() || attrs.is_bold,
attrs.is_italic,
);
let postscript_name = Some(handle.postscript_name());
Ok(Self {
data: None,
path: Some(path),
offset: 0,
key: CacheKey::new(),
weight,
style,
stretch: swash::Stretch::NORMAL,
synth: Synthesis::default(),
should_embolden,
should_italicize,
wght_variation: font_spec.weight.map(f32::from),
is_emoji: attrs.is_color,
metrics_cache: FxHashMap::default(),
handle: Some(handle),
postscript_name,
})
}
#[inline]
pub fn from_static_slice(
data: &'static [u8],
) -> Result<Self, Box<dyn std::error::Error>> {
Self::from_static_slice_with_wght(data, None)
}
pub fn from_static_slice_with_wght(
data: &'static [u8],
wght: Option<f32>,
) -> Result<Self, Box<dyn std::error::Error>> {
let font = FontRef::from_index(data, 0).unwrap();
let (offset, key) = (font.offset, font.key);
let attributes = font.attributes();
let style = attributes.style();
let weight = match wght {
Some(v) => swash::Weight(v.round().clamp(0.0, u16::MAX as f32) as u16),
None => attributes.weight(),
};
let stretch = attributes.stretch();
let synth = attributes.synthesize(attributes);
let is_emoji = has_color_tables(&font);
let postscript_name = parse_postscript_name(data, 0);
#[cfg(target_os = "macos")]
let handle = {
let base = crate::font::macos::FontHandle::from_static_bytes(data);
match (base, wght) {
(Some(h), Some(v)) => Some(h.clone().with_wght_variation(v).unwrap_or(h)),
(h, _) => h,
}
};
Ok(Self {
data: Some(SharedData::from_static(data)),
offset,
key,
synth,
style,
should_embolden: false,
should_italicize: false,
wght_variation: wght,
weight,
stretch,
path: None,
is_emoji,
metrics_cache: FxHashMap::default(),
#[cfg(target_os = "macos")]
handle,
postscript_name,
})
}
#[inline]
pub fn from_slice(data: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
let font = FontRef::from_index(data, 0).unwrap();
let (offset, key) = (font.offset, font.key);
let attributes = font.attributes();
let style = attributes.style();
let weight = attributes.weight();
let stretch = attributes.stretch();
let synth = attributes.synthesize(attributes);
let is_emoji = has_color_tables(&font);
let postscript_name = parse_postscript_name(data, 0);
Ok(Self {
data: Some(SharedData::new(data.to_vec())),
offset,
key,
synth,
style,
should_embolden: false,
should_italicize: false,
wght_variation: None,
weight,
stretch,
path: None,
is_emoji,
metrics_cache: FxHashMap::default(),
#[cfg(target_os = "macos")]
handle: None,
postscript_name,
})
}
#[cfg(any(
all(unix, not(target_os = "macos"), not(target_os = "android")),
target_os = "windows"
))]
pub fn from_discovered_path(
path: PathBuf,
face_index: u32,
) -> Result<Self, Box<dyn std::error::Error>> {
let data = load_from_font_source(&path).ok_or_else(|| {
format!("failed to load discovered font: {}", path.display())
})?;
let font = FontRef::from_index(&data, face_index as usize).ok_or_else(|| {
format!(
"failed to parse discovered font {} face {}",
path.display(),
face_index
)
})?;
let (offset, key) = (font.offset, font.key);
let attributes = font.attributes();
let style = attributes.style();
let weight = attributes.weight();
let stretch = attributes.stretch();
let synth = attributes.synthesize(attributes);
let is_emoji = has_color_tables(&font);
let postscript_name = parse_postscript_name(&data, face_index);
Ok(Self {
data: Some(data),
offset,
should_italicize: false,
should_embolden: false,
wght_variation: None,
key,
synth,
style,
weight,
stretch,
path: Some(path),
is_emoji,
metrics_cache: FxHashMap::default(),
postscript_name,
})
}
}
fn parse_postscript_name(data: &[u8], face_index: u32) -> Option<String> {
let face = ttf_parser::Face::parse(data, face_index).ok()?;
face.names()
.into_iter()
.find(|n| n.name_id == ttf_parser::name_id::POST_SCRIPT_NAME && n.is_unicode())
.and_then(|n| n.to_string())
.or_else(|| {
face.names()
.into_iter()
.find(|n| n.name_id == ttf_parser::name_id::FAMILY && n.is_unicode())
.and_then(|n| n.to_string())
})
}
fn has_color_tables(font: &FontRef<'_>) -> bool {
font.table(tag_from_bytes(b"COLR")).is_some()
|| font.table(tag_from_bytes(b"CBDT")).is_some()
|| font.table(tag_from_bytes(b"CBLC")).is_some()
|| font.table(tag_from_bytes(b"sbix")).is_some()
}
pub type SugarloafFont = fonts::SugarloafFont;
pub type SugarloafFonts = fonts::SugarloafFonts;
#[cfg(not(target_arch = "wasm32"))]
use tracing::{info, warn};
enum FindResult {
Found(FontData),
NotFound(SugarloafFont),
}
pub fn select_color_bitmap(
font: swash::FontRef<'_>,
glyph_id: u16,
size: f32,
) -> Option<swash::scale::Source> {
use swash::scale::{Source, StrikeWith};
use swash::tag_from_bytes;
if font.table(tag_from_bytes(b"sbix")).is_some() {
return Some(Source::ColorBitmap(StrikeWith::BestFit));
}
font.table(tag_from_bytes(b"CBDT"))?;
let table = font.table(tag_from_bytes(b"CBLC"))?;
let read_u16 = |offset| {
table
.get(offset..)?
.get(..2)
.map(|bytes| u16::from_be_bytes(bytes.try_into().unwrap()))
};
let read_u32 = |offset| {
table
.get(offset..)?
.get(..4)
.map(|bytes| u32::from_be_bytes(bytes.try_into().unwrap()) as usize)
};
let mut selected = None;
for index in 0..read_u32(4)? {
let strike = 8usize.checked_add(index.checked_mul(48)?)?;
let first_glyph = read_u16(strike.checked_add(40)?)?;
let last_glyph = read_u16(strike.checked_add(42)?)?;
if !(first_glyph..=last_glyph).contains(&glyph_id) {
continue;
}
let array = read_u32(strike)?;
for subtable in 0..read_u32(strike.checked_add(8)?)? {
let entry = array.checked_add(subtable.checked_mul(8)?)?;
let first_glyph = read_u16(entry)?;
if glyph_id < first_glyph {
break;
}
if (first_glyph..=read_u16(entry.checked_add(2)?)?).contains(&glyph_id) {
let subtable = array.checked_add(read_u32(entry.checked_add(4)?)?)?;
selected = Some((
table.get(strike.checked_add(45)?).copied()?,
table.get(strike.checked_add(46)?).copied()?,
read_u16(subtable.checked_add(2)?)?,
));
break;
}
}
if selected.is_some_and(|(ppem, _, _)| u16::from(ppem) >= size as u16) {
break;
}
}
match selected? {
(_, _, 17..=19) => Some(Source::ColorBitmap(StrikeWith::BestFit)),
(ppem, 32, 1 | 6) if size == f32::from(ppem) => {
Some(Source::ColorBitmap(StrikeWith::ExactSize))
}
(_, 32, 2 | 5 | 7) => None,
(_, bit_depth, format) => {
tracing::warn!(glyph_id, bit_depth, format, "unsupported color strike");
None
}
}
}
pub fn normalize_color_bitmap(image: &mut swash::scale::image::Image) {
use swash::scale::{Source, StrikeWith};
let png = match image.source {
Source::ColorBitmap(StrikeWith::BestFit) => true,
Source::ColorBitmap(StrikeWith::ExactSize) => false,
Source::ColorBitmap(mode) => {
tracing::error!(?mode, "unexpected Swash color bitmap selection mode");
return;
}
_ => return,
};
let byte_len = image.data.len();
let (pixels, remainder) = image.data.as_chunks_mut::<4>();
if !remainder.is_empty() {
tracing::error!(byte_len, "Swash color bitmap is not RGBA-aligned");
return;
}
for pixel in pixels {
if png {
let alpha = pixel[3] as u16;
for channel in &mut pixel[..3] {
*channel = ((*channel as u16 * alpha + 127) / 255) as u8;
}
} else {
pixel.swap(0, 2);
}
}
}
#[cfg(test)]
mod color_bitmap_tests {
use super::{normalize_color_bitmap, select_color_bitmap};
use swash::scale::{image::Image, Source, StrikeWith};
#[test]
fn normalizes_png_and_raw_color_bitmaps() {
let mut png = Image::new();
png.source = Source::ColorBitmap(StrikeWith::BestFit);
png.data = vec![71, 112, 76, 0, 200, 100, 50, 128, 9, 8, 7, 255];
let mut bgra = png.clone();
bgra.source = Source::ColorBitmap(StrikeWith::ExactSize);
bgra.data = vec![25, 50, 100, 128, 7, 8, 9, 255];
let mut outline = bgra.clone();
outline.source = Source::ColorOutline(0);
normalize_color_bitmap(&mut png);
normalize_color_bitmap(&mut bgra);
normalize_color_bitmap(&mut outline);
assert_eq!(png.data, [0, 0, 0, 0, 100, 50, 25, 128, 9, 8, 7, 255]);
assert_eq!(bgra.data, [100, 50, 25, 128, 9, 8, 7, 255]);
assert_eq!(outline.data, [25, 50, 100, 128, 7, 8, 9, 255]);
}
#[test]
fn normalize_rejects_unaligned_data_untouched() {
let mut image = Image::new();
image.source = Source::ColorBitmap(StrikeWith::BestFit);
image.data = vec![10, 20, 30, 128, 40];
normalize_color_bitmap(&mut image);
assert_eq!(image.data, [10, 20, 30, 128, 40]);
let mut empty = Image::new();
empty.source = Source::ColorBitmap(StrikeWith::ExactSize);
normalize_color_bitmap(&mut empty);
assert!(empty.data.is_empty());
}
fn build_font(tables: &[(&[u8; 4], Vec<u8>)]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&0x00010000u32.to_be_bytes());
out.extend_from_slice(&(tables.len() as u16).to_be_bytes());
out.extend_from_slice(&[0u8; 6]);
let mut offset = 12 + tables.len() * 16;
for (tag, data) in tables {
out.extend_from_slice(*tag);
out.extend_from_slice(&0u32.to_be_bytes());
out.extend_from_slice(&(offset as u32).to_be_bytes());
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
offset += data.len();
}
for (_, data) in tables {
out.extend_from_slice(data);
}
out
}
fn build_cblc(strikes: &[(u8, u8, u16, std::ops::RangeInclusive<u16>)]) -> Vec<u8> {
let mut out = vec![0u8; 8 + strikes.len() * 48];
out[1] = 3; out[4..8].copy_from_slice(&(strikes.len() as u32).to_be_bytes());
for (i, (ppem, depth, format, glyphs)) in strikes.iter().enumerate() {
let strike = 8 + i * 48;
let array = out.len() as u32;
out[strike..strike + 4].copy_from_slice(&array.to_be_bytes());
out[strike + 8..strike + 12].copy_from_slice(&1u32.to_be_bytes());
out[strike + 40..strike + 42].copy_from_slice(&glyphs.start().to_be_bytes());
out[strike + 42..strike + 44].copy_from_slice(&glyphs.end().to_be_bytes());
out[strike + 44] = *ppem;
out[strike + 45] = *ppem;
out[strike + 46] = *depth;
out.extend_from_slice(&glyphs.start().to_be_bytes());
out.extend_from_slice(&glyphs.end().to_be_bytes());
out.extend_from_slice(&8u32.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&format.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); }
out
}
#[allow(clippy::type_complexity)]
fn build_cblc_multi(
strikes: &[(
u8,
u8,
std::ops::RangeInclusive<u16>,
&[(std::ops::RangeInclusive<u16>, u16)],
)],
) -> Vec<u8> {
let mut out = vec![0u8; 8 + strikes.len() * 48];
out[1] = 3; out[4..8].copy_from_slice(&(strikes.len() as u32).to_be_bytes());
for (i, (ppem, depth, header, subtables)) in strikes.iter().enumerate() {
let strike = 8 + i * 48;
let array = out.len() as u32;
out[strike..strike + 4].copy_from_slice(&array.to_be_bytes());
out[strike + 8..strike + 12]
.copy_from_slice(&(subtables.len() as u32).to_be_bytes());
out[strike + 40..strike + 42].copy_from_slice(&header.start().to_be_bytes());
out[strike + 42..strike + 44].copy_from_slice(&header.end().to_be_bytes());
out[strike + 44] = *ppem;
out[strike + 45] = *ppem;
out[strike + 46] = *depth;
let entries_len = (subtables.len() * 8) as u32;
for (j, (glyphs, _)) in subtables.iter().enumerate() {
out.extend_from_slice(&glyphs.start().to_be_bytes());
out.extend_from_slice(&glyphs.end().to_be_bytes());
out.extend_from_slice(&(entries_len + (j as u32) * 8).to_be_bytes());
}
for (_, format) in subtables.iter() {
out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&format.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); }
}
out
}
fn color_font(cblc: Vec<u8>) -> Vec<u8> {
build_font(&[(b"CBDT", vec![0u8; 4]), (b"CBLC", cblc)])
}
fn select(data: &[u8], glyph_id: u16, size: f32) -> Option<Source> {
let font = swash::FontRef::from_index(data, 0).expect("synthetic font parses");
select_color_bitmap(font, glyph_id, size)
}
#[test]
fn png_strikes_use_best_fit() {
let font = color_font(build_cblc(&[(128, 32, 17, 0..=10)]));
for size in [16.0, 128.0, 200.0] {
assert!(matches!(
select(&font, 5, size),
Some(Source::ColorBitmap(StrikeWith::BestFit))
));
}
}
#[test]
fn raw_strikes_only_at_exact_size() {
let font = color_font(build_cblc(&[(64, 32, 1, 0..=10)]));
assert!(matches!(
select(&font, 5, 64.0),
Some(Source::ColorBitmap(StrikeWith::ExactSize))
));
assert!(select(&font, 5, 32.0).is_none());
assert!(select(&font, 5, 65.0).is_none());
}
#[test]
fn packed_strikes_are_skipped() {
for format in [2u16, 5, 7] {
let font = color_font(build_cblc(&[(64, 32, format, 0..=10)]));
assert!(select(&font, 5, 64.0).is_none());
}
}
#[test]
fn glyph_outside_strike_range_finds_nothing() {
let font = color_font(build_cblc(&[(64, 32, 17, 4..=10)]));
assert!(select(&font, 3, 64.0).is_none());
assert!(select(&font, 11, 64.0).is_none());
}
#[test]
fn sbix_fonts_use_best_fit_without_cblc() {
let font = build_font(&[(b"sbix", vec![0u8; 8])]);
assert!(matches!(
select(&font, 5, 20.0),
Some(Source::ColorBitmap(StrikeWith::BestFit))
));
}
#[test]
fn nearest_strike_decides_the_format() {
let font = color_font(build_cblc(&[(32, 32, 5, 0..=10), (128, 32, 17, 0..=10)]));
assert!(matches!(
select(&font, 5, 40.0),
Some(Source::ColorBitmap(StrikeWith::BestFit))
));
assert!(select(&font, 5, 32.0).is_none());
}
#[test]
fn truncated_cblc_is_rejected_without_panicking() {
let mut cblc = build_cblc(&[(64, 32, 17, 0..=10)]);
for len in [0, 4, 8, 20, 50] {
cblc.truncate(len);
let font = color_font(cblc.clone());
assert!(select(&font, 5, 64.0).is_none());
}
let mut lying = build_cblc(&[(64, 32, 17, 0..=10)]);
lying[4..8].copy_from_slice(&9u32.to_be_bytes());
let font = color_font(lying);
assert!(matches!(
select(&font, 5, 64.0),
Some(Source::ColorBitmap(StrikeWith::BestFit))
));
}
#[test]
fn cblc_without_cbdt_is_ignored() {
let font = build_font(&[(b"CBLC", build_cblc(&[(64, 32, 17, 0..=10)]))]);
assert!(select(&font, 5, 64.0).is_none());
}
#[test]
fn out_of_order_subtables_match_swash() {
let font = color_font(build_cblc_multi(&[
(64, 32, 0..=10, &[(8..=10, 17), (0..=5, 17)]),
(64, 32, 0..=10, &[(0..=10, 2)]),
]));
assert!(select(&font, 5, 64.0).is_none());
let sorted = color_font(build_cblc_multi(&[(
64,
32,
0..=10,
&[(0..=3, 17), (4..=10, 17)],
)]));
assert!(matches!(
select(&sorted, 5, 64.0),
Some(Source::ColorBitmap(StrikeWith::BestFit))
));
}
}
pub fn parse_font_features(entries: &[String]) -> Vec<swash::Setting<u16>> {
let mut out: Vec<swash::Setting<u16>> = Vec::with_capacity(entries.len());
for raw in entries {
let s = raw.trim();
let (name, value) = if let Some(rest) = s.strip_prefix('-') {
(rest.trim(), 0u16)
} else if let Some(rest) = s.strip_prefix('+') {
(rest.trim(), 1u16)
} else if let Some((name, v)) = s.split_once('=') {
match v.trim().parse::<u16>() {
Ok(v) => (name.trim(), v),
Err(_) => {
warn!("ignoring invalid font feature '{raw}'");
continue;
}
}
} else {
(s, 1u16)
};
if name.is_empty() || name.len() > 4 || !name.is_ascii() {
warn!("ignoring invalid font feature '{raw}'");
continue;
}
let mut tag = [b' '; 4];
tag[..name.len()].copy_from_slice(name.as_bytes());
out.push(swash::Setting {
tag: u32::from_be_bytes(tag),
value,
});
}
out
}
#[inline]
fn synth_decisions(
slot: Slot,
font_spec: &SugarloafFont,
matched_is_bold: bool,
matched_is_italic: bool,
) -> (bool, bool) {
let allowed = !matches!(font_spec.style, FontStyle::Named(_));
let embolden = allowed && slot.is_bold() && !matched_is_bold;
let italicize = allowed && slot.is_italic() && !matched_is_italic;
(embolden, italicize)
}
#[cfg(target_os = "macos")]
#[inline]
fn find_font(font_spec: SugarloafFont, slot: Slot, evictable: bool) -> FindResult {
if font_spec.is_default_family() {
return FindResult::NotFound(font_spec);
}
let family = font_spec.family.to_string();
let style_name = font_spec.style.name();
let bold = slot.is_bold();
let italic = slot.is_italic();
info!(
"Font search (CoreText): family='{family}' bold={bold} italic={italic} style={:?}",
style_name
);
let Some(path) =
crate::font::macos::find_font_path(&family, bold, italic, style_name)
else {
warn!("CoreText found no match for family='{family}'");
return FindResult::NotFound(font_spec);
};
let _ = evictable;
match FontData::from_path_macos(path.clone(), slot, &font_spec) {
Ok(d) => {
info!("Font '{family}' matched via CoreText at {}", path.display());
FindResult::Found(d)
}
Err(e) => {
warn!("Failed to open font '{family}' via CoreText: {e}");
FindResult::NotFound(font_spec)
}
}
}
#[cfg(all(not(target_os = "macos"), not(target_arch = "wasm32")))]
#[inline]
fn find_font(
db: &crate::font::loader::Database,
font_spec: SugarloafFont,
slot: Slot,
evictable: bool,
) -> FindResult {
if !font_spec.is_default_family() {
let family = font_spec.family.to_string();
let mut query = crate::font::loader::Query {
families: &[crate::font::loader::Family::Name(&family)],
..crate::font::loader::Query::default()
};
query.weight = match font_spec.weight {
Some(w) => crate::font::loader::Weight(w),
None if slot.is_bold() => crate::font::loader::Weight::BOLD,
None => crate::font::loader::Weight::NORMAL,
};
query.style = if slot.is_italic() {
crate::font::loader::Style::Italic
} else {
crate::font::loader::Style::Normal
};
info!(
"Font search: '{query:?}' style_override={:?}",
font_spec.style.name()
);
match db.query(&query) {
Some(id) => {
match db.face_source(id) {
Some((crate::font::loader::Source::File(ref path), index)) => {
if let Some(font_data_arc) =
load_from_font_source(&path.to_path_buf())
{
match FontData::from_data(
font_data_arc,
path.to_path_buf(),
evictable,
slot,
&font_spec,
index,
) {
Ok(d) => {
tracing::info!(
"Font '{}' found in {}",
family,
path.display()
);
return FindResult::Found(d);
}
Err(err_message) => {
tracing::info!(
"Failed to load font '{query:?}', {err_message}"
);
return FindResult::NotFound(font_spec);
}
}
}
}
Some((crate::font::loader::Source::Binary(font_data), index)) => {
tracing::debug!(
"Using binary font data, {} bytes",
font_data.len()
);
match FontData::from_data(
font_data,
std::path::PathBuf::from(&family),
evictable,
slot,
&font_spec,
index,
) {
Ok(d) => {
tracing::info!("Font '{}' loaded from memory", family);
return FindResult::Found(d);
}
Err(err_message) => {
tracing::info!(
"Failed to load font '{query:?}' from memory, {err_message}"
);
return FindResult::NotFound(font_spec);
}
}
}
None => {
tracing::warn!("face_source returned None for font ID");
}
}
}
None => {
warn!("Failed to find font '{query:?}'");
}
}
}
FindResult::NotFound(font_spec)
}
fn load_fallback_from_memory(slot: Slot) -> FontData {
use constants::{FONT_CASCADIA_CODE_NF, FONT_CASCADIA_CODE_NF_ITALIC, WGHT_BOLD};
let (data, wght) = match slot {
Slot::Regular => (FONT_CASCADIA_CODE_NF, None),
Slot::Bold => (FONT_CASCADIA_CODE_NF, Some(WGHT_BOLD)),
Slot::Italic => (FONT_CASCADIA_CODE_NF_ITALIC, None),
Slot::BoldItalic => (FONT_CASCADIA_CODE_NF_ITALIC, Some(WGHT_BOLD)),
};
FontData::from_static_slice_with_wght(data, wght).unwrap()
}
#[cfg(test)]
mod alias_tests {
use super::*;
#[test]
fn insert_alias_resolves_to_target() {
let mut lib = FontLibraryData::default();
lib.insert(
FontData::from_static_slice(constants::FONT_CASCADIA_CODE_NF)
.expect("load regular"),
);
lib.insert_alias(0);
assert_eq!(lib.len(), 2, "alias takes a slot");
assert_eq!(lib.resolve_id(1), 0, "alias resolves to its target");
let owned_key = lib.get(&0).key;
let aliased_key = lib.get(&1).key;
assert_eq!(
owned_key, aliased_key,
"aliased slot must surface the target FontData"
);
}
#[test]
fn alias_of_alias_collapses_to_root() {
let mut lib = FontLibraryData::default();
lib.insert(
FontData::from_static_slice(constants::FONT_CASCADIA_CODE_NF)
.expect("load regular"),
);
lib.insert_alias(0);
lib.insert_alias(1);
assert_eq!(
lib.resolve_id(2),
0,
"alias pointing at an alias must collapse to the owning id"
);
assert!(matches!(lib.inner.get(&2), Some(FontEntry::Alias(0))));
}
#[test]
fn fallback_bold_slot_reports_is_bold() {
let regular = load_fallback_from_memory(Slot::Regular);
let bold = load_fallback_from_memory(Slot::Bold);
let italic = load_fallback_from_memory(Slot::Italic);
let bold_italic = load_fallback_from_memory(Slot::BoldItalic);
assert!(!regular.is_bold(), "regular slot must not be bold");
assert!(bold.is_bold(), "bold slot must report is_bold");
assert!(!italic.is_bold(), "italic slot must not be bold");
assert!(
bold_italic.is_bold(),
"bold-italic slot must report is_bold"
);
assert!(!regular.is_italic(), "regular slot must not be italic");
assert!(!bold.is_italic(), "bold slot must not be italic");
assert!(italic.is_italic(), "italic slot must report is_italic");
assert!(
bold_italic.is_italic(),
"bold-italic slot must report is_italic"
);
assert_eq!(bold.wght_variation, Some(constants::WGHT_BOLD));
assert_eq!(bold_italic.wght_variation, Some(constants::WGHT_BOLD));
assert_eq!(regular.wght_variation, None);
assert_eq!(italic.wght_variation, None);
}
#[test]
fn bold_slot_wins_regardless_of_weight_metadata() {
use std::sync::Arc;
let mut data = FontLibraryData::default();
data.insert(
FontData::from_static_slice(constants::FONT_CASCADIA_CODE_NF)
.expect("load regular"),
);
data.insert(
FontData::from_static_slice(constants::FONT_CASCADIA_CODE_NF_ITALIC)
.expect("load italic"),
);
let bold = FontData::from_static_slice_with_wght(
constants::FONT_CASCADIA_CODE_NF,
Some(600.0),
)
.expect("load bold");
assert!(!bold.is_bold(), "test premise: bold face under 700");
data.insert(bold);
data.insert_alias(FONT_ID_REGULAR);
let lib = FontLibrary {
inner: Arc::new(parking_lot::RwLock::new(data)),
};
let style = crate::SpanStyle {
font_attrs: swash::Attributes::new(
swash::Stretch::NORMAL,
swash::Weight::BOLD,
swash::Style::Normal,
),
..Default::default()
};
let (font_id, _) = lib
.inner
.read()
.find_best_font_match_strict('A', &style, None)
.expect("cluster must resolve");
assert_eq!(
font_id, FONT_ID_BOLD,
"bold cells must use the configured bold slot"
);
}
#[test]
fn parse_font_features_forms() {
let entries: Vec<String> = ["ss01", "+dlig", "-liga", "cv01=2", "bogus_tag", ""]
.iter()
.map(|s| s.to_string())
.collect();
let parsed = parse_font_features(&entries);
let tag = |s: &str| {
let mut t = [b' '; 4];
t[..s.len()].copy_from_slice(s.as_bytes());
u32::from_be_bytes(t)
};
assert_eq!(parsed.len(), 4);
assert_eq!((parsed[0].tag, parsed[0].value), (tag("ss01"), 1));
assert_eq!((parsed[1].tag, parsed[1].value), (tag("dlig"), 1));
assert_eq!((parsed[2].tag, parsed[2].value), (tag("liga"), 0));
assert_eq!((parsed[3].tag, parsed[3].value), (tag("cv01"), 2));
}
#[test]
fn alias_shares_metrics_with_target() {
let mut lib = FontLibraryData::default();
lib.insert(
FontData::from_static_slice(constants::FONT_CASCADIA_CODE_NF)
.expect("load regular"),
);
lib.insert_alias(0);
let from_regular = lib.get_font_metrics(&0, 14.0).expect("regular metrics");
let from_alias = lib.get_font_metrics(&1, 14.0).expect("alias metrics");
assert_eq!(from_regular, from_alias);
}
}
#[allow(dead_code)]
fn find_font_path(
db: &crate::font::loader::Database,
font_family: String,
) -> Option<PathBuf> {
info!("Font path search: family '{font_family}'");
let query = crate::font::loader::Query {
families: &[crate::font::loader::Family::Name(&font_family)],
..crate::font::loader::Query::default()
};
if let Some(id) = db.query(&query) {
if let Some((crate::font::loader::Source::File(ref path), _index)) =
db.face_source(id)
{
return Some(path.to_path_buf());
}
}
None
}
#[cfg(not(target_arch = "wasm32"))]
fn load_from_font_source(path: &PathBuf) -> Option<SharedData> {
let cache = get_font_data_cache();
if let Some(cached_data) = cache.get(path) {
return Some(cached_data.clone());
}
let file = std::fs::File::open(path).ok()?;
let mmap = unsafe { memmap2::Mmap::map(&file).ok()? };
let shared_data = SharedData::from_mmap(mmap);
let entry = cache
.entry(path.clone())
.or_insert_with(|| shared_data.clone());
Some(entry.clone())
}
#[cfg(all(test, target_os = "macos"))]
mod postscript_resolver_tests {
use super::*;
#[test]
fn insert_populates_postscript_lookup() {
let handle =
crate::font::macos::FontHandle::from_static_bytes(FONT_CASCADIA_CODE_NF)
.expect("parse CascadiaMono");
let ps_name = handle.postscript_name();
let mut lib = FontLibraryData::default();
let font_data = FontData::from_static_slice(FONT_CASCADIA_CODE_NF)
.expect("load CascadiaMono");
lib.insert(font_data);
assert_eq!(
lib.font_id_for_postscript_name(&ps_name),
Some(0),
"inserted PS name '{ps_name}' should resolve to font_id 0"
);
assert_eq!(
lib.font_id_for_postscript_name("not-a-real-font"),
None,
"unknown PS names must return None, not a stale hit"
);
}
#[test]
fn duplicate_insert_keeps_first_id() {
let handle =
crate::font::macos::FontHandle::from_static_bytes(FONT_CASCADIA_CODE_NF)
.expect("parse CascadiaMono");
let ps_name = handle.postscript_name();
let mut lib = FontLibraryData::default();
lib.insert(FontData::from_static_slice(FONT_CASCADIA_CODE_NF).expect("load a"));
lib.insert(FontData::from_static_slice(FONT_CASCADIA_CODE_NF).expect("load b"));
assert_eq!(
lib.font_id_for_postscript_name(&ps_name),
Some(0),
"second insert of same face must not clobber the first's font_id"
);
}
#[test]
fn resolve_font_for_char_lazy_discovers_cascade_font() {
use crate::SpanStyle;
use std::sync::Arc;
let mut data = FontLibraryData::default();
data.insert(FontData::from_static_slice(FONT_CASCADIA_CODE_NF).expect("load"));
let lib = FontLibrary {
inner: Arc::new(parking_lot::RwLock::new(data)),
};
let starting_len = lib.inner.read().inner.len();
let style = SpanStyle::default();
let (font_id, _is_emoji) = lib.resolve_font_for_char('\u{6C34}', &style, None);
assert_ne!(
font_id, 0,
"lazy discovery should register a new font_id distinct from primary"
);
assert!(
font_id < lib.inner.read().inner.len(),
"returned font_id should index into the library"
);
assert_eq!(
lib.inner.read().inner.len(),
starting_len + 1,
"lazy discovery should have registered exactly one new font"
);
}
#[test]
fn resolve_font_for_char_reuses_discovered_font() {
use crate::SpanStyle;
use std::sync::Arc;
let mut data = FontLibraryData::default();
data.insert(FontData::from_static_slice(FONT_CASCADIA_CODE_NF).expect("load"));
let lib = FontLibrary {
inner: Arc::new(parking_lot::RwLock::new(data)),
};
let style = SpanStyle::default();
let (id_a, _) = lib.resolve_font_for_char('\u{6C34}', &style, None);
let len_after_first = lib.inner.read().inner.len();
let (id_b, _) = lib.resolve_font_for_char('\u{6728}', &style, None);
let len_after_second = lib.inner.read().inner.len();
assert_eq!(
id_a, id_b,
"two CJK codepoints from the same cascade font should reuse the same font_id"
);
assert_eq!(
len_after_first, len_after_second,
"the second resolve must not register a duplicate font"
);
}
}
#[cfg(test)]
mod glyph_registry_install_tests {
use super::*;
use crate::font::glyph_registry::GlyphRegistry;
#[test]
fn install_then_lookup_returns_same_arc() {
let library = FontLibrary::default();
let registry = GlyphRegistry::new();
library.install_glyph_registry(42, registry.clone());
let fetched = library
.glyph_registry_for(42)
.expect("entry installed at 42");
assert!(fetched.ptr_eq(®istry));
}
#[test]
fn lookup_returns_none_for_unknown_route() {
let library = FontLibrary::default();
assert!(library.glyph_registry_for(999).is_none());
}
#[test]
fn install_overwrites_same_route() {
let library = FontLibrary::default();
let first = GlyphRegistry::new();
let second = GlyphRegistry::new();
assert!(!first.ptr_eq(&second));
library.install_glyph_registry(7, first.clone());
library.install_glyph_registry(7, second.clone());
let fetched = library.glyph_registry_for(7).expect("entry at 7");
assert!(fetched.ptr_eq(&second));
assert!(!fetched.ptr_eq(&first));
}
#[test]
fn remove_drops_the_entry() {
let library = FontLibrary::default();
let registry = GlyphRegistry::new();
library.install_glyph_registry(3, registry);
assert!(library.glyph_registry_for(3).is_some());
library.remove_glyph_registry(3);
assert!(library.glyph_registry_for(3).is_none());
}
#[test]
fn distinct_routes_hold_distinct_registries() {
let library = FontLibrary::default();
let a = GlyphRegistry::new();
let b = GlyphRegistry::new();
library.install_glyph_registry(1, a.clone());
library.install_glyph_registry(2, b.clone());
assert!(library.glyph_registry_for(1).unwrap().ptr_eq(&a));
assert!(library.glyph_registry_for(2).unwrap().ptr_eq(&b));
}
}