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