-
Notifications
You must be signed in to change notification settings - Fork 143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Restore glyph hinting support #544
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,8 +7,8 @@ use super::{Encoding, StreamOffsets}; | |
|
||
use peniko::kurbo::{BezPath, Shape}; | ||
use peniko::{Fill, Style}; | ||
use skrifa::instance::NormalizedCoord; | ||
use skrifa::outline::OutlinePen; | ||
use skrifa::instance::{NormalizedCoord, Size}; | ||
use skrifa::outline::{HintingInstance, HintingMode, LcdLayout, OutlineGlyphFormat, OutlinePen}; | ||
use skrifa::{GlyphId, OutlineGlyphCollection}; | ||
|
||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)] | ||
|
@@ -24,12 +24,14 @@ pub struct GlyphKey { | |
pub struct GlyphCache { | ||
pub encoding: Encoding, | ||
glyphs: HashMap<GlyphKey, CachedRange>, | ||
hinting: HintCache, | ||
} | ||
|
||
impl GlyphCache { | ||
pub fn clear(&mut self) { | ||
self.encoding.reset(); | ||
self.glyphs.clear(); | ||
// No need to clear the hinting cache | ||
} | ||
|
||
pub fn get_or_insert( | ||
|
@@ -43,6 +45,7 @@ impl GlyphCache { | |
let size = skrifa::instance::Size::new(font_size); | ||
let is_var = !coords.is_empty(); | ||
let encoding_cache = &mut self.encoding; | ||
let hinting_cache = &mut self.hinting; | ||
let mut encode_glyph = || { | ||
let start = encoding_cache.stream_offsets(); | ||
let fill = match style { | ||
|
@@ -55,9 +58,18 @@ impl GlyphCache { | |
encoding_cache.encode_fill_style(fill); | ||
let mut path = encoding_cache.encode_path(true); | ||
let outline = outlines.get(GlyphId::new(key.glyph_id as u16))?; | ||
// FIXME: Re-add hinting when skrifa supports it | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔥 |
||
// Tracking issue <https://github.com/googlefonts/fontations/issues/620> | ||
let draw_settings = skrifa::outline::DrawSettings::unhinted(size, coords); | ||
use skrifa::outline::DrawSettings; | ||
let draw_settings = if key.hint { | ||
if let Some(hint_instance) = | ||
hinting_cache.get(&HintKey::new(outlines, &key, font_size, coords)) | ||
{ | ||
DrawSettings::hinted(hint_instance, false) | ||
} else { | ||
DrawSettings::unhinted(size, coords) | ||
} | ||
} else { | ||
DrawSettings::unhinted(size, coords) | ||
}; | ||
match style { | ||
Style::Fill(_) => { | ||
outline.draw(draw_settings, &mut path).ok()?; | ||
|
@@ -145,3 +157,111 @@ impl OutlinePen for BezPathPen { | |
self.0.close_path(); | ||
} | ||
} | ||
|
||
/// We keep this small to enable a simple LRU cache with a linear | ||
/// search. Regenerating hinting data is low to medium cost so it's fine | ||
/// to redo it occassionally. | ||
const MAX_CACHED_HINT_INSTANCES: usize = 8; | ||
|
||
pub struct HintKey<'a> { | ||
font_id: u64, | ||
font_index: u32, | ||
outlines: &'a OutlineGlyphCollection<'a>, | ||
size: Size, | ||
coords: &'a [NormalizedCoord], | ||
} | ||
|
||
impl<'a> HintKey<'a> { | ||
fn new( | ||
outlines: &'a OutlineGlyphCollection<'a>, | ||
glyph_key: &GlyphKey, | ||
size: f32, | ||
coords: &'a [NormalizedCoord], | ||
) -> Self { | ||
Self { | ||
font_id: glyph_key.font_id, | ||
font_index: glyph_key.font_index, | ||
outlines, | ||
size: Size::new(size), | ||
coords, | ||
} | ||
} | ||
|
||
fn instance(&self) -> Option<HintingInstance> { | ||
HintingInstance::new(self.outlines, self.size, self.coords, HINTING_MODE).ok() | ||
} | ||
} | ||
|
||
const HINTING_MODE: HintingMode = HintingMode::Smooth { | ||
lcd_subpixel: Some(LcdLayout::Horizontal), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing this will need to be wired through at some point? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That’s an open question. The skrifa hinter supports a lot of modes because we have a requirement of matching FreeType. There’s an argument to be made that vello should always just use a modern hinting mode but that’s open to discussion. Ultimately, most fonts these days have hints generated by ttfautohint which ignores these things. Only the MS ClearType fonts really take advantage of them and we can potentially choose a sane default for those. |
||
preserve_linear_metrics: true, | ||
}; | ||
|
||
#[derive(Default)] | ||
struct HintCache { | ||
// Split caches for glyf/cff because the instance type can reuse | ||
// internal memory when reconfigured for the same format. | ||
glyf_entries: Vec<HintEntry>, | ||
cff_entries: Vec<HintEntry>, | ||
serial: u64, | ||
} | ||
|
||
impl HintCache { | ||
fn get(&mut self, key: &HintKey) -> Option<&HintingInstance> { | ||
let entries = match key.outlines.format()? { | ||
OutlineGlyphFormat::Glyf => &mut self.glyf_entries, | ||
OutlineGlyphFormat::Cff | OutlineGlyphFormat::Cff2 => &mut self.cff_entries, | ||
}; | ||
let (entry_ix, is_current) = find_hint_entry(entries, key)?; | ||
let entry = entries.get_mut(entry_ix)?; | ||
self.serial += 1; | ||
entry.serial = self.serial; | ||
if !is_current { | ||
entry.font_id = key.font_id; | ||
entry.font_index = key.font_index; | ||
entry | ||
.instance | ||
.reconfigure(key.outlines, key.size, key.coords, HINTING_MODE) | ||
.ok()?; | ||
} | ||
Some(&entry.instance) | ||
} | ||
} | ||
|
||
struct HintEntry { | ||
font_id: u64, | ||
font_index: u32, | ||
instance: HintingInstance, | ||
serial: u64, | ||
} | ||
|
||
fn find_hint_entry(entries: &mut Vec<HintEntry>, key: &HintKey) -> Option<(usize, bool)> { | ||
let mut found_serial = u64::MAX; | ||
let mut found_index = 0; | ||
for (ix, entry) in entries.iter().enumerate() { | ||
if entry.font_id == key.font_id | ||
&& entry.font_index == key.font_index | ||
&& entry.instance.size() == key.size | ||
&& entry.instance.location().coords() == key.coords | ||
{ | ||
return Some((ix, true)); | ||
} | ||
if entry.serial < found_serial { | ||
found_serial = entry.serial; | ||
found_index = ix; | ||
} | ||
} | ||
if entries.len() < MAX_CACHED_HINT_INSTANCES { | ||
let instance = key.instance()?; | ||
let ix = entries.len(); | ||
entries.push(HintEntry { | ||
font_id: key.font_id, | ||
font_index: key.font_index, | ||
instance, | ||
serial: 0, | ||
}); | ||
Some((ix, true)) | ||
} else { | ||
Some((found_index, false)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,6 +92,7 @@ impl SimpleText { | |
.glyph_transform(glyph_transform) | ||
.normalized_coords(var_loc.coords()) | ||
.brush(brush) | ||
.hint(false) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would expect us to want to test this? I see in Zulip that you've mentioned that text should be on a pixel baseline, which we don't enforce in our current examples. I still would imagine that hinting could be productively used in e.g. the stats displays? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is disabled for the test scenes because we have some animated text which doesn’t work well with hinting (it snaps to pixels as it animates causing annoying jitter) and everything currently uses the same code path. We can definitely change this but I wanted to get the functionality in for xilem where hinting is more important. |
||
.draw( | ||
style, | ||
text.chars().filter_map(|ch| { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? Is it because it's already bounded in size?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly. In reality, none of this is retained right now so it’s mostly irrelevant. This module will need some substantial rework when we do rasterized glyph caching.