From 840cb1b93a68389fab63e5a8f94cdaa2d607d1c3 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 16 Apr 2020 19:46:17 +0100 Subject: [PATCH] Add Windows font fallback --- CHANGELOG.md | 1 + Cargo.lock | 17 +- font/Cargo.toml | 3 +- font/src/directwrite/mod.rs | 333 +++++++++++++++++++++++++----------- 4 files changed, 253 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e39a66..8011be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Vi mode for copying text and opening links - `CopySelection` action which copies into selection buffer on Linux/BSD - Option `cursor.thickness` to set terminal cursor thickness +- Font fallback on Windows ### Changed diff --git a/Cargo.lock b/Cargo.lock index bb9bb52..b91f66b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -465,7 +465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "dwrote" -version = "0.9.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -473,6 +473,7 @@ dependencies = [ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "wio 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -548,13 +549,14 @@ dependencies = [ "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "core-graphics 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)", "core-text 13.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "dwrote 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dwrote 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "euclid 0.20.7 (registry+https://github.com/rust-lang/crates.io-index)", "foreign-types 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "freetype-rs 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "servo-fontconfig 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2123,6 +2125,14 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -2239,7 +2249,7 @@ dependencies = [ "checksum dlib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "77e51249a9d823a4cb79e3eca6dcd756153e8ed0157b6c04775d04bf1b13b76a" "checksum downcast-rs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "52ba6eb47c2131e784a38b726eb54c1e1484904f013e576a25354d0124161af6" "checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" -"checksum dwrote 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0bd1369e02db5e9b842a9b67bce8a2fcc043beafb2ae8a799dd482d46ea1ff0d" +"checksum dwrote 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" "checksum embed-resource 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bbaba4684ab0af1cbb3ef0b1f540ddc4b57b31940c920ea594efe09ab86e2a6c" "checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" "checksum euclid 0.20.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3f852d320142e1cceb15dccef32ed72a9970b83109d8a4e24b1ab04d579f485d" @@ -2431,6 +2441,7 @@ dependencies = [ "checksum winpty 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "92c5a39bb2408a307dd5ab774039ec3f2c68d052970ae4dacc346101abe202b2" "checksum winpty-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed8a179a59760dc51d30b5e6eaf1bd6da88461f72f2616e962ddebef7e413210" "checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +"checksum wio 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" "checksum x11-clipboard 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e5e937afd03b64b7be4f959cc044e09260a47241b71e56933f37db097bf7859d" "checksum x11-dl 2.18.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2bf981e3a5b3301209754218f962052d4d9ee97e478f4d26d4a6eced34c1fef8" diff --git a/font/Cargo.toml b/font/Cargo.toml index f647c27..866b14c 100644 --- a/font/Cargo.toml +++ b/font/Cargo.toml @@ -23,4 +23,5 @@ core-graphics = "0.17" core-foundation-sys = "0.6" [target.'cfg(windows)'.dependencies] -dwrote = { version = "0.9.0" } +dwrote = { version = "0.11" } +winapi = { version = "0.3", features = ["impl-default"] } diff --git a/font/src/directwrite/mod.rs b/font/src/directwrite/mod.rs index 5f3bdfc..87130a3 100644 --- a/font/src/directwrite/mod.rs +++ b/font/src/directwrite/mod.rs @@ -13,30 +13,164 @@ // limitations under the License. // //! Rasterization powered by DirectWrite -use dwrote::{FontCollection, FontStretch, FontStyle, FontWeight, GlyphOffset, GlyphRunAnalysis}; +use std::borrow::Cow; +use std::collections::HashMap; +use std::ffi::OsString; +use std::fmt::{self, Display, Formatter}; +use std::os::windows::ffi::OsStringExt; + +use dwrote::{ + FontCollection, FontFace, FontFallback, FontStretch, FontStyle, FontWeight, GlyphOffset, + GlyphRunAnalysis, TextAnalysisSource, TextAnalysisSourceMethods, DWRITE_GLYPH_RUN, +}; + +use winapi::shared::ntdef::{HRESULT, LOCALE_NAME_MAX_LENGTH}; +use winapi::um::dwrite; +use winapi::um::winnls::GetUserDefaultLocaleName; use super::{ BitmapBuffer, FontDesc, FontKey, GlyphKey, Metrics, RasterizedGlyph, Size, Slant, Style, Weight, }; +/// Cached DirectWrite font. +struct Font { + face: FontFace, + family_name: String, + weight: FontWeight, + style: FontStyle, + stretch: FontStretch, +} + pub struct DirectWriteRasterizer { - fonts: Vec, + fonts: HashMap, + keys: HashMap, device_pixel_ratio: f32, + available_fonts: FontCollection, + fallback_sequence: Option, +} + +impl DirectWriteRasterizer { + fn rasterize_glyph( + &self, + face: &FontFace, + size: Size, + c: char, + ) -> Result { + let glyph_index = self.get_glyph_index(face, c)?; + + let em_size = em_size(size); + + let glyph_run = DWRITE_GLYPH_RUN { + fontFace: unsafe { face.as_ptr() }, + fontEmSize: em_size, + glyphCount: 1, + glyphIndices: &glyph_index, + glyphAdvances: &0.0, + glyphOffsets: &GlyphOffset::default(), + isSideways: 0, + bidiLevel: 0, + }; + + let rendering_mode = face.get_recommended_rendering_mode_default_params( + em_size, + self.device_pixel_ratio, + dwrote::DWRITE_MEASURING_MODE_NATURAL, + ); + + let glyph_analysis = GlyphRunAnalysis::create( + &glyph_run, + self.device_pixel_ratio, + None, + rendering_mode, + dwrote::DWRITE_MEASURING_MODE_NATURAL, + 0.0, + 0.0, + ) + .map_err(Error::DirectWriteError)?; + + let bounds = glyph_analysis + .get_alpha_texture_bounds(dwrote::DWRITE_TEXTURE_CLEARTYPE_3x1) + .map_err(Error::DirectWriteError)?; + + let buf = glyph_analysis + .create_alpha_texture(dwrote::DWRITE_TEXTURE_CLEARTYPE_3x1, bounds) + .map_err(Error::DirectWriteError)?; + + Ok(RasterizedGlyph { + c, + width: (bounds.right - bounds.left) as i32, + height: (bounds.bottom - bounds.top) as i32, + top: -bounds.top, + left: bounds.left, + buf: BitmapBuffer::RGB(buf), + }) + } + + fn get_loaded_font(&self, font_key: FontKey) -> Result<&Font, Error> { + self.fonts.get(&font_key).ok_or(Error::FontNotLoaded) + } + + fn get_glyph_index(&self, face: &FontFace, c: char) -> Result { + let idx = *face + .get_glyph_indices(&[c as u32]) + .first() + // DirectWrite returns 0 if the glyph does not exist in the font + .filter(|glyph_index| **glyph_index != 0) + .ok_or_else(|| Error::MissingGlyph(c))?; + + Ok(idx) + } + + fn get_fallback_font(&self, loaded_font: &Font, c: char) -> Option { + let fallback = self.fallback_sequence.as_ref()?; + + let mut buf = [0u16; 2]; + c.encode_utf16(&mut buf); + + let length = c.len_utf16() as u32; + let utf16_codepoints = &buf[..length as usize]; + + let locale = get_current_locale(); + + let text_analysis_source_data = TextAnalysisSourceData { locale: &locale, length }; + let text_analysis_source = TextAnalysisSource::from_text( + Box::new(text_analysis_source_data), + Cow::Borrowed(utf16_codepoints), + ); + + let fallback_result = fallback.map_characters( + &text_analysis_source, + 0, + length, + &self.available_fonts, + Some(&loaded_font.family_name), + loaded_font.weight, + loaded_font.style, + loaded_font.stretch, + ); + + fallback_result.mapped_font + } } impl crate::Rasterize for DirectWriteRasterizer { type Err = Error; fn new(device_pixel_ratio: f32, _: bool) -> Result { - Ok(DirectWriteRasterizer { fonts: Vec::new(), device_pixel_ratio }) + Ok(DirectWriteRasterizer { + fonts: HashMap::new(), + keys: HashMap::new(), + device_pixel_ratio, + available_fonts: FontCollection::system(), + fallback_sequence: FontFallback::get_system_fallback(), + }) } fn metrics(&self, key: FontKey, size: Size) -> Result { - let font = self.fonts.get(key.token as usize).ok_or(Error::FontNotLoaded)?; + let face = &self.get_loaded_font(key)?.face; + let vmetrics = face.metrics().metrics0(); - let vmetrics = font.metrics(); - let scale = (size.as_f32_pts() * self.device_pixel_ratio * (96.0 / 72.0)) - / f32::from(vmetrics.designUnitsPerEm); + let scale = em_size(size) * self.device_pixel_ratio / f32::from(vmetrics.designUnitsPerEm); let underline_position = f32::from(vmetrics.underlinePosition) * scale; let underline_thickness = f32::from(vmetrics.underlineThickness) * scale; @@ -50,10 +184,12 @@ impl crate::Rasterize for DirectWriteRasterizer { let line_height = f64::from(ascent - descent + line_gap); - // We assume that all monospace characters have the same width - // Because of this we take '!', the first drawable character, for measurements - let glyph_metrics = font.get_design_glyph_metrics(&[33], false); - let hmetrics = glyph_metrics.first().ok_or(Error::MissingGlyph('!'))?; + // Since all monospace characters have the same width, we use `!` for horizontal metrics + let c = '!'; + let glyph_index = self.get_glyph_index(face, c)?; + + let glyph_metrics = face.get_design_glyph_metrics(&[glyph_index], false); + let hmetrics = glyph_metrics.first().ok_or_else(|| Error::MissingGlyph(c))?; let average_advance = f64::from(hmetrics.advanceWidth) * f64::from(scale); @@ -69,27 +205,22 @@ impl crate::Rasterize for DirectWriteRasterizer { } fn load_font(&mut self, desc: &FontDesc, _size: Size) -> Result { - let system_fc = FontCollection::system(); + // Fast path if face is already loaded + if let Some(key) = self.keys.get(desc) { + return Ok(*key); + } - let family = system_fc + let family = self + .available_fonts .get_font_family_by_name(&desc.name) .ok_or_else(|| Error::MissingFont(desc.clone()))?; let font = match desc.style { Style::Description { weight, slant } => { - let weight = - if weight == Weight::Bold { FontWeight::Bold } else { FontWeight::Regular }; - - let style = match slant { - Slant::Normal => FontStyle::Normal, - Slant::Oblique => FontStyle::Oblique, - Slant::Italic => FontStyle::Italic, - }; - // This searches for the "best" font - should mean we don't have to worry about // fallbacks if our exact desired weight/style isn't available - Ok(family.get_first_matching_font(weight, FontStretch::Normal, style)) - }, + Ok(family.get_first_matching_font(weight.into(), FontStretch::Normal, slant.into())) + } Style::Specific(ref style) => { let mut idx = 0; let count = family.get_font_count(); @@ -107,73 +238,26 @@ impl crate::Rasterize for DirectWriteRasterizer { idx += 1; } - }, + } }?; - let face = font.create_font_face(); - self.fonts.push(face); + let key = FontKey::next(); + self.keys.insert(desc.clone(), key); + self.fonts.insert(key, font.into()); - Ok(FontKey { token: (self.fonts.len() - 1) as u32 }) + Ok(key) } fn get_glyph(&mut self, glyph: GlyphKey) -> Result { - let font = self.fonts.get(glyph.font_key.token as usize).ok_or(Error::FontNotLoaded)?; + let loaded_font = self.get_loaded_font(glyph.font_key)?; - let offset = GlyphOffset { advanceOffset: 0.0, ascenderOffset: 0.0 }; - - let glyph_index = *font - .get_glyph_indices(&[glyph.c as u32]) - .first() - .ok_or_else(|| Error::MissingGlyph(glyph.c))?; - if glyph_index == 0 { - // The DirectWrite documentation states that we should get 0 returned if the glyph - // does not exist in the font - return Err(Error::MissingGlyph(glyph.c)); + match self.rasterize_glyph(&loaded_font.face, glyph.size, glyph.c) { + Err(err @ Error::MissingGlyph(_)) => { + let fallback_font = self.get_fallback_font(&loaded_font, glyph.c).ok_or(err)?; + self.rasterize_glyph(&fallback_font.create_font_face(), glyph.size, glyph.c) + }, + result => result, } - - let glyph_run = dwrote::DWRITE_GLYPH_RUN { - fontFace: unsafe { font.as_ptr() }, - fontEmSize: glyph.size.as_f32_pts(), - glyphCount: 1, - glyphIndices: &(glyph_index), - glyphAdvances: &(0.0), - glyphOffsets: &(offset), - isSideways: 0, - bidiLevel: 0, - }; - - let rendering_mode = font.get_recommended_rendering_mode_default_params( - glyph.size.as_f32_pts(), - self.device_pixel_ratio * (96.0 / 72.0), - dwrote::DWRITE_MEASURING_MODE_NATURAL, - ); - - let glyph_analysis = GlyphRunAnalysis::create( - &glyph_run, - self.device_pixel_ratio * (96.0 / 72.0), - None, - rendering_mode, - dwrote::DWRITE_MEASURING_MODE_NATURAL, - 0.0, - 0.0, - ) - .or_else(|_| Err(Error::MissingGlyph(glyph.c)))?; - - let bounds = glyph_analysis - .get_alpha_texture_bounds(dwrote::DWRITE_TEXTURE_CLEARTYPE_3x1) - .or_else(|_| Err(Error::MissingGlyph(glyph.c)))?; - let buf = glyph_analysis - .create_alpha_texture(dwrote::DWRITE_TEXTURE_CLEARTYPE_3x1, bounds) - .or_else(|_| Err(Error::MissingGlyph(glyph.c)))?; - - Ok(RasterizedGlyph { - c: glyph.c, - width: (bounds.right - bounds.left) as i32, - height: (bounds.bottom - bounds.top) as i32, - top: -bounds.top, - left: bounds.left, - buf: BitmapBuffer::RGB(buf), - }) } fn update_dpr(&mut self, device_pixel_ratio: f32) { @@ -186,29 +270,84 @@ pub enum Error { MissingFont(FontDesc), MissingGlyph(char), FontNotLoaded, + DirectWriteError(HRESULT), } -impl ::std::error::Error for Error { - fn description(&self) -> &str { - match *self { - Error::MissingFont(ref _desc) => "Couldn't find the requested font", - Error::MissingGlyph(ref _c) => "Couldn't find the requested glyph", - Error::FontNotLoaded => "Tried to operate on font that hasn't been loaded", - } - } -} +impl std::error::Error for Error {} -impl ::std::fmt::Display for Error { - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - match *self { - Error::MissingGlyph(ref c) => write!(f, "Glyph not found for char {:?}", c), - Error::MissingFont(ref desc) => write!( +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Error::MissingGlyph(c) => write!(f, "Glyph not found for char {:?}", c), + Error::MissingFont(desc) => write!( f, "Couldn't find a font with {}\n\tPlease check the font config in your \ alacritty.yml.", desc ), Error::FontNotLoaded => f.write_str("Tried to use a font that hasn't been loaded"), + Error::DirectWriteError(hresult) => { + write!(f, "A DirectWrite rendering error occurred: {:#X}", hresult) + } } } } + +fn em_size(size: Size) -> f32 { + size.as_f32_pts() * (96.0 / 72.0) +} + +impl From for Font { + fn from(font: dwrote::Font) -> Font { + Font { + face: font.create_font_face(), + family_name: font.family_name(), + weight: font.weight(), + style: font.style(), + stretch: font.stretch(), + } + } +} + +impl From for FontWeight { + fn from(weight: Weight) -> FontWeight { + match weight { + Weight::Bold => FontWeight::Bold, + Weight::Normal => FontWeight::Regular, + } + } +} + +impl From for FontStyle { + fn from(slant: Slant) -> FontStyle { + match slant { + Slant::Oblique => FontStyle::Oblique, + Slant::Italic => FontStyle::Italic, + Slant::Normal => FontStyle::Normal, + } + } +} + +fn get_current_locale() -> String { + let mut buf = vec![0u16; LOCALE_NAME_MAX_LENGTH]; + let len = unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as i32) as usize }; + + // `len` includes null byte, which we don't need in Rust + OsString::from_wide(&buf[..len - 1]).into_string().expect("Locale not valid unicode") +} + +/// Font fallback information for dwrote's TextAnalysisSource. +struct TextAnalysisSourceData<'a> { + locale: &'a str, + length: u32, +} + +impl TextAnalysisSourceMethods for TextAnalysisSourceData<'_> { + fn get_locale_name(&self, _text_position: u32) -> (Cow, u32) { + (Cow::Borrowed(self.locale), self.length) + } + + fn get_paragraph_reading_direction(&self) -> dwrite::DWRITE_READING_DIRECTION { + dwrite::DWRITE_READING_DIRECTION_LEFT_TO_RIGHT + } +}