From ba44548d469ad7608327c06c3ece1bfbf6db24ff Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Wed, 25 Nov 2020 14:53:42 -0800 Subject: [PATCH] wezterm-font: teach gdi locator how to search for fallback fonts This commit uses a bit of DirectWrite to discover which font(s) can be used to render a set of codepoints. While hooking this up, I found that the method we were using to extract the font data didn't handle TTC data so this commit improves some parser diagnostics and handling for that. refs: https://github.com/wez/wezterm/issues/299 --- Cargo.lock | 14 +- wezterm-font/Cargo.toml | 2 +- wezterm-font/src/lib.rs | 60 ++++----- wezterm-font/src/locator/gdi.rs | 232 +++++++++++++++++++++++++------- wezterm-font/src/parser.rs | 99 +++++++++++--- 5 files changed, 305 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7b538eea..8a48d089c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1048,15 +1048,16 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "dwrote" -version = "0.9.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd1369e02db5e9b842a9b67bce8a2fcc043beafb2ae8a799dd482d46ea1ff0d" +checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" dependencies = [ "lazy_static", "libc", "serde", "serde_derive", "winapi 0.3.9", + "wio", ] [[package]] @@ -4552,6 +4553,15 @@ dependencies = [ "xml-rs 0.6.1", ] +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/wezterm-font/Cargo.toml b/wezterm-font/Cargo.toml index d9e4113d9..01629eaf9 100644 --- a/wezterm-font/Cargo.toml +++ b/wezterm-font/Cargo.toml @@ -31,7 +31,7 @@ window = { path = "../window" } fontconfig = { path = "../deps/fontconfig" } [target."cfg(windows)".dependencies] -dwrote = "0.9" +dwrote = "0.11" winapi = "0.3" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/wezterm-font/src/lib.rs b/wezterm-font/src/lib.rs index 52bf20293..58c716c75 100644 --- a/wezterm-font/src/lib.rs +++ b/wezterm-font/src/lib.rs @@ -1,7 +1,7 @@ use crate::locator::{new_locator, FontDataHandle, FontLocator, FontLocatorSelection}; use crate::rasterizer::{new_rasterizer, FontRasterizer}; use crate::shaper::{new_shaper, FontShaper, FontShaperSelection}; -use anyhow::{anyhow, Error}; +use anyhow::Error; use config::{configuration, ConfigHandle, FontRasterizerSelection, TextStyle}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; @@ -24,7 +24,7 @@ pub use crate::rasterizer::RasterizedGlyph; pub use crate::shaper::{FallbackIdx, FontMetrics, GlyphInfo}; pub struct LoadedFont { - rasterizers: Vec>>>, + rasterizers: RefCell>>, handles: RefCell>, shaper: RefCell>, metrics: FontMetrics, @@ -58,22 +58,29 @@ impl LoadedFont { err, no_glyphs.iter().collect::().escape_debug() ), - Ok(handles) if handles.is_empty() => { - log::error!( - "No fonts have glyphs for {}", - no_glyphs.iter().collect::().escape_debug() - ) - } + Ok(handles) if handles.is_empty() => log::error!( + "No fonts have glyphs for {}", + no_glyphs.iter().collect::().escape_debug() + ), Ok(extra_handles) => { let mut loaded = false; { let mut handles = self.handles.borrow_mut(); for h in extra_handles { if !handles.iter().any(|existing| *existing == h) { - if crate::parser::ParsedFont::from_locator(&h).is_ok() { - let idx = handles.len() - 1; - handles.insert(idx, h); - loaded = true; + match crate::parser::ParsedFont::from_locator(&h) { + Ok(_parsed) => { + let idx = handles.len() - 1; + handles.insert(idx, h); + loaded = true; + } + Err(err) => { + log::error!( + "Failed to parse font from {:?}: {:?}", + h, + err + ); + } } } } @@ -87,8 +94,8 @@ impl LoadedFont { return self.shape(text); } else { log::error!( - "No fonts have glyphs for {}", - no_glyphs.iter().collect::().escape_debug() + "No fonts have glyphs for {}, even though fallback suggested some.", + no_glyphs.iter().collect::().escape_debug(), ) } } @@ -110,23 +117,18 @@ impl LoadedFont { glyph_pos: u32, fallback: FallbackIdx, ) -> anyhow::Result { - let cell = self - .rasterizers - .get(fallback) - .ok_or_else(|| anyhow!("no such fallback index: {}", fallback))?; - let mut opt_raster = cell.borrow_mut(); - if opt_raster.is_none() { + let mut rasterizers = self.rasterizers.borrow_mut(); + if let Some(raster) = rasterizers.get(&fallback) { + raster.rasterize_glyph(glyph_pos, self.font_size, self.dpi) + } else { let raster = new_rasterizer( FontRasterizerSelection::get_default(), &(self.handles.borrow())[fallback], )?; - opt_raster.replace(raster); + let result = raster.rasterize_glyph(glyph_pos, self.font_size, self.dpi); + rasterizers.insert(fallback, raster); + result } - - opt_raster - .as_ref() - .unwrap() - .rasterize_glyph(glyph_pos, self.font_size, self.dpi) } } @@ -256,10 +258,6 @@ impl FontConfigInner { } } - let mut rasterizers = vec![]; - for _ in &handles { - rasterizers.push(RefCell::new(None)); - } let shaper = new_shaper(FontShaperSelection::get_default(), &handles)?; let config = configuration(); @@ -268,7 +266,7 @@ impl FontConfigInner { let metrics = shaper.metrics(font_size, dpi)?; let loaded = Rc::new(LoadedFont { - rasterizers, + rasterizers: RefCell::new(HashMap::new()), handles: RefCell::new(handles), shaper: RefCell::new(shaper), metrics, diff --git a/wezterm-font/src/locator/gdi.rs b/wezterm-font/src/locator/gdi.rs index 1b3111553..c9eba5f1e 100644 --- a/wezterm-font/src/locator/gdi.rs +++ b/wezterm-font/src/locator/gdi.rs @@ -2,8 +2,11 @@ use crate::locator::{FontDataHandle, FontLocator}; use config::FontAttributes; +use dwrote::{FontStretch, FontStyle, FontWeight}; +use std::borrow::Cow; use std::collections::HashSet; use winapi::shared::windef::HFONT; +use winapi::um::dwrite::*; use winapi::um::wingdi::{ CreateCompatibleDC, CreateFontIndirectW, DeleteDC, DeleteObject, GetFontData, SelectObject, FIXED_PITCH, GDI_ERROR, LF_FACESIZE, LOGFONTW, OUT_TT_ONLY_PRECIS, @@ -13,24 +16,55 @@ use winapi::um::wingdi::{ /// functions provided by the font-loader crate. pub struct GdiFontLocator {} -fn extract_font_data(font: HFONT, name: &str) -> anyhow::Result { +fn extract_font_data(font: HFONT, attr: &FontAttributes) -> anyhow::Result { unsafe { let hdc = CreateCompatibleDC(std::ptr::null_mut()); SelectObject(hdc, font as *mut _); - let size = GetFontData(hdc, 0, 0, std::ptr::null_mut(), 0); - let result = match size { - _ if size > 0 && size != GDI_ERROR => { - let mut data = vec![0u8; size as usize]; - GetFontData(hdc, 0, 0, data.as_mut_ptr() as *mut _, size); - Ok(FontDataHandle::Memory { - data, - index: 0, - name: name.to_string(), - }) + // GetFontData can retrieve different parts of the font data. + // We want to fetch the entire font file, but things are made + // more complicated because the file may be a TTC file. + // In that case, the full file data isn't full parsable + // as a TTF so we need to ask specifically for the TTC file, + // and then try to reverse engineer which element of the TTC + // is the one we were looking for. + + // See if we can retrieve the ttc data as a first try + let ttc_table = 0x66637474; // 'ttcf' + + let ttc_size = GetFontData(hdc, ttc_table, 0, std::ptr::null_mut(), 0); + + let result = if ttc_size > 0 && ttc_size != GDI_ERROR { + let mut data = vec![0u8; ttc_size as usize]; + GetFontData(hdc, ttc_table, 0, data.as_mut_ptr() as *mut _, ttc_size); + + // Determine which of the contained fonts is the one + // that we asked for. + let index = + crate::parser::resolve_font_from_ttc_data(&attr, &data)?.unwrap_or(0) as u32; + Ok(FontDataHandle::Memory { + data, + index, + name: attr.family.clone(), + }) + } else { + // Otherwise: presumably a regular ttf + + let size = GetFontData(hdc, 0, 0, std::ptr::null_mut(), 0); + match size { + _ if size > 0 && size != GDI_ERROR => { + let mut data = vec![0u8; size as usize]; + GetFontData(hdc, 0, 0, data.as_mut_ptr() as *mut _, size); + Ok(FontDataHandle::Memory { + data, + index: 0, + name: attr.family.clone(), + }) + } + _ => Err(anyhow::anyhow!("Failed to get font data")), } - _ => Err(anyhow::anyhow!("Failed to get font data")), }; + DeleteDC(hdc); result } @@ -45,6 +79,43 @@ fn wide_string(s: &str) -> Vec { .collect() } +fn load_font(font_attr: &FontAttributes) -> anyhow::Result { + let mut log_font = LOGFONTW { + lfHeight: 0, + lfWidth: 0, + lfEscapement: 0, + lfOrientation: 0, + lfWeight: if font_attr.bold { 700 } else { 0 }, + lfItalic: if font_attr.italic { 1 } else { 0 }, + lfUnderline: 0, + lfStrikeOut: 0, + lfCharSet: 0, + lfOutPrecision: OUT_TT_ONLY_PRECIS as u8, + lfClipPrecision: 0, + lfQuality: 0, + lfPitchAndFamily: FIXED_PITCH as u8, + lfFaceName: [0u16; 32], + }; + + let name = wide_string(&font_attr.family); + if name.len() > LF_FACESIZE { + anyhow::bail!( + "family name {:?} is too large for LOGFONTW", + font_attr.family + ); + } + for (i, &c) in name.iter().enumerate() { + log_font.lfFaceName[i] = c; + } + + unsafe { + let font = CreateFontIndirectW(&log_font); + let result = extract_font_data(font, font_attr); + DeleteObject(font as *mut _); + result + } +} + impl FontLocator for GdiFontLocator { fn load_fonts( &self, @@ -53,43 +124,7 @@ impl FontLocator for GdiFontLocator { ) -> anyhow::Result> { let mut fonts = Vec::new(); for font_attr in fonts_selection { - let mut log_font = LOGFONTW { - lfHeight: 0, - lfWidth: 0, - lfEscapement: 0, - lfOrientation: 0, - lfWeight: if font_attr.bold { 700 } else { 0 }, - lfItalic: if font_attr.italic { 1 } else { 0 }, - lfUnderline: 0, - lfStrikeOut: 0, - lfCharSet: 0, - lfOutPrecision: OUT_TT_ONLY_PRECIS as u8, - lfClipPrecision: 0, - lfQuality: 0, - lfPitchAndFamily: FIXED_PITCH as u8, - lfFaceName: [0u16; 32], - }; - - let name = wide_string(&font_attr.family); - if name.len() > LF_FACESIZE { - log::error!( - "family name {:?} is too large for LOGFONTW", - font_attr.family - ); - continue; - } - for (i, &c) in name.iter().enumerate() { - log_font.lfFaceName[i] = c; - } - - let handle = unsafe { - let font = CreateFontIndirectW(&log_font); - let result = extract_font_data(font, &font_attr.family); - DeleteObject(font as *mut _); - result - }; - - if let Ok(handle) = handle { + if let Ok(handle) = load_font(font_attr) { if let Ok(parsed) = crate::parser::ParsedFont::from_locator(&handle) { if crate::parser::font_info_matches(font_attr, parsed.names()) { fonts.push(handle); @@ -104,8 +139,103 @@ impl FontLocator for GdiFontLocator { fn locate_fallback_for_codepoints( &self, - _codepoints: &[char], + codepoints: &[char], ) -> anyhow::Result> { - Ok(vec![]) + let text: Vec = codepoints + .iter() + .map(|&c| c as u16) + .chain(std::iter::once(0)) + .collect(); + + let collection = dwrote::FontCollection::system(); + struct Source { + locale: String, + len: u32, + }; + impl dwrote::TextAnalysisSourceMethods for Source { + fn get_locale_name<'a>(&'a self, _: u32) -> (Cow<'a, str>, u32) { + (Cow::Borrowed(&self.locale), self.len) + } + fn get_paragraph_reading_direction(&self) -> u32 { + DWRITE_READING_DIRECTION_LEFT_TO_RIGHT + } + } + + let source = dwrote::TextAnalysisSource::from_text( + Box::new(Source { + locale: "".to_string(), + len: codepoints.len() as u32, + }), + Cow::Borrowed(&text), + ); + + let mut handles = vec![]; + let mut resolved = HashSet::new(); + + if let Some(fallback) = dwrote::FontFallback::get_system_fallback() { + let mut start = 0usize; + let mut len = codepoints.len(); + loop { + let result = fallback.map_characters( + &source, + start as u32, + len as u32, + &collection, + None, + FontWeight::Regular, + FontStyle::Normal, + FontStretch::Normal, + ); + + if let Some(font) = result.mapped_font { + log::trace!( + "DirectWrite Suggested fallback: {} {}", + font.family_name(), + font.face_name() + ); + let attr = FontAttributes { + bold: match font.weight() { + FontWeight::Thin + | FontWeight::ExtraLight + | FontWeight::Light + | FontWeight::SemiLight + | FontWeight::Regular + | FontWeight::Medium => false, + FontWeight::SemiBold + | FontWeight::Bold + | FontWeight::ExtraBold + | FontWeight::Black + | FontWeight::ExtraBlack => true, + FontWeight::Unknown(n) => n > 80, + }, + italic: false, + family: font.family_name(), + is_fallback: true, + }; + + if !resolved.contains(&attr) { + resolved.insert(attr.clone()); + + match load_font(&attr) { + Ok(handle) => handles.push(handle), + Err(err) => log::error!("Failed to load {:?} {:?}", attr, err), + } + } + } + if result.mapped_length > 0 { + start += result.mapped_length + } else { + break; + } + if start == codepoints.len() { + break; + } + len = codepoints.len() - start; + } + } else { + log::error!("Unable to get system fallback from dwrote"); + } + + Ok(handles) } } diff --git a/wezterm-font/src/parser.rs b/wezterm-font/src/parser.rs index 2087395de..967aa580d 100644 --- a/wezterm-font/src/parser.rs +++ b/wezterm-font/src/parser.rs @@ -16,7 +16,7 @@ use allsorts::tables::{ HeadTable, HheaTable, HmtxTable, MaxpTable, OffsetTable, OpenTypeFile, OpenTypeFont, }; use allsorts::tag; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use config::{Config, FontAttributes}; use std::collections::HashSet; use std::convert::TryInto; @@ -62,7 +62,7 @@ pub struct Names { impl Names { fn from_name_table_data(name_table: &[u8]) -> anyhow::Result { Ok(Names { - full_name: get_name(name_table, 4)?, + full_name: get_name(name_table, 4).context("full_name")?, unique: get_name(name_table, 3).ok(), family: get_name(name_table, 1).ok(), sub_family: get_name(name_table, 2).ok(), @@ -160,21 +160,30 @@ impl ParsedFont { // extend the lifetime of the OpenTypeFile that we produce here. // That in turn allows us to store all of these derived items // into a struct and manage their lifetimes together. - let file: OpenTypeFile<'static> = - unsafe { std::mem::transmute(owned_scope.scope().read::()?) }; + let file: OpenTypeFile<'static> = unsafe { + std::mem::transmute( + owned_scope + .scope() + .read::() + .context("read OpenTypeFile")?, + ) + }; - let otf = locate_offset_table(&file, index)?; - let name_table = name_table_data(&otf, &file.scope)?; - let names = Names::from_name_table_data(name_table)?; + let otf = locate_offset_table(&file, index).context("locate_offset_table")?; + let name_table = name_table_data(&otf, &file.scope).context("name_table_data")?; + let names = + Names::from_name_table_data(name_table).context("Names::from_name_table_data")?; let head = otf .read_table(&file.scope, tag::HEAD)? .ok_or_else(|| anyhow!("HEAD table missing or broken"))? - .read::()?; + .read::() + .context("read HeadTable")?; let cmap = otf .read_table(&file.scope, tag::CMAP)? .ok_or_else(|| anyhow!("CMAP table missing or broken"))? - .read::()?; + .read::() + .context("read Cmap")?; let cmap_subtable: CmapSubtable<'static> = read_cmap_subtable(&cmap)? .ok_or_else(|| anyhow!("CMAP subtable not found"))? .1; @@ -182,30 +191,37 @@ impl ParsedFont { let maxp = otf .read_table(&file.scope, tag::MAXP)? .ok_or_else(|| anyhow!("MAXP table not found"))? - .read::()?; + .read::() + .context("read MaxpTable")?; let num_glyphs = maxp.num_glyphs; let post = otf .read_table(&file.scope, tag::POST)? .ok_or_else(|| anyhow!("POST table not found"))? - .read::()?; + .read::() + .context("read PostTable")?; let hhea = otf .read_table(&file.scope, tag::HHEA)? .ok_or_else(|| anyhow!("HHEA table not found"))? - .read::()?; + .read::() + .context("read HheaTable")?; let hmtx = otf .read_table(&file.scope, tag::HMTX)? .ok_or_else(|| anyhow!("HMTX table not found"))? .read_dep::(( usize::from(maxp.num_glyphs), usize::from(hhea.num_h_metrics), - ))?; + )) + .context("read_dep HmtxTable")?; let gdef_table: Option = otf .find_table_record(tag::GDEF) .map(|gdef_record| -> anyhow::Result { - Ok(gdef_record.read_table(&file.scope)?.read::()?) + Ok(gdef_record + .read_table(&file.scope)? + .read::() + .context("read GDEFTable")?) }) .transpose()?; let opt_gpos_table = otf @@ -213,7 +229,8 @@ impl ParsedFont { .map(|gpos_record| -> anyhow::Result> { Ok(gpos_record .read_table(&file.scope)? - .read::>()?) + .read::>() + .context("read LayoutTable")?) }) .transpose()?; let gpos_cache = opt_gpos_table.map(new_layout_cache); @@ -221,7 +238,10 @@ impl ParsedFont { let gsub_cache = otf .find_table_record(tag::GSUB) .map(|gsub| -> anyhow::Result> { - Ok(gsub.read_table(&file.scope)?.read::>()?) + Ok(gsub + .read_table(&file.scope)? + .read::>() + .context("read LayoutTable")?) }) .transpose()? .map(new_layout_cache); @@ -559,6 +579,50 @@ pub fn font_info_matches(attr: &FontAttributes, names: &Names) -> bool { } } +/// Given a blob representing a True Type Collection (.ttc) file, +/// and a desired font, enumerate the collection to resolve the index of +/// the font inside that collection that matches it. +/// Even though this is intended to work with a TTC, this also returns +/// the index of a singular TTF file, if it matches. +pub fn resolve_font_from_ttc_data( + attr: &FontAttributes, + data: &[u8], +) -> anyhow::Result> { + let scope = allsorts::binary::read::ReadScope::new(&data); + let file = scope.read::()?; + + match &file.font { + OpenTypeFont::Single(ttf) => { + let name_table_data = ttf + .read_table(&file.scope, allsorts::tag::NAME)? + .ok_or_else(|| anyhow!("name table is not present"))?; + + let names = Names::from_name_table_data(name_table_data.data())?; + if font_info_matches(attr, &names) { + Ok(Some(0)) + } else { + Ok(None) + } + } + OpenTypeFont::Collection(ttc) => { + for (index, offset_table_offset) in ttc.offset_tables.iter().enumerate() { + let ttf = file + .scope + .offset(offset_table_offset as usize) + .read::()?; + let name_table_data = ttf + .read_table(&file.scope, allsorts::tag::NAME)? + .ok_or_else(|| anyhow!("name table is not present"))?; + let names = Names::from_name_table_data(name_table_data.data())?; + if font_info_matches(attr, &names) { + return Ok(Some(index)); + } + } + Ok(None) + } + } +} + /// In case the user has a broken configuration, or no configuration, /// we bundle JetBrains Mono and Noto Color Emoji to act as reasonably /// sane fallback fonts. @@ -696,7 +760,8 @@ fn name_table_data<'a>(otf: &OffsetTable<'a>, scope: &ReadScope<'a>) -> anyhow:: /// Extract a name from the name table fn get_name(name_table_data: &[u8], name_id: u16) -> anyhow::Result { - let cstr = allsorts::get_name::fontcode_get_name(name_table_data, name_id)? + let cstr = allsorts::get_name::fontcode_get_name(name_table_data, name_id) + .with_context(|| anyhow!("fontcode_get_name name_id:{}", name_id))? .ok_or_else(|| anyhow!("name_id {} not found", name_id))?; cstr.into_string() .map_err(|e| anyhow!("name_id {} is not representable as String: {}", name_id, e))