1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 21:32:13 +03:00

fonts: add an Allsorts shaper

Adds the ability to specify `--font-shaper Allsorts` and use that
for metrics and shaping.

It is sufficient to expand things like ligatures, but there's something
slightly off about how the metrics are computed and they differ slightly
from the freetype renderer, which leads to some artifacts when rendering
with opengl.

One of my tests is to `grep message src/main.rs` to pull out the line
that has a selection of emoji.  The heart emoji is missing from that
line currently.

Refs: https://github.com/wez/wezterm/issues/66
This commit is contained in:
Wez Furlong 2019-12-14 08:46:06 -08:00
parent 72f4ec58e4
commit a9b0197075
7 changed files with 323 additions and 51 deletions

View File

@ -5,6 +5,7 @@ pub use fontconfig::*;
use std::ffi::{CStr, CString};
use std::fmt;
use std::mem;
use std::os::raw::c_int;
use std::ptr;
static FC_MONO: i32 = 100;
@ -77,13 +78,12 @@ impl FcResultWrap {
pub fn as_err(&self) -> Error {
// the compiler thinks we defined these globals, when all
// we did was import them from elsewhere
#[allow(non_upper_case_globals)]
match self.0 {
FcResultMatch => err_msg("FcResultMatch"),
FcResultNoMatch => err_msg("FcResultNoMatch"),
FcResultTypeMismatch => err_msg("FcResultTypeMismatch"),
FcResultNoId => err_msg("FcResultNoId"),
FcResultOutOfMemory => err_msg("FcResultOutOfMemory"),
fontconfig::FcResultMatch => err_msg("FcResultMatch"),
fontconfig::FcResultNoMatch => err_msg("FcResultNoMatch"),
fontconfig::FcResultTypeMismatch => err_msg("FcResultTypeMismatch"),
fontconfig::FcResultNoId => err_msg("FcResultNoId"),
fontconfig::FcResultOutOfMemory => err_msg("FcResultOutOfMemory"),
_ => format_err!("FcResult holds invalid value {}", self.0),
}
}
@ -235,6 +235,24 @@ impl Pattern {
}
}
pub fn get_integer(&self, key: &str) -> Result<c_int, Error> {
unsafe {
let key = CString::new(key)?;
let mut ival: c_int = 0;
let res = FcResultWrap(FcPatternGetInteger(
self.pat,
key.as_ptr(),
0,
&mut ival as *mut _,
));
if !res.succeeded() {
Err(res.as_err())
} else {
Ok(ival)
}
}
}
pub fn get_string(&self, key: &str) -> Result<String, Error> {
unsafe {
let key = CString::new(key)?;
@ -266,9 +284,10 @@ impl Drop for Pattern {
impl fmt::Debug for Pattern {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
// unsafe{FcPatternPrint(self.pat);}
fmt.write_str(
&self
.format("Pattern(%{+family,style,weight,slant,spacing{%{=unparse}}})")
.format("Pattern(%{+family,style,weight,slant,spacing,file,index,fontformat{%{=unparse}}})")
.unwrap(),
)
}

View File

@ -3,6 +3,7 @@ use crate::font::fcwrap;
use crate::font::locator::{FontDataHandle, FontLocator};
use failure::Fallible;
use fcwrap::Pattern as FontPattern;
use std::convert::TryInto;
/// A FontLocator implemented using the system font loading
/// functions provided by font-config
@ -16,12 +17,8 @@ impl FontLocator for FontConfigFontLocator {
for attr in fonts_selection {
let mut pattern = FontPattern::new()?;
pattern.family(&attr.family)?;
if attr.bold {
pattern.add_integer("weight", 200)?;
}
if attr.italic {
pattern.add_integer("slant", 100)?;
}
pattern.add_integer("weight", if attr.bold { 200 } else { 80 })?;
pattern.add_integer("slant", if attr.italic { 100 } else { 0 })?;
/*
pattern.add_double("size", config.font_size * font_scale)?;
pattern.add_double("dpi", config.dpi)?;
@ -39,7 +36,7 @@ impl FontLocator for FontConfigFontLocator {
let handle = FontDataHandle::OnDisk {
path: file.into(),
index: 0, // FIXME: extract this from pat!
index: pat.get_integer("index")?.try_into()?,
};
// When it comes to handling fallback, we prefer our

View File

@ -9,26 +9,27 @@ use crate::font::shaper::{FallbackIdx, FontMetrics, GlyphInfo};
use crate::font::units::*;
use allsorts::binary::read::{ReadScope, ReadScopeOwned};
use allsorts::font_data_impl::read_cmap_subtable;
use allsorts::fontfile::FontFile;
use allsorts::gpos::{gpos_apply, Info, Placement};
use allsorts::gsub::{gsub_apply_default, GlyphOrigin, RawGlyph};
use allsorts::layout::{new_layout_cache, GDEFTable, LayoutCache, LayoutTable, GPOS, GSUB};
use allsorts::post::PostTable;
use allsorts::tables::cmap::{Cmap, CmapSubtable};
use allsorts::tables::{
FontTableProvider, HeadTable, HheaTable, HmtxTable, MaxpTable, NameTable, OffsetTable,
OpenTypeFile, OpenTypeFont, TTCHeader,
HeadTable, HheaTable, HmtxTable, MaxpTable, OffsetTable, OpenTypeFile, OpenTypeFont,
};
use allsorts::tag;
use failure::{bail, format_err, Fallible};
use failure::{format_err, Fallible};
use std::convert::TryInto;
use std::path::{Path, PathBuf};
use termwiz::cell::unicode_column_width;
#[derive(Debug)]
enum MaybeShaped {
pub enum MaybeShaped {
Resolved(GlyphInfo),
Unresolved(RawGlyph<()>),
Unresolved {
raw: RawGlyph<()>,
slice_start: usize,
},
}
/// Represents a parsed font
@ -38,7 +39,7 @@ pub struct ParsedFont {
cmap_subtable: CmapSubtable<'static>,
gpos_cache: Option<LayoutCache<GPOS>>,
gsub_cache: LayoutCache<GSUB>,
gsub_cache: Option<LayoutCache<GSUB>>,
gdef_table: Option<GDEFTable>,
hmtx: HmtxTable<'static>,
post: PostTable<'static>,
@ -174,11 +175,6 @@ impl ParsedFont {
usize::from(hhea.num_h_metrics),
))?;
let gsub_table = otf
.find_table_record(tag::GSUB)
.ok_or_else(|| format_err!("GSUB table record not found"))?
.read_table(&file.scope)?
.read::<LayoutTable<GSUB>>()?;
let gdef_table: Option<GDEFTable> = otf
.find_table_record(tag::GDEF)
.map(|gdef_record| -> Fallible<GDEFTable> {
@ -193,9 +189,16 @@ impl ParsedFont {
.read::<LayoutTable<GPOS>>()?)
})
.transpose()?;
let gsub_cache = new_layout_cache(gsub_table);
let gpos_cache = opt_gpos_table.map(new_layout_cache);
let gsub_cache = otf
.find_table_record(tag::GSUB)
.map(|gsub| -> Fallible<LayoutTable<GSUB>> {
Ok(gsub.read_table(&file.scope)?.read::<LayoutTable<GSUB>>()?)
})
.transpose()?
.map(new_layout_cache);
Ok(Self {
otf,
names,
@ -230,25 +233,54 @@ impl ParsedFont {
let underline_position =
PixelLength::new(self.post.header.underline_position as f64 * pixel_scale);
let descender = PixelLength::new(self.hhea.descender as f64 * pixel_scale);
let cell_height = PixelLength::new(self.hhea.line_gap as f64 * pixel_scale);
let cell_height = PixelLength::new(
(self.hhea.ascender - self.hhea.descender + self.hhea.line_gap) as f64 * pixel_scale,
);
log::error!(
"hhea: ascender={} descender={} line_gap={} \
advance_width_max={} min_lsb={} min_rsb={} \
x_max_extent={}",
self.hhea.ascender,
self.hhea.descender,
self.hhea.line_gap,
self.hhea.advance_width_max,
self.hhea.min_left_side_bearing,
self.hhea.min_right_side_bearing,
self.hhea.x_max_extent
);
// FIXME: for freetype/harfbuzz, we look at a number of glyphs and compute this for
// ourselves
let cell_width = PixelLength::new(self.hhea.advance_width_max as f64 * pixel_scale);
let mut cell_width = 0;
// Compute the max width based on ascii chars
for i in 0x20..0x7fu8 {
if let Ok(Some(glyph_index)) = self.glyph_index_for_char(i as char) {
if let Ok(h) = self
.hmtx
.horizontal_advance(glyph_index, self.hhea.num_h_metrics)
{
cell_width = cell_width.max(h);
}
}
}
let cell_width = PixelLength::new(cell_width as f64) * pixel_scale;
FontMetrics {
let metrics = FontMetrics {
cell_width,
cell_height,
descender,
underline_thickness,
underline_position,
}
};
log::error!("metrics: {:?}", metrics);
metrics
}
pub fn shape_text<T: AsRef<str>>(
&self,
text: T,
font_index: usize,
slice_index: usize,
font_index: FallbackIdx,
script: u32,
lang: u32,
point_size: f64,
@ -272,16 +304,18 @@ impl ParsedFont {
let vertical = false;
gsub_apply_default(
&|| vec![], //map_char('\u{25cc}')],
&self.gsub_cache,
self.gdef_table.as_ref(),
script,
lang,
vertical,
self.num_glyphs,
&mut glyphs,
)?;
if let Some(gsub_cache) = self.gsub_cache.as_ref() {
gsub_apply_default(
&|| vec![], //map_char('\u{25cc}')],
gsub_cache,
self.gdef_table.as_ref(),
script,
lang,
vertical,
self.num_glyphs,
&mut glyphs,
)?;
}
// Note: init_from_glyphs silently elides entries that
// have no glyph in the current font! we need to deal
@ -304,8 +338,13 @@ impl ParsedFont {
let mut pos = Vec::new();
let mut glyph_iter = glyphs.into_iter();
let mut cluster = slice_index;
for (cluster, glyph_info) in infos.into_iter().enumerate() {
fn reverse_engineer_glyph_text(glyph: &RawGlyph<()>) -> String {
glyph.unicodes.iter().collect()
}
for glyph_info in infos.into_iter() {
let mut input_glyph = glyph_iter
.next()
.ok_or_else(|| format_err!("more output infos than input glyphs!"))?;
@ -313,7 +352,13 @@ impl ParsedFont {
while input_glyph.unicodes != glyph_info.glyph.unicodes {
// Info::init_from_glyphs skipped the input glyph, so let's be
// sure to emit a placeholder for it
pos.push(MaybeShaped::Unresolved(input_glyph));
let text = reverse_engineer_glyph_text(&input_glyph);
pos.push(MaybeShaped::Unresolved {
raw: input_glyph,
slice_start: cluster,
});
cluster += text.len();
input_glyph = glyph_iter.next().ok_or_else(|| {
format_err!("more output infos than input glyphs! (loop bottom)")
@ -348,10 +393,22 @@ impl ParsedFont {
let x_advance = PixelLength::new(x_advance as f64 * pixel_scale);
let y_advance = PixelLength::new(y_advance as f64 * pixel_scale);
let text: String = glyph_info.glyph.unicodes.iter().collect();
let num_cells = unicode_column_width(&text);
let text = reverse_engineer_glyph_text(&glyph_info.glyph);
let text_len = text.len();
pos.push(MaybeShaped::Resolved(GlyphInfo {
// let num_cells = glyph_info.glyph.unicodes.len();
let num_cells = unicode_column_width(&text);
if num_cells != 1 {
log::error!(
"x_advance={} num_cells={} font_idx={} kern={}",
x_advance,
num_cells,
font_index,
glyph_info.kerning
);
}
let info = GlyphInfo {
#[cfg(debug_assertions)]
text,
cluster: cluster as u32,
@ -362,7 +419,15 @@ impl ParsedFont {
y_advance,
x_offset: PixelLength::new(0.),
y_offset: PixelLength::new(0.),
}));
};
cluster += text_len;
if num_cells != 1 {
log::error!("{:?}", info);
}
pos.push(MaybeShaped::Resolved(info));
}
Ok(pos)

162
src/font/shaper/allsorts.rs Normal file
View File

@ -0,0 +1,162 @@
use crate::font::locator::FontDataHandle;
use crate::font::parser::*;
use crate::font::shaper::{FallbackIdx, FontMetrics, FontShaper, GlyphInfo};
use failure::{bail, format_err, Fallible};
pub struct AllsortsShaper {
fonts: Vec<Option<ParsedFont>>,
}
impl AllsortsShaper {
pub fn new(handles: &[FontDataHandle]) -> Fallible<Self> {
let mut fonts = vec![];
let mut success = false;
for handle in handles {
match ParsedFont::from_locator(handle) {
Ok(font) => {
fonts.push(Some(font));
success = true;
}
Err(err) => {
log::warn!("Failed to parse {:?}: {}", handle, err);
fonts.push(None);
}
}
}
if !success {
bail!("failed to load any fonts in this fallback set!?");
}
Ok(Self { fonts })
}
fn shape_into(
&self,
font_index: FallbackIdx,
s: &str,
slice_index: usize,
script: u32,
lang: u32,
font_size: f64,
dpi: u32,
results: &mut Vec<GlyphInfo>,
) -> Fallible<()> {
let font = match self.fonts.get(font_index) {
Some(Some(font)) => font,
Some(None) => {
return self.shape_into(
font_index + 1,
s,
slice_index,
script,
lang,
font_size,
dpi,
results,
);
}
None => {
// We ran out of fallback fonts, so use a replacement
// character that is likely to be in one of those fonts
let mut alt_text = String::new();
let num_chars = s.chars().count();
for _ in 0..num_chars {
alt_text.push('?');
}
if alt_text == s {
// We already tried to fallback to this and failed
return Err(format_err!("could not fallback to ? character"));
}
return self.shape_into(
0,
&alt_text,
slice_index,
script,
lang,
font_size,
dpi,
results,
);
}
};
let first_pass =
font.shape_text(s, slice_index, font_index, script, lang, font_size, dpi)?;
let mut item_iter = first_pass.into_iter();
let mut fallback_run = String::new();
let mut fallback_start_pos = 0;
while let Some(item) = item_iter.next() {
match item {
MaybeShaped::Resolved(info) => {
results.push(info);
}
MaybeShaped::Unresolved { raw, slice_start } => {
// There was no glyph in that font, so we'll need to shape
// using a fallback. Let's collect together any potential
// run of unresolved entries first
fallback_start_pos = slice_start + slice_index;
for &c in &raw.unicodes {
fallback_run.push(c);
}
while let Some(item) = item_iter.next() {
match item {
MaybeShaped::Unresolved { raw, .. } => {
for &c in &raw.unicodes {
fallback_run.push(c);
}
}
MaybeShaped::Resolved(info) => {
self.shape_into(
font_index + 1,
&fallback_run,
fallback_start_pos,
script,
lang,
font_size,
dpi,
results,
)?;
fallback_run.clear();
results.push(info);
}
}
}
}
}
}
if !fallback_run.is_empty() {
self.shape_into(
font_index + 1,
&fallback_run,
fallback_start_pos,
script,
lang,
font_size,
dpi,
results,
)?;
}
Ok(())
}
}
impl FontShaper for AllsortsShaper {
fn shape(&self, text: &str, size: f64, dpi: u32) -> Fallible<Vec<GlyphInfo>> {
let mut results = vec![];
let script = allsorts::tag::LATN;
let lang = allsorts::tag::DFLT;
self.shape_into(0, text, 0, script, lang, size, dpi, &mut results)?;
Ok(results)
}
fn metrics(&self, size: f64, dpi: u32) -> Fallible<FontMetrics> {
for font in &self.fonts {
if let Some(font) = font {
return Ok(font.get_metrics(size, dpi));
}
}
bail!("no fonts available for collecting metrics!?");
}
}

View File

@ -4,6 +4,7 @@ use failure::{format_err, Error, Fallible};
use serde_derive::*;
use std::sync::Mutex;
pub mod allsorts;
pub mod harfbuzz;
/// Holds information about a shaped glyph
@ -65,6 +66,7 @@ pub trait FontShaper {
#[derive(Debug, Deserialize, Clone, Copy)]
pub enum FontShaperSelection {
Allsorts,
Harfbuzz,
}
@ -90,12 +92,13 @@ impl FontShaperSelection {
}
pub fn variants() -> Vec<&'static str> {
vec!["Harfbuzz"]
vec!["Harfbuzz", "AllSorts"]
}
pub fn new_shaper(self, handles: &[FontDataHandle]) -> Fallible<Box<dyn FontShaper>> {
match self {
Self::Harfbuzz => Ok(Box::new(harfbuzz::HarfbuzzShaper::new(handles)?)),
Self::Allsorts => Ok(Box::new(allsorts::AllsortsShaper::new(handles)?)),
}
}
}
@ -105,6 +108,7 @@ impl std::str::FromStr for FontShaperSelection {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_ref() {
"harfbuzz" => Ok(Self::Harfbuzz),
"allsorts" => Ok(Self::Allsorts),
_ => Err(format_err!(
"{} is not a valid FontShaperSelection variant, possible values are {:?}",
s,

View File

@ -32,6 +32,20 @@ pub struct CachedGlyph<T: Texture2d> {
pub scale: f64,
}
impl<T: Texture2d> std::fmt::Debug for CachedGlyph<T> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
fmt.debug_struct("CachedGlyph")
.field("has_color", &self.has_color)
.field("x_offset", &self.x_offset)
.field("y_offset", &self.y_offset)
.field("bearing_x", &self.bearing_x)
.field("bearing_y", &self.bearing_y)
.field("scale", &self.scale)
.field("texture", &self.texture)
.finish()
}
}
pub struct GlyphCache<T: Texture2d> {
glyph_cache: HashMap<GlyphKey, Rc<CachedGlyph<T>>>,
pub atlas: Atlas<T>,

View File

@ -123,6 +123,16 @@ where
pub coords: Rect,
}
impl<T: Texture2d> std::fmt::Debug for Sprite<T> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
fmt.debug_struct("Sprite")
.field("coords", &self.coords)
.field("texture_width", &self.texture.width())
.field("texture_height", &self.texture.height())
.finish()
}
}
impl<T> Clone for Sprite<T>
where
T: Texture2d,
@ -149,6 +159,7 @@ where
/// These are used to handle multi-cell wide glyphs.
/// Each cell is nominally `cell_width` wide but font metrics
/// may result in the glyphs being wider than this.
#[derive(Debug)]
pub struct SpriteSlice {
/// This is glyph X out of num_cells
pub cell_idx: usize,