Align glyphs correctly using font-kit's raster_bounds

Antonio Scandurra created

Change summary

gpui/src/fonts.rs                     | 129 ++++++++++++++++------------
gpui/src/platform/mac/geometry.rs     |   1 
gpui/src/platform/mac/renderer.rs     |  11 +-
gpui/src/platform/mac/sprite_cache.rs |  44 ++++++---
4 files changed, 110 insertions(+), 75 deletions(-)

Detailed changes

gpui/src/fonts.rs 🔗

@@ -1,17 +1,20 @@
 use crate::geometry::{
     rect::RectI,
-    vector::{vec2f, Vector2F, Vector2I},
+    transform2d::Transform2F,
+    vector::{vec2f, Vector2F},
 };
 use anyhow::{anyhow, Result};
-use cocoa::appkit::CGPoint;
-use core_graphics::{base::CGGlyph, color_space::CGColorSpace, context::CGContext};
-use parking_lot::{RwLock, RwLockUpgradableReadGuard};
-
+use cocoa::appkit::{CGFloat, CGPoint};
+use core_graphics::{
+    base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform,
+};
 pub use font_kit::properties::{Properties, Weight};
 use font_kit::{
-    font::Font, loaders::core_text::NativeFont, metrics::Metrics, source::SystemSource,
+    canvas::RasterizationOptions, font::Font, hinting::HintingOptions,
+    loaders::core_text::NativeFont, metrics::Metrics, source::SystemSource,
 };
 use ordered_float::OrderedFloat;
+use parking_lot::{RwLock, RwLockUpgradableReadGuard};
 use std::{collections::HashMap, sync::Arc};
 
 #[allow(non_upper_case_globals)]
@@ -193,35 +196,51 @@ impl FontCache {
         font_size: f32,
         glyph_id: GlyphId,
         scale_factor: f32,
-    ) -> Option<(Vector2I, Vec<u8>)> {
-        let native_font = self.native_font(font_id, font_size);
-        let glyph_id = glyph_id as CGGlyph;
-        let glyph_bounds =
-            native_font.get_bounding_rects_for_glyphs(Default::default(), &[glyph_id]);
-        let position = CGPoint::new(-glyph_bounds.origin.x, -glyph_bounds.origin.y);
-        let width = (glyph_bounds.size.width * scale_factor as f64).ceil() as usize;
-        let height = (glyph_bounds.size.height * scale_factor as f64).ceil() as usize;
-
-        if width == 0 || height == 0 {
+    ) -> Option<(RectI, Vec<u8>)> {
+        let font = self.font(font_id);
+        let scale = Transform2F::from_scale(scale_factor);
+        let bounds = font
+            .raster_bounds(
+                glyph_id,
+                font_size,
+                scale,
+                HintingOptions::None,
+                RasterizationOptions::GrayscaleAa,
+            )
+            .ok()?;
+
+        if bounds.width() == 0 || bounds.height() == 0 {
             None
         } else {
-            let mut ctx = CGContext::create_bitmap_context(
-                None,
-                width,
-                height,
+            let mut pixels = vec![0; bounds.width() as usize * bounds.height() as usize];
+            let ctx = CGContext::create_bitmap_context(
+                Some(pixels.as_mut_ptr() as *mut _),
+                bounds.width() as usize,
+                bounds.height() as usize,
                 8,
-                width,
+                bounds.width() as usize,
                 &CGColorSpace::create_device_gray(),
                 kCGImageAlphaOnly,
             );
-            ctx.scale(scale_factor as f64, scale_factor as f64);
-            native_font.draw_glyphs(&[glyph_id], &[position], ctx.clone());
-            ctx.flush();
-
-            Some((
-                Vector2I::new(width as i32, height as i32),
-                Vec::from(ctx.data()),
-            ))
+
+            // Move the origin to bottom left and account for scaling, this
+            // makes drawing text consistent with the font-kit's raster_bounds.
+            ctx.translate(0.0, bounds.height() as CGFloat);
+            let transform = scale.translate(-bounds.origin().to_f32());
+            ctx.set_text_matrix(&CGAffineTransform {
+                a: transform.matrix.m11() as CGFloat,
+                b: -transform.matrix.m21() as CGFloat,
+                c: -transform.matrix.m12() as CGFloat,
+                d: transform.matrix.m22() as CGFloat,
+                tx: transform.vector.x() as CGFloat,
+                ty: -transform.vector.y() as CGFloat,
+            });
+
+            ctx.set_font(&font.native_font().copy_to_CGFont());
+            ctx.set_font_size(font_size as CGFloat);
+            ctx.show_glyphs_at_positions(&[glyph_id as CGGlyph], &[CGPoint::new(0.0, 0.0)]);
+
+            Some((bounds, pixels))
         }
     }
 
@@ -290,30 +309,28 @@ fn push_font(state: &mut FontCacheState, font: Font) -> FontId {
     font_id
 }
 
-#[cfg(test)]
-mod tests {
-    use std::{fs::File, io::BufWriter, path::Path};
-
-    use super::*;
-
-    #[test]
-    fn test_render_glyph() {
-        let cache = FontCache::new();
-        let family_id = cache.load_family(&["Fira Code"]).unwrap();
-        let font_id = cache.select_font(family_id, &Default::default()).unwrap();
-        let glyph_id = cache.font(font_id).glyph_for_char('m').unwrap();
-        let (size, bytes) = cache.render_glyph(font_id, 16.0, glyph_id, 1.).unwrap();
-
-        let path = Path::new(r"/Users/as-cii/Desktop/image.png");
-        let file = File::create(path).unwrap();
-        let ref mut w = BufWriter::new(file);
-
-        let mut encoder = png::Encoder::new(w, size.x() as u32, size.y() as u32);
-        encoder.set_color(png::ColorType::Grayscale);
-        encoder.set_depth(png::BitDepth::Eight);
-        let mut writer = encoder.write_header().unwrap();
-
-        writer.write_image_data(&bytes).unwrap(); // Save
-        dbg!(size, bytes);
-    }
-}
+// #[cfg(test)]
+// mod tests {
+//     use std::{fs::File, io::BufWriter, path::Path};
+
+//     use super::*;
+
+//     #[test]
+//     fn test_render_glyph() {
+//         let cache = FontCache::new();
+//         let family_id = cache.load_family(&["Fira Code"]).unwrap();
+//         let font_id = cache.select_font(family_id, &Default::default()).unwrap();
+//         let glyph_id = cache.font(font_id).glyph_for_char('G').unwrap();
+//         let (bounds, bytes) = cache.render_glyph(font_id, 16.0, glyph_id, 1.).unwrap();
+
+//         let path = Path::new(r"/Users/as-cii/Desktop/image.png");
+//         let file = File::create(path).unwrap();
+//         let ref mut w = BufWriter::new(file);
+
+//         let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32);
+//         encoder.set_color(png::ColorType::Grayscale);
+//         encoder.set_depth(png::BitDepth::Eight);
+//         let mut writer = encoder.write_header().unwrap();
+//         writer.write_image_data(&bytes).unwrap();
+//     }
+// }

gpui/src/platform/mac/geometry.rs 🔗

@@ -1,5 +1,6 @@
 use cocoa::foundation::{NSPoint, NSRect, NSSize};
 use pathfinder_geometry::{rect::RectF, vector::Vector2F};
+
 pub trait Vector2FExt {
     fn to_ns_point(&self) -> NSPoint;
     fn to_ns_size(&self) -> NSSize;

gpui/src/platform/mac/renderer.rs 🔗

@@ -267,19 +267,20 @@ impl Renderer {
 
         let mut sprites_by_atlas = HashMap::new();
         for glyph in layer.glyphs() {
-            if let Some((atlas, bounds)) = self.sprite_cache.render_glyph(
+            if let Some(sprite) = self.sprite_cache.render_glyph(
                 glyph.font_id,
                 glyph.font_size,
                 glyph.id,
                 scene.scale_factor(),
             ) {
                 sprites_by_atlas
-                    .entry(atlas)
+                    .entry(sprite.atlas_id)
                     .or_insert_with(Vec::new)
                     .push(shaders::GPUISprite {
-                        origin: (glyph.origin * scene.scale_factor()).to_float2(),
-                        size: bounds.size().to_float2(),
-                        atlas_origin: bounds.origin().to_float2(),
+                        origin: (glyph.origin * scene.scale_factor() + sprite.offset.to_f32())
+                            .to_float2(),
+                        size: sprite.size.to_float2(),
+                        atlas_origin: sprite.atlas_origin.to_float2(),
                         color: glyph.color.to_uchar4(),
                     });
             }

gpui/src/platform/mac/sprite_cache.rs 🔗

@@ -19,12 +19,20 @@ struct GlyphDescriptor {
     glyph_id: GlyphId,
 }
 
+#[derive(Clone)]
+pub struct GlyphSprite {
+    pub atlas_id: usize,
+    pub atlas_origin: Vector2I,
+    pub offset: Vector2I,
+    pub size: Vector2I,
+}
+
 pub struct SpriteCache {
     device: metal::Device,
     atlas_size: Vector2I,
     font_cache: Arc<FontCache>,
     atlasses: Vec<Atlas>,
-    glyphs: HashMap<GlyphDescriptor, Option<(usize, RectI)>>,
+    glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
 }
 
 impl SpriteCache {
@@ -49,7 +57,7 @@ impl SpriteCache {
         font_size: f32,
         glyph_id: GlyphId,
         scale_factor: f32,
-    ) -> Option<(usize, RectI)> {
+    ) -> Option<GlyphSprite> {
         let font_cache = &self.font_cache;
         let atlasses = &mut self.atlasses;
         let atlas_size = self.atlas_size;
@@ -61,20 +69,28 @@ impl SpriteCache {
                 glyph_id,
             })
             .or_insert_with(|| {
-                let (size, mask) =
+                let (glyph_bounds, mask) =
                     font_cache.render_glyph(font_id, font_size, glyph_id, scale_factor)?;
-                assert!(size.x() < atlas_size.x());
-                assert!(size.y() < atlas_size.y());
+                assert!(glyph_bounds.width() < atlas_size.x());
+                assert!(glyph_bounds.height() < atlas_size.y());
+
+                let atlas_bounds = atlasses
+                    .last_mut()
+                    .unwrap()
+                    .try_insert(glyph_bounds.size(), &mask)
+                    .unwrap_or_else(|| {
+                        let mut atlas = Atlas::new(device, atlas_size);
+                        let bounds = atlas.try_insert(glyph_bounds.size(), &mask).unwrap();
+                        atlasses.push(atlas);
+                        bounds
+                    });
 
-                let atlas = atlasses.last_mut().unwrap();
-                if let Some(bounds) = atlas.try_insert(size, &mask) {
-                    Some((atlasses.len() - 1, RectI::new(bounds.origin(), size)))
-                } else {
-                    let mut atlas = Atlas::new(device, atlas_size);
-                    let bounds = atlas.try_insert(size, &mask).unwrap();
-                    atlasses.push(atlas);
-                    Some((atlasses.len() - 1, RectI::new(bounds.origin(), size)))
-                }
+                Some(GlyphSprite {
+                    atlas_id: atlasses.len() - 1,
+                    atlas_origin: atlas_bounds.origin(),
+                    offset: glyph_bounds.origin(),
+                    size: glyph_bounds.size(),
+                })
             })
             .clone()
     }