feat(font): port the Linux font engine

Ports the rest of ZIG/src/font for the freetype/fontconfig/harfbuzz
backend: face loading and rasterization (synthetic bold/italic, color
emoji bitmaps, glyph constraints for nerd font icons), fontconfig
discovery, DeferredFace, Collection, CodepointResolver, CodepointMap,
SharedGrid, opentype table parsers, the nerd font attribute table, and
the harfbuzz shaper with feature parsing and shaping cache.

Embedded fonts (JetBrains Mono 2.304 variable, Symbols Nerd Font 3.4.0,
Noto emoji) and the test fonts from Ghostty live in res/ with their OFL/
MIT licenses.

The crate opts into unsafe_code = "deny"; FFI is confined to ft.rs,
fontconfig.rs and hb.rs with SAFETY comments. Glyph resampling uses an
stb-style resampler that is not bit-identical to stb.

Deferred to the next font pass (needs ghostty-terminal/config):
shaper/run.zig, SharedGridSet.zig and the 23 terminal-driven shaper
tests. Config-facing types are mirrored locally in config_types.rs.

Test Plan:
- cargo test -p ghostty-font (139 tests)
This commit is contained in:
ddidderr committed 2026-10-11 22:13:06 +02:00
1 parent 6cf16ed2e6
commit 25801765eb
64 files changed
+35316 -23

No files matched your search

+1448
View File
@@ -0,0 +1,1448 @@
//! Face represents a single font face. A single font face has a single set
//! of properties associated with it such as style, weight, etc.
//!
//! A Face isn't typically meant to be used directly. It is usually used
//! via a Family in order to store it in an Atlas.
//!
//! Port of `font/face/freetype.zig`.
use std::ffi::CString;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use super::{DesiredSize, FontData, FreetypeLoadFlags, Options, RenderOptions, Variation};
use crate::atlas::{self, Atlas};
use crate::glyph::Glyph;
use crate::library::Library;
use crate::metrics::FaceMetrics;
pub(crate) mod ft;
mod resize;
pub use ft::HbFont;
#[derive(Debug, thiserror::Error)]
pub enum FaceError {
#[error("freetype error {0}")]
Freetype(i32),
#[error("harfbuzz font creation failed")]
HarfbuzzFailed,
#[error("invalid font path")]
InvalidPath,
}
#[derive(Debug, thiserror::Error)]
pub enum RenderError {
#[error(transparent)]
Atlas(#[from] atlas::Error),
#[error("wrong atlas")]
WrongAtlas,
#[error("freetype error {0}")]
Freetype(i32),
#[error("bitmap handling error")]
BitmapHandlingError,
#[error("glyph resize failed")]
GlyphResizeFailed,
#[error("unsupported glyph format")]
UnsupportedGlyphFormat,
/// Zig panics ("unsupported pixel mode") in this situation.
#[error("unsupported pixel mode {0}")]
UnsupportedPixelMode(u32),
}
/// Set to true to apply a synthetic italic to the face.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Synthetic {
pub italic: bool,
pub bold: bool,
}
/// Convert 26.6 fixed point to f64 (`F26Dot6.to(f64)`).
fn f26dot6_to_f64(v: i64) -> f64 {
// The Zig code bit-casts the (truncated) i32 value.
f64::from(v as i32) / 64.0
}
/// `F26Dot6.from(f64)` as the raw 26.6 integer.
fn f64_to_f26dot6(v: f64) -> i32 {
(v * 64.0).round() as i32
}
fn lock(m: &Mutex<()>) -> MutexGuard<'_, ()> {
m.lock().unwrap_or_else(PoisonError::into_inner)
}
pub struct Face {
/// Freetype load flags for this font face.
pub load_flags: FreetypeLoadFlags,
/// Set quirks.disableDefaultFontFeatures
pub quirks_disable_default_font_features: bool,
/// Set to true to apply a synthetic italic to the face.
pub synthetic: Synthetic,
/// The current size this font is set to.
pub size: DesiredSize,
// NOTE: field order matters for drop order. The harfbuzz font holds its
// own FreeType reference and the font data must outlive every face
// reference.
/// Our font face.
#[allow(clippy::struct_field_names)]
face: ft::FtFace,
/// Harfbuzz font corresponding to this face.
hb_font: HbFont,
/// This mutex MUST be held while doing anything with the
/// glyph slot on the freetype face, because this struct
/// may be shared across multiple surfaces.
///
/// This means that anywhere where `load_glyph` is called, this
/// mutex must be held. (Deviation: faces that share an `FT_Face`
/// through `synthetic_bold`/`synthetic_italic` also share this mutex.)
ft_mutex: Arc<Mutex<()>>,
/// Our Library
lib: Library,
/// Keeps in-memory font data alive (FreeType does not copy it).
_data: Option<FontData>,
}
impl std::fmt::Debug for Face {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Face")
.field("size", &self.size)
.field("synthetic", &self.synthetic)
.finish_non_exhaustive()
}
}
// NOTE: `Face` is `Send + Sync` automatically: the unsafe impls live on the
// FFI handle wrappers in `ft.rs`. Mutable FreeType state (size, glyph slot)
// is only touched through `&mut self` or while holding `ft_mutex`.
impl Face {
/// Initialize a new font face from a file.
pub fn init_file(
lib: &Library,
path: &str,
index: i32,
opts: Options,
) -> Result<Face, FaceError> {
let cpath = CString::new(path).map_err(|_| FaceError::InvalidPath)?;
let face = {
let _g = lib.lock();
ft::FtFace::new_file(&lib.inner.lib, &cpath, index).map_err(FaceError::Freetype)?
};
Face::init_face(lib, face, None, opts)
}
/// Initialize a new font face with the given source in-memory.
pub fn init(lib: &Library, source: FontData, opts: Options) -> Result<Face, FaceError> {
let face = {
let _g = lib.lock();
ft::FtFace::new_memory(&lib.inner.lib, source.as_slice(), 0)
.map_err(FaceError::Freetype)?
};
Face::init_face(lib, face, Some(source), opts)
}
fn init_face(
lib: &Library,
face: ft::FtFace,
data: Option<FontData>,
opts: Options,
) -> Result<Face, FaceError> {
Face::init_face_with_mutex(lib, face, data, opts, Arc::new(Mutex::new(())))
}
fn init_face_with_mutex(
lib: &Library,
face: ft::FtFace,
data: Option<FontData>,
opts: Options,
ft_mutex: Arc<Mutex<()>>,
) -> Result<Face, FaceError> {
face.select_unicode_charmap().map_err(FaceError::Freetype)?;
set_size_(&face, opts.size)?;
let hb_font = face.create_hb_font().ok_or(FaceError::HarfbuzzFailed)?;
let mut result = Face {
load_flags: opts.freetype_load_flags,
quirks_disable_default_font_features: false,
synthetic: Synthetic::default(),
size: opts.size,
face,
hb_font,
ft_mutex,
lib: lib.clone(),
_data: data,
};
result.quirks_disable_default_font_features = quirks_disable_default_font_features(&result);
// We output information about available variation axes, if they exist.
if log::log_enabled!(log::Level::Debug) && result.face.has_multiple_masters() {
log::debug!("variation axes font={}", result.name());
if let Ok(axes) = result.face.mm_axes(&result.lib.inner.lib) {
for axis in axes {
let id = super::VariationId::from_u32(axis.tag);
log::debug!(
"variation axis: id={} min={} max={} def={}",
String::from_utf8_lossy(&id.str()),
axis.minimum >> 16,
axis.maximum >> 16,
axis.def >> 16,
);
}
}
}
Ok(result)
}
fn lock_ft(&self) -> MutexGuard<'_, ()> {
lock(&self.ft_mutex)
}
/// Returns the font name (family entry of the SFNT name table), or an
/// empty string if there is none.
#[allow(clippy::chunks_exact_to_as_chunks)]
pub fn name(&self) -> String {
let count = self.face.sfnt_name_count();
// We look for the font family entry.
for i in 0..count {
let Some(entry) = self.face.sfnt_name(i) else {
continue;
};
if entry.name_id == ft::TT_NAME_ID_FONT_FAMILY {
let string = &entry.string;
// There are other encodings that are something other than UTF-8
// but this is one we've seen "in the wild" so far.
if entry.platform_id == ft::TT_PLATFORM_MICROSOFT
&& entry.encoding_id == ft::TT_MS_ID_UNICODE_CS
&& string.len() % 2 == 0
&& string.len() <= 1024
{
// UTF-16 big endian.
let units: Vec<u16> = string
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
if let Ok(s) = String::from_utf16(&units) {
return s;
}
}
return String::from_utf8_lossy(string).into_owned();
}
}
String::new()
}
/// Return a new face that is the same as this but also has synthetic
/// bold applied.
pub fn synthetic_bold(&self, opts: Options) -> Result<Face, FaceError> {
// Increase face ref count
let face = self.face.reference().map_err(FaceError::Freetype)?;
let mut f = Face::init_face_with_mutex(
&self.lib,
face,
self._data.clone(),
opts,
Arc::clone(&self.ft_mutex),
)?;
f.synthetic = self.synthetic;
f.synthetic.bold = true;
Ok(f)
}
/// Return a new face that is the same as this but has a transformation
/// matrix applied to italicize it.
pub fn synthetic_italic(&self, opts: Options) -> Result<Face, FaceError> {
// Increase face ref count
let face = self.face.reference().map_err(FaceError::Freetype)?;
let mut f = Face::init_face_with_mutex(
&self.lib,
face,
self._data.clone(),
opts,
Arc::clone(&self.ft_mutex),
)?;
f.synthetic = self.synthetic;
f.synthetic.italic = true;
Ok(f)
}
/// Resize the font in-place. If this succeeds, the caller is responsible
/// for clearing any glyph caches, font atlas data, etc.
pub fn set_size(&mut self, opts: Options) -> Result<(), FaceError> {
let _g = lock(&self.ft_mutex);
set_size_(&self.face, opts.size)?;
self.size = opts.size;
Ok(())
}
/// Set the variation axes for this font. This will modify this font
/// in-place.
pub fn set_variations(&mut self, vs: &[Variation], _opts: Options) -> Result<(), FaceError> {
// If this font doesn't support variations, we can't do anything.
if !self.face.has_multiple_masters() || vs.is_empty() {
return Ok(());
}
// Freetype requires that we send ALL coordinates in at once so the
// first thing we have to do is get all the vars and put them into
// an array.
let axes = self
.face
.mm_axes(&self.lib.inner.lib)
.map_err(FaceError::Freetype)?;
// To avoid allocations, we cap the number of variation axes we can
// support. This is arbitrary but Firefox caps this at 16 so I
// feel like that's probably safe... and we do double cause its
// cheap.
let n = axes.len().min(32);
let mut coords = self
.face
.var_design_coordinates(n)
.map_err(FaceError::Freetype)?;
// Now we go through each axis and see if its set. This is slow
// but there usually aren't many axes and usually not many set
// variations, either.
for (i, axis) in axes.iter().take(n).enumerate() {
for v in vs {
if axis.tag == v.id.as_u32() {
coords[i] = (v.value * 65536.0) as i64;
break;
}
}
}
// Set them!
let _g = lock(&self.ft_mutex);
self.face
.set_var_design_coordinates(&coords)
.map_err(FaceError::Freetype)
}
/// Returns the glyph index for the given Unicode code point. If this
/// face doesn't support this glyph, null is returned.
pub fn glyph_index(&self, cp: u32) -> Option<u32> {
self.face.char_index(cp)
}
/// Returns true if this font is colored. This can be used by callers to
/// determine what kind of atlas to pass in.
pub fn has_color(&self) -> bool {
self.face.has_color()
}
/// Returns true if the given glyph ID is colorized.
pub fn is_color_glyph(&self, glyph_id: u32) -> bool {
let _g = self.lock_ft();
// Load the glyph and see what pixel mode it renders with.
// All modes other than BGRA are non-color.
// If the glyph fails to load, just return false.
//
// NO_SVG set to true because we don't currently support rendering
// SVG glyphs under FreeType, since that requires bundling another
// dependency to handle rendering the SVG.
let mut flags = ft::LOAD_RENDER | ft::FT_LOAD_NO_SVG;
if self.face.has_color() {
flags |= ft::LOAD_COLOR;
}
if self.face.load_glyph(glyph_id, flags).is_err() {
return false;
}
self.face.slot_info().pixel_mode == ft::PIXEL_MODE_BGRA
}
/// Set the load flags to use when loading a glyph for measurement or
/// rendering.
fn glyph_load_flags(&self, constrained: bool) -> i32 {
// Hinting should only be enabled if the configured load flags specify
// it and the provided constraint doesn't actually do anything, since
// if it does, then it'll mess up the hinting anyway when it moves or
// resizes the glyph.
let do_hinting = self.load_flags.hinting && !constrained;
// If our glyph has color, we want to render the color
let mut flags = 0;
if self.face.has_color() {
flags |= ft::LOAD_COLOR;
}
// We don't render, because we'll invoke the render
// manually after applying constraints further down.
// use options from config
if !do_hinting {
flags |= ft::LOAD_NO_HINTING;
}
if self.load_flags.force_autohint {
flags |= ft::LOAD_FORCE_AUTOHINT;
}
if !self.load_flags.autohint {
flags |= ft::LOAD_NO_AUTOHINT;
}
// If we're gonna be rendering this glyph in monochrome,
// then we should use the monochrome hinter as well, or
// else it won't look very good at all.
//
// Otherwise if the user asked for light hinting we
// use that, otherwise we just use the normal target.
flags |= if self.load_flags.monochrome {
ft::TARGET_MONO
} else if self.load_flags.light {
ft::TARGET_LIGHT
} else {
ft::TARGET_NORMAL
};
// NO_SVG set to true because we don't currently support rendering
// SVG glyphs under FreeType, since that requires bundling another
// dependency to handle rendering the SVG.
flags | ft::FT_LOAD_NO_SVG
}
/// Get a rect that represents the position and size of the loaded glyph.
fn get_glyph_size(&self) -> super::GlyphSize {
let slot = self.face.slot_info();
// If we're dealing with an outline glyph then we get the
// outline's bounding box instead of using the built-in
// metrics, since that's more precise and allows better
// cell-fitting.
if slot.format == ft::GLYPH_FORMAT_OUTLINE {
// Get the glyph's bounding box before we transform it at all.
// We use this rather than the metrics, since it's more precise.
let (x_min, y_min, x_max, y_max) = self.face.slot_outline_bbox();
return super::GlyphSize {
x: f26dot6_to_f64(x_min),
y: f26dot6_to_f64(y_min),
width: f26dot6_to_f64(x_max - x_min),
height: f26dot6_to_f64(y_max - y_min),
};
}
super::GlyphSize {
x: f26dot6_to_f64(slot.hori_bearing_x),
y: f26dot6_to_f64(slot.hori_bearing_y - slot.metrics_height),
width: f26dot6_to_f64(slot.metrics_width),
height: f26dot6_to_f64(slot.metrics_height),
}
}
/// Render a glyph using the glyph index. The rendered glyph is stored in the
/// given texture atlas.
pub fn render_glyph(
&self,
atlas: &mut Atlas,
glyph_index: u32,
opts: RenderOptions,
) -> Result<Glyph, RenderError> {
let _g = self.lock_ft();
// Load the glyph.
self.face
.load_glyph(
glyph_index,
self.glyph_load_flags(opts.constraint.does_anything()),
)
.map_err(RenderError::Freetype)?;
let slot = self.face.slot_info();
// For synthetic bold, we embolden the glyph.
if self.synthetic.bold {
// We need to scale the embolden amount based on the font size.
// This is a heuristic I found worked well across a variety of
// founts: 1 pixel per 64 units of height.
let font_height = self.face.size_metrics().height as f64;
let ratio: f64 = 64.0 / 2048.0;
let amount = (font_height * ratio).ceil();
self.face.slot_embolden(amount as i64);
}
// We get a rect that represents the position
// and size of the glyph before constraints.
let rect = self.get_glyph_size();
// If our glyph is smaller than a quarter pixel in either axis
// then it has no outlines or they're too small to render.
//
// In this case we just return 0-sized glyph struct.
if rect.width < 0.25 || rect.height < 0.25 {
return Ok(Glyph::default());
}
let metrics = opts.grid_metrics;
let cell_width = f64::from(metrics.cell_width);
let cell_height = f64::from(metrics.cell_height);
// Next we apply any constraints to get the final size of the glyph.
let constraint = opts.constraint;
// We need to add the baseline position before passing to the constrain
// function since it operates on cell-relative positions, not baseline.
let cell_baseline = f64::from(metrics.cell_baseline);
let glyph_size = constraint.constrain(
super::GlyphSize {
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y + cell_baseline,
},
&metrics,
opts.constraint_width,
);
let mut width = glyph_size.width;
let mut height = glyph_size.height;
let mut x = glyph_size.x;
let mut y = glyph_size.y;
// We center all glyphs within the pixel-rounded and adjusted
// cell width if it's larger than the face width, so that they
// aren't weirdly off to the left.
//
// We don't do this if the glyph has a stretch constraint,
// since in that case the position was already calculated with the
// new cell width in mind.
if constraint.size != super::ConstraintSize::Stretch && metrics.face_width < cell_width {
// We add half the difference to re-center.
//
// NOTE: We round this to a whole-pixel amount because under
// FreeType, the outlines will be hinted, which isn't
// the case under CoreText. If we move the outlines by
// a non-whole-pixel amount, it completely ruins the
// hinting.
x += ((cell_width - metrics.face_width) / 2.0).round();
}
// If this is a bitmap glyph, it will always render as full pixels,
// not fractional pixels, so we need to quantize its position and
// size accordingly to align to full pixels so we get good results.
if slot.format == ft::GLYPH_FORMAT_BITMAP {
width = cell_width - (cell_width - width - x).round() - x.round();
height = cell_height - (cell_height - height - y).round() - y.round();
x = x.round();
y = y.round();
}
// Now we can render the glyph.
let mut bitmap: ft::Bitmap;
let bitmap_left: i32;
let bitmap_top: i32;
match slot.format {
ft::GLYPH_FORMAT_OUTLINE => {
// Manually adjust the glyph outline with this transform.
//
// This offers better precision than using the freetype transform
// matrix, since that has 16.16 coefficients, and also I was having
// weird issues that I can only assume where due to freetype doing
// some bad caching or something when I did this using the matrix.
let scale_x = width / rect.width;
let scale_y = height / rect.height;
let skew: f64 = if self.synthetic.italic {
// We skew by 12 degrees to synthesize italics.
12.0_f64.to_radians().tan()
} else {
0.0
};
self.face.slot_map_outline_points(|ox, oy| {
// Convert to f64 for processing
let mut px = f26dot6_to_f64(ox);
let mut py = f26dot6_to_f64(oy);
// Subtract original bearings
px -= rect.x;
py -= rect.y;
// Scale
px *= scale_x;
py *= scale_y;
// Add new bearings
px += x;
py += y - cell_baseline;
// Skew
px += py * skew;
// Convert back and store
(f64_to_f26dot6(px), f64_to_f26dot6(py))
});
self.face
.slot_render(self.load_flags.monochrome)
.map_err(RenderError::Freetype)?;
// Copy the glyph's bitmap, making sure
// that it's 8bpp and densely packed.
bitmap = self
.face
.slot_bitmap_converted(&self.lib.inner.lib)
.map_err(|_| RenderError::BitmapHandlingError)?;
let after = self.face.slot_info();
bitmap_left = after.bitmap_left;
bitmap_top = after.bitmap_top;
}
ft::GLYPH_FORMAT_BITMAP => {
// If our glyph has a non-color bitmap, we need
// to convert it to dense 8bpp so that the scale
// operation works correctly.
let glyph_bitmap = match slot.pixel_mode {
ft::PIXEL_MODE_BGRA | ft::PIXEL_MODE_GRAY => self.face.slot_bitmap(),
_ => self
.face
.slot_bitmap_converted(&self.lib.inner.lib)
.map_err(|_| RenderError::BitmapHandlingError)?,
};
// The resize below interprets pixels using the atlas depth, so
// a mismatch would read garbage.
match glyph_bitmap.pixel_mode {
ft::PIXEL_MODE_GRAY if atlas.format != atlas::Format::Grayscale => {
return Err(RenderError::WrongAtlas);
}
ft::PIXEL_MODE_BGRA if atlas.format != atlas::Format::Bgra => {
return Err(RenderError::WrongAtlas);
}
_ => {}
}
// Round our target width and height
// as the size for our scaled bitmap.
let w = width.round().max(0.0) as u32;
let h = height.round().max(0.0) as u32;
let depth = u32::from(atlas.format.depth());
let pitch = w * depth;
// Resize
let buf = resize::resize_uint8(
&glyph_bitmap.buffer,
glyph_bitmap.width as usize,
glyph_bitmap.rows as usize,
glyph_bitmap.pitch.unsigned_abs() as usize,
w as usize,
h as usize,
depth as usize,
)
// This should never fail because this is a
// fairly straightforward in-memory operation...
.ok_or(RenderError::GlyphResizeFailed)?;
bitmap = ft::Bitmap {
buffer: buf,
width: w,
rows: h,
pitch: pitch as i32,
pixel_mode: glyph_bitmap.pixel_mode,
num_grays: glyph_bitmap.num_grays,
};
// Update the bearings to account for the new positioning.
bitmap_top = (y - cell_baseline + height).floor() as i32;
bitmap_left = x.floor() as i32;
}
f => {
// Glyph formats are tags, so we can
// output a semi-readable error here.
log::error!(
"Can't render glyph with unsupported glyph format \"{}\"",
String::from_utf8_lossy(&f.to_be_bytes())
);
return Err(RenderError::UnsupportedGlyphFormat);
}
}
// If this is a color glyph but we're trying to render it to the
// grayscale atlas, or vice versa, then we throw and error. Maybe
// in the future we could convert, but for now it should be fine.
match bitmap.pixel_mode {
ft::PIXEL_MODE_GRAY => {
if atlas.format != atlas::Format::Grayscale {
return Err(RenderError::WrongAtlas);
}
}
ft::PIXEL_MODE_BGRA => {
if atlas.format != atlas::Format::Bgra {
return Err(RenderError::WrongAtlas);
}
}
mode => {
log::warn!("glyph={glyph_index} pixel mode={mode}");
return Err(RenderError::UnsupportedPixelMode(mode));
}
}
// Our whole-pixel bearings for the final glyph.
// The fractional portion will be included in the rasterized position.
//
// For the Y position, FreeType's `bitmap_top` is the distance from the
// baseline to the top of the glyph, but we need the distance from the
// bottom of the cell to the bottom of the glyph, so first we add the
// baseline to get the distance from the bottom of the cell to the top
// of the glyph, then we subtract the height of the glyph to get the
// bottom.
let px_x: i32 = bitmap_left;
let px_y: i32 = bitmap_top + metrics.cell_baseline as i32 - bitmap.rows as i32;
let px_width = bitmap.width;
let px_height = bitmap.rows;
let len = bitmap.pitch.unsigned_abs() as usize * bitmap.rows as usize;
// If our bitmap is grayscale, make sure to multiply all pixel
// values by the right factor to bring `num_grays` up to 256.
//
// This is necessary because FT_Bitmap_Convert doesn't do this,
// it just sets num_grays to the correct number and uses the
// original smaller pixel values.
if bitmap.pixel_mode == ft::PIXEL_MODE_GRAY && bitmap.num_grays < 256 {
let factor = (255 / (i32::from(bitmap.num_grays) - 1)) as u8;
for p in &mut bitmap.buffer[..len] {
*p = p.wrapping_mul(factor);
}
bitmap.num_grays = 256;
}
// Must have non-empty bitmap because we return earlier if zero.
// We assume the rest of this that it is non-zero so this is important.
debug_assert!(px_width > 0 && px_height > 0);
// If this doesn't match then something is wrong.
debug_assert_eq!(
px_width * u32::from(atlas.format.depth()),
bitmap.pitch.unsigned_abs()
);
// Allocate our texture atlas region and copy our bitmap in to it.
let region = atlas.reserve(px_width, px_height)?;
atlas.set(region, &bitmap.buffer[..len]);
// This should be the distance from the bottom of
// the cell to the top of the glyph's bounding box.
let offset_y: i32 = px_y + px_height as i32;
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
let offset_x: i32 = px_x;
Ok(Glyph {
width: px_width,
height: px_height,
offset_x,
offset_y,
atlas_x: region.x,
atlas_y: region.y,
})
}
/// Convert 16.6 pixel format to pixels based on the scale factor of the
/// current font size.
#[allow(dead_code)]
fn units_to_px_y(&self, units: i32) -> i32 {
// FT_MulFix(units, y_scale) >> 6
let scale = self.face.size_metrics().y_scale;
let prod = i64::from(units) * scale;
(((prod + 0x8000 + (prod >> 63)) >> 16) >> 6) as i32
}
/// Get the `FaceMetrics` for this face.
pub fn get_metrics(&self) -> FaceMetrics {
let face = &self.face;
let size_metrics = face.size_metrics();
// This code relies on this assumption, and it should always be
// true since we don't do any non-uniform scaling on the font ever.
debug_assert_eq!(size_metrics.x_ppem, size_metrics.y_ppem);
// Read the 'head' table out of the font data.
let head_units_per_em = face.head_units_per_em();
// Read the 'post' table out of the font data.
let post_ = face.post();
// Read the 'OS/2' table out of the font data.
let os2_ = face.os2();
// Read the 'hhea' table out of the font data.
let hhea_ = face.hhea();
// Whether the font is in a scalable format. We need to know this
// because many of the metrics provided by FreeType are invalid for
// non-scalable fonts.
let is_scalable = face.is_scalable();
// We get the UPM from the head table.
//
// If we have no head, but it is a scalable face, take the UPM from
// FreeType's units_per_EM, otherwise we'll assume that UPM == PPEM.
let units_per_em: u16 = if let Some(u) = head_units_per_em {
u
} else if is_scalable {
face.units_per_em()
} else {
size_metrics.y_ppem
};
let px_per_em = f64::from(size_metrics.y_ppem);
let px_per_unit = px_per_em / f64::from(units_per_em);
let (ascent, descent, line_gap): (f64, f64, f64) = 'vertical_metrics: {
let Some(hhea) = hhea_ else {
// If we couldn't get the hhea table, rely on metrics from FreeType.
let ascender = f26dot6_to_f64(size_metrics.ascender);
let descender = f26dot6_to_f64(size_metrics.descender);
let height = f26dot6_to_f64(size_metrics.height);
break 'vertical_metrics (
ascender,
descender,
// We compute the line gap by adding the (negative) descender
// and subtracting the (positive) ascender from the line height
// to get the remaining gap size.
//
// NOTE: This might always be 0... but it doesn't hurt to do.
height + descender - ascender,
);
};
let hhea_ascent = f64::from(hhea.ascender);
let hhea_descent = f64::from(hhea.descender);
let hhea_line_gap = f64::from(hhea.line_gap);
// If our font has no OS/2 table, then we just
// blindly use the metrics from the hhea table.
let Some(os2) = os2_ else {
break 'vertical_metrics (
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
);
};
let os2_ascent = f64::from(os2.s_typo_ascender);
let os2_descent = f64::from(os2.s_typo_descender);
let os2_line_gap = f64::from(os2.s_typo_line_gap);
// If the font says to use typo metrics, trust it.
// (The USE_TYPO_METRICS bit is bit 7)
if os2.fs_selection & (1 << 7) != 0 {
break 'vertical_metrics (
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
);
}
// Otherwise we prefer the height metrics from 'hhea' if they
// are available, or else OS/2 sTypo* metrics, and if all else
// fails then we use OS/2 usWin* metrics.
//
// This is not "standard" behavior, but it's our best bet to
// account for fonts being... just weird. It's pretty much what
// FreeType does to get its generic ascent and descent metrics.
if hhea.ascender != 0 || hhea.descender != 0 {
break 'vertical_metrics (
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
);
}
if os2_ascent != 0.0 || os2_descent != 0.0 {
break 'vertical_metrics (
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
);
}
let win_ascent = f64::from(os2.us_win_ascent);
let win_descent = f64::from(os2.us_win_descent);
(
win_ascent * px_per_unit,
// usWinDescent is *positive* -> down unlike sTypoDescender
// and hhea.Descender, so we flip its sign to fix this.
-win_descent * px_per_unit,
0.0,
)
};
let (underline_position, underline_thickness): (Option<f64>, Option<f64>) =
if let Some(post) = post_ {
// Some fonts have degenerate 'post' tables where the underline
// thickness (and often position) are 0. We consider them null
// if this is the case and use our own fallbacks when we calculate.
let has_broken_underline = post.underline_thickness == 0;
// If the underline position isn't 0 then we do use it,
// even if the thickness is't properly specified.
let pos = if has_broken_underline && post.underline_position == 0 {
None
} else {
Some(f64::from(post.underline_position) * px_per_unit)
};
let thick = if has_broken_underline {
None
} else {
Some(f64::from(post.underline_thickness) * px_per_unit)
};
(pos, thick)
} else {
(None, None)
};
// Similar logic to the underline above.
let (strikethrough_position, strikethrough_thickness): (Option<f64>, Option<f64>) =
if let Some(os2) = os2_ {
let has_broken_strikethrough = os2.y_strikeout_size == 0;
let pos = if has_broken_strikethrough && os2.y_strikeout_position == 0 {
None
} else {
Some(f64::from(os2.y_strikeout_position) * px_per_unit)
};
let thick = if has_broken_strikethrough {
None
} else {
Some(f64::from(os2.y_strikeout_size) * px_per_unit)
};
(pos, thick)
} else {
(None, None)
};
// Cell width is calculated by calculating the widest width of the
// visible ASCII characters. Usually 'M' is widest but we just take
// whatever is widest.
//
// ASCII height is calculated as the height of the overall bounding
// box of the same characters.
//
// If we fail to load any visible ASCII we just use max_advance from
// the metrics provided by FreeType, and set ascii_height to null as
// it's optional.
let (cell_width, ascii_height): (f64, Option<f64>) = {
let _g = self.lock_ft();
let mut max: f64 = 0.0;
let mut top: f64 = 0.0;
let mut bottom: f64 = 0.0;
for c in b' '..127u8 {
if let Some(glyph_index) = face.char_index(u32::from(c))
&& face
.load_glyph(glyph_index, self.glyph_load_flags(false))
.is_ok()
{
max = f26dot6_to_f64(face.slot_info().advance_x).max(max);
let rect = self.get_glyph_size();
top = (rect.y + rect.height).max(top);
bottom = rect.y.min(bottom);
}
}
// If we couldn't get valid measurements, just use
// FreeType's max_advance and null, respectively.
if max == 0.0 {
max = f26dot6_to_f64(size_metrics.max_advance);
}
let estimate = top - bottom;
let rect_height = if estimate <= 0.0 {
None
} else {
Some(estimate)
};
(max, rect_height)
};
// We use the cap and ex heights specified by the font if they're
// available, otherwise we try to measure the `H` and `x` glyphs.
let (cap_height, ex_height): (Option<f64>, Option<f64>) = 'heights: {
if let Some(os2) = os2_ {
// The OS/2 table does not include these metrics in version 1.
if os2.version >= 2 {
break 'heights (
Some(f64::from(os2.s_cap_height) * px_per_unit),
Some(f64::from(os2.sx_height) * px_per_unit),
);
}
}
let measure = |ch: char| -> Option<f64> {
let _g = self.lock_ft();
let glyph_index = face.char_index(ch as u32)?;
face.load_glyph(glyph_index, self.glyph_load_flags(false))
.ok()?;
Some(self.get_glyph_size().height)
};
(measure('H'), measure('x'))
};
// Measure "水" (CJK water ideograph, U+6C34) for our ic width.
let ic_width: Option<f64> = 'ic_width: {
let _g = self.lock_ft();
let Some(glyph) = face.char_index('水' as u32) else {
break 'ic_width None;
};
if face
.load_glyph(glyph, self.glyph_load_flags(false))
.is_err()
{
break 'ic_width None;
}
// If the advance of the glyph is less than the width of the actual
// glyph then we just treat it as invalid since it's probably wrong
// and using it for size normalization will instead make the font
// way too big.
//
// This can sometimes happen if there's a CJK font that has been
// patched with the nerd fonts patcher and it butchers the advance
// values so the advance ends up half the width of the actual glyph.
let ft_glyph_width = self.get_glyph_size().width;
let advance = f26dot6_to_f64(face.slot_info().advance_x);
if ft_glyph_width > advance {
log::warn!(
"(getMetrics) Width of glyph '水' for font \"{}\" is greater than its advance ({} > {}), discarding ic_width metric.",
self.name(),
ft_glyph_width,
advance,
);
break 'ic_width None;
}
Some(advance)
};
FaceMetrics {
px_per_em,
cell_width,
ascent,
descent,
line_gap,
underline_position,
underline_thickness,
strikethrough_position,
strikethrough_thickness,
cap_height,
ex_height,
ascii_height,
ic_width,
}
}
/// Copy the font table data for the given tag.
pub fn copy_table(&self, tag: &[u8; 4]) -> Result<Option<Vec<u8>>, FaceError> {
self.face.load_sfnt_table(*tag).map_err(FaceError::Freetype)
}
/// The harfbuzz font corresponding to this face (for shapers).
pub fn hb_font(&self) -> &HbFont {
&self.hb_font
}
/// The library this face was created with.
pub fn library(&self) -> &Library {
&self.lib
}
}
/// If true, the default font features should be disabled for the given face.
///
/// Port of `quirks.disableDefaultFontFeatures`: this used to check specific
/// font names but the logic was integrated directly into the shaping
/// algorithm, so it is always false.
fn quirks_disable_default_font_features(_face: &Face) -> bool {
false
}
fn set_size_(face: &ft::FtFace, size: DesiredSize) -> Result<(), FaceError> {
// If we have fixed sizes, we just have to try to pick the one closest
// to what the user requested. Otherwise, we can choose an arbitrary
// pixel size.
if face.is_scalable() {
let size_26dot6 = (size.points * 64.0).round() as i32;
face.set_char_size(0, size_26dot6, size.xdpi, size.ydpi)
.map_err(FaceError::Freetype)
} else {
select_size_nearest(face, size.pixels().round() as u32)
}
}
/// Selects the fixed size in the loaded face that is closest to the
/// requested pixel size.
fn select_size_nearest(face: &ft::FtFace, size: u32) -> Result<(), FaceError> {
let mut best_i: i32 = 0;
let mut best_diff: i32 = 0;
for i in 0..face.num_fixed_sizes() {
let width = face.fixed_size_width(i);
let diff = size as i32 - width;
if i == 0 || diff < best_diff {
best_diff = diff;
best_i = i;
}
}
face.select_size(best_i).map_err(FaceError::Freetype)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Metrics, embedded};
fn opts(points: f32, dpi: u16) -> Options {
Options::new(DesiredSize {
points,
xdpi: dpi,
ydpi: dpi,
})
}
fn init(lib: &Library, data: &'static [u8], points: f32, dpi: u16) -> Face {
Face::init(lib, data.into(), opts(points, dpi)).unwrap()
}
#[test]
fn face_name() {
let lib = Library::init().unwrap();
{
let face = init(&lib, embedded::VARIABLE, 14.0, 96);
assert_eq!("JetBrains Mono", face.name());
}
{
let face = init(&lib, embedded::INCONSOLATA, 14.0, 96);
assert_eq!("Inconsolata", face.name());
}
}
#[test]
fn render_ascii_and_resize() {
let lib = Library::init().unwrap();
let mut atlas = Atlas::new(512, atlas::Format::Grayscale);
let mut ft_font = init(&lib, embedded::INCONSOLATA, 12.0, 96);
// Generate all visible ASCII
for i in 32u8..127 {
ft_font
.render_glyph(
&mut atlas,
ft_font.glyph_index(u32::from(i)).unwrap(),
RenderOptions::new(Metrics::calc(ft_font.get_metrics())),
)
.unwrap();
}
// Test resizing
let g1 = ft_font
.render_glyph(
&mut atlas,
ft_font.glyph_index('A' as u32).unwrap(),
RenderOptions::new(Metrics::calc(ft_font.get_metrics())),
)
.unwrap();
assert_eq!(11, g1.height);
ft_font.set_size(opts(24.0, 96)).unwrap();
let g2 = ft_font
.render_glyph(
&mut atlas,
ft_font.glyph_index('A' as u32).unwrap(),
RenderOptions::new(Metrics::calc(ft_font.get_metrics())),
)
.unwrap();
assert_eq!(21, g2.height);
}
#[test]
fn color_emoji() {
let lib = Library::init().unwrap();
let mut atlas = Atlas::new(512, atlas::Format::Bgra);
let ft_font = init(&lib, embedded::EMOJI, 12.0, 96);
ft_font
.render_glyph(
&mut atlas,
ft_font.glyph_index('🥸' as u32).unwrap(),
RenderOptions::new(Metrics::calc(ft_font.get_metrics())),
)
.unwrap();
// Make sure this glyph has color
assert!(ft_font.has_color());
let glyph_id = ft_font.glyph_index('🥸' as u32).unwrap();
assert!(ft_font.is_color_glyph(glyph_id));
}
#[test]
fn mono_to_bgra() {
let lib = Library::init().unwrap();
let mut atlas = Atlas::new(512, atlas::Format::Bgra);
let ft_font = init(&lib, embedded::EMOJI, 12.0, 72);
// glyph 3 is mono in Noto
ft_font
.render_glyph(
&mut atlas,
3,
RenderOptions::new(Metrics::calc(ft_font.get_metrics())),
)
.unwrap();
}
#[test]
fn svg_font_table() {
let lib = Library::init().unwrap();
let face = init(&lib, embedded::JULIA_MONO, 12.0, 72);
let table = face.copy_table(b"SVG ").unwrap().unwrap();
assert_eq!(430, table.len());
}
const TERMINUS_I: &str = "\
........\n\
........\n\
...#....\n\
...#....\n\
........\n\
..##....\n\
...#....\n\
...#....\n\
...#....\n\
...#....\n\
...#....\n\
..###...\n\
........\n\
........\n\
........\n\
........";
// Including the newline
const TERMINUS_I_PITCH: usize = 9;
fn check_pattern(atlas: &Atlas, glyph: &Glyph, pattern: &str, pitch: usize) {
let pat = pattern.as_bytes();
for y in 0..glyph.height as usize {
for x in 0..glyph.width as usize {
let pixel = pat[y * pitch + x];
assert_eq!(
if pixel == b'#' { 255 } else { 0 },
atlas.data[(glyph.atlas_y as usize + y) * atlas.size as usize
+ (glyph.atlas_x as usize + x)],
"pixel ({x},{y})"
);
}
}
}
#[test]
fn bitmap_glyph() {
let lib = Library::init().unwrap();
let mut atlas = Atlas::new(512, atlas::Format::Grayscale);
// Any glyph at 12pt @ 96 DPI is a bitmap
let ft_font = init(&lib, embedded::TERMINUS_TTF, 12.0, 96);
// glyph 77 = 'i'
let glyph = ft_font
.render_glyph(
&mut atlas,
77,
RenderOptions::new(Metrics::calc(ft_font.get_metrics())),
)
.unwrap();
// should render crisp
assert_eq!(8, glyph.width);
assert_eq!(16, glyph.height);
check_pattern(&atlas, &glyph, TERMINUS_I, TERMINUS_I_PITCH);
}
// Expected pixel pattern for Spleen 8x16 'A' (glyph index from char 'A')
// Derived from BDF BITMAP data: 00,00,7C,C6,C6,C6,FE,C6,C6,C6,C6,C6,00,00,00,00
const SPLEEN_A: &str = "\
........\n\
........\n\
.#####..\n\
##...##.\n\
##...##.\n\
##...##.\n\
#######.\n\
##...##.\n\
##...##.\n\
##...##.\n\
##...##.\n\
##...##.\n\
........\n\
........\n\
........\n\
........";
// Including the newline
const SPLEEN_A_PITCH: usize = 9;
// Test parameters for bitmap font tests
const SPLEEN_TEST_POINT_SIZE: f32 = 12.0;
const SPLEEN_TEST_DPI: u16 = 96;
fn spleen_test(data: &'static [u8]) {
let lib = Library::init().unwrap();
let mut atlas = Atlas::new(512, atlas::Format::Grayscale);
// Spleen 8x16 is a pure bitmap font at 16px height
let ft_font = init(&lib, data, SPLEEN_TEST_POINT_SIZE, SPLEEN_TEST_DPI);
// Get glyph index for 'A'
let glyph_index = ft_font.glyph_index('A' as u32).expect("glyph not found");
let glyph = ft_font
.render_glyph(
&mut atlas,
glyph_index,
RenderOptions::new(Metrics::calc(ft_font.get_metrics())),
)
.unwrap();
// Verify dimensions match Spleen 8x16
assert_eq!(8, glyph.width);
assert_eq!(16, glyph.height);
// Verify pixel-perfect rendering
check_pattern(&atlas, &glyph, SPLEEN_A, SPLEEN_A_PITCH);
}
#[test]
fn bitmap_glyph_bdf() {
spleen_test(embedded::SPLEEN_BDF);
}
#[test]
fn bitmap_glyph_pcf() {
spleen_test(embedded::SPLEEN_PCF);
}
#[test]
fn bitmap_glyph_otb() {
spleen_test(embedded::SPLEEN_OTB);
}
// ---- extra tests ----
#[test]
fn variation_axes_can_be_set() {
let lib = Library::init().unwrap();
let mut face = init(&lib, embedded::VARIABLE, 12.0, 96);
let o = opts(12.0, 96);
face.set_variations(
&[Variation {
id: super::super::VariationId::new(b"wght"),
value: 800.0,
}],
o,
)
.unwrap();
// A non-variable font ignores variations.
let mut face = init(&lib, embedded::INCONSOLATA, 12.0, 96);
face.set_variations(
&[Variation {
id: super::super::VariationId::new(b"wght"),
value: 800.0,
}],
o,
)
.unwrap();
}
#[test]
fn synthetic_bold_and_italic_render() {
let lib = Library::init().unwrap();
let mut atlas = Atlas::new(512, atlas::Format::Grayscale);
let face = init(&lib, embedded::INCONSOLATA, 12.0, 96);
let o = opts(12.0, 96);
let bold = face.synthetic_bold(o).unwrap();
let italic = face.synthetic_italic(o).unwrap();
assert!(bold.synthetic.bold && !bold.synthetic.italic);
assert!(italic.synthetic.italic && !italic.synthetic.bold);
let ro = RenderOptions::new(Metrics::calc(face.get_metrics()));
let gi = face.glyph_index('A' as u32).unwrap();
let g0 = face.render_glyph(&mut atlas, gi, ro).unwrap();
let g1 = bold.render_glyph(&mut atlas, gi, ro).unwrap();
let g2 = italic.render_glyph(&mut atlas, gi, ro).unwrap();
assert!(g1.width >= g0.width);
assert!(g2.width >= g0.width);
// The original face is still usable after the copies are dropped.
drop(bold);
drop(italic);
assert_eq!("Inconsolata", face.name());
}
#[test]
fn wrong_atlas_is_an_error() {
let lib = Library::init().unwrap();
let mut atlas = Atlas::new(512, atlas::Format::Bgra);
let face = init(&lib, embedded::INCONSOLATA, 12.0, 96);
let r = face.render_glyph(
&mut atlas,
face.glyph_index('A' as u32).unwrap(),
RenderOptions::new(Metrics::calc(face.get_metrics())),
);
assert!(matches!(r, Err(RenderError::WrongAtlas)));
}
#[test]
fn metrics_are_sane() {
let lib = Library::init().unwrap();
let face = init(&lib, embedded::VARIABLE, 12.0, 96);
let m = face.get_metrics();
assert!(m.px_per_em > 0.0);
assert!(m.cell_width > 0.0);
assert!(m.ascent > 0.0 && m.descent < 0.0);
assert!(m.cap_height.is_some() && m.ex_height.is_some());
}
#[test]
fn hb_font_is_available() {
let lib = Library::init().unwrap();
let face = init(&lib, embedded::INCONSOLATA, 12.0, 96);
assert!(!face.hb_font().as_ptr().is_null());
}
#[test]
fn init_file_from_disk() {
let lib = Library::init().unwrap();
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/res/Inconsolata-Regular.ttf");
let face = Face::init_file(&lib, path, 0, opts(12.0, 96)).unwrap();
assert_eq!("Inconsolata", face.name());
assert!(Face::init_file(&lib, "/nonexistent/font.ttf", 0, opts(12.0, 96)).is_err());
}
}
+676
View File
@@ -0,0 +1,676 @@
//! Thin, safe-ish wrappers over the raw `freetype-sys` / `harfbuzz-sys`
//! bindings. All FFI in the FreeType face lives here so that the rest of the
//! port (`freetype.rs`) is safe Rust.
//!
//! Callers are responsible for serialising access to a face's glyph slot
//! (see `Face::ft_mutex`); nothing in this module locks.
#![allow(unsafe_code)]
// c_long is i64 on Linux but not everywhere; keep the casts portable.
#![allow(clippy::unnecessary_cast)]
use std::ffi::{CStr, c_int, c_long, c_uint, c_ulong, c_void};
use std::ptr::{self, NonNull};
use freetype_sys as sys;
/// FreeType error code (0 is never used for an error).
pub type FtError = i32;
// ---------------------------------------------------------------------------
// Declarations missing from `freetype-sys`.
unsafe extern "C" {
fn FT_Bitmap_Init(abitmap: *mut sys::FT_Bitmap);
fn FT_Bitmap_Convert(
library: sys::FT_Library,
source: *const sys::FT_Bitmap,
target: *mut sys::FT_Bitmap,
alignment: c_int,
) -> sys::FT_Error;
fn FT_Bitmap_Done(library: sys::FT_Library, bitmap: *mut sys::FT_Bitmap) -> sys::FT_Error;
}
/// `FT_LOAD_NO_SVG`
pub const FT_LOAD_NO_SVG: i32 = 1 << 24;
/// Prefix of `TT_Header` (only fields up to `Units_Per_EM` are needed).
#[repr(C)]
struct TtHeader {
table_version: c_long,
font_revision: c_long,
checksum_adjust: c_long,
magic_number: c_long,
flags: u16,
units_per_em: u16,
}
/// Prefix of `TT_HoriHeader` (only fields up to `Line_Gap` are needed).
#[repr(C)]
struct TtHoriHeader {
version: c_long,
ascender: i16,
descender: i16,
line_gap: i16,
}
// ---------------------------------------------------------------------------
/// An owned FT_Library.
pub struct Lib {
handle: sys::FT_Library,
}
// SAFETY: FT_Library is only used from behind the `Library` mutex for
// face creation/destruction; FreeType libraries may be moved between threads.
unsafe impl Send for Lib {}
// SAFETY: see above; shared use is serialised by `Library::mutex`.
unsafe impl Sync for Lib {}
impl Lib {
pub fn new() -> Result<Lib, FtError> {
let mut handle: sys::FT_Library = ptr::null_mut();
// SAFETY: `handle` is a valid out pointer.
let err = unsafe { sys::FT_Init_FreeType(&raw mut handle) };
if err != 0 {
return Err(err);
}
Ok(Lib { handle })
}
}
impl Drop for Lib {
fn drop(&mut self) {
// SAFETY: `handle` was returned by FT_Init_FreeType and is freed once.
unsafe {
sys::FT_Done_FreeType(self.handle);
}
}
}
/// Copy of the `FT_Size_Metrics` fields we use.
#[derive(Debug, Clone, Copy)]
pub struct SizeMetrics {
pub x_ppem: u16,
pub y_ppem: u16,
pub y_scale: i64,
pub ascender: i64,
pub descender: i64,
pub height: i64,
pub max_advance: i64,
}
/// Copy of the `TT_OS2` fields we use.
#[derive(Debug, Clone, Copy)]
pub struct Os2 {
pub version: u16,
pub fs_selection: u16,
pub s_typo_ascender: i16,
pub s_typo_descender: i16,
pub s_typo_line_gap: i16,
pub us_win_ascent: u16,
pub us_win_descent: u16,
pub y_strikeout_size: i16,
pub y_strikeout_position: i16,
pub sx_height: i16,
pub s_cap_height: i16,
}
/// Copy of the `TT_Postscript` fields we use.
#[derive(Debug, Clone, Copy)]
pub struct Post {
pub underline_position: i16,
pub underline_thickness: i16,
}
/// Copy of the `TT_HoriHeader` fields we use.
#[derive(Debug, Clone, Copy)]
pub struct Hhea {
pub ascender: i16,
pub descender: i16,
pub line_gap: i16,
}
/// A `FT_SfntName` entry (string copied).
#[derive(Debug, Clone)]
pub struct SfntName {
pub platform_id: u16,
pub encoding_id: u16,
pub name_id: u16,
pub string: Vec<u8>,
}
/// A variation axis of a multiple-master / variable font.
#[derive(Debug, Clone, Copy)]
pub struct VarAxis {
pub tag: u32,
pub minimum: i64,
pub def: i64,
pub maximum: i64,
}
/// A copy of an `FT_Bitmap` (bytes included).
#[derive(Debug, Clone, Default)]
pub struct Bitmap {
pub buffer: Vec<u8>,
pub rows: u32,
pub width: u32,
pub pitch: i32,
pub num_grays: u16,
pub pixel_mode: u32,
}
/// Copy of the glyph-slot fields we use.
#[derive(Debug, Clone, Copy)]
pub struct SlotInfo {
pub format: u32,
pub metrics_width: i64,
pub metrics_height: i64,
pub hori_bearing_x: i64,
pub hori_bearing_y: i64,
pub advance_x: i64,
pub bitmap_left: i32,
pub bitmap_top: i32,
pub pixel_mode: u32,
}
pub const GLYPH_FORMAT_OUTLINE: u32 = sys::FT_GLYPH_FORMAT_OUTLINE;
pub const GLYPH_FORMAT_BITMAP: u32 = sys::FT_GLYPH_FORMAT_BITMAP;
pub const PIXEL_MODE_GRAY: u32 = sys::FT_PIXEL_MODE_GRAY;
pub const PIXEL_MODE_BGRA: u32 = sys::FT_PIXEL_MODE_BGRA;
/// Target load/render modes.
pub const TARGET_NORMAL: i32 = sys::FT_LOAD_TARGET_NORMAL;
pub const TARGET_LIGHT: i32 = sys::FT_LOAD_TARGET_LIGHT;
pub const TARGET_MONO: i32 = sys::FT_LOAD_TARGET_MONO;
/// Load flag bits.
pub const LOAD_NO_HINTING: i32 = sys::FT_LOAD_NO_HINTING;
pub const LOAD_RENDER: i32 = sys::FT_LOAD_RENDER;
pub const LOAD_FORCE_AUTOHINT: i32 = sys::FT_LOAD_FORCE_AUTOHINT;
pub const LOAD_NO_AUTOHINT: i32 = sys::FT_LOAD_NO_AUTOHINT;
pub const LOAD_COLOR: i32 = sys::FT_LOAD_COLOR;
pub const RENDER_NORMAL: c_uint = sys::FT_RENDER_MODE_NORMAL;
pub const RENDER_MONO: c_uint = sys::FT_RENDER_MODE_MONO;
pub const TT_NAME_ID_FONT_FAMILY: u16 = 1;
pub const TT_PLATFORM_MICROSOFT: u16 = 3;
pub const TT_MS_ID_UNICODE_CS: u16 = 1;
/// An owned reference to an `FT_Face` (released with `FT_Done_Face`).
pub struct FtFace {
ptr: NonNull<sys::FT_FaceRec>,
}
// SAFETY: FT_Face is moved between threads freely; concurrent use is
// serialised by the owner (`Face::ft_mutex`).
unsafe impl Send for FtFace {}
// SAFETY: see above.
unsafe impl Sync for FtFace {}
impl Drop for FtFace {
fn drop(&mut self) {
// SAFETY: we own one reference to the face.
unsafe {
sys::FT_Done_Face(self.ptr.as_ptr());
}
}
}
impl FtFace {
fn from_raw(raw: sys::FT_Face) -> Result<FtFace, FtError> {
NonNull::new(raw)
.map(|ptr| FtFace { ptr })
.ok_or(sys::FT_Err_Invalid_Handle)
}
/// Open a face from memory. The memory must outlive the face.
pub fn new_memory(lib: &Lib, data: &[u8], index: i32) -> Result<FtFace, FtError> {
let mut raw: sys::FT_Face = ptr::null_mut();
// SAFETY: `data` is valid for the call; the caller keeps it alive for
// the lifetime of the face.
let err = unsafe {
sys::FT_New_Memory_Face(
lib.handle,
data.as_ptr(),
data.len() as c_long,
index as c_long,
&raw mut raw,
)
};
if err != 0 {
return Err(err);
}
FtFace::from_raw(raw)
}
pub fn new_file(lib: &Lib, path: &CStr, index: i32) -> Result<FtFace, FtError> {
let mut raw: sys::FT_Face = ptr::null_mut();
// SAFETY: `path` is NUL terminated.
let err =
unsafe { sys::FT_New_Face(lib.handle, path.as_ptr(), index as c_long, &raw mut raw) };
if err != 0 {
return Err(err);
}
FtFace::from_raw(raw)
}
/// Create another owning handle to the same face (`FT_Reference_Face`).
pub fn reference(&self) -> Result<FtFace, FtError> {
// SAFETY: valid face.
let err = unsafe { sys::FT_Reference_Face(self.ptr.as_ptr()) };
if err != 0 {
return Err(err);
}
Ok(FtFace { ptr: self.ptr })
}
pub fn as_ptr(&self) -> sys::FT_Face {
self.ptr.as_ptr()
}
fn rec(&self) -> &sys::FT_FaceRec {
// SAFETY: the pointer is valid while we hold a reference.
unsafe { self.ptr.as_ref() }
}
pub fn select_unicode_charmap(&self) -> Result<(), FtError> {
// SAFETY: valid face.
let err = unsafe { sys::FT_Select_Charmap(self.as_ptr(), sys::FT_ENCODING_UNICODE) };
if err == 0 { Ok(()) } else { Err(err) }
}
pub fn set_char_size(
&self,
char_width: i32,
char_height: i32,
xdpi: u16,
ydpi: u16,
) -> Result<(), FtError> {
// SAFETY: valid face.
let err = unsafe {
sys::FT_Set_Char_Size(
self.as_ptr(),
char_width as c_long,
char_height as c_long,
c_uint::from(xdpi),
c_uint::from(ydpi),
)
};
if err == 0 { Ok(()) } else { Err(err) }
}
pub fn select_size(&self, idx: i32) -> Result<(), FtError> {
// SAFETY: valid face.
let err = unsafe { sys::FT_Select_Size(self.as_ptr(), idx) };
if err == 0 { Ok(()) } else { Err(err) }
}
pub fn num_fixed_sizes(&self) -> i32 {
self.rec().num_fixed_sizes
}
/// Width of the `i`th available fixed size.
pub fn fixed_size_width(&self, i: i32) -> i32 {
debug_assert!(i >= 0 && i < self.num_fixed_sizes());
// SAFETY: `i` is within `num_fixed_sizes`.
i32::from(unsafe { (*self.rec().available_sizes.add(i as usize)).width })
}
fn flags(&self) -> c_long {
self.rec().face_flags
}
pub fn is_scalable(&self) -> bool {
self.flags() & sys::FT_FACE_FLAG_SCALABLE != 0
}
pub fn has_color(&self) -> bool {
self.flags() & sys::FT_FACE_FLAG_COLOR != 0
}
pub fn has_multiple_masters(&self) -> bool {
self.flags() & sys::FT_FACE_FLAG_MULTIPLE_MASTERS != 0
}
pub fn units_per_em(&self) -> u16 {
self.rec().units_per_EM
}
pub fn char_index(&self, cp: u32) -> Option<u32> {
// SAFETY: valid face.
let i = unsafe { sys::FT_Get_Char_Index(self.as_ptr(), c_ulong::from(cp)) };
if i == 0 { None } else { Some(i) }
}
pub fn load_glyph(&self, glyph_index: u32, flags: i32) -> Result<(), FtError> {
// SAFETY: valid face; glyph slot access is serialised by the caller.
let err = unsafe { sys::FT_Load_Glyph(self.as_ptr(), glyph_index, flags) };
if err == 0 { Ok(()) } else { Err(err) }
}
pub fn size_metrics(&self) -> SizeMetrics {
// SAFETY: a face always has an active size once a size has been set.
let m = unsafe { (*self.rec().size).metrics };
SizeMetrics {
x_ppem: m.x_ppem,
y_ppem: m.y_ppem,
y_scale: m.y_scale as i64,
ascender: m.ascender as i64,
descender: m.descender as i64,
height: m.height as i64,
max_advance: m.max_advance as i64,
}
}
fn sfnt_table(&self, tag: u32) -> *mut c_void {
// SAFETY: valid face; the returned pointer is owned by the face.
unsafe { sys::FT_Get_Sfnt_Table(self.as_ptr(), tag) }
}
/// `Units_Per_EM` from the `head` table.
pub fn head_units_per_em(&self) -> Option<u16> {
let p = self.sfnt_table(sys::FT_SFNT_HEAD).cast::<TtHeader>();
// SAFETY: non-null pointers refer to a `TT_Header` owned by the face.
unsafe { p.as_ref() }.map(|h| h.units_per_em)
}
pub fn post(&self) -> Option<Post> {
let p = self
.sfnt_table(sys::FT_SFNT_POST)
.cast::<sys::TT_Postscript>();
// SAFETY: non-null pointers refer to a `TT_Postscript` owned by the face.
unsafe { p.as_ref() }.map(|p| Post {
underline_position: p.underlinePosition,
underline_thickness: p.underlineThickness,
})
}
/// The OS/2 table; `None` if absent or if FreeType flags it as invalid
/// (version 0xFFFF).
pub fn os2(&self) -> Option<Os2> {
let p = self.sfnt_table(sys::FT_SFNT_OS2).cast::<sys::TT_OS2>();
// SAFETY: non-null pointers refer to a `TT_OS2` owned by the face.
let o = unsafe { p.as_ref() }?;
if o.version == 0xFFFF {
return None;
}
Some(Os2 {
version: o.version,
fs_selection: o.fsSelection,
s_typo_ascender: o.sTypoAscender,
s_typo_descender: o.sTypoDescender,
s_typo_line_gap: o.sTypoLineGap,
us_win_ascent: o.usWinAscent,
us_win_descent: o.usWinDescent,
y_strikeout_size: o.yStrikeoutSize,
y_strikeout_position: o.yStrikeoutPosition,
sx_height: o.sxHeight,
s_cap_height: o.sCapHeight,
})
}
pub fn hhea(&self) -> Option<Hhea> {
let p = self.sfnt_table(sys::FT_SFNT_HHEA).cast::<TtHoriHeader>();
// SAFETY: non-null pointers refer to a `TT_HoriHeader` owned by the face.
unsafe { p.as_ref() }.map(|h| Hhea {
ascender: h.ascender,
descender: h.descender,
line_gap: h.line_gap,
})
}
pub fn sfnt_name_count(&self) -> usize {
// SAFETY: valid face.
unsafe { sys::FT_Get_Sfnt_Name_Count(self.as_ptr()) as usize }
}
pub fn sfnt_name(&self, i: usize) -> Option<SfntName> {
let mut name = std::mem::MaybeUninit::<sys::FT_SfntName>::uninit();
// SAFETY: out pointer is valid for a FT_SfntName.
let err = unsafe { sys::FT_Get_Sfnt_Name(self.as_ptr(), i as c_uint, name.as_mut_ptr()) };
if err != 0 {
return None;
}
// SAFETY: initialised by the successful call above.
let name = unsafe { name.assume_init() };
let string = if name.string.is_null() {
Vec::new()
} else {
// SAFETY: FreeType guarantees `string_len` readable bytes.
unsafe { std::slice::from_raw_parts(name.string, name.string_len as usize) }.to_vec()
};
Some(SfntName {
platform_id: name.platform_id,
encoding_id: name.encoding_id,
name_id: name.name_id,
string,
})
}
/// Copy of an SFNT table. `Ok(None)` if the table has zero length.
pub fn load_sfnt_table(&self, tag: [u8; 4]) -> Result<Option<Vec<u8>>, FtError> {
let tag_c = c_ulong::from(u32::from_be_bytes(tag));
let mut len: c_ulong = 0;
// SAFETY: a null buffer asks only for the length.
let err = unsafe {
sys::FT_Load_Sfnt_Table(self.as_ptr(), tag_c, 0, ptr::null_mut(), &raw mut len)
};
if err != 0 {
return Err(err);
}
if len == 0 {
return Ok(None);
}
let mut buf = vec![0u8; len as usize];
// SAFETY: `buf` has `len` bytes.
let err = unsafe {
sys::FT_Load_Sfnt_Table(self.as_ptr(), tag_c, 0, buf.as_mut_ptr(), &raw mut len)
};
if err != 0 {
return Err(err);
}
Ok(Some(buf))
}
/// The variation axes of this face (`FT_Get_MM_Var`).
pub fn mm_axes(&self, lib: &Lib) -> Result<Vec<VarAxis>, FtError> {
let mut mm: *mut sys::FT_MM_Var = ptr::null_mut();
// SAFETY: out pointer is valid.
let err = unsafe { sys::FT_Get_MM_Var(self.as_ptr(), &raw mut mm) };
if err != 0 {
return Err(err);
}
// SAFETY: `mm` is valid until FT_Done_MM_Var; `axis` has `num_axis` entries.
let axes = unsafe {
let m = &*mm;
(0..m.num_axis as usize)
.map(|i| {
let a = &*m.axis.add(i);
VarAxis {
tag: a.tag as u32,
minimum: a.minimum as i64,
def: a.def as i64,
maximum: a.maximum as i64,
}
})
.collect::<Vec<_>>()
};
// SAFETY: `mm` came from FT_Get_MM_Var on this library.
unsafe {
sys::FT_Done_MM_Var(lib.handle, mm);
}
Ok(axes)
}
pub fn var_design_coordinates(&self, n: usize) -> Result<Vec<i64>, FtError> {
let mut coords: Vec<c_long> = vec![0; n];
// SAFETY: buffer has `n` entries.
let err = unsafe {
sys::FT_Get_Var_Design_Coordinates(self.as_ptr(), n as c_uint, coords.as_mut_ptr())
};
if err != 0 {
return Err(err);
}
Ok(coords.into_iter().map(|c| c as i64).collect())
}
pub fn set_var_design_coordinates(&self, coords: &[i64]) -> Result<(), FtError> {
let c: Vec<c_long> = coords.iter().map(|&v| v as c_long).collect();
// SAFETY: buffer has `c.len()` entries.
let err = unsafe {
sys::FT_Set_Var_Design_Coordinates(self.as_ptr(), c.len() as c_uint, c.as_ptr())
};
if err == 0 { Ok(()) } else { Err(err) }
}
// -- glyph slot ---------------------------------------------------------
fn slot(&self) -> &sys::FT_GlyphSlotRec {
// SAFETY: a face always has a glyph slot; access is serialised by caller.
unsafe { &*self.rec().glyph }
}
pub fn slot_info(&self) -> SlotInfo {
let s = self.slot();
SlotInfo {
format: s.format,
metrics_width: s.metrics.width as i64,
metrics_height: s.metrics.height as i64,
hori_bearing_x: s.metrics.horiBearingX as i64,
hori_bearing_y: s.metrics.horiBearingY as i64,
advance_x: s.advance.x as i64,
bitmap_left: s.bitmap_left,
bitmap_top: s.bitmap_top,
pixel_mode: s.bitmap.pixel_mode as u8 as u32,
}
}
/// Outline bounding box `(xMin, yMin, xMax, yMax)` in 26.6 units.
pub fn slot_outline_bbox(&self) -> (i64, i64, i64, i64) {
let mut bbox = std::mem::MaybeUninit::<sys::FT_BBox>::zeroed();
// SAFETY: valid outline in the slot; out pointer valid.
unsafe {
sys::FT_Outline_Get_BBox(&raw const self.slot().outline, bbox.as_mut_ptr());
let b = bbox.assume_init();
(b.xMin as i64, b.yMin as i64, b.xMax as i64, b.yMax as i64)
}
}
pub fn slot_embolden(&self, strength: i64) {
// SAFETY: the slot's outline is modified in place; access is serialised.
unsafe {
let slot = self.rec().glyph;
sys::FT_Outline_Embolden(&raw mut (*slot).outline, strength as c_long);
}
}
/// Rewrites each outline point (26.6 coordinates) in place.
pub fn slot_map_outline_points(&self, mut f: impl FnMut(i64, i64) -> (i32, i32)) {
// SAFETY: `points` has `n_points` entries; access is serialised.
unsafe {
let slot = self.rec().glyph;
let outline = &mut (*slot).outline;
if outline.points.is_null() {
return;
}
let pts =
std::slice::from_raw_parts_mut(outline.points, outline.n_points.max(0) as usize);
for p in pts {
let (x, y) = f(p.x as i64, p.y as i64);
p.x = c_long::from(x);
p.y = c_long::from(y);
}
}
}
pub fn slot_render(&self, mono: bool) -> Result<(), FtError> {
let mode = if mono { RENDER_MONO } else { RENDER_NORMAL };
// SAFETY: valid glyph slot.
let err = unsafe { sys::FT_Render_Glyph(self.rec().glyph, mode) };
if err == 0 { Ok(()) } else { Err(err) }
}
/// Copy the slot's bitmap as-is.
pub fn slot_bitmap(&self) -> Bitmap {
let b = &self.slot().bitmap;
Bitmap {
buffer: copy_bitmap_bytes(b),
rows: b.rows as u32,
width: b.width as u32,
pitch: b.pitch,
num_grays: b.num_grays as u16,
pixel_mode: b.pixel_mode as u8 as u32,
}
}
/// `FT_Bitmap_Convert(slot.bitmap, 1)`: a dense 8bpp copy of the slot's
/// bitmap.
pub fn slot_bitmap_converted(&self, lib: &Lib) -> Result<Bitmap, FtError> {
let mut target = std::mem::MaybeUninit::<sys::FT_Bitmap>::uninit();
// SAFETY: initialises `target`; converts into it; freed with FT_Bitmap_Done.
unsafe {
FT_Bitmap_Init(target.as_mut_ptr());
let mut target = target.assume_init();
if FT_Bitmap_Convert(
lib.handle,
&raw const self.slot().bitmap,
&raw mut target,
1,
) != 0
{
FT_Bitmap_Done(lib.handle, &raw mut target);
return Err(sys::FT_Err_Invalid_Argument);
}
let out = Bitmap {
buffer: copy_bitmap_bytes(&target),
rows: target.rows as u32,
width: target.width as u32,
pitch: target.pitch,
num_grays: target.num_grays as u16,
pixel_mode: target.pixel_mode as u8 as u32,
};
FT_Bitmap_Done(lib.handle, &raw mut target);
Ok(out)
}
}
/// Creates a harfbuzz font for this face (`hb_ft_font_create_referenced`).
pub fn create_hb_font(&self) -> Option<HbFont> {
// SAFETY: valid face; harfbuzz takes its own FreeType reference.
let p = unsafe { harfbuzz_sys::freetype::hb_ft_font_create_referenced(self.as_ptr()) };
NonNull::new(p).map(HbFont)
}
}
fn copy_bitmap_bytes(b: &sys::FT_Bitmap) -> Vec<u8> {
let len = b.pitch.unsigned_abs() as usize * b.rows as usize;
if b.buffer.is_null() || len == 0 {
return Vec::new();
}
// SAFETY: FreeType bitmaps own `|pitch| * rows` bytes at `buffer`.
unsafe { std::slice::from_raw_parts(b.buffer, len) }.to_vec()
}
/// A harfbuzz font owned by a [`Face`](super::Face).
pub struct HbFont(NonNull<harfbuzz_sys::hb_font_t>);
// SAFETY: hb_font_t is thread-safe for shaping when not mutated; Ghostty uses
// it behind the collection lock exactly like upstream.
unsafe impl Send for HbFont {}
// SAFETY: see above.
unsafe impl Sync for HbFont {}
impl HbFont {
/// The raw `hb_font_t` pointer (valid while the `Face` lives).
pub fn as_ptr(&self) -> *mut harfbuzz_sys::hb_font_t {
self.0.as_ptr()
}
}
impl Drop for HbFont {
fn drop(&mut self) {
// SAFETY: we own one reference to the hb font.
unsafe { harfbuzz_sys::hb_font_destroy(self.0.as_ptr()) };
}
}
@@ -0,0 +1,145 @@
//! A small separable image resampler that mirrors the defaults of
//! `stbir_resize_uint8` (stb_image_resize v0.9x): 8-bit linear data, no alpha
//! handling, edge clamping, Catmull-Rom when upsampling and Mitchell when
//! downsampling, with the filter support scaled by the resize ratio.
fn catmull_rom(x: f32) -> f32 {
let x = x.abs();
if x < 1.0 {
1.0 - x * x * (2.5 - 1.5 * x)
} else if x < 2.0 {
2.0 - x * (4.0 + x * (0.5 * x - 2.5))
} else {
0.0
}
}
fn mitchell(x: f32) -> f32 {
let x = x.abs();
if x < 1.0 {
(16.0 + x * x * (21.0 * x - 36.0)) / 18.0
} else if x < 2.0 {
(32.0 + x * (-60.0 + x * (36.0 - 7.0 * x))) / 18.0
} else {
0.0
}
}
/// Filter taps for one axis: for each output pixel the first input pixel and
/// its normalised weights.
fn contributors(in_size: usize, out_size: usize) -> Vec<(i64, Vec<f32>)> {
let scale = out_size as f32 / in_size as f32;
let upsample = scale > 1.0;
let mut out = Vec::with_capacity(out_size);
for n in 0..out_size {
let out_center = n as f32 + 0.5;
let in_center = out_center / scale;
// Radius of the filter support in input pixels.
let radius = if upsample { 2.0 } else { 2.0 / scale };
let first = (in_center - radius + 0.5).floor() as i64;
let last = (in_center + radius - 0.5).floor() as i64;
let mut weights = Vec::with_capacity((last - first + 1).max(0) as usize);
for i in first..=last {
let d = (i as f32 + 0.5) - in_center;
let w = if upsample {
catmull_rom(d)
} else {
mitchell(d * scale)
};
weights.push(w);
}
let sum: f32 = weights.iter().sum();
if sum != 0.0 {
for w in &mut weights {
*w /= sum;
}
}
out.push((first, weights));
}
out
}
/// Equivalent of `stbir_resize_uint8`. Returns `None` on invalid sizes.
pub fn resize_uint8(
input: &[u8],
in_w: usize,
in_h: usize,
in_stride: usize,
out_w: usize,
out_h: usize,
channels: usize,
) -> Option<Vec<u8>> {
if in_w == 0 || in_h == 0 || out_w == 0 || out_h == 0 || channels == 0 {
return None;
}
if input.len() < in_stride * (in_h - 1) + in_w * channels {
return None;
}
// Horizontal pass: in_h rows x out_w columns, f32.
let hx = contributors(in_w, out_w);
let mut tmp = vec![0f32; in_h * out_w * channels];
for y in 0..in_h {
let row = &input[y * in_stride..y * in_stride + in_w * channels];
for (x, (first, weights)) in hx.iter().enumerate() {
for c in 0..channels {
let mut acc = 0f32;
for (k, w) in weights.iter().enumerate() {
let sx = (first + k as i64).clamp(0, in_w as i64 - 1) as usize;
acc += f32::from(row[sx * channels + c]) * w;
}
tmp[(y * out_w + x) * channels + c] = acc;
}
}
}
// Vertical pass.
let vy = contributors(in_h, out_h);
let mut out = vec![0u8; out_h * out_w * channels];
for (y, (first, weights)) in vy.iter().enumerate() {
for x in 0..out_w {
for c in 0..channels {
let mut acc = 0f32;
for (k, w) in weights.iter().enumerate() {
let sy = (first + k as i64).clamp(0, in_h as i64 - 1) as usize;
acc += tmp[(sy * out_w + x) * channels + c] * w;
}
out[(y * out_w + x) * channels + c] = (acc + 0.5).floor().clamp(0.0, 255.0) as u8;
}
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_image_stays_constant() {
let src = vec![200u8; 5 * 7];
for (w, h) in [(3, 4), (10, 14), (5, 7), (1, 1)] {
let out = resize_uint8(&src, 5, 7, 5, w, h, 1).unwrap();
assert_eq!(out.len(), w * h);
assert!(out.iter().all(|&p| (199..=201).contains(&p)));
}
}
#[test]
fn rejects_empty() {
assert!(resize_uint8(&[], 0, 0, 0, 1, 1, 1).is_none());
assert!(resize_uint8(&[0], 1, 1, 1, 0, 1, 1).is_none());
}
#[test]
fn four_channels_are_independent() {
let mut src = Vec::new();
for _ in 0..4 {
src.extend_from_slice(&[10, 20, 30, 40]);
}
let out = resize_uint8(&src, 2, 2, 8, 4, 4, 4).unwrap();
for px in out.chunks(4) {
assert_eq!(px, &[10, 20, 30, 40]);
}
}
}