text_system.rs

  1use crate::{
  2    point, px, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun,
  3    FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point,
  4    RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS,
  5};
  6use anyhow::anyhow;
  7use cocoa::appkit::{CGFloat, CGPoint};
  8use collections::{BTreeSet, HashMap};
  9use core_foundation::{
 10    array::CFIndex,
 11    attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
 12    base::{CFRange, TCFType},
 13    string::CFString,
 14};
 15use core_graphics::{
 16    base::{kCGImageAlphaPremultipliedLast, CGGlyph},
 17    color_space::CGColorSpace,
 18    context::CGContext,
 19};
 20use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
 21use font_kit::{
 22    font::Font as FontKitFont,
 23    handle::Handle,
 24    hinting::HintingOptions,
 25    metrics::Metrics,
 26    properties::{Style as FontkitStyle, Weight as FontkitWeight},
 27    source::SystemSource,
 28    sources::mem::MemSource,
 29};
 30use parking_lot::{RwLock, RwLockUpgradableReadGuard};
 31use pathfinder_geometry::{
 32    rect::{RectF, RectI},
 33    transform2d::Transform2F,
 34    vector::{Vector2F, Vector2I},
 35};
 36use smallvec::SmallVec;
 37use std::{char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
 38
 39use super::open_type;
 40
 41#[allow(non_upper_case_globals)]
 42const kCGImageAlphaOnly: u32 = 7;
 43
 44pub struct MacTextSystem(RwLock<MacTextSystemState>);
 45
 46struct MacTextSystemState {
 47    memory_source: MemSource,
 48    system_source: SystemSource,
 49    fonts: Vec<FontKitFont>,
 50    font_selections: HashMap<Font, FontId>,
 51    font_ids_by_postscript_name: HashMap<String, FontId>,
 52    font_ids_by_family_name: HashMap<SharedString, SmallVec<[FontId; 4]>>,
 53    postscript_names_by_font_id: HashMap<FontId, String>,
 54}
 55
 56impl MacTextSystem {
 57    pub fn new() -> Self {
 58        Self(RwLock::new(MacTextSystemState {
 59            memory_source: MemSource::empty(),
 60            system_source: SystemSource::new(),
 61            fonts: Vec::new(),
 62            font_selections: HashMap::default(),
 63            font_ids_by_postscript_name: HashMap::default(),
 64            font_ids_by_family_name: HashMap::default(),
 65            postscript_names_by_font_id: HashMap::default(),
 66        }))
 67    }
 68}
 69
 70impl Default for MacTextSystem {
 71    fn default() -> Self {
 72        Self::new()
 73    }
 74}
 75
 76impl PlatformTextSystem for MacTextSystem {
 77    fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
 78        self.0.write().add_fonts(fonts)
 79    }
 80
 81    fn all_font_names(&self) -> Vec<String> {
 82        let collection = core_text::font_collection::create_for_all_families();
 83        let Some(descriptors) = collection.get_descriptors() else {
 84            return vec![];
 85        };
 86        let mut names = BTreeSet::new();
 87        for descriptor in descriptors.into_iter() {
 88            names.insert(descriptor.display_name());
 89        }
 90        if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() {
 91            names.extend(fonts_in_memory);
 92        }
 93        names.into_iter().collect()
 94    }
 95
 96    fn font_id(&self, font: &Font) -> Result<FontId> {
 97        let lock = self.0.upgradable_read();
 98        if let Some(font_id) = lock.font_selections.get(font) {
 99            Ok(*font_id)
100        } else {
101            let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
102            let candidates = if let Some(font_ids) = lock.font_ids_by_family_name.get(&font.family)
103            {
104                font_ids.as_slice()
105            } else {
106                let font_ids = lock.load_family(&font.family, font.features)?;
107                lock.font_ids_by_family_name
108                    .insert(font.family.clone(), font_ids);
109                lock.font_ids_by_family_name[&font.family].as_ref()
110            };
111
112            let candidate_properties = candidates
113                .iter()
114                .map(|font_id| lock.fonts[font_id.0].properties())
115                .collect::<SmallVec<[_; 4]>>();
116
117            let ix = font_kit::matching::find_best_match(
118                &candidate_properties,
119                &font_kit::properties::Properties {
120                    style: font.style.into(),
121                    weight: font.weight.into(),
122                    stretch: Default::default(),
123                },
124            )?;
125
126            let font_id = candidates[ix];
127            lock.font_selections.insert(font.clone(), font_id);
128            Ok(font_id)
129        }
130    }
131
132    fn font_metrics(&self, font_id: FontId) -> FontMetrics {
133        self.0.read().fonts[font_id.0].metrics().into()
134    }
135
136    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
137        Ok(self.0.read().fonts[font_id.0]
138            .typographic_bounds(glyph_id.into())?
139            .into())
140    }
141
142    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
143        self.0.read().advance(font_id, glyph_id)
144    }
145
146    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
147        self.0.read().glyph_for_char(font_id, ch)
148    }
149
150    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
151        self.0.read().raster_bounds(params)
152    }
153
154    fn rasterize_glyph(
155        &self,
156        glyph_id: &RenderGlyphParams,
157        raster_bounds: Bounds<DevicePixels>,
158    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
159        self.0.read().rasterize_glyph(glyph_id, raster_bounds)
160    }
161
162    fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
163        self.0.write().layout_line(text, font_size, font_runs)
164    }
165
166    fn wrap_line(
167        &self,
168        text: &str,
169        font_id: FontId,
170        font_size: Pixels,
171        width: Pixels,
172    ) -> Vec<usize> {
173        self.0.read().wrap_line(text, font_id, font_size, width)
174    }
175}
176
177impl MacTextSystemState {
178    fn add_fonts(&mut self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
179        self.memory_source.add_fonts(
180            fonts
181                .iter()
182                .map(|bytes| Handle::from_memory(bytes.clone(), 0)),
183        )?;
184        Ok(())
185    }
186
187    fn load_family(
188        &mut self,
189        name: &SharedString,
190        features: FontFeatures,
191    ) -> Result<SmallVec<[FontId; 4]>> {
192        let mut font_ids = SmallVec::new();
193        let family = self
194            .memory_source
195            .select_family_by_name(name.as_ref())
196            .or_else(|_| self.system_source.select_family_by_name(name.as_ref()))?;
197        for font in family.fonts() {
198            let mut font = font.load()?;
199            open_type::apply_features(&mut font, features);
200            let Some(_) = font.glyph_for_char('m') else {
201                continue;
202            };
203            let font_id = FontId(self.fonts.len());
204            font_ids.push(font_id);
205            let postscript_name = font.postscript_name().unwrap();
206            self.font_ids_by_postscript_name
207                .insert(postscript_name.clone(), font_id);
208            self.postscript_names_by_font_id
209                .insert(font_id, postscript_name);
210            self.fonts.push(font);
211        }
212        Ok(font_ids)
213    }
214
215    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
216        Ok(self.fonts[font_id.0].advance(glyph_id.into())?.into())
217    }
218
219    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
220        self.fonts[font_id.0].glyph_for_char(ch).map(Into::into)
221    }
222
223    fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId {
224        let postscript_name = requested_font.postscript_name();
225        if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) {
226            *font_id
227        } else {
228            let font_id = FontId(self.fonts.len());
229            self.font_ids_by_postscript_name
230                .insert(postscript_name.clone(), font_id);
231            self.postscript_names_by_font_id
232                .insert(font_id, postscript_name);
233            self.fonts
234                .push(font_kit::font::Font::from_core_graphics_font(
235                    requested_font.copy_to_CGFont(),
236                ));
237            font_id
238        }
239    }
240
241    fn is_emoji(&self, font_id: FontId) -> bool {
242        self.postscript_names_by_font_id
243            .get(&font_id)
244            .map_or(false, |postscript_name| {
245                postscript_name == "AppleColorEmoji"
246            })
247    }
248
249    fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
250        let font = &self.fonts[params.font_id.0];
251        let scale = Transform2F::from_scale(params.scale_factor);
252        Ok(font
253            .raster_bounds(
254                params.glyph_id.into(),
255                params.font_size.into(),
256                scale,
257                HintingOptions::None,
258                font_kit::canvas::RasterizationOptions::GrayscaleAa,
259            )?
260            .into())
261    }
262
263    fn rasterize_glyph(
264        &self,
265        params: &RenderGlyphParams,
266        glyph_bounds: Bounds<DevicePixels>,
267    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
268        if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
269            Err(anyhow!("glyph bounds are empty"))
270        } else {
271            // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing.
272            let mut bitmap_size = glyph_bounds.size;
273            if params.subpixel_variant.x > 0 {
274                bitmap_size.width += DevicePixels(1);
275            }
276            if params.subpixel_variant.y > 0 {
277                bitmap_size.height += DevicePixels(1);
278            }
279            let bitmap_size = bitmap_size;
280
281            let mut bytes;
282            let cx;
283            if params.is_emoji {
284                bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize];
285                cx = CGContext::create_bitmap_context(
286                    Some(bytes.as_mut_ptr() as *mut _),
287                    bitmap_size.width.0 as usize,
288                    bitmap_size.height.0 as usize,
289                    8,
290                    bitmap_size.width.0 as usize * 4,
291                    &CGColorSpace::create_device_rgb(),
292                    kCGImageAlphaPremultipliedLast,
293                );
294            } else {
295                bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize];
296                cx = CGContext::create_bitmap_context(
297                    Some(bytes.as_mut_ptr() as *mut _),
298                    bitmap_size.width.0 as usize,
299                    bitmap_size.height.0 as usize,
300                    8,
301                    bitmap_size.width.0 as usize,
302                    &CGColorSpace::create_device_gray(),
303                    kCGImageAlphaOnly,
304                );
305            }
306
307            // Move the origin to bottom left and account for scaling, this
308            // makes drawing text consistent with the font-kit's raster_bounds.
309            cx.translate(
310                -glyph_bounds.origin.x.0 as CGFloat,
311                (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat,
312            );
313            cx.scale(
314                params.scale_factor as CGFloat,
315                params.scale_factor as CGFloat,
316            );
317
318            let subpixel_shift = params
319                .subpixel_variant
320                .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32);
321            cx.set_allows_font_subpixel_positioning(true);
322            cx.set_should_subpixel_position_fonts(true);
323            cx.set_allows_font_subpixel_quantization(false);
324            cx.set_should_subpixel_quantize_fonts(false);
325            self.fonts[params.font_id.0]
326                .native_font()
327                .clone_with_font_size(f32::from(params.font_size) as CGFloat)
328                .draw_glyphs(
329                    &[u32::from(params.glyph_id) as CGGlyph],
330                    &[CGPoint::new(
331                        (subpixel_shift.x / params.scale_factor) as CGFloat,
332                        (subpixel_shift.y / params.scale_factor) as CGFloat,
333                    )],
334                    cx,
335                );
336
337            if params.is_emoji {
338                // Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
339                for pixel in bytes.chunks_exact_mut(4) {
340                    pixel.swap(0, 2);
341                    let a = pixel[3] as f32 / 255.;
342                    pixel[0] = (pixel[0] as f32 / a) as u8;
343                    pixel[1] = (pixel[1] as f32 / a) as u8;
344                    pixel[2] = (pixel[2] as f32 / a) as u8;
345                }
346            }
347
348            Ok((bitmap_size, bytes))
349        }
350    }
351
352    fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
353        // Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
354        let mut string = CFMutableAttributedString::new();
355        {
356            string.replace_str(&CFString::new(text), CFRange::init(0, 0));
357            let utf16_line_len = string.char_len() as usize;
358
359            let mut ix_converter = StringIndexConverter::new(text);
360            for run in font_runs {
361                let utf8_end = ix_converter.utf8_ix + run.len;
362                let utf16_start = ix_converter.utf16_ix;
363
364                if utf16_start >= utf16_line_len {
365                    break;
366                }
367
368                ix_converter.advance_to_utf8_ix(utf8_end);
369                let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len);
370
371                let cf_range =
372                    CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
373
374                let font: &FontKitFont = &self.fonts[run.font_id.0];
375                unsafe {
376                    string.set_attribute(
377                        cf_range,
378                        kCTFontAttributeName,
379                        &font.native_font().clone_with_font_size(font_size.into()),
380                    );
381                }
382
383                if utf16_end == utf16_line_len {
384                    break;
385                }
386            }
387        }
388
389        // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets.
390        let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
391
392        let mut runs = Vec::new();
393        for run in line.glyph_runs().into_iter() {
394            let attributes = run.attributes().unwrap();
395            let font = unsafe {
396                attributes
397                    .get(kCTFontAttributeName)
398                    .downcast::<CTFont>()
399                    .unwrap()
400            };
401            let font_id = self.id_for_native_font(font);
402
403            let mut ix_converter = StringIndexConverter::new(text);
404            let mut glyphs = SmallVec::new();
405            for ((glyph_id, position), glyph_utf16_ix) in run
406                .glyphs()
407                .iter()
408                .zip(run.positions().iter())
409                .zip(run.string_indices().iter())
410            {
411                let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap();
412                ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
413                glyphs.push(ShapedGlyph {
414                    id: (*glyph_id).into(),
415                    position: point(position.x as f32, position.y as f32).map(px),
416                    index: ix_converter.utf8_ix,
417                    is_emoji: self.is_emoji(font_id),
418                });
419            }
420
421            runs.push(ShapedRun { font_id, glyphs })
422        }
423
424        let typographic_bounds = line.get_typographic_bounds();
425        LineLayout {
426            runs,
427            font_size,
428            width: typographic_bounds.width.into(),
429            ascent: typographic_bounds.ascent.into(),
430            descent: typographic_bounds.descent.into(),
431            len: text.len(),
432        }
433    }
434
435    fn wrap_line(
436        &self,
437        text: &str,
438        font_id: FontId,
439        font_size: Pixels,
440        width: Pixels,
441    ) -> Vec<usize> {
442        let mut string = CFMutableAttributedString::new();
443        string.replace_str(&CFString::new(text), CFRange::init(0, 0));
444        let cf_range = CFRange::init(0, text.encode_utf16().count() as isize);
445        let font = &self.fonts[font_id.0];
446        unsafe {
447            string.set_attribute(
448                cf_range,
449                kCTFontAttributeName,
450                &font.native_font().clone_with_font_size(font_size.into()),
451            );
452
453            let typesetter = CTTypesetterCreateWithAttributedString(string.as_concrete_TypeRef());
454            let mut ix_converter = StringIndexConverter::new(text);
455            let mut break_indices = Vec::new();
456            while ix_converter.utf8_ix < text.len() {
457                let utf16_len = CTTypesetterSuggestLineBreak(
458                    typesetter,
459                    ix_converter.utf16_ix as isize,
460                    width.into(),
461                ) as usize;
462                ix_converter.advance_to_utf16_ix(ix_converter.utf16_ix + utf16_len);
463                if ix_converter.utf8_ix >= text.len() {
464                    break;
465                }
466                break_indices.push(ix_converter.utf8_ix);
467            }
468            break_indices
469        }
470    }
471}
472
473#[derive(Clone)]
474struct StringIndexConverter<'a> {
475    text: &'a str,
476    utf8_ix: usize,
477    utf16_ix: usize,
478}
479
480impl<'a> StringIndexConverter<'a> {
481    fn new(text: &'a str) -> Self {
482        Self {
483            text,
484            utf8_ix: 0,
485            utf16_ix: 0,
486        }
487    }
488
489    fn advance_to_utf8_ix(&mut self, utf8_target: usize) {
490        for (ix, c) in self.text[self.utf8_ix..].char_indices() {
491            if self.utf8_ix + ix >= utf8_target {
492                self.utf8_ix += ix;
493                return;
494            }
495            self.utf16_ix += c.len_utf16();
496        }
497        self.utf8_ix = self.text.len();
498    }
499
500    fn advance_to_utf16_ix(&mut self, utf16_target: usize) {
501        for (ix, c) in self.text[self.utf8_ix..].char_indices() {
502            if self.utf16_ix >= utf16_target {
503                self.utf8_ix += ix;
504                return;
505            }
506            self.utf16_ix += c.len_utf16();
507        }
508        self.utf8_ix = self.text.len();
509    }
510}
511
512#[repr(C)]
513pub(crate) struct __CFTypesetter(c_void);
514
515type CTTypesetterRef = *const __CFTypesetter;
516
517#[link(name = "CoreText", kind = "framework")]
518extern "C" {
519    fn CTTypesetterCreateWithAttributedString(string: CFAttributedStringRef) -> CTTypesetterRef;
520
521    fn CTTypesetterSuggestLineBreak(
522        typesetter: CTTypesetterRef,
523        start_index: CFIndex,
524        width: f64,
525    ) -> CFIndex;
526}
527
528impl From<Metrics> for FontMetrics {
529    fn from(metrics: Metrics) -> Self {
530        FontMetrics {
531            units_per_em: metrics.units_per_em,
532            ascent: metrics.ascent,
533            descent: metrics.descent,
534            line_gap: metrics.line_gap,
535            underline_position: metrics.underline_position,
536            underline_thickness: metrics.underline_thickness,
537            cap_height: metrics.cap_height,
538            x_height: metrics.x_height,
539            bounding_box: metrics.bounding_box.into(),
540        }
541    }
542}
543
544impl From<RectF> for Bounds<f32> {
545    fn from(rect: RectF) -> Self {
546        Bounds {
547            origin: point(rect.origin_x(), rect.origin_y()),
548            size: size(rect.width(), rect.height()),
549        }
550    }
551}
552
553impl From<RectI> for Bounds<DevicePixels> {
554    fn from(rect: RectI) -> Self {
555        Bounds {
556            origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
557            size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
558        }
559    }
560}
561
562impl From<Vector2I> for Size<DevicePixels> {
563    fn from(value: Vector2I) -> Self {
564        size(value.x().into(), value.y().into())
565    }
566}
567
568impl From<RectI> for Bounds<i32> {
569    fn from(rect: RectI) -> Self {
570        Bounds {
571            origin: point(rect.origin_x(), rect.origin_y()),
572            size: size(rect.width(), rect.height()),
573        }
574    }
575}
576
577impl From<Point<u32>> for Vector2I {
578    fn from(size: Point<u32>) -> Self {
579        Vector2I::new(size.x as i32, size.y as i32)
580    }
581}
582
583impl From<Vector2F> for Size<f32> {
584    fn from(vec: Vector2F) -> Self {
585        size(vec.x(), vec.y())
586    }
587}
588
589impl From<FontWeight> for FontkitWeight {
590    fn from(value: FontWeight) -> Self {
591        FontkitWeight(value.0)
592    }
593}
594
595impl From<FontStyle> for FontkitStyle {
596    fn from(style: FontStyle) -> Self {
597        match style {
598            FontStyle::Normal => FontkitStyle::Normal,
599            FontStyle::Italic => FontkitStyle::Italic,
600            FontStyle::Oblique => FontkitStyle::Oblique,
601        }
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use crate::{font, px, FontRun, MacTextSystem, PlatformTextSystem};
608
609    #[test]
610    fn test_wrap_line() {
611        let fonts = MacTextSystem::new();
612        let font_id = fonts.font_id(&font("Helvetica")).unwrap();
613
614        let line = "one two three four five\n";
615        let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
616        assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
617
618        let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
619        let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
620        assert_eq!(
621            wrap_boundaries,
622            &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
623        );
624    }
625
626    #[test]
627    fn test_layout_line_bom_char() {
628        let fonts = MacTextSystem::new();
629        let font_id = fonts.font_id(&font("Helvetica")).unwrap();
630        let line = "\u{feff}";
631        let mut style = FontRun {
632            font_id,
633            len: line.len(),
634        };
635
636        let layout = fonts.layout_line(line, px(16.), &[style]);
637        assert_eq!(layout.len, line.len());
638        assert!(layout.runs.is_empty());
639
640        let line = "a\u{feff}b";
641        style.len = line.len();
642        let layout = fonts.layout_line(line, px(16.), &[style]);
643        assert_eq!(layout.len, line.len());
644        assert_eq!(layout.runs.len(), 1);
645        assert_eq!(layout.runs[0].glyphs.len(), 2);
646        assert_eq!(layout.runs[0].glyphs[0].id, 68u32.into()); // a
647                                                               // There's no glyph for \u{feff}
648        assert_eq!(layout.runs[0].glyphs[1].id, 69u32.into()); // b
649    }
650}