use crate::{ point, px, size, swap_rgba_pa_to_bgra, Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS,};use anyhow::anyhow;use cocoa::appkit::CGFloat;use collections::HashMap;use core_foundation::{ attributed_string::CFMutableAttributedString, base::{CFRange, TCFType}, number::CFNumber, string::CFString,};use core_graphics::{ base::{kCGImageAlphaPremultipliedLast, CGGlyph}, color_space::CGColorSpace, context::CGContext, display::CGPoint,};use core_text::{ font::CTFont, font_descriptor::{ kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, kCTFontWidthTrait, }, line::CTLine, string_attributes::kCTFontAttributeName,};use font_kit::{ font::Font as FontKitFont, handle::Handle, hinting::HintingOptions, metrics::Metrics, properties::{Style as FontkitStyle, Weight as FontkitWeight}, source::SystemSource, sources::mem::MemSource,};use parking_lot::{RwLock, RwLockUpgradableReadGuard};use pathfinder_geometry::{ rect::{RectF, RectI}, transform2d::Transform2F, vector::{Vector2F, Vector2I},};use smallvec::SmallVec;use std::{borrow::Cow, char, cmp, convert::TryFrom, sync::Arc}; use super::open_type::apply_features_and_fallbacks; #[allow(non_upper_case_globals)]const kCGImageAlphaOnly: u32 = 7; pub(crate) struct MacTextSystem(RwLock<MacTextSystemState>); #[derive(Clone, PartialEq, Eq, Hash)]struct FontKey { font_family: SharedString, font_features: FontFeatures, font_fallbacks: Option<FontFallbacks>,} struct MacTextSystemState { memory_source: MemSource, system_source: SystemSource, fonts: Vec<FontKitFont>, font_selections: HashMap<Font, FontId>, font_ids_by_postscript_name: HashMap<String, FontId>, font_ids_by_font_key: HashMap<FontKey, SmallVec<[FontId; 4]>>, postscript_names_by_font_id: HashMap<FontId, String>,} impl MacTextSystem { pub(crate) fn new() -> Self { Self(RwLock::new(MacTextSystemState { memory_source: MemSource::empty(), system_source: SystemSource::new(), fonts: Vec::new(), font_selections: HashMap::default(), font_ids_by_postscript_name: HashMap::default(), font_ids_by_font_key: HashMap::default(), postscript_names_by_font_id: HashMap::default(), })) }} impl Default for MacTextSystem { fn default() -> Self { Self::new() }} impl PlatformTextSystem for MacTextSystem { fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> { self.0.write().add_fonts(fonts) } fn all_font_names(&self) -> Vec<String> { let mut names = Vec::new(); let collection = core_text::font_collection::create_for_all_families(); let Some(descriptors) = collection.get_descriptors() else { return names; }; for descriptor in descriptors.into_iter() { names.extend(lenient_font_attributes::family_name(&descriptor)); } if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() { names.extend(fonts_in_memory); } names } fn font_id(&self, font: &Font) -> Result<FontId> { let lock = self.0.upgradable_read(); if let Some(font_id) = lock.font_selections.get(font) { Ok(*font_id) } else { let mut lock = RwLockUpgradableReadGuard::upgrade(lock); let font_key = FontKey { font_family: font.family.clone(), font_features: font.features.clone(), font_fallbacks: font.fallbacks.clone(), }; let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) { font_ids.as_slice() } else { let font_ids = lock.load_family(&font.family, &font.features, font.fallbacks.as_ref())?; lock.font_ids_by_font_key.insert(font_key.clone(), font_ids); lock.font_ids_by_font_key[&font_key].as_ref() }; let candidate_properties = candidates .iter() .map(|font_id| lock.fonts[font_id.0].properties()) .collect::<SmallVec<[_; 4]>>(); let ix = font_kit::matching::find_best_match( &candidate_properties, &font_kit::properties::Properties { style: font.style.into(), weight: font.weight.into(), stretch: Default::default(), }, )?; let font_id = candidates[ix]; lock.font_selections.insert(font.clone(), font_id); Ok(font_id) } } fn font_metrics(&self, font_id: FontId) -> FontMetrics { self.0.read().fonts[font_id.0].metrics().into() } fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> { Ok(self.0.read().fonts[font_id.0] .typographic_bounds(glyph_id.0)? .into()) } fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> { self.0.read().advance(font_id, glyph_id) } fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> { self.0.read().glyph_for_char(font_id, ch) } fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> { self.0.read().raster_bounds(params) } fn rasterize_glyph( &self, glyph_id: &RenderGlyphParams, raster_bounds: Bounds<DevicePixels>, ) -> Result<(Size<DevicePixels>, Vec<u8>)> { self.0.read().rasterize_glyph(glyph_id, raster_bounds) } fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { self.0.write().layout_line(text, font_size, font_runs) }} impl MacTextSystemState { fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> { let fonts = fonts .into_iter() .map(|bytes| match bytes { Cow::Borrowed(embedded_font) => { let data_provider = unsafe { core_graphics::data_provider::CGDataProvider::from_slice(embedded_font) }; let font = core_graphics::font::CGFont::from_data_provider(data_provider) .map_err(|_| anyhow!("Could not load an embedded font."))?; let font = font_kit::loaders::core_text::Font::from_core_graphics_font(font); Ok(Handle::from_native(&font)) } Cow::Owned(bytes) => Ok(Handle::from_memory(Arc::new(bytes), 0)), }) .collect::<Result<Vec<_>>>()?; self.memory_source.add_fonts(fonts.into_iter())?; Ok(()) } fn load_family( &mut self, name: &str, features: &FontFeatures, fallbacks: Option<&FontFallbacks>, ) -> Result<SmallVec<[FontId; 4]>> { let name = if name == ".SystemUIFont" { ".AppleSystemUIFont" } else { name }; let mut font_ids = SmallVec::new(); let family = self .memory_source .select_family_by_name(name) .or_else(|_| self.system_source.select_family_by_name(name))?; for font in family.fonts() { let mut font = font.load()?; apply_features_and_fallbacks(&mut font, features, fallbacks)?; // This block contains a precautionary fix to guard against loading fonts // that might cause panics due to `.unwrap()`s up the chain. { // We use the 'm' character for text measurements in various spots // (e.g., the editor). However, at time of writing some of those usages // will panic if the font has no 'm' glyph. // // Therefore, we check up front that the font has the necessary glyph. let has_m_glyph = font.glyph_for_char('m').is_some(); // HACK: The 'Segoe Fluent Icons' font does not have an 'm' glyph, // but we need to be able to load it for rendering Windows icons in // the Storybook (on macOS). let is_segoe_fluent_icons = font.full_name() == "Segoe Fluent Icons"; if !has_m_glyph && !is_segoe_fluent_icons { // I spent far too long trying to track down why a font missing the 'm' // character wasn't loading. This log statement will hopefully save // someone else from suffering the same fate. log::warn!( "font '{}' has no 'm' character and was not loaded", font.full_name() ); continue; } } // We've seen a number of panics in production caused by calling font.properties() // which unwraps a downcast to CFNumber. This is an attempt to avoid the panic, // and to try and identify the incalcitrant font. let traits = font.native_font().all_traits(); if unsafe { !(traits .get(kCTFontSymbolicTrait) .downcast::<CFNumber>() .is_some() && traits .get(kCTFontWidthTrait) .downcast::<CFNumber>() .is_some() && traits .get(kCTFontWeightTrait) .downcast::<CFNumber>() .is_some() && traits .get(kCTFontSlantTrait) .downcast::<CFNumber>() .is_some()) } { log::error!( "Failed to read traits for font {:?}", font.postscript_name().unwrap() ); continue; } let font_id = FontId(self.fonts.len()); font_ids.push(font_id); let postscript_name = font.postscript_name().unwrap(); self.font_ids_by_postscript_name .insert(postscript_name.clone(), font_id); self.postscript_names_by_font_id .insert(font_id, postscript_name); self.fonts.push(font); } Ok(font_ids) } fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> { Ok(self.fonts[font_id.0].advance(glyph_id.0)?.into()) } fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> { self.fonts[font_id.0].glyph_for_char(ch).map(GlyphId) } fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId { let postscript_name = requested_font.postscript_name(); if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) { *font_id } else { let font_id = FontId(self.fonts.len()); self.font_ids_by_postscript_name .insert(postscript_name.clone(), font_id); self.postscript_names_by_font_id .insert(font_id, postscript_name); self.fonts .push(font_kit::font::Font::from_core_graphics_font( requested_font.copy_to_CGFont(), )); font_id } } fn is_emoji(&self, font_id: FontId) -> bool { self.postscript_names_by_font_id .get(&font_id) .map_or(false, |postscript_name| { postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI" }) } fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> { let font = &self.fonts[params.font_id.0]; let scale = Transform2F::from_scale(params.scale_factor); Ok(font .raster_bounds( params.glyph_id.0, params.font_size.into(), scale, HintingOptions::None, font_kit::canvas::RasterizationOptions::GrayscaleAa, )? .into()) } fn rasterize_glyph( &self, params: &RenderGlyphParams, glyph_bounds: Bounds<DevicePixels>, ) -> Result<(Size<DevicePixels>, Vec<u8>)> { if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { Err(anyhow!("glyph bounds are empty")) } else { // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing. let mut bitmap_size = glyph_bounds.size; if params.subpixel_variant.x > 0 { bitmap_size.width += DevicePixels(1); } if params.subpixel_variant.y > 0 { bitmap_size.height += DevicePixels(1); } let bitmap_size = bitmap_size; let mut bytes; let cx; if params.is_emoji { bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize]; cx = CGContext::create_bitmap_context( Some(bytes.as_mut_ptr() as *mut _), bitmap_size.width.0 as usize, bitmap_size.height.0 as usize, 8, bitmap_size.width.0 as usize * 4, &CGColorSpace::create_device_rgb(), kCGImageAlphaPremultipliedLast, ); } else { bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize]; cx = CGContext::create_bitmap_context( Some(bytes.as_mut_ptr() as *mut _), bitmap_size.width.0 as usize, bitmap_size.height.0 as usize, 8, bitmap_size.width.0 as usize, &CGColorSpace::create_device_gray(), kCGImageAlphaOnly, ); } // Move the origin to bottom left and account for scaling, this // makes drawing text consistent with the font-kit's raster_bounds. cx.translate( -glyph_bounds.origin.x.0 as CGFloat, (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat, ); cx.scale( params.scale_factor as CGFloat, params.scale_factor as CGFloat, ); let subpixel_shift = params .subpixel_variant .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); cx.set_allows_font_subpixel_positioning(true); cx.set_should_subpixel_position_fonts(true); cx.set_allows_font_subpixel_quantization(false); cx.set_should_subpixel_quantize_fonts(false); self.fonts[params.font_id.0] .native_font() .clone_with_font_size(f32::from(params.font_size) as CGFloat) .draw_glyphs( &[params.glyph_id.0 as CGGlyph], &[CGPoint::new( (subpixel_shift.x / params.scale_factor) as CGFloat, (subpixel_shift.y / params.scale_factor) as CGFloat, )], cx, ); if params.is_emoji { // Convert from RGBA with premultiplied alpha to BGRA with straight alpha. for pixel in bytes.chunks_exact_mut(4) { swap_rgba_pa_to_bgra(pixel); } } Ok((bitmap_size, bytes)) } } fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. let mut string = CFMutableAttributedString::new(); { string.replace_str(&CFString::new(text), CFRange::init(0, 0)); let utf16_line_len = string.char_len() as usize; let mut ix_converter = StringIndexConverter::new(text); for run in font_runs { let utf8_end = ix_converter.utf8_ix + run.len; let utf16_start = ix_converter.utf16_ix; if utf16_start >= utf16_line_len { break; } ix_converter.advance_to_utf8_ix(utf8_end); let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len); let cf_range = CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize); let font: &FontKitFont = &self.fonts[run.font_id.0]; unsafe { string.set_attribute( cf_range, kCTFontAttributeName, &font.native_font().clone_with_font_size(font_size.into()), ); } if utf16_end == utf16_line_len { break; } } } // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef()); let glyph_runs = line.glyph_runs(); let mut runs = Vec::with_capacity(glyph_runs.len() as usize); let mut ix_converter = StringIndexConverter::new(text); for run in glyph_runs.into_iter() { let attributes = run.attributes().unwrap(); let font = unsafe { attributes .get(kCTFontAttributeName) .downcast::<CTFont>() .unwrap() }; let font_id = self.id_for_native_font(font); let mut glyphs = SmallVec::new(); for ((glyph_id, position), glyph_utf16_ix) in run .glyphs() .iter() .zip(run.positions().iter()) .zip(run.string_indices().iter()) { let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap(); if ix_converter.utf16_ix > glyph_utf16_ix { // We cannot reuse current index converter, as it can only seek forward. Restart the search. ix_converter = StringIndexConverter::new(text); } ix_converter.advance_to_utf16_ix(glyph_utf16_ix); glyphs.push(ShapedGlyph { id: GlyphId(*glyph_id as u32), position: point(position.x as f32, position.y as f32).map(px), index: ix_converter.utf8_ix, is_emoji: self.is_emoji(font_id),Lines 1–500 of 701. Load more
use crate::{ point, px, size, swap_rgba_pa_to_bgra, Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS, }; use anyhow::anyhow; use cocoa::appkit::CGFloat; use collections::HashMap; use core_foundation::{ attributed_string::CFMutableAttributedString, base::{CFRange, TCFType}, number::CFNumber, string::CFString, }; use core_graphics::{ base::{kCGImageAlphaPremultipliedLast, CGGlyph}, color_space::CGColorSpace, context::CGContext, display::CGPoint, }; use core_text::{ font::CTFont, font_descriptor::{ kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, kCTFontWidthTrait, }, line::CTLine, string_attributes::kCTFontAttributeName, }; use font_kit::{ font::Font as FontKitFont, handle::Handle, hinting::HintingOptions, metrics::Metrics, properties::{Style as FontkitStyle, Weight as FontkitWeight}, source::SystemSource, sources::mem::MemSource, }; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use pathfinder_geometry::{ rect::{RectF, RectI}, transform2d::Transform2F, vector::{Vector2F, Vector2I}, }; use smallvec::SmallVec; use std::{borrow::Cow, char, cmp, convert::TryFrom, sync::Arc}; use super::open_type::apply_features_and_fallbacks; #[allow(non_upper_case_globals)] const kCGImageAlphaOnly: u32 = 7; pub(crate) struct MacTextSystem(RwLock<MacTextSystemState>); #[derive(Clone, PartialEq, Eq, Hash)] struct FontKey { font_family: SharedString, font_features: FontFeatures, font_fallbacks: Option<FontFallbacks>, } struct MacTextSystemState { memory_source: MemSource, system_source: SystemSource, fonts: Vec<FontKitFont>, font_selections: HashMap<Font, FontId>, font_ids_by_postscript_name: HashMap<String, FontId>, font_ids_by_font_key: HashMap<FontKey, SmallVec<[FontId; 4]>>, postscript_names_by_font_id: HashMap<FontId, String>, } impl MacTextSystem { pub(crate) fn new() -> Self { Self(RwLock::new(MacTextSystemState { memory_source: MemSource::empty(), system_source: SystemSource::new(), fonts: Vec::new(), font_selections: HashMap::default(), font_ids_by_postscript_name: HashMap::default(), font_ids_by_font_key: HashMap::default(), postscript_names_by_font_id: HashMap::default(), })) } } impl Default for MacTextSystem { fn default() -> Self { Self::new() } } impl PlatformTextSystem for MacTextSystem { fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> { self.0.write().add_fonts(fonts) } fn all_font_names(&self) -> Vec<String> { let mut names = Vec::new(); let collection = core_text::font_collection::create_for_all_families(); let Some(descriptors) = collection.get_descriptors() else { return names; }; for descriptor in descriptors.into_iter() { names.extend(lenient_font_attributes::family_name(&descriptor)); } if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() { names.extend(fonts_in_memory); } names } fn font_id(&self, font: &Font) -> Result<FontId> { let lock = self.0.upgradable_read(); if let Some(font_id) = lock.font_selections.get(font) { Ok(*font_id) } else { let mut lock = RwLockUpgradableReadGuard::upgrade(lock); let font_key = FontKey { font_family: font.family.clone(), font_features: font.features.clone(), font_fallbacks: font.fallbacks.clone(), }; let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) { font_ids.as_slice() } else { let font_ids = lock.load_family(&font.family, &font.features, font.fallbacks.as_ref())?; lock.font_ids_by_font_key.insert(font_key.clone(), font_ids); lock.font_ids_by_font_key[&font_key].as_ref() }; let candidate_properties = candidates .iter() .map(|font_id| lock.fonts[font_id.0].properties()) .collect::<SmallVec<[_; 4]>>(); let ix = font_kit::matching::find_best_match( &candidate_properties, &font_kit::properties::Properties { style: font.style.into(), weight: font.weight.into(), stretch: Default::default(), }, )?; let font_id = candidates[ix]; lock.font_selections.insert(font.clone(), font_id); Ok(font_id) } } fn font_metrics(&self, font_id: FontId) -> FontMetrics { self.0.read().fonts[font_id.0].metrics().into() } fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> { Ok(self.0.read().fonts[font_id.0] .typographic_bounds(glyph_id.0)? .into()) } fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> { self.0.read().advance(font_id, glyph_id) } fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> { self.0.read().glyph_for_char(font_id, ch) } fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> { self.0.read().raster_bounds(params) } fn rasterize_glyph( &self, glyph_id: &RenderGlyphParams, raster_bounds: Bounds<DevicePixels>, ) -> Result<(Size<DevicePixels>, Vec<u8>)> { self.0.read().rasterize_glyph(glyph_id, raster_bounds) } fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { self.0.write().layout_line(text, font_size, font_runs) } } impl MacTextSystemState { fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> { let fonts = fonts .into_iter() .map(|bytes| match bytes { Cow::Borrowed(embedded_font) => { let data_provider = unsafe { core_graphics::data_provider::CGDataProvider::from_slice(embedded_font) }; let font = core_graphics::font::CGFont::from_data_provider(data_provider) .map_err(|_| anyhow!("Could not load an embedded font."))?; let font = font_kit::loaders::core_text::Font::from_core_graphics_font(font); Ok(Handle::from_native(&font)) } Cow::Owned(bytes) => Ok(Handle::from_memory(Arc::new(bytes), 0)), }) .collect::<Result<Vec<_>>>()?; self.memory_source.add_fonts(fonts.into_iter())?; Ok(()) } fn load_family( &mut self, name: &str, features: &FontFeatures, fallbacks: Option<&FontFallbacks>, ) -> Result<SmallVec<[FontId; 4]>> { let name = if name == ".SystemUIFont" { ".AppleSystemUIFont" } else { name }; let mut font_ids = SmallVec::new(); let family = self .memory_source .select_family_by_name(name) .or_else(|_| self.system_source.select_family_by_name(name))?; for font in family.fonts() { let mut font = font.load()?; apply_features_and_fallbacks(&mut font, features, fallbacks)?; // This block contains a precautionary fix to guard against loading fonts // that might cause panics due to `.unwrap()`s up the chain. { // We use the 'm' character for text measurements in various spots // (e.g., the editor). However, at time of writing some of those usages // will panic if the font has no 'm' glyph. // // Therefore, we check up front that the font has the necessary glyph. let has_m_glyph = font.glyph_for_char('m').is_some(); // HACK: The 'Segoe Fluent Icons' font does not have an 'm' glyph, // but we need to be able to load it for rendering Windows icons in // the Storybook (on macOS). let is_segoe_fluent_icons = font.full_name() == "Segoe Fluent Icons"; if !has_m_glyph && !is_segoe_fluent_icons { // I spent far too long trying to track down why a font missing the 'm' // character wasn't loading. This log statement will hopefully save // someone else from suffering the same fate. log::warn!( "font '{}' has no 'm' character and was not loaded", font.full_name() ); continue; } } // We've seen a number of panics in production caused by calling font.properties() // which unwraps a downcast to CFNumber. This is an attempt to avoid the panic, // and to try and identify the incalcitrant font. let traits = font.native_font().all_traits(); if unsafe { !(traits .get(kCTFontSymbolicTrait) .downcast::<CFNumber>() .is_some() && traits .get(kCTFontWidthTrait) .downcast::<CFNumber>() .is_some() && traits .get(kCTFontWeightTrait) .downcast::<CFNumber>() .is_some() && traits .get(kCTFontSlantTrait) .downcast::<CFNumber>() .is_some()) } { log::error!( "Failed to read traits for font {:?}", font.postscript_name().unwrap() ); continue; } let font_id = FontId(self.fonts.len()); font_ids.push(font_id); let postscript_name = font.postscript_name().unwrap(); self.font_ids_by_postscript_name .insert(postscript_name.clone(), font_id); self.postscript_names_by_font_id .insert(font_id, postscript_name); self.fonts.push(font); } Ok(font_ids) } fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> { Ok(self.fonts[font_id.0].advance(glyph_id.0)?.into()) } fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> { self.fonts[font_id.0].glyph_for_char(ch).map(GlyphId) } fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId { let postscript_name = requested_font.postscript_name(); if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) { *font_id } else { let font_id = FontId(self.fonts.len()); self.font_ids_by_postscript_name .insert(postscript_name.clone(), font_id); self.postscript_names_by_font_id .insert(font_id, postscript_name); self.fonts .push(font_kit::font::Font::from_core_graphics_font( requested_font.copy_to_CGFont(), )); font_id } } fn is_emoji(&self, font_id: FontId) -> bool { self.postscript_names_by_font_id .get(&font_id) .map_or(false, |postscript_name| { postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI" }) } fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> { let font = &self.fonts[params.font_id.0]; let scale = Transform2F::from_scale(params.scale_factor); Ok(font .raster_bounds( params.glyph_id.0, params.font_size.into(), scale, HintingOptions::None, font_kit::canvas::RasterizationOptions::GrayscaleAa, )? .into()) } fn rasterize_glyph( &self, params: &RenderGlyphParams, glyph_bounds: Bounds<DevicePixels>, ) -> Result<(Size<DevicePixels>, Vec<u8>)> { if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { Err(anyhow!("glyph bounds are empty")) } else { // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing. let mut bitmap_size = glyph_bounds.size; if params.subpixel_variant.x > 0 { bitmap_size.width += DevicePixels(1); } if params.subpixel_variant.y > 0 { bitmap_size.height += DevicePixels(1); } let bitmap_size = bitmap_size; let mut bytes; let cx; if params.is_emoji { bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize]; cx = CGContext::create_bitmap_context( Some(bytes.as_mut_ptr() as *mut _), bitmap_size.width.0 as usize, bitmap_size.height.0 as usize, 8, bitmap_size.width.0 as usize * 4, &CGColorSpace::create_device_rgb(), kCGImageAlphaPremultipliedLast, ); } else { bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize]; cx = CGContext::create_bitmap_context( Some(bytes.as_mut_ptr() as *mut _), bitmap_size.width.0 as usize, bitmap_size.height.0 as usize, 8, bitmap_size.width.0 as usize, &CGColorSpace::create_device_gray(), kCGImageAlphaOnly, ); } // Move the origin to bottom left and account for scaling, this // makes drawing text consistent with the font-kit's raster_bounds. cx.translate( -glyph_bounds.origin.x.0 as CGFloat, (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat, ); cx.scale( params.scale_factor as CGFloat, params.scale_factor as CGFloat, ); let subpixel_shift = params .subpixel_variant .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); cx.set_allows_font_subpixel_positioning(true); cx.set_should_subpixel_position_fonts(true); cx.set_allows_font_subpixel_quantization(false); cx.set_should_subpixel_quantize_fonts(false); self.fonts[params.font_id.0] .native_font() .clone_with_font_size(f32::from(params.font_size) as CGFloat) .draw_glyphs( &[params.glyph_id.0 as CGGlyph], &[CGPoint::new( (subpixel_shift.x / params.scale_factor) as CGFloat, (subpixel_shift.y / params.scale_factor) as CGFloat, )], cx, ); if params.is_emoji { // Convert from RGBA with premultiplied alpha to BGRA with straight alpha. for pixel in bytes.chunks_exact_mut(4) { swap_rgba_pa_to_bgra(pixel); } } Ok((bitmap_size, bytes)) } } fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. let mut string = CFMutableAttributedString::new(); { string.replace_str(&CFString::new(text), CFRange::init(0, 0)); let utf16_line_len = string.char_len() as usize; let mut ix_converter = StringIndexConverter::new(text); for run in font_runs { let utf8_end = ix_converter.utf8_ix + run.len; let utf16_start = ix_converter.utf16_ix; if utf16_start >= utf16_line_len { break; } ix_converter.advance_to_utf8_ix(utf8_end); let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len); let cf_range = CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize); let font: &FontKitFont = &self.fonts[run.font_id.0]; unsafe { string.set_attribute( cf_range, kCTFontAttributeName, &font.native_font().clone_with_font_size(font_size.into()), ); } if utf16_end == utf16_line_len { break; } } } // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef()); let glyph_runs = line.glyph_runs(); let mut runs = Vec::with_capacity(glyph_runs.len() as usize); let mut ix_converter = StringIndexConverter::new(text); for run in glyph_runs.into_iter() { let attributes = run.attributes().unwrap(); let font = unsafe { attributes .get(kCTFontAttributeName) .downcast::<CTFont>() .unwrap() }; let font_id = self.id_for_native_font(font); let mut glyphs = SmallVec::new(); for ((glyph_id, position), glyph_utf16_ix) in run .glyphs() .iter() .zip(run.positions().iter()) .zip(run.string_indices().iter()) { let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap(); if ix_converter.utf16_ix > glyph_utf16_ix { // We cannot reuse current index converter, as it can only seek forward. Restart the search. ix_converter = StringIndexConverter::new(text); } ix_converter.advance_to_utf16_ix(glyph_utf16_ix); glyphs.push(ShapedGlyph { id: GlyphId(*glyph_id as u32), position: point(position.x as f32, position.y as f32).map(px), index: ix_converter.utf8_ix, is_emoji: self.is_emoji(font_id),