use std::path::PathBuf;
use core_foundation::{
attributed_string::CFMutableAttributedString,
base::{CFRange, CFType, TCFType},
dictionary::CFDictionary,
number::CFNumber,
string::CFString,
url::{CFURLRef, CFURL},
};
use core_graphics::{
base::{kCGBitmapByteOrder32Little, kCGImageAlphaPremultipliedFirst, CGFloat},
color_space::{kCGColorSpaceDisplayP3, CGColorSpace},
context::{CGContext, CGTextDrawingMode},
font::CGGlyph,
geometry::{CGAffineTransform, CGPoint, CGRect, CGSize},
};
use core_text::{
font as ct_font,
font::{CTFont, CTFontRef},
font_collection,
font_descriptor::{
self, kCTFontBoldTrait, kCTFontFamilyNameAttribute, kCTFontItalicTrait,
kCTFontMonoSpaceTrait, kCTFontOrientationDefault, kCTFontStyleNameAttribute,
kCTFontSymbolicTrait, kCTFontTraitsAttribute, kCTFontVariationAttribute,
CTFontDescriptor, CTFontDescriptorCopyAttribute, CTFontDescriptorRef,
},
font_manager,
line::CTLine,
run::CTRun,
string_attributes::kCTFontAttributeName,
};
#[allow(non_upper_case_globals)]
const kCGImageAlphaOnly: u32 = 7;
type CTFontManagerScope = u32;
#[allow(non_upper_case_globals)]
const kCTFontManagerScopeProcess: CTFontManagerScope = 1;
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
fn CFDataCreateWithBytesNoCopy(
allocator: core_foundation::base::CFAllocatorRef,
bytes: *const u8,
length: core_foundation::base::CFIndex,
bytes_deallocator: core_foundation::base::CFAllocatorRef,
) -> core_foundation::data::CFDataRef;
#[allow(non_upper_case_globals)]
static kCFAllocatorNull: core_foundation::base::CFAllocatorRef;
}
#[allow(non_snake_case)]
#[link(name = "CoreText", kind = "framework")]
extern "C" {
fn CTFontManagerRegisterFontsForURL(
fontURL: CFURLRef,
scope: CTFontManagerScope,
error: *mut core_foundation::base::CFTypeRef,
) -> bool;
fn CTFontCreateCopyWithAttributes(
font: CTFontRef,
size: CGFloat,
matrix: *const CGAffineTransform,
attributes: CTFontDescriptorRef,
) -> CTFontRef;
fn CTFontManagerCreateFontDescriptorsFromData(
data: core_foundation::data::CFDataRef,
) -> core_foundation::array::CFArrayRef;
fn CTFontCreateForString(
current_font: CTFontRef,
string: core_foundation::string::CFStringRef,
range: CFRange,
) -> CTFontRef;
}
const SYNTHETIC_ITALIC_SKEW: CGAffineTransform = CGAffineTransform {
a: 1.0,
b: 0.0,
c: 0.267_949,
d: 1.0,
tx: 0.0,
ty: 0.0,
};
fn ct_font_sheared(base: &CTFont, size: f64) -> CTFont {
use core_foundation::base::TCFType;
unsafe {
let raw = CTFontCreateCopyWithAttributes(
base.as_concrete_TypeRef(),
size as CGFloat,
&SYNTHETIC_ITALIC_SKEW,
std::ptr::null(),
);
CTFont::wrap_under_create_rule(raw)
}
}
pub fn register_fonts_in_dir(dir: &std::path::Path) {
let walker = walkdir::WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok());
for entry in walker {
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
continue;
};
let ext = ext.to_ascii_lowercase();
if !matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc") {
continue;
}
let Some(url) = CFURL::from_path(path, false) else {
continue;
};
let mut err: core_foundation::base::CFTypeRef = std::ptr::null();
let ok = unsafe {
CTFontManagerRegisterFontsForURL(
url.as_concrete_TypeRef(),
kCTFontManagerScopeProcess,
&mut err,
)
};
if !ok {
tracing::debug!(
"CTFontManagerRegisterFontsForURL skipped {}",
path.display()
);
}
}
}
#[derive(Clone)]
pub struct FontHandle {
base_font: CTFont,
}
impl FontHandle {
pub fn from_bytes(font_bytes: &[u8]) -> Option<Self> {
let desc = font_manager::create_font_descriptor(font_bytes).ok()?;
let base_font = ct_font::new_from_descriptor(&desc, 1.0);
Some(Self { base_font })
}
pub fn from_bytes_index(font_bytes: &[u8], index: usize) -> Option<Self> {
use core_foundation::array::CFArray;
use core_foundation::base::TCFType;
use core_foundation::data::CFData;
let data = CFData::from_buffer(font_bytes);
let array_ref = unsafe {
CTFontManagerCreateFontDescriptorsFromData(data.as_concrete_TypeRef())
};
if array_ref.is_null() {
return None;
}
let descriptors: CFArray<CTFontDescriptor> =
unsafe { CFArray::wrap_under_create_rule(array_ref) };
let desc_ref = descriptors.get(index as isize)?;
let base_font = ct_font::new_from_descriptor(&desc_ref, 1.0);
Some(Self { base_font })
}
pub fn from_static_bytes(font_bytes: &'static [u8]) -> Option<Self> {
use core_foundation::base::{CFIndex, TCFType};
use core_foundation::data::CFData;
let data_ref = unsafe {
CFDataCreateWithBytesNoCopy(
std::ptr::null(), font_bytes.as_ptr(),
font_bytes.len() as CFIndex,
kCFAllocatorNull, )
};
if data_ref.is_null() {
return None;
}
let data = unsafe { CFData::wrap_under_create_rule(data_ref) };
let desc = font_manager::create_font_descriptor_with_data(data).ok()?;
let base_font = ct_font::new_from_descriptor(&desc, 1.0);
Some(Self { base_font })
}
pub fn from_path(path: &std::path::Path) -> Option<Self> {
use core_foundation::array::CFArray;
let url = CFURL::from_path(path, false)?;
let array_ref = unsafe {
core_text::font_manager::CTFontManagerCreateFontDescriptorsFromURL(
url.as_concrete_TypeRef(),
)
};
if array_ref.is_null() {
return None;
}
let descriptors: CFArray<CTFontDescriptor> =
unsafe { CFArray::wrap_under_create_rule(array_ref) };
let desc_ref = descriptors.get(0)?;
let base_font = ct_font::new_from_descriptor(&desc_ref, 1.0);
Some(Self { base_font })
}
pub fn postscript_name(&self) -> String {
self.base_font.postscript_name()
}
pub fn with_wght_variation(self, value: f32) -> Option<Self> {
use core_foundation::base::TCFType;
use core_foundation::dictionary::CFDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
const WGHT_TAG: i64 = u32::from_be_bytes(*b"wght") as i64;
let id_num = CFNumber::from(WGHT_TAG);
let val_num = CFNumber::from(value as f64);
let variation: CFDictionary<CFType, CFType> =
CFDictionary::from_CFType_pairs(&[(id_num.as_CFType(), val_num.as_CFType())]);
let var_attr_key =
unsafe { CFString::wrap_under_get_rule(kCTFontVariationAttribute) };
let attrs: CFDictionary<CFString, CFType> =
CFDictionary::from_CFType_pairs(&[(var_attr_key, variation.as_CFType())]);
let desc = font_descriptor::new_from_attributes(&attrs);
let derived_ref = unsafe {
CTFontCreateCopyWithAttributes(
self.base_font.as_concrete_TypeRef(),
0.0,
std::ptr::null(),
desc.as_concrete_TypeRef(),
)
};
if derived_ref.is_null() {
return None;
}
let derived = unsafe { CTFont::wrap_under_create_rule(derived_ref) };
Some(Self { base_font: derived })
}
}
#[derive(Debug)]
pub struct RasterizedGlyph {
pub width: u32,
pub height: u32,
pub left: i32,
pub top: i32,
pub is_color: bool,
pub bytes: Vec<u8>,
}
pub fn rasterize_glyph(
handle: &FontHandle,
glyph_id: u16,
size_px: f32,
is_color: bool,
synthetic_italic: bool,
synthetic_bold: bool,
) -> Option<RasterizedGlyph> {
let ct_font = if synthetic_italic {
ct_font_sheared(&handle.base_font, size_px as f64)
} else {
handle.base_font.clone_with_font_size(size_px as f64)
};
let glyphs = [glyph_id as CGGlyph];
let mut raw_bounds =
ct_font.get_bounding_rects_for_glyphs(kCTFontOrientationDefault, &glyphs);
if synthetic_bold && !is_color {
let line_width = (size_px as f64 / 14.0).max(1.0);
raw_bounds.size.width += line_width;
raw_bounds.size.height += line_width;
raw_bounds.origin.x -= line_width / 2.0;
raw_bounds.origin.y -= line_width / 2.0;
}
let bounds =
if is_color && (raw_bounds.size.width <= 0.0 || raw_bounds.size.height <= 0.0) {
let ascent = ct_font.ascent();
let descent = ct_font.descent();
let mut advance = CGSize::new(0.0, 0.0);
unsafe {
ct_font.get_advances_for_glyphs(
kCTFontOrientationDefault,
glyphs.as_ptr(),
&mut advance,
1,
);
}
if advance.width <= 0.0 || ascent + descent <= 0.0 {
return Some(RasterizedGlyph {
width: 0,
height: 0,
left: 0,
top: 0,
is_color,
bytes: Vec::new(),
});
}
CGRect::new(
&CGPoint::new(0.0, -descent),
&CGSize::new(advance.width, ascent + descent),
)
} else if raw_bounds.size.width <= 0.0 || raw_bounds.size.height <= 0.0 {
return Some(RasterizedGlyph {
width: 0,
height: 0,
left: 0,
top: 0,
is_color,
bytes: Vec::new(),
});
} else {
raw_bounds
};
const PAD: i32 = 1;
let left = (bounds.origin.x.floor() as i32) - PAD;
let bottom = (bounds.origin.y.floor() as i32) - PAD;
let width = ((bounds.size.width.ceil() as i32) + 2 * PAD).max(1) as usize;
let height = ((bounds.size.height.ceil() as i32) + 2 * PAD).max(1) as usize;
let top = bottom + height as i32;
let (mut bytes, cx) = if is_color {
let mut bytes = vec![0u8; width * height * 4];
let colorspace =
CGColorSpace::create_with_name(unsafe { kCGColorSpaceDisplayP3 })
.unwrap_or_else(CGColorSpace::create_device_rgb);
let cx = CGContext::create_bitmap_context(
Some(bytes.as_mut_ptr() as *mut _),
width,
height,
8,
width * 4,
&colorspace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little,
);
(bytes, cx)
} else {
let mut bytes = vec![0u8; width * height];
let cx = CGContext::create_bitmap_context(
Some(bytes.as_mut_ptr() as *mut _),
width,
height,
8,
width,
&CGColorSpace::create_device_gray(),
kCGImageAlphaOnly,
);
(bytes, cx)
};
cx.set_should_antialias(true);
cx.set_allows_antialiasing(true);
cx.set_should_smooth_fonts(true);
cx.set_gray_fill_color(0.0, 1.0);
if synthetic_bold {
cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFillStroke);
let line_width = (size_px as CGFloat / 14.0).max(1.0);
cx.set_line_width(line_width);
} else {
cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFill);
}
cx.set_allows_font_subpixel_positioning(false);
cx.set_should_subpixel_position_fonts(false);
cx.set_allows_font_subpixel_quantization(false);
cx.set_should_subpixel_quantize_fonts(false);
let origin = CGPoint::new(-left as CGFloat, -bottom as CGFloat);
ct_font.draw_glyphs(&glyphs, &[origin], cx);
if is_color {
bgra_to_rgba_in_place(&mut bytes);
}
Some(RasterizedGlyph {
width: width as u32,
height: height as u32,
left,
top,
is_color,
bytes,
})
}
pub fn find_font_path(
family: &str,
bold: bool,
italic: bool,
style_name: Option<&str>,
) -> Option<PathBuf> {
use core_foundation::array::CFArray;
let family_cf = CFString::new(family);
let family_key = unsafe { CFString::wrap_under_get_rule(kCTFontFamilyNameAttribute) };
let mut pairs: Vec<(CFString, CFType)> = vec![(family_key, family_cf.as_CFType())];
let mut symbolic: u32 = 0;
if style_name.is_none() {
if bold {
symbolic |= kCTFontBoldTrait;
}
if italic {
symbolic |= kCTFontItalicTrait;
}
}
if symbolic != 0 {
let symbolic_key = unsafe { CFString::wrap_under_get_rule(kCTFontSymbolicTrait) };
let traits: CFDictionary<CFString, CFType> =
CFDictionary::from_CFType_pairs(&[(
symbolic_key,
CFNumber::from(symbolic as i64).as_CFType(),
)]);
let traits_attr_key =
unsafe { CFString::wrap_under_get_rule(kCTFontTraitsAttribute) };
pairs.push((traits_attr_key, traits.as_CFType()));
}
if let Some(name) = style_name {
let style_key =
unsafe { CFString::wrap_under_get_rule(kCTFontStyleNameAttribute) };
pairs.push((style_key, CFString::new(name).as_CFType()));
}
let attrs: CFDictionary<CFString, CFType> = CFDictionary::from_CFType_pairs(&pairs);
let desc = font_descriptor::new_from_attributes(&attrs);
let descs_arr = CFArray::from_CFTypes(&[desc]);
let collection = font_collection::new_from_descriptors(&descs_arr);
let candidates = collection.get_descriptors()?;
let desired_styles = derive_desired_styles(bold, italic, style_name);
candidates
.iter()
.max_by_key(|d| score_candidate(d, bold, italic, &desired_styles))
.and_then(|d| d.font_path())
}
fn derive_desired_styles(
bold: bool,
italic: bool,
style_name: Option<&str>,
) -> Vec<String> {
if let Some(user) = style_name {
return vec![user.to_string()];
}
let primary = match (bold, italic) {
(true, true) => "Bold Italic",
(true, false) => "Bold",
(false, true) => "Italic",
(false, false) => "Regular",
};
vec![primary.to_string()]
}
fn score_candidate(
desc: &CTFontDescriptor,
want_bold: bool,
want_italic: bool,
desired_styles: &[String],
) -> (bool, bool, bool, bool, u8, u16) {
let font = ct_font::new_from_descriptor(desc, 12.0);
let traits = font.symbolic_traits();
let mut is_bold = (traits & kCTFontBoldTrait) != 0;
let mut is_italic = (traits & kCTFontItalicTrait) != 0;
let monospace = (traits & kCTFontMonoSpaceTrait) != 0;
apply_head_table_traits(&font, &mut is_bold, &mut is_italic);
apply_os2_table_traits(&font, &mut is_bold, &mut is_italic);
apply_variation_overrides(desc, &font, &mut is_bold, &mut is_italic);
let style_str = desc.style_name();
let style_lower = style_str.to_ascii_lowercase();
let exact_style = desired_styles
.first()
.map(|s| s.eq_ignore_ascii_case(&style_str))
.unwrap_or(false);
let mut diff: usize = style_str.len().min(255);
for s in desired_styles {
if style_lower.contains(&s.to_ascii_lowercase()) {
diff = diff.saturating_sub(s.len());
}
}
let fuzzy_style = (255usize.saturating_sub(diff)).min(255) as u8;
let glyph_count = (font.glyph_count() as u64).min(u16::MAX as u64) as u16;
(
monospace,
exact_style,
is_italic == want_italic,
is_bold == want_bold,
fuzzy_style,
glyph_count,
)
}
fn apply_head_table_traits(font: &CTFont, is_bold: &mut bool, is_italic: &mut bool) {
let Some(data) = font.get_font_table(u32::from_be_bytes(*b"head")) else {
return;
};
let bytes = data.bytes();
if bytes.len() < 46 {
return;
}
let mac_style = u16::from_be_bytes([bytes[44], bytes[45]]);
if mac_style & 0x0001 != 0 {
*is_bold = true;
}
if mac_style & 0x0002 != 0 {
*is_italic = true;
}
}
fn apply_os2_table_traits(font: &CTFont, is_bold: &mut bool, is_italic: &mut bool) {
let Some(data) = font.get_font_table(u32::from_be_bytes(*b"OS/2")) else {
return;
};
let bytes = data.bytes();
if bytes.len() < 64 {
return;
}
let fs_selection = u16::from_be_bytes([bytes[62], bytes[63]]);
if fs_selection & 0x0001 != 0 {
*is_italic = true;
}
if fs_selection & 0x0020 != 0 {
*is_bold = true;
}
}
fn apply_variation_overrides(
desc: &CTFontDescriptor,
font: &CTFont,
is_bold: &mut bool,
is_italic: &mut bool,
) {
use core_foundation::base::CFType;
use core_foundation::dictionary::CFDictionary as CFDict;
use core_foundation::number::CFNumber;
let var_value = unsafe {
CTFontDescriptorCopyAttribute(
desc.as_concrete_TypeRef(),
kCTFontVariationAttribute,
)
};
if var_value.is_null() {
return;
}
let values_untyped: CFDict<CFType, CFType> =
unsafe { CFDict::wrap_under_create_rule(var_value as _) };
let Some(axes) = font.get_variation_axes() else {
return;
};
let id_key = unsafe { kCTFontVariationAxisIdentifierKeyFFI };
const WGHT_TAG: i64 = u32::from_be_bytes(*b"wght") as i64;
const ITAL_TAG: i64 = u32::from_be_bytes(*b"ital") as i64;
const SLNT_TAG: i64 = u32::from_be_bytes(*b"slnt") as i64;
let mut ital_seen = false;
for axis in axes.iter() {
let Some(id_item) = axis.find(id_key) else {
continue;
};
let Some(id_num) = id_item.downcast::<CFNumber>() else {
continue;
};
let Some(tag) = id_num.to_i64() else {
continue;
};
let id_as_key: CFType = id_num.as_CFType();
let val: f64 = match values_untyped.find(&id_as_key) {
Some(v) => match v.downcast::<CFNumber>() {
Some(n) => n.to_f64().unwrap_or(0.0),
None => continue,
},
None => continue,
};
match tag {
WGHT_TAG => *is_bold = val > 600.0,
ITAL_TAG => {
*is_italic = val > 0.5;
ital_seen = true;
}
SLNT_TAG if !ital_seen => *is_italic = val <= -5.0,
_ => {}
}
}
}
#[link(name = "CoreText", kind = "framework")]
extern "C" {
#[link_name = "kCTFontVariationAxisIdentifierKey"]
static kCTFontVariationAxisIdentifierKeyFFI: core_foundation::string::CFStringRef;
}
pub fn default_cascade_list(handle: &FontHandle) -> Vec<PathBuf> {
use core_foundation::array::CFArray;
let languages: CFArray<CFString> = CFArray::from_CFTypes(&[]);
let cascade =
core_text::font::cascade_list_for_languages(&handle.base_font, &languages);
cascade.iter().filter_map(|desc| desc.font_path()).collect()
}
pub fn all_families() -> Vec<String> {
let collection = font_collection::create_for_all_families();
let Some(descriptors) = collection.get_descriptors() else {
return Vec::new();
};
let mut families: Vec<String> =
descriptors.iter().map(|desc| desc.family_name()).collect();
families.sort_unstable();
families.dedup();
families
}
fn bgra_to_rgba_in_place(bytes: &mut [u8]) {
for px in bytes.chunks_exact_mut(4) {
px.swap(0, 2);
}
}
pub fn design_unit_metrics(handle: &FontHandle) -> swash::Metrics {
let ct = &handle.base_font;
let upem = ct.units_per_em() as f32;
let ascent = ct.ascent() as f32 * upem;
let descent = ct.descent() as f32 * upem;
let leading = ct.leading() as f32 * upem;
let underline_offset = ct.underline_position() as f32 * upem;
let stroke_size = ct.underline_thickness() as f32 * upem;
let x_height = ct.x_height() as f32 * upem;
let cap_height = ct.cap_height() as f32 * upem;
let (strikeout_offset, strikeout_stroke) = read_os2_strikeout(ct, 1.0)
.map(|(off, thick)| (off * upem, thick * upem))
.unwrap_or((x_height * 0.5, stroke_size));
let is_monospace = (ct.symbolic_traits() & (1 << 10)) != 0;
swash::Metrics {
units_per_em: upem as u16,
glyph_count: ct.glyph_count() as u16,
is_monospace,
has_vertical_metrics: false,
ascent,
descent,
leading,
vertical_ascent: 0.0,
vertical_descent: 0.0,
vertical_leading: 0.0,
cap_height,
x_height,
average_width: 0.0,
max_width: 0.0,
underline_offset,
strikeout_offset,
stroke_size: strikeout_stroke.max(stroke_size),
}
}
pub fn cjk_ic_width(handle: &FontHandle) -> Option<f64> {
const WATER: char = '\u{6C34}';
advance_units_for_char(handle, WATER).and_then(|(units, _upem)| {
if units > 0.0 {
Some(units as f64)
} else {
None
}
})
}
pub fn advance_units_for_char(handle: &FontHandle, ch: char) -> Option<(f32, u16)> {
use core_foundation::base::CFIndex;
use core_graphics::geometry::CGSize;
let mut utf16 = [0u16; 2];
let encoded = ch.encode_utf16(&mut utf16);
let count = encoded.len();
let mut glyphs = [0 as CGGlyph; 2];
let ok = unsafe {
handle.base_font.get_glyphs_for_characters(
utf16.as_ptr(),
glyphs.as_mut_ptr(),
count as CFIndex,
)
};
if !ok || glyphs[0] == 0 {
return None;
}
let mut advance = CGSize::new(0.0, 0.0);
unsafe {
handle.base_font.get_advances_for_glyphs(
kCTFontOrientationDefault,
glyphs.as_ptr(),
&mut advance,
1,
);
}
let units_per_em = handle.base_font.units_per_em() as u16;
Some((advance.width as f32 * units_per_em as f32, units_per_em))
}
pub fn max_ascii_advance_px(handle: &FontHandle, size_px: f32) -> Option<f32> {
use core_foundation::base::CFIndex;
use core_graphics::geometry::CGSize;
if size_px <= 0.0 {
return None;
}
let ct_font = handle.base_font.clone_with_font_size(size_px as f64);
const FIRST: u16 = 0x20;
const LAST: u16 = 0x7E;
const COUNT: usize = (LAST - FIRST + 1) as usize;
let mut utf16 = [0u16; COUNT];
for (i, slot) in utf16.iter_mut().enumerate() {
*slot = FIRST + i as u16;
}
let mut glyphs = [0 as CGGlyph; COUNT];
let ok = unsafe {
ct_font.get_glyphs_for_characters(
utf16.as_ptr(),
glyphs.as_mut_ptr(),
COUNT as CFIndex,
)
};
if !ok {
return None;
}
let mut advances = [CGSize::new(0.0, 0.0); COUNT];
unsafe {
ct_font.get_advances_for_glyphs(
kCTFontOrientationDefault,
glyphs.as_ptr(),
advances.as_mut_ptr(),
COUNT as CFIndex,
);
}
let mut max_px: f32 = 0.0;
for i in 0..COUNT {
if glyphs[i] == 0 {
continue;
}
let w = advances[i].width as f32;
if w > max_px {
max_px = w;
}
}
if max_px <= 0.0 {
None
} else {
Some(max_px)
}
}
#[derive(Debug, Clone, Copy)]
pub struct FontAttributes {
pub weight: u16,
pub is_bold: bool,
pub is_italic: bool,
pub is_monospace: bool,
pub is_color: bool,
}
pub fn font_attributes(handle: &FontHandle) -> FontAttributes {
const K_CTFONT_TRAIT_ITALIC: u32 = 1 << 0;
const K_CTFONT_TRAIT_BOLD: u32 = 1 << 1;
const K_CTFONT_TRAIT_MONOSPACE: u32 = 1 << 10;
const K_CTFONT_TRAIT_COLOR_GLYPHS: u32 = 1 << 13;
let traits: u32 = handle.base_font.symbolic_traits();
let is_bold = (traits & K_CTFONT_TRAIT_BOLD) != 0;
FontAttributes {
weight: if is_bold { 700 } else { 400 },
is_bold,
is_italic: (traits & K_CTFONT_TRAIT_ITALIC) != 0,
is_monospace: (traits & K_CTFONT_TRAIT_MONOSPACE) != 0,
is_color: (traits & K_CTFONT_TRAIT_COLOR_GLYPHS) != 0,
}
}
pub fn discover_fallback(primary: &FontHandle, ch: char) -> Option<FontHandle> {
use core_foundation::base::CFIndex;
let ch_str = ch.to_string();
let cf_string = CFString::new(&ch_str);
let range = CFRange::init(0, ch.len_utf16() as CFIndex);
let ctfont_ref = unsafe {
CTFontCreateForString(
primary.base_font.as_concrete_TypeRef(),
cf_string.as_concrete_TypeRef(),
range,
)
};
if ctfont_ref.is_null() {
return None;
}
let ct = unsafe { CTFont::wrap_under_create_rule(ctfont_ref) };
Some(FontHandle {
base_font: ct.clone_with_font_size(1.0),
})
}
pub fn font_has_char(handle: &FontHandle, ch: char) -> bool {
use core_foundation::base::CFIndex;
let mut utf16 = [0u16; 2];
let encoded = ch.encode_utf16(&mut utf16);
let count = encoded.len();
let mut glyphs = [0 as CGGlyph; 2];
let ok = unsafe {
handle.base_font.get_glyphs_for_characters(
utf16.as_ptr(),
glyphs.as_mut_ptr(),
count as CFIndex,
)
};
ok && glyphs[0] != 0
}
#[derive(Debug, Clone, Copy)]
pub struct FontMetrics {
pub ascent: f32,
pub descent: f32,
pub leading: f32,
pub underline_offset: f32,
pub underline_thickness: f32,
pub strikeout_offset: f32,
pub strikeout_thickness: f32,
pub x_height: f32,
}
pub fn font_metrics(handle: &FontHandle, size_px: f32) -> FontMetrics {
let ct_font = handle.base_font.clone_with_font_size(size_px as f64);
let ascent = ct_font.ascent() as f32;
let descent = ct_font.descent() as f32;
let leading = ct_font.leading() as f32;
let underline_offset = ct_font.underline_position() as f32;
let underline_thickness = ct_font.underline_thickness() as f32;
let x_height = ct_font.x_height() as f32;
let (strikeout_offset, strikeout_thickness) = read_os2_strikeout(&ct_font, size_px)
.unwrap_or((x_height * 0.5, underline_thickness));
FontMetrics {
ascent,
descent,
leading,
underline_offset,
underline_thickness,
strikeout_offset,
strikeout_thickness,
x_height,
}
}
fn read_os2_strikeout(ct_font: &CTFont, size_px: f32) -> Option<(f32, f32)> {
const OS2_TAG: u32 = u32::from_be_bytes(*b"OS/2");
let cg_font = ct_font.copy_to_CGFont();
let table = cg_font.copy_table_for_tag(OS2_TAG)?;
let bytes = table.bytes();
if bytes.len() < 30 {
return None;
}
let size_units = i16::from_be_bytes([bytes[26], bytes[27]]);
let pos_units = i16::from_be_bytes([bytes[28], bytes[29]]);
if size_units == 0 && pos_units == 0 {
return None;
}
let units_per_em = ct_font.units_per_em() as f32;
if units_per_em <= 0.0 {
return None;
}
let scale = size_px / units_per_em;
Some((pos_units as f32 * scale, size_units as f32 * scale))
}
#[derive(Debug, Clone, Copy)]
pub struct ShapedGlyph {
pub id: u16,
pub x: f32,
pub y: f32,
pub advance: f32,
pub cluster: u32,
}
pub fn shape_text(handle: &FontHandle, text: &str, size_px: f32) -> Vec<ShapedGlyph> {
if text.is_empty() {
return Vec::new();
}
let primary_ct_font = handle.base_font.clone_with_font_size(size_px as f64);
let mut attr = CFMutableAttributedString::new();
attr.replace_str(&CFString::new(text), CFRange::init(0, 0));
let utf16_len = attr.char_len();
unsafe {
attr.set_attribute(
CFRange::init(0, utf16_len),
kCTFontAttributeName,
&primary_ct_font,
);
}
let line = CTLine::new_with_attributed_string(attr.as_concrete_TypeRef());
let utf16_to_utf8 = if text.is_ascii() {
None
} else {
Some(build_utf16_to_utf8_map(text))
};
let line_width = line.get_typographic_bounds().width as f32;
let glyph_runs = line.glyph_runs();
let runs: Vec<CTRun> = glyph_runs.iter().map(|r| (*r).clone()).collect();
if runs.is_empty() {
return Vec::new();
}
let mut shaped = Vec::new();
let mut pen_x = 0.0f32;
for run_idx in 0..runs.len() {
let run = &runs[run_idx];
let glyphs = run.glyphs();
if glyphs.is_empty() {
continue;
}
let positions = run.positions();
let indices = run.string_indices();
let n = glyphs.len();
let after_run_x = runs[run_idx + 1..]
.iter()
.find_map(|r| r.positions().first().map(|p| p.x as f32))
.unwrap_or(line_width);
for i in 0..n {
let pos_x = positions[i].x as f32;
let next_x = if i + 1 < n {
positions[i + 1].x as f32
} else {
after_run_x
};
let advance = next_x - pos_x;
let offset_x = pos_x - pen_x;
let offset_y = positions[i].y as f32;
let utf16_idx = indices[i] as usize;
let cluster = match &utf16_to_utf8 {
Some(map) => map.get(utf16_idx).copied().unwrap_or(0) as u32,
None => utf16_idx as u32,
};
shaped.push(ShapedGlyph {
id: glyphs[i],
x: offset_x,
y: offset_y,
advance,
cluster,
});
pen_x = next_x;
}
}
shaped
}
fn build_utf16_to_utf8_map(text: &str) -> Vec<usize> {
let mut map = Vec::with_capacity(text.len());
for (byte_idx, ch) in text.char_indices() {
for _ in 0..ch.len_utf16() {
map.push(byte_idx);
}
}
map.push(text.len());
map
}
pub fn shape_text_utf16(
handle: &FontHandle,
utf16: &[u16],
size_px: f32,
) -> Vec<ShapedGlyph> {
if utf16.is_empty() {
return Vec::new();
}
let primary_ct_font = handle.base_font.clone_with_font_size(size_px as f64);
let cf_string = unsafe {
use core_foundation::base::{kCFAllocatorDefault, kCFAllocatorNull, TCFType};
use core_foundation::string::{CFString, CFStringCreateWithCharactersNoCopy};
let string_ref = CFStringCreateWithCharactersNoCopy(
kCFAllocatorDefault,
utf16.as_ptr(),
utf16.len() as core_foundation::base::CFIndex,
kCFAllocatorNull,
);
if string_ref.is_null() {
return Vec::new();
}
CFString::wrap_under_create_rule(string_ref)
};
let mut attr = CFMutableAttributedString::new();
attr.replace_str(&cf_string, CFRange::init(0, 0));
let utf16_len = attr.char_len();
unsafe {
attr.set_attribute(
CFRange::init(0, utf16_len),
kCTFontAttributeName,
&primary_ct_font,
);
}
let line = CTLine::new_with_attributed_string(attr.as_concrete_TypeRef());
let line_width = line.get_typographic_bounds().width as f32;
let glyph_runs = line.glyph_runs();
let runs: Vec<CTRun> = glyph_runs.iter().map(|r| (*r).clone()).collect();
if runs.is_empty() {
return Vec::new();
}
let mut shaped = Vec::new();
let mut pen_x = 0.0f32;
for run_idx in 0..runs.len() {
let run = &runs[run_idx];
let glyphs = run.glyphs();
if glyphs.is_empty() {
continue;
}
let positions = run.positions();
let indices = run.string_indices();
let n = glyphs.len();
let after_run_x = runs[run_idx + 1..]
.iter()
.find_map(|r| r.positions().first().map(|p| p.x as f32))
.unwrap_or(line_width);
for i in 0..n {
let pos_x = positions[i].x as f32;
let next_x = if i + 1 < n {
positions[i + 1].x as f32
} else {
after_run_x
};
let advance = next_x - pos_x;
let offset_x = pos_x - pen_x;
let offset_y = positions[i].y as f32;
shaped.push(ShapedGlyph {
id: glyphs[i],
x: offset_x,
y: offset_y,
advance,
cluster: indices[i] as u32,
});
pen_x = next_x;
}
}
shaped
}
#[cfg(test)]
mod tests {
use super::*;
use crate::font::constants::FONT_CASCADIA_CODE_NF;
use core_foundation::base::CFIndex;
fn glyph_id_for_char(handle: &FontHandle, size: f64, ch: char) -> u16 {
let ct_font = handle.base_font.clone_with_font_size(size);
let mut utf16 = [0u16; 2];
let encoded = ch.encode_utf16(&mut utf16);
let count = encoded.len();
let mut glyphs = [0 as CGGlyph; 2];
let ok = unsafe {
ct_font.get_glyphs_for_characters(
utf16.as_ptr(),
glyphs.as_mut_ptr(),
count as CFIndex,
)
};
assert!(ok, "{ch:?} not in font");
glyphs[0]
}
#[test]
fn shapes_ascii_monospace() {
let handle = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load font");
let glyphs = shape_text(&handle, "Hello", 18.0);
assert_eq!(glyphs.len(), 5, "one glyph per ASCII char");
let first_advance = glyphs[0].advance;
for g in &glyphs {
assert!(
(g.advance - first_advance).abs() < 0.01,
"expected constant advance in monospace, got {:?}",
glyphs
);
}
for g in &glyphs {
assert!(
g.x.abs() < 0.001,
"x offset should be zero for LTR Latin, got {}",
g.x
);
assert!(g.y.abs() < 0.001, "y offset should be zero, got {}", g.y);
}
for (i, g) in glyphs.iter().enumerate() {
assert_eq!(g.cluster, i as u32);
}
}
#[test]
fn reads_strikeout_from_os2_table() {
let handle = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load font");
let m = font_metrics(&handle, 24.0);
assert!(m.strikeout_thickness > 0.0, "thickness should be positive");
assert!(m.strikeout_offset > 0.0);
assert!(
m.strikeout_offset < m.ascent,
"strikeout offset should be below ascent"
);
eprintln!(
"CascadiaMono @24px: strikeout offset={} size={} x_height={}",
m.strikeout_offset, m.strikeout_thickness, m.x_height
);
}
#[test]
fn shape_ascii_skips_utf16_map() {
let handle = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load font");
let glyphs = shape_text(&handle, "abcde", 18.0);
for (i, g) in glyphs.iter().enumerate() {
assert_eq!(g.cluster, i as u32);
}
}
#[test]
fn shape_non_ascii_keeps_correct_clusters() {
let handle = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load font");
let glyphs = shape_text(&handle, "aébc", 18.0);
assert_eq!(glyphs.len(), 4);
assert_eq!(glyphs[0].cluster, 0);
assert_eq!(glyphs[1].cluster, 1);
assert_eq!(glyphs[2].cluster, 3);
assert_eq!(glyphs[3].cluster, 4);
}
#[test]
fn shapes_empty_input() {
let handle = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load font");
assert!(shape_text(&handle, "", 18.0).is_empty());
}
#[test]
fn discover_fallback_handles_covered_char() {
let primary =
FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load primary");
let result = discover_fallback(&primary, 'A');
assert!(
result.is_some(),
"discover_fallback should return a font for a covered char"
);
}
#[test]
fn discover_fallback_returns_a_font_for_cjk() {
let primary =
FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load primary");
let fallback = discover_fallback(&primary, '\u{6C34}')
.expect("CoreText should cascade to a CJK font for 水");
assert!(
font_has_char(&fallback, '\u{6C34}'),
"CTFontCreateForString returned a font that doesn't cover U+6C34"
);
}
#[test]
fn shape_cascades_cjk_and_keeps_advances_positive() {
let handle = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load font");
let glyphs = shape_text(&handle, "A水B", 18.0);
assert!(!glyphs.is_empty(), "expected at least one glyph, got none");
for g in &glyphs {
assert!(
g.advance > 0.0,
"non-positive advance {} at cluster {}",
g.advance,
g.cluster
);
}
let mut cursor = 0.0f32;
for g in &glyphs {
let glyph_x = cursor + g.x;
assert!(
glyph_x + 0.001 >= cursor - g.advance,
"pen moved backwards at cluster {}: cursor={}, glyph_x={}",
g.cluster,
cursor,
glyph_x
);
cursor += g.advance;
}
let total: f32 = glyphs.iter().map(|g| g.advance).sum();
assert!(total > 0.0, "expected positive total advance, got {total}");
}
#[test]
fn static_bytes_path_rasterizes() {
let handle = FontHandle::from_static_bytes(FONT_CASCADIA_CODE_NF)
.expect("static bytes should parse");
let size = 18.0;
let gid = glyph_id_for_char(&handle, size as f64, 'M');
let g = rasterize_glyph(&handle, gid, size, false, false, false)
.expect("rasterize returned None");
assert!(g.width > 0 && g.height > 0);
assert!(g.bytes.iter().any(|&b| b > 0));
}
#[test]
fn rasterizes_an_inked_glyph() {
let handle = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load font");
let size = 24.0;
let gid = glyph_id_for_char(&handle, size as f64, 'A');
let g = rasterize_glyph(&handle, gid, size, false, false, false)
.expect("rasterize returned None");
assert!(g.width > 0, "A should have non-zero width");
assert!(g.height > 0, "A should have non-zero height");
assert!(!g.is_color);
assert_eq!(g.bytes.len(), (g.width * g.height) as usize);
let total: u64 = g.bytes.iter().map(|&b| b as u64).sum();
assert!(total > 0, "A should have some inked pixels");
}
#[test]
fn find_font_path_resolves_system_family() {
let path =
find_font_path("Menlo", false, false, None).expect("Menlo should resolve");
assert!(path.exists(), "resolved path should exist: {path:?}");
assert!(
path.extension()
.is_some_and(|e| e == "ttf" || e == "ttc" || e == "otf"),
"unexpected font extension: {path:?}"
);
}
#[test]
fn default_cascade_list_is_nonempty() {
let handle = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load font");
let paths = default_cascade_list(&handle);
assert!(
!paths.is_empty(),
"CoreText should surface a non-empty cascade"
);
for p in &paths {
assert!(p.exists(), "cascade path should exist: {p:?}");
}
}
#[test]
fn from_bytes_index_zero_matches_from_bytes() {
let a = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("a");
let b = FontHandle::from_bytes_index(FONT_CASCADIA_CODE_NF, 0).expect("b");
let gid_a = glyph_id_for_char(&a, 18.0, 'A');
let gid_b = glyph_id_for_char(&b, 18.0, 'A');
assert_eq!(gid_a, gid_b);
}
#[test]
fn from_bytes_index_out_of_range_returns_none() {
let h = FontHandle::from_bytes_index(FONT_CASCADIA_CODE_NF, 99);
assert!(h.is_none(), "index 99 on a single-font TTF should fail");
}
#[test]
fn all_families_returns_sorted_nonempty_list() {
let families = all_families();
assert!(
!families.is_empty(),
"system should expose some font families"
);
let mut sorted = families.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(families, sorted);
}
#[test]
fn zero_ink_glyph_yields_empty_bitmap() {
let handle = FontHandle::from_bytes(FONT_CASCADIA_CODE_NF).expect("load font");
let size = 24.0;
let gid = glyph_id_for_char(&handle, size as f64, ' ');
let g = rasterize_glyph(&handle, gid, size, false, false, false)
.expect("rasterize returned None");
assert_eq!(g.width, 0);
assert_eq!(g.height, 0);
assert!(g.bytes.is_empty());
}
}