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