1
1
mirror of https://github.com/wez/wezterm.git synced 2024-09-11 14:25:57 +03:00

fonts: improve cap-height and use_cap_height_to_scale_fallback_fonts

We now compute the cap-height from the rasterized glyph data.

Moved the scaling action of use_cap_height_to_scale_fallback_fonts from
glyphcache into the font resolver: when enabled, and we have data
about the baseline font and the font being resolved, then the resolving
font will be scaled such that the cap-height of both fonts has the same
pixel size.

The effect of this is that `I` glyphs from both fonts should appear to
have the same height.

Added a row of `I`'s in differing styles at the bottom of styles.txt
to make this easier to visualize.

refs: #1189
This commit is contained in:
Wez Furlong 2021-10-02 18:20:45 -07:00
parent c51c282ad8
commit 401719fb01
10 changed files with 356 additions and 73 deletions

View File

@ -6,3 +6,4 @@
#include <freetype/ftoutln.h>
#include <freetype/ftmm.h>
#include <freetype/ftsynth.h>
#include <freetype/ftglyph.h>

View File

@ -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;
}

View File

@ -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

View File

@ -8,3 +8,5 @@ Regular
Blink
Rapid Blink
I I I I I I

View File

@ -98,13 +98,23 @@ struct FaceSize {
dpi: u32,
cell_width: f64,
cell_height: f64,
cap_height: Option<f64>,
cap_height_to_height_ratio: Option<f64>,
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<f64>,
pub cap_height_to_height_ratio: Option<f64>,
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.
// <https://github.com/wez/wezterm/issues/1165>
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<f64> {
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,
}
}
}
}

View File

@ -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<Self>, style: &TextStyle) -> anyhow::Result<Rc<LoadedFont>> {
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<dyn FontShaper>, Vec<ParsedFont>)> {
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<Self>, style: &TextStyle) -> anyhow::Result<Rc<LoadedFont>> {
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),

View File

@ -227,6 +227,7 @@ impl AllsortsParsedFont {
underline_thickness,
underline_position,
cap_height_ratio: None,
cap_height: None,
is_scaled: true, // FIXME
};

View File

@ -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,
};

View File

@ -54,6 +54,7 @@ pub struct FontMetrics {
/// Fraction of the EM square occupied by the cap height
pub cap_height_ratio: Option<f64>,
pub cap_height: Option<PixelLength>,
/// True if the font is scalable and this is a scaled metric.
/// False if the font only has bitmap strikes and what we

View File

@ -437,43 +437,24 @@ impl<T: Texture2d> GlyphCache<T> {
}
} 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