diff --git a/deps/freetype/bindings.h b/deps/freetype/bindings.h index 9dcd91360..0567b1311 100644 --- a/deps/freetype/bindings.h +++ b/deps/freetype/bindings.h @@ -6,3 +6,4 @@ #include #include #include +#include diff --git a/deps/freetype/src/lib.rs b/deps/freetype/src/lib.rs index 98db74dfb..415d76e4f 100644 --- a/deps/freetype/src/lib.rs +++ b/deps/freetype/src/lib.rs @@ -40,6 +40,7 @@ pub const FT_RASTER_FLAG_DEFAULT: u32 = 0; pub const FT_RASTER_FLAG_AA: u32 = 1; pub const FT_RASTER_FLAG_DIRECT: u32 = 2; pub const FT_RASTER_FLAG_CLIP: u32 = 4; +pub const FT_RASTER_FLAG_SDF: u32 = 8; pub const FT_ERR_BASE: u32 = 0; pub const FT_FACE_FLAG_SCALABLE: u32 = 1; pub const FT_FACE_FLAG_FIXED_SIZES: u32 = 2; @@ -275,12 +276,6 @@ pub enum FT_Glyph_Format_ { pub use self::FT_Glyph_Format_ as FT_Glyph_Format; #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct FT_RasterRec_ { - _unused: [u8; 0], -} -pub type FT_Raster = *mut FT_RasterRec_; -#[repr(C)] -#[derive(Debug, Copy, Clone)] pub struct FT_Span_ { pub x: ::std::os::raw::c_short, pub len: ::std::os::raw::c_ushort, @@ -323,6 +318,12 @@ pub struct FT_Raster_Params_ { pub clip_box: FT_BBox, } pub type FT_Raster_Params = FT_Raster_Params_; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct FT_RasterRec_ { + _unused: [u8; 0], +} +pub type FT_Raster = *mut FT_RasterRec_; pub type FT_Raster_NewFunc = ::std::option::Option< unsafe extern "C" fn( memory: *mut ::std::os::raw::c_void, @@ -454,6 +455,7 @@ pub const FT_Mod_Err_Type1: _bindgen_ty_1 = _bindgen_ty_1::FT_Mod_Err_Base; pub const FT_Mod_Err_Type42: _bindgen_ty_1 = _bindgen_ty_1::FT_Mod_Err_Base; pub const FT_Mod_Err_Winfonts: _bindgen_ty_1 = _bindgen_ty_1::FT_Mod_Err_Base; pub const FT_Mod_Err_GXvalid: _bindgen_ty_1 = _bindgen_ty_1::FT_Mod_Err_Base; +pub const FT_Mod_Err_Sdf: _bindgen_ty_1 = _bindgen_ty_1::FT_Mod_Err_Base; pub const FT_Mod_Err_Max: _bindgen_ty_1 = _bindgen_ty_1::FT_Mod_Err_Max; #[repr(u32)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -993,6 +995,9 @@ extern "C" { extern "C" { pub fn FT_Set_Transform(face: FT_Face, matrix: *mut FT_Matrix, delta: *mut FT_Vector); } +extern "C" { + pub fn FT_Get_Transform(face: FT_Face, matrix: *mut FT_Matrix, delta: *mut FT_Vector); +} #[repr(u32)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum FT_Render_Mode_ { @@ -1001,7 +1006,8 @@ pub enum FT_Render_Mode_ { FT_RENDER_MODE_MONO = 2, FT_RENDER_MODE_LCD = 3, FT_RENDER_MODE_LCD_V = 4, - FT_RENDER_MODE_MAX = 5, + FT_RENDER_MODE_SDF = 5, + FT_RENDER_MODE_MAX = 6, } pub use self::FT_Render_Mode_ as FT_Render_Mode; extern "C" { @@ -1082,23 +1088,6 @@ extern "C" { p_transform: *mut FT_Matrix, ) -> FT_Error; } -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct FT_LayerIterator_ { - pub num_layers: FT_UInt, - pub layer: FT_UInt, - pub p: *mut FT_Byte, -} -pub type FT_LayerIterator = FT_LayerIterator_; -extern "C" { - pub fn FT_Get_Color_Glyph_Layer( - face: FT_Face, - base_glyph: FT_UInt, - aglyph_index: *mut FT_UInt, - acolor_index: *mut FT_UInt, - iterator: *mut FT_LayerIterator, - ) -> FT_Bool; -} extern "C" { pub fn FT_Get_FSType_Flags(face: FT_Face) -> FT_UShort; } @@ -1799,3 +1788,90 @@ extern "C" { extern "C" { pub fn FT_GlyphSlot_Oblique(slot: FT_GlyphSlot); } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct FT_Glyph_Class_ { + _unused: [u8; 0], +} +pub type FT_Glyph_Class = FT_Glyph_Class_; +pub type FT_Glyph = *mut FT_GlyphRec_; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct FT_GlyphRec_ { + pub library: FT_Library, + pub clazz: *const FT_Glyph_Class, + pub format: FT_Glyph_Format, + pub advance: FT_Vector, +} +pub type FT_GlyphRec = FT_GlyphRec_; +pub type FT_BitmapGlyph = *mut FT_BitmapGlyphRec_; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct FT_BitmapGlyphRec_ { + pub root: FT_GlyphRec, + pub left: FT_Int, + pub top: FT_Int, + pub bitmap: FT_Bitmap, +} +pub type FT_BitmapGlyphRec = FT_BitmapGlyphRec_; +pub type FT_OutlineGlyph = *mut FT_OutlineGlyphRec_; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct FT_OutlineGlyphRec_ { + pub root: FT_GlyphRec, + pub outline: FT_Outline, +} +pub type FT_OutlineGlyphRec = FT_OutlineGlyphRec_; +extern "C" { + pub fn FT_New_Glyph( + library: FT_Library, + format: FT_Glyph_Format, + aglyph: *mut FT_Glyph, + ) -> FT_Error; +} +extern "C" { + pub fn FT_Get_Glyph(slot: FT_GlyphSlot, aglyph: *mut FT_Glyph) -> FT_Error; +} +extern "C" { + pub fn FT_Glyph_Copy(source: FT_Glyph, target: *mut FT_Glyph) -> FT_Error; +} +extern "C" { + pub fn FT_Glyph_Transform( + glyph: FT_Glyph, + matrix: *mut FT_Matrix, + delta: *mut FT_Vector, + ) -> FT_Error; +} +impl FT_Glyph_BBox_Mode_ { + pub const FT_GLYPH_BBOX_SUBPIXELS: FT_Glyph_BBox_Mode_ = + FT_Glyph_BBox_Mode_::FT_GLYPH_BBOX_UNSCALED; +} +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum FT_Glyph_BBox_Mode_ { + FT_GLYPH_BBOX_UNSCALED = 0, + FT_GLYPH_BBOX_GRIDFIT = 1, + FT_GLYPH_BBOX_TRUNCATE = 2, + FT_GLYPH_BBOX_PIXELS = 3, +} +pub use self::FT_Glyph_BBox_Mode_ as FT_Glyph_BBox_Mode; +extern "C" { + pub fn FT_Glyph_Get_CBox(glyph: FT_Glyph, bbox_mode: FT_UInt, acbox: *mut FT_BBox); +} +extern "C" { + pub fn FT_Glyph_To_Bitmap( + the_glyph: *mut FT_Glyph, + render_mode: FT_Render_Mode, + origin: *mut FT_Vector, + destroy: FT_Bool, + ) -> FT_Error; +} +extern "C" { + pub fn FT_Done_Glyph(glyph: FT_Glyph); +} +extern "C" { + pub fn FT_Matrix_Multiply(a: *const FT_Matrix, b: *mut FT_Matrix); +} +extern "C" { + pub fn FT_Matrix_Invert(matrix: *mut FT_Matrix) -> FT_Error; +} diff --git a/docs/changelog.md b/docs/changelog.md index 61e460943..bd21e8734 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -53,6 +53,7 @@ As features stabilize some brief notes about them will accumulate here. * New: [wezterm.action_callback](config/lua/wezterm/action_callback.md) function to make it easier to use custom events. Thanks to [@bew](https://github.com/bew)! [#1151](https://github.com/wez/wezterm/pull/1151) * New: `wezterm connect` now also supports the `--class` parameter to override the window class * Fixed: wezterm can now match bitmap fonts that are spread across multiple font files [#1189](https://github.com/wez/wezterm/issues/1189) +* Improved: [use_cap_height_to_scale_fallback_fonts](config/lua/config/use_cap_height_to_scale_fallback_fonts.md) now computes *cap-height* based on the rasterized glyph bitmap which means that the data is accurate in more cases, including for bitmap fonts. Scaling is now also applied to across varying text styles; previously it only applied to a font within an `wezterm.font_with_fallback` font list. ### 20210814-124438-54e29167 diff --git a/test-data/styles.txt b/test-data/styles.txt index fa17580e4..7e216c846 100644 --- a/test-data/styles.txt +++ b/test-data/styles.txt @@ -8,3 +8,5 @@ Regular Blink Rapid Blink + +I I I I I I diff --git a/wezterm-font/src/ftwrap.rs b/wezterm-font/src/ftwrap.rs index 50267e2fd..870a4db64 100644 --- a/wezterm-font/src/ftwrap.rs +++ b/wezterm-font/src/ftwrap.rs @@ -98,13 +98,23 @@ struct FaceSize { dpi: u32, cell_width: f64, cell_height: f64, + cap_height: Option, + cap_height_to_height_ratio: Option, is_scaled: bool, } +#[derive(Debug)] +struct ComputedCellMetrics { + pub width: f64, + pub height: f64, +} + #[derive(Debug)] pub struct SelectedFontSize { pub width: f64, pub height: f64, + pub cap_height: Option, + pub cap_height_to_height_ratio: Option, pub is_scaled: bool, } @@ -331,6 +341,8 @@ impl Face { width: face_size.cell_width, height: face_size.cell_height, is_scaled: face_size.is_scaled, + cap_height: face_size.cap_height, + cap_height_to_height_ratio: face_size.cap_height_to_height_ratio, }); } } @@ -350,10 +362,12 @@ impl Face { let selected_size = match self.set_char_size(size, size, dpi, dpi) { Ok(_) => { // Compute metrics for the nominal monospace cell - let (width, height) = self.cell_metrics(); + let ComputedCellMetrics { width, height } = self.cell_metrics(); SelectedFontSize { width, height, + cap_height: None, + cap_height_to_height_ratio: None, is_scaled: true, } } @@ -408,11 +422,14 @@ impl Face { // 4 pixels is too thin for this font, so we take the max of the // known dimensions to produce the size. // - let (m_width, m_height) = self.cell_metrics(); + let m = self.cell_metrics(); + let height = f64::from(best.height).max(m.height); SelectedFontSize { - width: f64::from(best.width).max(m_width), - height: f64::from(best.height).max(m_height), + width: f64::from(best.width).max(m.width), + height, is_scaled: false, + cap_height: None, + cap_height_to_height_ratio: None, } } }; @@ -421,11 +438,34 @@ impl Face { size: point_size, dpi, cell_width: selected_size.width, + cap_height: None, + cap_height_to_height_ratio: None, cell_height: selected_size.height, is_scaled: selected_size.is_scaled, }); - Ok(selected_size) + // Can't compute cap height until after we've assigned self.size + if let Ok(cap_height) = self.compute_cap_height() { + let cap_height_to_height_ratio = cap_height / selected_size.height; + + self.size.replace(FaceSize { + size: point_size, + dpi, + cell_width: selected_size.width, + cap_height: Some(cap_height), + cap_height_to_height_ratio: Some(cap_height_to_height_ratio), + cell_height: selected_size.height, + is_scaled: selected_size.is_scaled, + }); + + Ok(SelectedFontSize { + cap_height: Some(cap_height), + cap_height_to_height_ratio: Some(cap_height_to_height_ratio), + ..selected_size + }) + } else { + Ok(selected_size) + } } fn set_char_size( @@ -516,7 +556,129 @@ impl Face { } } - pub fn cell_metrics(&mut self) -> (f64, f64) { + /// Compute the cap-height metric in pixels. + /// This is pixel-perfect based on the rendered glyph data for `I`, + /// which is a technique that works for any font regardless + /// of the integrity of its internal cap-height metric or + /// whether the font is a bitmap font. + /// `I` is chosen rather than `O` as `O` glyphs are often optically + /// compensated and overshoot a little. + fn compute_cap_height(&mut self) -> anyhow::Result { + let glyph_pos = unsafe { FT_Get_Char_Index(self.face, b'I' as u64) }; + if glyph_pos == 0 { + anyhow::bail!("no I from which to compute cap height"); + } + let (load_flags, render_mode) = compute_load_flags_from_config(); + let ft_glyph = self.load_and_render_glyph(glyph_pos, load_flags, render_mode, false)?; + + let mode: FT_Pixel_Mode = + unsafe { std::mem::transmute(u32::from(ft_glyph.bitmap.pixel_mode)) }; + + // pitch is the number of bytes per source row + let pitch = ft_glyph.bitmap.pitch.abs() as usize; + let data = unsafe { + std::slice::from_raw_parts_mut( + ft_glyph.bitmap.buffer, + ft_glyph.bitmap.rows as usize * pitch, + ) + }; + + let mut first_row = None; + let mut last_row = None; + + match mode { + FT_Pixel_Mode::FT_PIXEL_MODE_LCD => { + let width = ft_glyph.bitmap.width as usize / 3; + let height = ft_glyph.bitmap.rows as usize; + + 'next_line_lcd: for y in 0..height { + let src_offset = y * pitch as usize; + for x in 0..width { + if data[src_offset + (x * 3)] != 0 + || data[src_offset + (x * 3) + 1] != 0 + || data[src_offset + (x * 3) + 2] != 0 + { + if first_row.is_none() { + first_row.replace(y); + } + last_row.replace(y); + continue 'next_line_lcd; + } + } + } + } + + FT_Pixel_Mode::FT_PIXEL_MODE_BGRA => { + let width = ft_glyph.bitmap.width as usize; + let height = ft_glyph.bitmap.rows as usize; + 'next_line_bgra: for y in 0..height { + let src_offset = y * pitch as usize; + for x in 0..width { + let alpha = data[src_offset + (x * 4) + 3]; + if alpha != 0 { + if first_row.is_none() { + first_row.replace(y); + } + last_row.replace(y); + continue 'next_line_bgra; + } + } + } + } + FT_Pixel_Mode::FT_PIXEL_MODE_GRAY => { + let width = ft_glyph.bitmap.width as usize; + let height = ft_glyph.bitmap.rows as usize; + 'next_line_gray: for y in 0..height { + let src_offset = y * pitch; + for x in 0..width { + if data[src_offset + x] != 0 { + if first_row.is_none() { + first_row.replace(y); + } + last_row.replace(y); + continue 'next_line_gray; + } + } + } + } + FT_Pixel_Mode::FT_PIXEL_MODE_MONO => { + let width = ft_glyph.bitmap.width as usize; + let height = ft_glyph.bitmap.rows as usize; + 'next_line_mono: for y in 0..height { + let src_offset = y * pitch; + let mut x = 0; + for i in 0..pitch { + if x >= width { + break; + } + let mut b = data[src_offset + i]; + for _ in 0..8 { + if x >= width { + break; + } + if b & 0x80 == 0x80 { + if first_row.is_none() { + first_row.replace(y); + } + last_row.replace(y); + continue 'next_line_mono; + } + b <<= 1; + x += 1; + } + } + } + } + _ => anyhow::bail!("unhandled pixel mode {:?}", mode), + } + + match (first_row, last_row) { + (Some(first), Some(last)) => Ok((last - first) as f64), + _ => anyhow::bail!("didn't find any rasterized rows?"), + } + } + + fn cell_metrics(&mut self) -> ComputedCellMetrics { unsafe { let metrics = &(*(*self.face).size).metrics; let height = (metrics.y_scale as f64 * f64::from((*self.face).height)) @@ -555,7 +717,11 @@ impl Face { width = height * 64.; } } - (width / 64.0, height) + + ComputedCellMetrics { + width: width / 64.0, + height, + } } } } diff --git a/wezterm-font/src/lib.rs b/wezterm-font/src/lib.rs index 97a46fc56..130225624 100644 --- a/wezterm-font/src/lib.rs +++ b/wezterm-font/src/lib.rs @@ -474,21 +474,12 @@ impl FontConfigInner { Ok(loaded) } - /// Given a text style, load (with caching) the font that best - /// matches according to the fontconfig pattern. - fn resolve_font(&self, myself: &Rc, style: &TextStyle) -> anyhow::Result> { - let config = self.config.borrow(); - - let mut fonts = self.fonts.borrow_mut(); - - if let Some(entry) = fonts.get(style) { - return Ok(Rc::clone(entry)); - } - - let font_size = config.font_size * *self.font_scale.borrow(); - let dpi = *self.dpi.borrow() as u32; - let pixel_size = (font_size * dpi as f64 / 72.0) as u16; - + fn resolve_font_helper( + &self, + style: &TextStyle, + config: &ConfigHandle, + pixel_size: u16, + ) -> anyhow::Result<(Box, Vec)> { let attributes = style.font_with_fallback(); let preferred_attributes = attributes .iter() @@ -561,15 +552,77 @@ impl FontConfigInner { } } - let shaper = new_shaper(&*config, &handles)?; + Ok((new_shaper(&*config, &handles)?, handles)) + } - let metrics = shaper.metrics(font_size, dpi).with_context(|| { + /// Given a text style, load (with caching) the font that best + /// matches according to the fontconfig pattern. + fn resolve_font(&self, myself: &Rc, style: &TextStyle) -> anyhow::Result> { + let config = self.config.borrow(); + let is_default = *style == config.font; + let def_font = if !is_default && config.use_cap_height_to_scale_fallback_fonts { + Some(self.default_font(myself)?) + } else { + None + }; + + let mut fonts = self.fonts.borrow_mut(); + + if let Some(entry) = fonts.get(style) { + return Ok(Rc::clone(entry)); + } + + let mut font_size = config.font_size * *self.font_scale.borrow(); + let dpi = *self.dpi.borrow() as u32; + let pixel_size = (font_size * dpi as f64 / 72.0) as u16; + + let (mut shaper, mut handles) = self.resolve_font_helper(style, &config, pixel_size)?; + + let mut metrics = shaper.metrics(font_size, dpi).with_context(|| { format!( "obtaining metrics for font_size={} @ dpi {}", font_size, dpi ) })?; + if let Some(def_font) = def_font { + let def_metrics = def_font.metrics(); + match (def_metrics.cap_height, metrics.cap_height) { + (Some(d), Some(m)) => { + // Scale by the ratio of the pixel heights of the default + // and this font; this causes the `I` glyphs to appear to + // have the same height. + let scale = d.get() / m.get(); + if scale != 1.0 { + let scaled_pixel_size = (pixel_size as f64 * scale) as u16; + let scaled_font_size = font_size * scale; + log::trace!( + "using cap height adjusted: pixel_size {} -> {}, font_size {} -> {}, {:?}", + pixel_size, + scaled_pixel_size, + font_size, + scaled_font_size, + metrics, + ); + let (alt_shaper, alt_handles) = + self.resolve_font_helper(style, &config, scaled_pixel_size)?; + shaper = alt_shaper; + handles = alt_handles; + + metrics = shaper.metrics(scaled_font_size, dpi).with_context(|| { + format!( + "obtaining cap-height adjusted metrics for font_size={} @ dpi {}", + scaled_font_size, dpi + ) + })?; + + font_size = scaled_font_size; + } + } + _ => {} + } + } + let loaded = Rc::new(LoadedFont { rasterizers: RefCell::new(HashMap::new()), handles: RefCell::new(handles), diff --git a/wezterm-font/src/shaper/allsorts.rs b/wezterm-font/src/shaper/allsorts.rs index ea3cafce8..717648cbb 100644 --- a/wezterm-font/src/shaper/allsorts.rs +++ b/wezterm-font/src/shaper/allsorts.rs @@ -227,6 +227,7 @@ impl AllsortsParsedFont { underline_thickness, underline_position, cap_height_ratio: None, + cap_height: None, is_scaled: true, // FIXME }; diff --git a/wezterm-font/src/shaper/harfbuzz.rs b/wezterm-font/src/shaper/harfbuzz.rs index 758cc2264..a0126c5f3 100644 --- a/wezterm-font/src/shaper/harfbuzz.rs +++ b/wezterm-font/src/shaper/harfbuzz.rs @@ -470,7 +470,8 @@ impl FontShaper for HarfbuzzShaper { underline_position: PixelLength::new( unsafe { (*pair.face.face).underline_position as f64 } * y_scale / 64., ), - cap_height_ratio: pair.face.cap_height(), + cap_height_ratio: selected_size.cap_height_to_height_ratio, + cap_height: selected_size.cap_height.map(PixelLength::new), is_scaled: selected_size.is_scaled, }; diff --git a/wezterm-font/src/shaper/mod.rs b/wezterm-font/src/shaper/mod.rs index 7dce98da7..334a69458 100644 --- a/wezterm-font/src/shaper/mod.rs +++ b/wezterm-font/src/shaper/mod.rs @@ -54,6 +54,7 @@ pub struct FontMetrics { /// Fraction of the EM square occupied by the cap height pub cap_height_ratio: Option, + pub cap_height: Option, /// True if the font is scalable and this is a scaled metric. /// False if the font only has bitmap strikes and what we diff --git a/wezterm-gui/src/glyphcache.rs b/wezterm-gui/src/glyphcache.rs index 553a5e9c8..262669966 100644 --- a/wezterm-gui/src/glyphcache.rs +++ b/wezterm-gui/src/glyphcache.rs @@ -437,43 +437,24 @@ impl GlyphCache { } } else { // a scalable fallback font - let y_scale = match ( - self.fonts.config().use_cap_height_to_scale_fallback_fonts, - base_metrics.cap_height_ratio, - idx_metrics.cap_height_ratio, - ) { - (true, Some(base_cap), Some(cap)) => { - // both fonts have cap-height metrics and we're in - // use_cap_height_to_scale_fallback_fonts mode, so - // scale based on their respective cap heights - base_cap / cap - } - _ => { - // Assume that the size we requested doesn't need - // any additional scaling - 1.0 - } - }; - // How wide the glyph would be using the y_scale we produced - let y_scaled_width = y_scale * glyph.width as f64; + let f_width = glyph.width as f64; - if allow_width_overflow || y_scaled_width <= max_pixel_width { - scale = y_scale; + if allow_width_overflow || f_width <= max_pixel_width { + scale = 1.0; } else { - scale = max_pixel_width / glyph.width as f64; + scale = max_pixel_width / f_width; } #[cfg(debug_assertions)] { log::debug!( "{} allow_width_overflow={} is_square_or_wide={} aspect={} \ - y_scaled_width={} max_pixel_width={} glyph.width={} -> scale={}", + max_pixel_width={} glyph.width={} -> scale={}", info.text, allow_width_overflow, is_square_or_wide, aspect, - y_scaled_width, max_pixel_width, glyph.width, scale