use std::collections::HashMap;
use std::ffi::CStr;
use std::fs;
use std::iter;
use std::path::{Path, PathBuf};
use std::ptr;
use core_foundation::array::{CFArray, CFIndex};
use core_foundation::base::{CFType, ItemRef, TCFType};
use core_foundation::number::{CFNumber, CFNumberRef};
use core_foundation::string::CFString;
use core_graphics::base::kCGImageAlphaPremultipliedFirst;
use core_graphics::color_space::CGColorSpace;
use core_graphics::context::CGContext;
use core_graphics::font::CGGlyph;
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use core_text::font::{
CTFont, cascade_list_for_languages as ct_cascade_list_for_languages,
new_from_descriptor as ct_new_from_descriptor, new_from_name,
};
use core_text::font_collection::create_for_family;
use core_text::font_descriptor::{
self, CTFontDescriptor, SymbolicTraitAccessors, kCTFontColorGlyphsTrait,
kCTFontDefaultOrientation, kCTFontEnabledAttribute,
};
use core_text::font_manager::create_font_descriptor;
use objc2::rc::autoreleasepool;
use objc2_foundation::{NSNumber, NSString, NSUserDefaults, ns_string};
use log::{trace, warn};
use once_cell::sync::Lazy;
pub mod byte_order;
use byte_order::kCGBitmapByteOrder32Host;
use super::{
BitmapBuffer, Error, FontDesc, FontKey, GlyphKey, Metrics, RasterizedGlyph, Size, Slant, Style,
Weight,
};
const MISSING_GLYPH_INDEX: u32 = 0;
const NON_ANTIALIASED_BOUNDS_PADDING: f64 = 8.0;
#[derive(Debug)]
struct Descriptor {
style_name: String,
font_path: PathBuf,
ct_descriptor: CTFontDescriptor,
}
impl Descriptor {
fn new(desc: CTFontDescriptor) -> Descriptor {
Descriptor {
style_name: desc.style_name(),
font_path: desc.font_path().unwrap_or_default(),
ct_descriptor: desc,
}
}
fn to_font(&self, size: f64, load_fallbacks: bool) -> Font {
let ct_font = ct_new_from_descriptor(&self.ct_descriptor, size);
let fallbacks = if load_fallbacks {
let mut fallbacks = cascade_list_for_languages(&ct_font, &["en".to_owned()])
.into_iter()
.filter(|desc| !desc.font_path.as_os_str().is_empty())
.map(|desc| desc.to_font(size, false))
.collect::<Vec<_>>();
if let Ok(apple_symbols) = new_from_name("Apple Symbols", size) {
fallbacks.push(Font {
ct_font: apple_symbols,
fallbacks: Vec::new(),
})
};
fallbacks
} else {
Vec::new()
};
Font { ct_font, fallbacks }
}
}
pub struct CoreTextRasterizer {
fonts: HashMap<FontKey, Font>,
keys: HashMap<(FontDesc, Size), FontKey>,
}
impl crate::crossfont::Rasterize for CoreTextRasterizer {
fn new() -> Result<CoreTextRasterizer, Error> {
Ok(CoreTextRasterizer {
fonts: HashMap::new(),
keys: HashMap::new(),
})
}
fn metrics(&self, key: FontKey, _size: Size) -> Result<Metrics, Error> {
let font = self.fonts.get(&key).ok_or(Error::UnknownFontKey)?;
Ok(font.metrics())
}
fn load_font(&mut self, desc: &FontDesc, size: Size) -> Result<FontKey, Error> {
let size = Size::new(size.as_pt());
self.keys
.get(&(desc.to_owned(), size))
.map(|k| Ok(*k))
.unwrap_or_else(|| {
let font = self.get_font(desc, size)?;
let key = FontKey::next();
self.fonts.insert(key, font);
self.keys.insert((desc.clone(), size), key);
Ok(key)
})
}
fn load_font_path(&mut self, path: &Path, size: Size) -> Result<FontKey, Error> {
let bytes = fs::read(path).map_err(|error| {
Error::PlatformError(format!(
"failed to read CoreText font '{}': {error}",
path.display()
))
})?;
self.load_font_bytes(&bytes, size)
}
fn load_font_bytes(&mut self, bytes: &[u8], size: Size) -> Result<FontKey, Error> {
let descriptor = create_font_descriptor(bytes)
.map_err(|_| Error::PlatformError("CoreText rejected font bytes".to_owned()))?;
let ct_font = ct_new_from_descriptor(&descriptor, f64::from(size.as_pt()));
let key = FontKey::next();
self.fonts.insert(
key,
Font {
ct_font,
fallbacks: Vec::new(),
},
);
Ok(key)
}
fn get_glyph(&mut self, glyph: GlyphKey) -> Result<RasterizedGlyph, Error> {
let font = self
.fonts
.get(&glyph.font_key)
.ok_or(Error::UnknownFontKey)?;
let (font, glyph_index) = iter::once(font)
.chain(font.fallbacks.iter())
.find_map(|font| match font.glyph_index(glyph.character) {
MISSING_GLYPH_INDEX => None,
glyph_index => Some((font, glyph_index)),
})
.unwrap_or((font, MISSING_GLYPH_INDEX));
let glyph = font.get_glyph(glyph.character, glyph_index);
if glyph_index == MISSING_GLYPH_INDEX {
Err(Error::MissingGlyph(glyph))
} else {
Ok(glyph)
}
}
fn get_glyph_id(
&mut self,
glyph: crate::crossfont::GlyphIdKey,
) -> Result<RasterizedGlyph, Error> {
let font = self
.fonts
.get(&glyph.font_key)
.ok_or(Error::UnknownFontKey)?;
Ok(font.get_glyph('\0', glyph.glyph_id))
}
fn drop_font(&mut self, key: FontKey) -> Result<(), Error> {
self.fonts.remove(&key).ok_or(Error::UnknownFontKey)?;
self.keys.retain(|_, existing| *existing != key);
Ok(())
}
fn evict_cache(&mut self) {
self.fonts.clear();
self.keys.clear();
}
fn kerning(&mut self, _left: GlyphKey, _right: GlyphKey) -> (f32, f32) {
(0., 0.)
}
}
impl CoreTextRasterizer {
fn get_specific_face(
&mut self,
desc: &FontDesc,
style: &str,
size: Size,
) -> Result<Font, Error> {
let descriptors = descriptors_for_family(&desc.name[..]);
for descriptor in descriptors {
if descriptor.style_name == style {
let size = f64::from(size.as_pt());
let font = descriptor.to_font(size, true);
return Ok(font);
}
}
Err(Error::FontNotFound(desc.to_owned()))
}
fn get_matching_face(
&mut self,
desc: &FontDesc,
slant: Slant,
weight: Weight,
size: Size,
) -> Result<Font, Error> {
let bold = weight == Weight::Bold;
let italic = slant != Slant::Normal;
let size = f64::from(size.as_pt());
let descriptors = descriptors_for_family(&desc.name[..]);
for descriptor in descriptors {
let font = descriptor.to_font(size, true);
if font.is_bold() == bold && font.is_italic() == italic {
return Ok(font);
}
}
Err(Error::FontNotFound(desc.to_owned()))
}
fn get_font(&mut self, desc: &FontDesc, size: Size) -> Result<Font, Error> {
match desc.style {
Style::Specific(ref style) => self.get_specific_face(desc, style, size),
Style::Description { slant, weight } => {
self.get_matching_face(desc, slant, weight, size)
}
}
}
}
fn cascade_list_for_languages(ct_font: &CTFont, languages: &[String]) -> Vec<Descriptor> {
let langarr: CFArray<CFString> = {
let tmp: Vec<CFString> = languages
.iter()
.map(|language| CFString::new(language))
.collect();
CFArray::from_CFTypes(&tmp)
};
let list = ct_cascade_list_for_languages(ct_font, &langarr);
list.into_iter()
.filter(is_enabled)
.map(|fontdesc| Descriptor::new(fontdesc.clone()))
.collect()
}
fn is_enabled(fontdesc: &ItemRef<'_, CTFontDescriptor>) -> bool {
unsafe {
let descriptor = fontdesc.as_concrete_TypeRef();
let attr_val =
font_descriptor::CTFontDescriptorCopyAttribute(descriptor, kCTFontEnabledAttribute);
if attr_val.is_null() {
return false;
}
let attr_val = CFType::wrap_under_create_rule(attr_val);
let attr_val = CFNumber::wrap_under_get_rule(attr_val.as_CFTypeRef() as CFNumberRef);
attr_val.to_i32().unwrap_or(0) != 0
}
}
fn descriptors_for_family(family: &str) -> Vec<Descriptor> {
let mut out = Vec::new();
trace!("Family: {}", family);
let ct_collection = create_for_family(family).unwrap_or_else(|| {
warn!(
"Unable to load specified font {}, falling back to Menlo",
&family
);
create_for_family("Menlo").expect("Menlo exists")
});
let descriptors = ct_collection.get_descriptors();
if let Some(descriptors) = descriptors {
for descriptor in descriptors.iter() {
out.push(Descriptor::new(descriptor.clone()));
}
}
out
}
static FONT_SMOOTHING_ENABLED: Lazy<bool> = Lazy::new(|| {
autoreleasepool(|_| {
let value =
NSUserDefaults::standardUserDefaults().objectForKey(ns_string!("AppleFontSmoothing"));
let value = match value {
Some(value) => value,
None => return true,
};
match value.downcast::<NSNumber>() {
Ok(value) => {
let int_specifiers: [&[u8]; 4] = [b"q", b"l", b"i", b"s"];
let encoding = unsafe { CStr::from_ptr(value.objCType().as_ptr()).to_bytes() };
if !int_specifiers.contains(&encoding) {
return true;
}
value.integerValue() != 0
}
Err(value) => match value.downcast::<NSString>() {
Ok(value) => value.integerValue() != 0,
Err(_) => true,
},
}
})
});
#[derive(Clone)]
struct Font {
ct_font: CTFont,
fallbacks: Vec<Font>,
}
unsafe impl Send for Font {}
impl Font {
fn metrics(&self) -> Metrics {
let average_advance = self.glyph_advance('0');
let ascent = self.ct_font.ascent().round();
let descent = self.ct_font.descent().round();
let leading = self.ct_font.leading().round();
let line_height = ascent + descent + leading;
let underline_position = self.ct_font.underline_position() as f32;
let underline_thickness = self.ct_font.underline_thickness() as f32;
let strikeout_position = (line_height / 2. - descent) as f32;
let strikeout_thickness = underline_thickness;
Metrics {
average_advance,
line_height,
descent: -(descent as f32),
underline_position,
underline_thickness,
strikeout_position,
strikeout_thickness,
}
}
fn is_bold(&self) -> bool {
self.ct_font.symbolic_traits().is_bold()
}
fn is_italic(&self) -> bool {
self.ct_font.symbolic_traits().is_italic()
}
fn is_colored(&self) -> bool {
(self.ct_font.symbolic_traits() & kCTFontColorGlyphsTrait) != 0
}
fn glyph_advance(&self, character: char) -> f64 {
let index = self.glyph_index(character);
let indices = [index as CGGlyph];
unsafe {
self.ct_font.get_advances_for_glyphs(
kCTFontDefaultOrientation,
&indices[0],
ptr::null_mut(),
1,
)
}
}
fn get_glyph(&self, character: char, glyph_index: u32) -> RasterizedGlyph {
let bounds = self
.ct_font
.get_bounding_rects_for_glyphs(kCTFontDefaultOrientation, &[glyph_index as CGGlyph]);
let antialias = macos_antialias_enabled();
let bounds_padding = if antialias {
0.0
} else {
NON_ANTIALIASED_BOUNDS_PADDING
};
let rasterized_left = (bounds.origin.x - bounds_padding).floor() as i32;
let rasterized_width = ((bounds.origin.x + bounds.size.width + bounds_padding).ceil()
as i32
- rasterized_left)
.max(0) as u32;
let rasterized_descent = (-bounds.origin.y + bounds_padding).ceil() as i32;
let rasterized_ascent =
(bounds.size.height + bounds.origin.y + bounds_padding).ceil() as i32;
let rasterized_height = (rasterized_descent + rasterized_ascent) as u32;
if rasterized_width == 0 || rasterized_height == 0 {
return RasterizedGlyph {
character: ' ',
width: 0,
height: 0,
top: 0,
left: 0,
advance: (0, 0),
buffer: BitmapBuffer::Rgb(Vec::new()),
};
}
let mut cg_context = CGContext::create_bitmap_context(
None,
rasterized_width as usize,
rasterized_height as usize,
8, rasterized_width as usize * 4,
&CGColorSpace::create_device_rgb(),
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host,
);
let is_colored = self.is_colored();
let bg_a = if is_colored { 0.0 } else { 1.0 };
cg_context.set_rgb_fill_color(0.0, 0.0, 0.0, bg_a);
let context_rect = CGRect::new(
&CGPoint::new(0.0, 0.0),
&CGSize::new(f64::from(rasterized_width), f64::from(rasterized_height)),
);
cg_context.fill_rect(context_rect);
cg_context.set_allows_font_smoothing(true);
cg_context.set_should_smooth_fonts(*FONT_SMOOTHING_ENABLED);
cg_context.set_allows_font_subpixel_quantization(true);
cg_context.set_should_subpixel_quantize_fonts(true);
cg_context.set_allows_font_subpixel_positioning(true);
cg_context.set_should_subpixel_position_fonts(true);
cg_context.set_allows_antialiasing(antialias);
cg_context.set_should_antialias(antialias);
cg_context.set_rgb_fill_color(1.0, 1.0, 1.0, 1.0);
let rasterization_origin = CGPoint {
x: f64::from(-rasterized_left),
y: f64::from(rasterized_descent),
};
self.ct_font.draw_glyphs(
&[glyph_index as CGGlyph],
&[rasterization_origin],
cg_context.clone(),
);
let rasterized_pixels = cg_context.data().to_vec();
let buffer = if is_colored {
BitmapBuffer::Rgba(byte_order::extract_rgba(&rasterized_pixels))
} else {
BitmapBuffer::Rgb(byte_order::extract_rgb(&rasterized_pixels))
};
RasterizedGlyph {
character,
left: rasterized_left,
top: (bounds.size.height + bounds.origin.y).ceil() as i32,
width: rasterized_width as i32,
height: rasterized_height as i32,
advance: (0, 0),
buffer,
}
}
fn glyph_index(&self, character: char) -> u32 {
let mut buffer = [0; 2];
let encoded: &[u16] = character.encode_utf16(&mut buffer);
self.glyph_index_utf16(encoded)
}
fn glyph_index_utf16(&self, encoded: &[u16]) -> u32 {
let mut glyphs: [CGGlyph; 2] = [0; 2];
let res = unsafe {
self.ct_font.get_glyphs_for_characters(
encoded.as_ptr(),
glyphs.as_mut_ptr(),
encoded.len() as CFIndex,
)
};
if res {
u32::from(glyphs[0])
} else {
MISSING_GLYPH_INDEX
}
}
}
#[cfg(test)]
mod tests {
use super::BitmapBuffer;
#[test]
fn get_descriptors_and_build_font() {
let list = super::descriptors_for_family("Menlo");
assert!(!list.is_empty());
println!("{:?}", list);
let fonts = list
.iter()
.map(|desc| desc.to_font(72., false))
.collect::<Vec<_>>();
for font in fonts {
for character in &['a', 'b', 'c', 'd'] {
let glyph_index = font.glyph_index(*character);
let glyph = font.get_glyph(*character, glyph_index);
let buffer = match &glyph.buffer {
BitmapBuffer::Rgb(buffer) | BitmapBuffer::Rgba(buffer) => buffer,
};
for row in 0..glyph.height {
for col in 0..glyph.width {
let index = ((glyph.width * 3 * row) + (col * 3)) as usize;
let value = buffer[index];
let c = match value {
0..=50 => ' ',
51..=100 => '.',
101..=150 => '~',
151..=200 => '*',
201..=255 => '#',
};
print!("{}", c);
}
println!();
}
}
}
}
}
fn macos_antialias_enabled() -> bool {
std::env::var_os("RASSA_CROSSFONT_MACOS_ANTIALIAS")
.and_then(|value| value.into_string().ok())
.is_none_or(|value| !matches!(value.as_str(), "0" | "false" | "FALSE" | "off" | "OFF"))
}