forked from NotAShelf/beer
font: bound the glyph cache; synthesize missing bold/italic
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I39a00a2847022f684060e94c816df7786a6a6964
This commit is contained in:
parent
5682027a94
commit
23ad00348d
3 changed files with 124 additions and 14 deletions
39
Cargo.lock
generated
39
Cargo.lock
generated
|
|
@ -11,6 +11,12 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.102"
|
version = "1.0.102"
|
||||||
|
|
@ -32,6 +38,7 @@ dependencies = [
|
||||||
"calloop-wayland-source",
|
"calloop-wayland-source",
|
||||||
"fontconfig",
|
"fontconfig",
|
||||||
"freetype-rs",
|
"freetype-rs",
|
||||||
|
"lru",
|
||||||
"pound",
|
"pound",
|
||||||
"rustix",
|
"rustix",
|
||||||
"smithay-client-toolkit",
|
"smithay-client-toolkit",
|
||||||
|
|
@ -146,6 +153,12 @@ version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
|
|
@ -162,6 +175,12 @@ version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fontconfig"
|
name = "fontconfig"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
|
@ -193,6 +212,17 @@ dependencies = [
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
|
@ -245,6 +275,15 @@ version = "0.4.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ calloop = "0.14.4"
|
||||||
calloop-wayland-source = "0.4.1"
|
calloop-wayland-source = "0.4.1"
|
||||||
fontconfig = "0.11.0"
|
fontconfig = "0.11.0"
|
||||||
freetype-rs = "0.38.0"
|
freetype-rs = "0.38.0"
|
||||||
|
lru = "0.18.0"
|
||||||
pound = "0.1.6"
|
pound = "0.1.6"
|
||||||
rustix = { version = "1.1.4", features = [
|
rustix = { version = "1.1.4", features = [
|
||||||
"pty",
|
"pty",
|
||||||
|
|
|
||||||
98
src/font.rs
98
src/font.rs
|
|
@ -4,19 +4,25 @@
|
||||||
//! FreeType rasterizes each glyph to an 8-bit coverage mask or, for colour
|
//! FreeType rasterizes each glyph to an 8-bit coverage mask or, for colour
|
||||||
//! fonts, a pre-multiplied BGRA bitmap. Layout is fixed-cell, so a glyph's own
|
//! fonts, a pre-multiplied BGRA bitmap. Layout is fixed-cell, so a glyph's own
|
||||||
//! advance is never consulted - only the [`CellMetrics`] taken from the primary
|
//! advance is never consulted - only the [`CellMetrics`] taken from the primary
|
||||||
//! face. All C interop is handled by the `freetype` and `fontconfig` safe
|
//! face. C interop goes through the `freetype`/`fontconfig` safe wrappers; the
|
||||||
//! wrappers, so this module contains no `unsafe`.
|
//! sole `unsafe` is reading a face's fixed-strike array (see `nearest_strike`).
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use fontconfig::{CharSet, Fontconfig, Pattern};
|
use fontconfig::{CharSet, Fontconfig, Pattern};
|
||||||
use freetype::bitmap::PixelMode;
|
use freetype::bitmap::PixelMode;
|
||||||
use freetype::face::LoadFlag;
|
use freetype::face::{LoadFlag, StyleFlag};
|
||||||
use freetype::{Face, Library};
|
use freetype::{Face, Library, Matrix, Vector};
|
||||||
|
use lru::LruCache;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Upper bound on cached glyphs; the working set of a terminal is far smaller,
|
||||||
|
/// but this caps memory under adversarial all-of-Unicode output.
|
||||||
|
const GLYPH_CACHE_CAP: usize = 4096;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum FontError {
|
pub enum FontError {
|
||||||
#[error("FreeType: {0}")]
|
#[error("FreeType: {0}")]
|
||||||
|
|
@ -99,7 +105,7 @@ pub struct Fonts {
|
||||||
styled: [Option<usize>; 4],
|
styled: [Option<usize>; 4],
|
||||||
/// Fallback faces resolved by coverage, deduplicated by file path.
|
/// Fallback faces resolved by coverage, deduplicated by file path.
|
||||||
fallbacks: HashMap<PathBuf, usize>,
|
fallbacks: HashMap<PathBuf, usize>,
|
||||||
cache: HashMap<(char, usize), Glyph>,
|
cache: LruCache<(char, usize), Glyph>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Fonts {
|
impl fmt::Debug for Fonts {
|
||||||
|
|
@ -132,7 +138,7 @@ impl Fonts {
|
||||||
faces: vec![regular],
|
faces: vec![regular],
|
||||||
styled: [Some(0), None, None, None],
|
styled: [Some(0), None, None, None],
|
||||||
fallbacks: HashMap::new(),
|
fallbacks: HashMap::new(),
|
||||||
cache: HashMap::new(),
|
cache: LruCache::new(NonZeroUsize::new(GLYPH_CACHE_CAP).expect("cap is nonzero")),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,12 +150,18 @@ impl Fonts {
|
||||||
/// it on first use.
|
/// it on first use.
|
||||||
pub fn glyph(&mut self, c: char, style: Style) -> Result<&Glyph, FontError> {
|
pub fn glyph(&mut self, c: char, style: Style) -> Result<&Glyph, FontError> {
|
||||||
let key = (c, style.index());
|
let key = (c, style.index());
|
||||||
if !self.cache.contains_key(&key) {
|
if self.cache.get(&key).is_none() {
|
||||||
let face = self.face_for(c, style)?;
|
let idx = self.face_for(c, style)?;
|
||||||
let glyph = rasterize(&self.faces[face], c)?;
|
let face = &self.faces[idx];
|
||||||
self.cache.insert(key, glyph);
|
// Synthesize bold/italic only when the resolved face lacks the real
|
||||||
|
// variant (most monospace families ship both).
|
||||||
|
let flags = face.style_flags();
|
||||||
|
let synth_bold = style.bold && !flags.contains(StyleFlag::BOLD);
|
||||||
|
let synth_italic = style.italic && !flags.contains(StyleFlag::ITALIC);
|
||||||
|
let glyph = rasterize(face, c, synth_bold, synth_italic)?;
|
||||||
|
self.cache.put(key, glyph);
|
||||||
}
|
}
|
||||||
Ok(&self.cache[&key])
|
Ok(self.cache.get(&key).expect("glyph was just inserted"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pick the face that should render `c`: the requested style if it has the
|
/// Pick the face that should render `c`: the requested style if it has the
|
||||||
|
|
@ -294,8 +306,23 @@ fn cell_metrics(face: &Face, family: &str) -> Result<CellMetrics, FontError> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rasterize(face: &Face, c: char) -> Result<Glyph, FontError> {
|
fn rasterize(
|
||||||
face.load_char(c as usize, LoadFlag::RENDER | LoadFlag::COLOR)?;
|
face: &Face,
|
||||||
|
c: char,
|
||||||
|
synth_bold: bool,
|
||||||
|
synth_italic: bool,
|
||||||
|
) -> Result<Glyph, FontError> {
|
||||||
|
// A shear transform fakes italics on a face that has no real oblique. It is
|
||||||
|
// applied to the outline at load time, so reset it immediately after.
|
||||||
|
if synth_italic {
|
||||||
|
face.set_transform(&mut shear_matrix(), &mut Vector { x: 0, y: 0 });
|
||||||
|
}
|
||||||
|
let result = face.load_char(c as usize, LoadFlag::RENDER | LoadFlag::COLOR);
|
||||||
|
if synth_italic {
|
||||||
|
face.set_transform(&mut identity_matrix(), &mut Vector { x: 0, y: 0 });
|
||||||
|
}
|
||||||
|
result?;
|
||||||
|
|
||||||
let slot = face.glyph();
|
let slot = face.glyph();
|
||||||
let bitmap = slot.bitmap();
|
let bitmap = slot.bitmap();
|
||||||
let width = bitmap.width().max(0) as usize;
|
let width = bitmap.width().max(0) as usize;
|
||||||
|
|
@ -303,12 +330,17 @@ fn rasterize(face: &Face, c: char) -> Result<Glyph, FontError> {
|
||||||
let pitch = bitmap.pitch();
|
let pitch = bitmap.pitch();
|
||||||
let src = bitmap.buffer();
|
let src = bitmap.buffer();
|
||||||
|
|
||||||
let data = match bitmap.pixel_mode()? {
|
let mut data = match bitmap.pixel_mode()? {
|
||||||
PixelMode::Gray => GlyphData::Mask(pack_rows(src, width, pitch, height)),
|
PixelMode::Gray => GlyphData::Mask(pack_rows(src, width, pitch, height)),
|
||||||
PixelMode::Bgra => GlyphData::Color(pack_rows(src, width * 4, pitch, height)),
|
PixelMode::Bgra => GlyphData::Color(pack_rows(src, width * 4, pitch, height)),
|
||||||
PixelMode::Mono => GlyphData::Mask(expand_mono(src, width, pitch, height)),
|
PixelMode::Mono => GlyphData::Mask(expand_mono(src, width, pitch, height)),
|
||||||
_ => GlyphData::Mask(vec![0; width * height]),
|
_ => GlyphData::Mask(vec![0; width * height]),
|
||||||
};
|
};
|
||||||
|
// Fake bold by widening coverage one pixel to the right (colour glyphs are
|
||||||
|
// left alone - there is no such thing as a bold emoji).
|
||||||
|
if synth_bold && let GlyphData::Mask(mask) = &mut data {
|
||||||
|
embolden(mask, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Glyph {
|
Ok(Glyph {
|
||||||
left: slot.bitmap_left(),
|
left: slot.bitmap_left(),
|
||||||
|
|
@ -319,6 +351,35 @@ fn rasterize(face: &Face, c: char) -> Result<Glyph, FontError> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shear_matrix() -> Matrix {
|
||||||
|
// ~0.2 horizontal shear in 16.16 fixed point.
|
||||||
|
Matrix {
|
||||||
|
xx: 0x1_0000,
|
||||||
|
xy: 0x3333,
|
||||||
|
yx: 0,
|
||||||
|
yy: 0x1_0000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn identity_matrix() -> Matrix {
|
||||||
|
Matrix {
|
||||||
|
xx: 0x1_0000,
|
||||||
|
xy: 0,
|
||||||
|
yx: 0,
|
||||||
|
yy: 0x1_0000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widen each row's coverage by one pixel (synthetic bold).
|
||||||
|
fn embolden(mask: &mut [u8], width: usize, height: usize) {
|
||||||
|
for y in 0..height {
|
||||||
|
let row = &mut mask[y * width..y * width + width];
|
||||||
|
for x in (1..width).rev() {
|
||||||
|
row[x] = row[x].max(row[x - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Copy `height` rows of `row_bytes` each out of FreeType's padded buffer,
|
/// Copy `height` rows of `row_bytes` each out of FreeType's padded buffer,
|
||||||
/// honouring pitch sign (positive = top-down).
|
/// honouring pitch sign (positive = top-down).
|
||||||
fn pack_rows(src: &[u8], row_bytes: usize, pitch: i32, height: usize) -> Vec<u8> {
|
fn pack_rows(src: &[u8], row_bytes: usize, pitch: i32, height: usize) -> Vec<u8> {
|
||||||
|
|
@ -385,6 +446,15 @@ mod tests {
|
||||||
f.glyph(' ', Style::default()).expect("rasterize space");
|
f.glyph(' ', Style::default()).expect("rasterize space");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embolden_widens_coverage() {
|
||||||
|
// 3x2 mask, one lit pixel per row at x=1.
|
||||||
|
let mut mask = vec![0, 255, 0, 0, 200, 0];
|
||||||
|
embolden(&mut mask, 3, 2);
|
||||||
|
// Each lit pixel bleeds one column to the right; the left edge is unchanged.
|
||||||
|
assert_eq!(mask, vec![0, 255, 255, 0, 200, 200]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn glyphs_are_cached() {
|
fn glyphs_are_cached() {
|
||||||
let mut f = fonts();
|
let mut f = fonts();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue