1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-11 15:14:16 +03:00

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
This commit is contained in:
Wez Furlong 2020-11-25 14:53:42 -08:00
parent 58eb8e3614
commit ba44548d46
5 changed files with 305 additions and 102 deletions

14
Cargo.lock generated
View File

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

View File

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

View File

@ -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<RefCell<Option<Box<dyn FontRasterizer>>>>,
rasterizers: RefCell<HashMap<FallbackIdx, Box<dyn FontRasterizer>>>,
handles: RefCell<Vec<FontDataHandle>>,
shaper: RefCell<Box<dyn FontShaper>>,
metrics: FontMetrics,
@ -58,22 +58,29 @@ impl LoadedFont {
err,
no_glyphs.iter().collect::<String>().escape_debug()
),
Ok(handles) if handles.is_empty() => {
log::error!(
"No fonts have glyphs for {}",
no_glyphs.iter().collect::<String>().escape_debug()
)
}
Ok(handles) if handles.is_empty() => log::error!(
"No fonts have glyphs for {}",
no_glyphs.iter().collect::<String>().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::<String>().escape_debug()
"No fonts have glyphs for {}, even though fallback suggested some.",
no_glyphs.iter().collect::<String>().escape_debug(),
)
}
}
@ -110,23 +117,18 @@ impl LoadedFont {
glyph_pos: u32,
fallback: FallbackIdx,
) -> anyhow::Result<RasterizedGlyph> {
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,

View File

@ -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<FontDataHandle> {
fn extract_font_data(font: HFONT, attr: &FontAttributes) -> anyhow::Result<FontDataHandle> {
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<u16> {
.collect()
}
fn load_font(font_attr: &FontAttributes) -> anyhow::Result<FontDataHandle> {
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<Vec<FontDataHandle>> {
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<Vec<FontDataHandle>> {
Ok(vec![])
let text: Vec<u16> = 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)
}
}

View File

@ -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<Names> {
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::<OpenTypeFile>()?) };
let file: OpenTypeFile<'static> = unsafe {
std::mem::transmute(
owned_scope
.scope()
.read::<OpenTypeFile>()
.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::<HeadTable>()?;
.read::<HeadTable>()
.context("read HeadTable")?;
let cmap = otf
.read_table(&file.scope, tag::CMAP)?
.ok_or_else(|| anyhow!("CMAP table missing or broken"))?
.read::<Cmap>()?;
.read::<Cmap>()
.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::<MaxpTable>()?;
.read::<MaxpTable>()
.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::<PostTable>()?;
.read::<PostTable>()
.context("read PostTable")?;
let hhea = otf
.read_table(&file.scope, tag::HHEA)?
.ok_or_else(|| anyhow!("HHEA table not found"))?
.read::<HheaTable>()?;
.read::<HheaTable>()
.context("read HheaTable")?;
let hmtx = otf
.read_table(&file.scope, tag::HMTX)?
.ok_or_else(|| anyhow!("HMTX table not found"))?
.read_dep::<HmtxTable>((
usize::from(maxp.num_glyphs),
usize::from(hhea.num_h_metrics),
))?;
))
.context("read_dep HmtxTable")?;
let gdef_table: Option<GDEFTable> = otf
.find_table_record(tag::GDEF)
.map(|gdef_record| -> anyhow::Result<GDEFTable> {
Ok(gdef_record.read_table(&file.scope)?.read::<GDEFTable>()?)
Ok(gdef_record
.read_table(&file.scope)?
.read::<GDEFTable>()
.context("read GDEFTable")?)
})
.transpose()?;
let opt_gpos_table = otf
@ -213,7 +229,8 @@ impl ParsedFont {
.map(|gpos_record| -> anyhow::Result<LayoutTable<GPOS>> {
Ok(gpos_record
.read_table(&file.scope)?
.read::<LayoutTable<GPOS>>()?)
.read::<LayoutTable<GPOS>>()
.context("read LayoutTable<GPOS>")?)
})
.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<LayoutTable<GSUB>> {
Ok(gsub.read_table(&file.scope)?.read::<LayoutTable<GSUB>>()?)
Ok(gsub
.read_table(&file.scope)?
.read::<LayoutTable<GSUB>>()
.context("read LayoutTable<GSUB>")?)
})
.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<Option<usize>> {
let scope = allsorts::binary::read::ReadScope::new(&data);
let file = scope.read::<OpenTypeFile>()?;
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::<OffsetTable>()?;
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<String> {
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))