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