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 b9ac54e794
commit fc7bb5c025
64 files changed
+35316 -23

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
//! Font backend selection. Port of `font/backend.zig`.
//!
//! The Rust rewrite only targets Linux, so [`OPTIONS`] always selects
//! [`Backend::FontconfigFreetype`] (fontconfig discovery, FreeType rendering,
//! HarfBuzz shaping). The other variants are kept for fidelity with the Zig
//! enum and its helper predicates.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Backend {
/// FreeType for font rendering with no font discovery enabled.
Freetype,
/// Fontconfig for font discovery and FreeType for font rendering.
FontconfigFreetype,
/// CoreText for font discovery, rendering, and shaping (macOS).
Coretext,
/// CoreText for font discovery, FreeType for rendering, and
/// HarfBuzz for shaping (macOS).
CoretextFreetype,
/// CoreText for font discovery and rendering, HarfBuzz for shaping
CoretextHarfbuzz,
/// CoreText for font discovery and rendering, no shaping.
CoretextNoshape,
/// Use the browser font system and the Canvas API (wasm).
WebCanvas,
}
impl Backend {
/// Returns the default backend for a build environment.
pub fn default_for_target() -> Backend {
if cfg!(target_os = "macos") {
Backend::Coretext
} else {
Backend::FontconfigFreetype
}
}
pub fn has_freetype(self) -> bool {
matches!(
self,
Backend::Freetype | Backend::FontconfigFreetype | Backend::CoretextFreetype
)
}
pub fn has_coretext(self) -> bool {
matches!(
self,
Backend::Coretext
| Backend::CoretextFreetype
| Backend::CoretextHarfbuzz
| Backend::CoretextNoshape
)
}
pub fn has_fontconfig(self) -> bool {
matches!(self, Backend::FontconfigFreetype)
}
pub fn has_harfbuzz(self) -> bool {
matches!(
self,
Backend::Freetype
| Backend::FontconfigFreetype
| Backend::CoretextFreetype
| Backend::CoretextHarfbuzz
)
}
}
/// Build options (the `font.options` of Zig).
#[derive(Debug, Clone, Copy)]
pub struct Options {
pub backend: Backend,
}
pub const OPTIONS: Options = Options {
backend: Backend::FontconfigFreetype,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linux_backend_capabilities() {
let b = OPTIONS.backend;
assert!(b.has_freetype());
assert!(b.has_fontconfig());
assert!(b.has_harfbuzz());
assert!(!b.has_coretext());
}
}
+139
View File
@@ -0,0 +1,139 @@
//! CodepointMap is a map of codepoints to a discovery descriptor of a font
//! to use for that codepoint. If the descriptor doesn't return any matching
//! font, the codepoint is rendered using the default font.
//! Port of `font/CodepointMap.zig`.
use std::hash::{DefaultHasher, Hash, Hasher};
use crate::discovery::Descriptor;
#[derive(Debug, Clone)]
pub struct Entry {
/// Unicode codepoint range (inclusive). Asserts range.0 <= range.1.
pub range: (u32, u32),
/// The discovery descriptor of the font to use for this range.
pub descriptor: Descriptor,
}
/// The list of entries.
///
/// Note: we just do a linear search because we expect to always have very
/// few entries, so the overhead of a binary search is not worth it. This is
/// possible to defeat with some pathological inputs, but there is no realistic
/// scenario where this will be a problem except people trying to fuck around.
#[derive(Debug, Clone, Default)]
pub struct CodepointMap {
pub list: Vec<Entry>,
}
impl CodepointMap {
/// Add an entry to the map.
///
/// For conflicting codepoints, entries added later take priority over
/// entries added earlier.
pub fn add(&mut self, entry: Entry) {
assert!(entry.range.0 <= entry.range.1);
self.list.push(entry);
}
/// Get a descriptor for a codepoint.
pub fn get(&self, cp: u32) -> Option<&Descriptor> {
self.list
.iter()
.rev()
.find(|e| e.range.0 <= cp && cp <= e.range.1)
.map(|e| &e.descriptor)
}
/// Hash with the given hasher.
pub fn hash_into<H: Hasher>(&self, hasher: &mut H) {
self.list.len().hash(hasher);
for entry in &self.list {
entry.range.hash(hasher);
hash_descriptor(&entry.descriptor, hasher);
}
}
/// Returns a hash code that can be used to uniquely identify this
/// action.
pub fn hashcode(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.hash_into(&mut hasher);
hasher.finish()
}
}
/// Mirrors `Descriptor.hash` of the Zig discovery module (same fields, same
/// float handling) without depending on how the discovery module exposes it.
fn hash_descriptor<H: Hasher>(d: &Descriptor, hasher: &mut H) {
d.family.hash(hasher);
d.style.hash(hasher);
d.codepoint.hash(hasher);
d.size.to_bits().hash(hasher);
d.bold.hash(hasher);
d.italic.hash(hasher);
d.monospace.hash(hasher);
d.variations.len().hash(hasher);
for v in &d.variations {
v.id.hash(hasher);
// This is not correct, but we don't currently depend on the
// hash value being different based on decimal values of variations.
(v.value as i64).hash(hasher);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn desc(family: &str) -> Descriptor {
Descriptor {
family: Some(family.to_string()),
..Default::default()
}
}
fn entry(a: u32, b: u32, family: &str) -> Entry {
Entry {
range: (a, b),
descriptor: desc(family),
}
}
#[test]
fn codepointmap() {
let mut m = CodepointMap::default();
// Exact range
assert!(m.get(1).is_none());
m.add(entry(1, 1, "A"));
assert_eq!("A", m.get(1).unwrap().family.as_deref().unwrap());
// Later entry takes priority
m.add(entry(1, 2, "B"));
assert_eq!("B", m.get(1).unwrap().family.as_deref().unwrap());
// Non-matching
assert!(m.get(0).is_none());
assert!(m.get(3).is_none());
m.add(entry(3, 4, "C"));
m.add(entry(5, 6, "D"));
assert_eq!("C", m.get(3).unwrap().family.as_deref().unwrap());
assert_eq!("B", m.get(1).unwrap().family.as_deref().unwrap());
}
#[test]
fn hashcode_depends_on_contents() {
let mut a = CodepointMap::default();
let mut b = CodepointMap::default();
assert_eq!(a.hashcode(), b.hashcode());
a.add(entry(1, 2, "A"));
assert_ne!(a.hashcode(), b.hashcode());
b.add(entry(1, 2, "A"));
assert_eq!(a.hashcode(), b.hashcode());
b.add(entry(3, 3, "B"));
assert_ne!(a.hashcode(), b.hashcode());
}
}
@@ -0,0 +1,603 @@
//! CodepointResolver maps a codepoint to a font. It is more dynamic
//! than "Collection" since it supports mapping codepoint ranges to
//! specific fonts, searching for fallback fonts, and more.
//!
//! To initialize the codepoint resolver, construct it with
//! [`CodepointResolver::new`], set the fields you want set, and begin using
//! the resolver.
#![allow(clippy::collapsible_if, clippy::if_not_else, clippy::float_cmp)]
use std::collections::HashMap;
use crate::collection::{
AddOptions, Collection, Entry, GetFaceError, Index, PresentationMode, Special,
};
use crate::discovery::Discover;
use crate::face::{RenderOptions, freetype::RenderError as FaceRenderError};
use crate::sprite;
use crate::{Atlas, CodepointMap, Glyph, Presentation, SpriteFace, Style};
/// Packed array of booleans to indicate if a style is enabled or not.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StyleStatus([bool; 4]);
impl StyleStatus {
/// All styles set to `v`.
pub fn init_fill(v: bool) -> StyleStatus {
StyleStatus([v; 4])
}
pub fn get(&self, style: Style) -> bool {
self.0[style as usize]
}
pub fn set(&mut self, style: Style, v: bool) {
self.0[style as usize] = v;
}
}
/// Errors from [`CodepointResolver::render_glyph`].
#[derive(Debug, thiserror::Error)]
pub enum RenderGlyphError {
#[error(transparent)]
GetFace(#[from] GetFaceError),
#[error(transparent)]
Face(#[from] FaceRenderError),
#[error(transparent)]
Sprite(#[from] sprite::face::RenderError),
/// The sprite face was not configured on the resolver.
#[error("sprite face not configured")]
SpriteUnavailable,
}
impl RenderGlyphError {
/// True if this error is the atlas-full condition (the caller should
/// grow the atlas and retry).
pub fn is_atlas_full(&self) -> bool {
use crate::atlas::Error::AtlasFull;
matches!(
self,
RenderGlyphError::Face(FaceRenderError::Atlas(AtlasFull))
| RenderGlyphError::Sprite(sprite::face::RenderError::Atlas(AtlasFull))
)
}
}
pub struct CodepointResolver {
/// The underlying collection of fonts. This will be modified as
/// new fonts are found via the resolver. The resolver takes ownership
/// of the collection.
pub collection: Collection,
/// The set of statuses and whether they're enabled or not. This defaults
/// to true. This can be changed at runtime with no ill effect.
pub styles: StyleStatus,
/// If discovery is available, we'll look up fonts where we can't find
/// the codepoint. This can be set after initialization.
pub discover: Option<Discover>,
/// A map of codepoints to font requests for codepoint-level overrides.
pub codepoint_map: Option<CodepointMap>,
/// The descriptor cache is used to cache the descriptor to font face
/// mapping for codepoint maps.
descriptor_cache: DescriptorCache,
/// Set this to a non-null value to enable sprite glyph drawing. If this
/// isn't enabled we'll just fall through to trying to use regular fonts
/// to render sprite glyphs. But more than likely, if this isn't set then
/// terminal rendering will look wrong.
pub sprite: Option<SpriteFace>,
}
/// Map of descriptors to faces. This is used with manual codepoint maps
/// to ensure that we don't load the same font multiple times.
///
/// Note that the current implementation will load the same font multiple
/// times if the font used for a codepoint map is identical to a font used
/// for a regular style. That's just an inefficient choice made now because
/// the implementation is simpler and codepoint maps matching a regular
/// font is a rare case.
///
/// As in upstream, descriptors are compared by their `hashcode()` only.
#[derive(Default)]
struct DescriptorCache(HashMap<u64, Option<Index>>);
impl CodepointResolver {
/// A resolver with defaults: all styles enabled, no discovery, no
/// codepoint map, no sprite face.
pub fn new(collection: Collection) -> CodepointResolver {
CodepointResolver {
collection,
styles: StyleStatus::init_fill(true),
discover: None,
codepoint_map: None,
descriptor_cache: DescriptorCache::default(),
sprite: None,
}
}
/// Looks up the font that should be used for a specific codepoint.
/// The font index is valid as long as font faces aren't removed. This
/// isn't cached; it is expected that downstream users handle caching if
/// that is important.
///
/// Optionally, a presentation format can be specified. This presentation
/// format will be preferred but if it can't be found in this format,
/// any format will be accepted. If presentation is null, the UCD
/// (Unicode Character Database) will be used to determine the default
/// presentation for the codepoint.
///
/// This function cannot error! If an error occurs for any reason,
/// the associated functionality is ignored and the
/// resolver attempts to use a different method to satisfy the codepoint.
/// This behavior is intentional to make the resolver apply best-effort
/// logic to satisfy the codepoint since its better to render something
/// than nothing.
///
/// This logic is relatively complex so the exact algorithm is documented
/// here. If this gets out of sync with the code, ask questions.
///
/// 1. If a font style is requested that is disabled (i.e. bold),
/// we start over with the regular font style. The regular font style
/// cannot be disabled, but it can be replaced with a stylized font
/// face.
///
/// 2. If there is a codepoint override for the codepoint, we satisfy
/// that requirement if we can, no matter what style or presentation.
///
/// 3. If this is a sprite codepoint (such as an underline), then the
/// sprite font always is the result.
///
/// 4. If the exact style and presentation request can be satisfied by
/// one of our loaded fonts, we return that value. We search loaded
/// fonts in the order they're added to the group, so the caller must
/// set the priority order.
///
/// 5. If the style isn't regular, we restart this process at this point
/// but with the regular style. This lets us fall back to regular with
/// our loaded fonts before trying a fallback. We'd rather show a regular
/// version of a codepoint from a loaded font than find a new font in
/// the correct style because styles in other fonts often change
/// metrics like glyph widths.
///
/// 6. If the style is regular, and font discovery is enabled, we look
/// for a fallback font to satisfy our request.
///
/// 7. Finally, as a last resort, we fall back to restarting this whole
/// process with a regular font face satisfying ANY presentation for
/// the codepoint. If this fails, we return null.
pub fn get_index(&mut self, cp: u32, style: Style, p: Option<Presentation>) -> Option<Index> {
// If we've disabled a font style, then fall back to regular.
if style != Style::Regular && !self.styles.get(style) {
return self.get_index(cp, Style::Regular, p);
}
// Codepoint overrides.
if let Some(idx) = self.get_index_codepoint_override(cp) {
return Some(idx);
}
// If we have sprite drawing enabled, check if our sprite face can
// handle this.
if let Some(sprite) = &self.sprite {
if sprite.has_codepoint(cp, p) {
return Some(Index::init_special(Special::Sprite));
}
}
// Build our presentation mode. If we don't have an explicit presentation
// given then we use the UCD (Unicode Character Database) to determine
// the default presentation. Note there is some inefficiency here because
// we'll do this multiple times if we recurse, but this is a cached function
// call higher up (GroupCache) so this should be rare.
let p_mode = match p {
Some(v) => PresentationMode::Explicit(v),
None => PresentationMode::Default(if ghostty_unicode::emoji_table::table().get(cp) {
Presentation::Emoji
} else {
Presentation::Text
}),
};
// If we can find the exact value, then return that.
if let Some(value) = self.collection.get_index(cp, style, p_mode) {
return Some(value);
}
// If we're not a regular font style, try looking for a regular font
// that will satisfy this request. Blindly looking for unmatched styled
// fonts to satisfy one codepoint results in some ugly rendering.
if style != Style::Regular {
if let Some(value) = self.get_index(cp, Style::Regular, p) {
return Some(value);
}
}
// If we are regular, try looking for a fallback using discovery.
if style == Style::Regular {
if let Some(idx) = self.discover_fallback(cp, style, p_mode) {
return Some(idx);
}
}
// If this is regular with any matching presentation, then we are done
// there is nothing more we can do. Otherwise we fall through and do
// an any presentation search.
if style == Style::Regular && p_mode == PresentationMode::Any {
return None;
}
// For non-regular fonts, we fall back to regular with any presentation
self.collection
.get_index(cp, Style::Regular, PresentationMode::Any)
}
fn discover_fallback(
&mut self,
cp: u32,
style: Style,
p_mode: PresentationMode,
) -> Option<Index> {
log::debug!("searching for a fallback font for cp={cp:X}");
let disco = self.discover.as_ref()?;
let load_opts = self.collection.load_options.as_ref()?;
let desc = crate::discovery::Descriptor {
codepoint: cp,
size: load_opts.size.points,
bold: style == Style::Bold || style == Style::BoldItalic,
italic: style == Style::Italic || style == Style::BoldItalic,
monospace: false,
..Default::default()
};
let mut disco_it = disco.discover_fallback(&self.collection, &desc).ok()?;
loop {
let deferred_face = match disco_it.next() {
Ok(Some(f)) => f,
Ok(None) => break,
Err(err) => {
log::warn!("fallback search failed with error err={err}");
break;
}
};
// Discovery is supposed to only return faces that have our
// codepoint but we can't search presentation in discovery so
// we have to check it here.
let entry = Entry {
face: crate::collection::AnyFace::Deferred(deferred_face),
fallback: true,
scale_factor: crate::collection::ScaleFactor::Scale(1.0),
};
if !entry.has_codepoint(cp, p_mode) {
continue;
}
let crate::collection::AnyFace::Deferred(deferred_face) = entry.face else {
unreachable!()
};
log::info!(
"found codepoint 0x{cp:X} in fallback face={}",
deferred_face
.name()
.unwrap_or_else(|_| "<error>".to_string())
);
return self
.collection
.add_deferred(
deferred_face,
AddOptions {
style,
fallback: true,
size_adjustment: crate::DEFAULT_FALLBACK_ADJUSTMENT,
},
)
.ok();
}
log::debug!("no fallback face found for cp={cp:X}");
None
}
/// Checks if the codepoint is in the map of codepoint overrides,
/// finds the override font, and returns it.
fn get_index_codepoint_override(&mut self, cp: u32) -> Option<Index> {
// Get our codepoint map. If we have no map set then we have no
// codepoint overrides and we're done.
let map = self.codepoint_map.as_ref()?;
// If the codepoint is in the map, the map returns a descriptor that
// can be used with font discovery to search for a matching font.
let desc = map.get(cp)?;
let key = desc.hashcode();
// Fast path: the descriptor is already loaded. This means that we
// already did the search before and we have an exact font for this
// codepoint.
let idx_: Option<Index> = if let Some(v) = self.descriptor_cache.0.get(&key) {
*v
} else {
// Slow path: we have to find this descriptor and load the font
let discover = self.discover.as_ref()?;
let mut disco_it = match discover.discover(desc) {
Ok(it) => it,
Err(err) => {
log::warn!("codepoint override failed codepoint={cp} err={err}");
return None;
}
};
let face = match disco_it.next() {
Ok(Some(f)) => f,
Ok(None) => {
log::warn!(
"font lookup for codepoint map failed codepoint={cp} err=FontNotFound"
);
// Add null to the cache so we don't do a lookup again later.
self.descriptor_cache.0.insert(key, None);
return None;
}
Err(err) => {
log::warn!("codepoint override failed codepoint={cp} err={err}");
return None;
}
};
// Add the font to our list of fonts so we can get an index for it,
// and ensure the index is stored in the descriptor cache for next time.
let idx = match self.collection.add_deferred(
face,
AddOptions {
style: Style::Regular,
fallback: false,
size_adjustment: crate::DEFAULT_FALLBACK_ADJUSTMENT,
},
) {
Ok(idx) => idx,
Err(err) => {
log::warn!("codepoint override failed codepoint={cp} err={err}");
return None;
}
};
self.descriptor_cache.0.insert(key, Some(idx));
Some(idx)
};
// The descriptor cache will populate null if the descriptor is not found
// to avoid expensive discoveries later, so if it is null then we already
// searched and found nothing.
let idx = idx_?;
// We need to verify that this index has the codepoint we want.
if self
.collection
.has_codepoint(idx, cp, PresentationMode::Any)
{
log::debug!(
"codepoint override based on config codepoint={cp} family={}",
desc.family.as_deref().unwrap_or("")
);
return Some(idx);
}
None
}
/// Returns the presentation for a specific font index. This is useful for
/// determining what atlas is needed.
pub fn get_presentation(
&mut self,
index: Index,
glyph_index: u32,
) -> Result<Presentation, GetFaceError> {
if let Some(sp) = index.special() {
return Ok(match sp {
Special::Sprite => Presentation::Text,
});
}
let face = self.collection.get_face(index)?;
Ok(if face.is_color_glyph(glyph_index) {
Presentation::Emoji
} else {
Presentation::Text
})
}
/// Render a glyph by glyph index into the given font atlas and return
/// metadata about it.
///
/// This performs no caching, it is up to the caller to cache calls to this
/// if they want. This will also not resize the atlas if it is full.
///
/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller
/// is expected to translate codepoints to glyph indexes in some way. The most
/// trivial way to do this is to get the Face and call glyph_index. If you're
/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically
/// determine glyph indexes for a text run.
pub fn render_glyph(
&mut self,
atlas: &mut Atlas,
index: Index,
glyph_index: u32,
opts: RenderOptions,
) -> Result<Glyph, RenderGlyphError> {
// Special-case fonts are rendered directly.
if let Some(sp) = index.special() {
match sp {
Special::Sprite => {
let sprite = self
.sprite
.as_ref()
.ok_or(RenderGlyphError::SpriteUnavailable)?;
return Ok(sprite.render_glyph(atlas, glyph_index, opts)?);
}
}
}
let face = self.collection.get_face(index)?;
Ok(face.render_glyph(atlas, glyph_index, opts)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collection::{LoadOptions, SizeAdjustment};
use crate::face::{DesiredSize, Options};
use crate::{Face, Library, embedded};
fn add(c: &mut Collection, lib: &Library, data: &'static [u8], style: Style, pts: f32) {
let size = if pts == 12.0 {
DesiredSize {
points: 12.0,
xdpi: 96,
ydpi: 96,
}
} else {
DesiredSize::new(pts)
};
c.add(
Face::init(lib, data.into(), Options::new(size)).unwrap(),
AddOptions {
style,
fallback: false,
size_adjustment: SizeAdjustment::None,
},
)
.unwrap();
}
#[test]
fn get_index() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone()));
add(&mut c, &lib, embedded::REGULAR, Style::Regular, 12.0);
add(&mut c, &lib, embedded::EMOJI, Style::Regular, 12.0);
add(&mut c, &lib, embedded::EMOJI_TEXT, Style::Regular, 12.0);
let mut r = CodepointResolver::new(c);
// Should find all visible ASCII
for i in 32u32..127 {
let idx = r.get_index(i, Style::Regular, None).unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(0, idx.idx);
}
// Try emoji
{
let idx = r.get_index('🥸' as u32, Style::Regular, None).unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(1, idx.idx);
}
// Try text emoji
{
let idx = r
.get_index(0x270C, Style::Regular, Some(Presentation::Text))
.unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(2, idx.idx);
}
{
let idx = r
.get_index(0x270C, Style::Regular, Some(Presentation::Emoji))
.unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(1, idx.idx);
}
// Box glyph should be null since we didn't set a box font
assert!(r.get_index(0x1FB00, Style::Regular, None).is_none());
}
#[test]
fn get_index_disabled_font_style() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone()));
add(&mut c, &lib, embedded::REGULAR, Style::Regular, 12.0);
add(&mut c, &lib, embedded::REGULAR, Style::Bold, 12.0);
add(&mut c, &lib, embedded::REGULAR, Style::Italic, 12.0);
let mut r = CodepointResolver::new(c);
r.styles.set(Style::Bold, false); // Disable bold
// Regular should work fine
{
let idx = r.get_index('A' as u32, Style::Regular, None).unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(0, idx.idx);
}
// Bold should go to regular
{
let idx = r.get_index('A' as u32, Style::Bold, None).unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(0, idx.idx);
}
// Italic should still work
{
let idx = r.get_index('A' as u32, Style::Italic, None).unwrap();
assert_eq!(Style::Italic, idx.style);
assert_eq!(0, idx.idx);
}
}
#[test]
fn get_index_box_glyph() {
let c = Collection::new();
let mut r = CodepointResolver::new(c);
r.sprite = Some(SpriteFace::new(crate::Metrics::calc(
crate::metrics::FaceMetrics {
px_per_em: 30.0,
cell_width: 18.0,
ascent: 30.0,
descent: -6.0,
line_gap: 0.0,
..Default::default()
},
)));
// Should find a box glyph
let idx = r.get_index(0x2500, Style::Regular, None).unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(Index::SPECIAL_START, idx.idx);
}
#[test]
fn get_presentation_and_render_sprite() {
let mut r = CodepointResolver::new(Collection::new());
let metrics = crate::Metrics::calc(crate::metrics::FaceMetrics {
px_per_em: 30.0,
cell_width: 18.0,
ascent: 30.0,
descent: -6.0,
line_gap: 0.0,
..Default::default()
});
r.sprite = Some(SpriteFace::new(metrics));
let sprite_idx = Index::init_special(Special::Sprite);
assert_eq!(
Presentation::Text,
r.get_presentation(sprite_idx, 0x2500).unwrap()
);
let mut atlas = Atlas::new(256, crate::atlas::Format::Grayscale);
let g = r
.render_glyph(&mut atlas, sprite_idx, 0x2500, RenderOptions::new(metrics))
.unwrap();
assert!(g.width > 0 && g.height > 0);
}
}
+1481
View File
@@ -0,0 +1,1481 @@
//! A font collection is a list of faces of different styles. The list is
//! ordered by priority (per style). All fonts in a collection share the same
//! size so they can be used interchangeably in cases a glyph is missing in one
//! and present in another.
//!
//! The purpose of a collection is to store a list of fonts by style
//! and priority order. A collection does not handle searching for font
//! callbacks, rasterization, etc. For this, see CodepointResolver.
//!
//! The collection can contain both loaded and deferred faces. Deferred faces
//! typically use less memory while still providing some necessary information
//! such as codepoint support, presentation, etc. This is useful for looking
//! for fallback fonts as efficiently as possible. For example, when the glyph
//! "X" is not found, we can quickly search through deferred fonts rather
//! than loading the font completely.
#![allow(clippy::collapsible_if, clippy::if_not_else, clippy::float_cmp)]
use crate::config_types::{FontSyntheticStyle, FreetypeLoadFlags};
use crate::face::{self, DesiredSize};
use crate::metrics::{FaceMetrics, ModifierSet};
use crate::{DeferredFace, Face, Library, Metrics, Presentation, Style};
/// The available faces we have. This shouldn't be modified manually.
/// Instead, use the functions available on Collection.
///
/// (Zig: `StyleArray`, an enum array of segmented lists. Aliases are
/// stored as the [`Index`] of the non-alias entry they point to, so there
/// is no need for pointer stability.)
type StyleArray = [Vec<EntryOrAlias>; 4];
pub struct Collection {
/// The available faces we have. This shouldn't be modified manually.
/// Instead, use the functions available on Collection.
faces: StyleArray,
/// The metric modifiers to use for this collection.
///
/// Call `Collection::update_metrics` to recompute the
/// collection's metrics after making changes to these.
pub metric_modifiers: ModifierSet,
/// Metrics for this collection. Call `Collection::update_metrics` to
/// (re)compute these after adding a primary font or making changes to
/// `metric_modifiers`.
pub metrics: Option<Metrics>,
/// The face metrics for the primary face in the collection.
///
/// We keep this around so we don't need to re-compute it when calculating
/// the scale factor for additional fonts added to the collection.
pub primary_face_metrics: Option<FaceMetrics>,
/// The load options for deferred faces in the face list. If this
/// is not set, then deferred faces will not be loaded. Attempting to
/// add a deferred face will result in an error.
pub load_options: Option<LoadOptions>,
}
impl Default for Collection {
fn default() -> Self {
Self::new()
}
}
/// Options for adding a face to the collection.
#[derive(Debug, Clone, Copy)]
pub struct AddOptions {
/// What style this face is.
pub style: Style,
/// What size adjustment to use.
pub size_adjustment: SizeAdjustment,
/// Whether this is a fallback face.
pub fallback: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum AddError {
/// There's no more room in the collection.
#[error("collection is full")]
CollectionFull,
/// The call to `face.set_size` failed.
#[error("set size failed")]
SetSizeFailed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum AddDeferredError {
/// There's no more room in the collection.
#[error("collection is full")]
CollectionFull,
/// `load_options` is None, can't do deferred loading.
#[error("deferred loading unavailable")]
DeferredLoadingUnavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum EntryError {
/// Index represents a special font (built-in) and these don't
/// have an associated face. This should be caught upstream and use
/// alternate logic.
#[error("special font has no face")]
SpecialHasNoFace,
/// Invalid index.
#[error("index out of bounds")]
IndexOutOfBounds,
}
/// Errors from [`Collection::get_face`].
#[derive(Debug, thiserror::Error)]
pub enum GetFaceError {
#[error(transparent)]
Entry(#[from] EntryError),
#[error("deferred loading unavailable")]
DeferredLoadingUnavailable,
#[error("face load failed: {0}")]
Load(#[from] crate::deferred_face::LoadError),
#[error("face error: {0}")]
Face(#[from] face::freetype::FaceError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum CompleteError {
#[error("default face unavailable")]
DefaultUnavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum UpdateMetricsError {
#[error("cannot load primary font")]
CannotLoadPrimaryFont,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum SetSizeError {
#[error(transparent)]
UpdateMetrics(#[from] UpdateMetricsError),
/// `load_options` is `None`.
#[error("deferred loading unavailable")]
DeferredLoadingUnavailable,
/// The call to `face.set_size` failed.
#[error("set size failed")]
SetSizeFailed,
}
#[derive(Debug, thiserror::Error)]
enum SyntheticError {
#[error("deferred loading unavailable")]
DeferredLoadingUnavailable,
#[error(transparent)]
GetFace(#[from] GetFaceError),
#[error(transparent)]
Face(#[from] face::freetype::FaceError),
}
impl Collection {
/// Initialize an empty collection.
pub fn new() -> Collection {
Collection {
faces: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
metric_modifiers: ModifierSet::new(),
metrics: None,
primary_face_metrics: None,
load_options: None,
}
}
/// Number of entries (including aliases) for a style.
pub fn count(&self, style: Style) -> usize {
self.faces[style as usize].len()
}
/// Add a face to the collection. This face will be added next in priority
/// if others exist already, i.e. it'll be the _last_ to be searched for a
/// glyph in that list.
///
/// If no error is encountered then the collection takes ownership of the
/// face. (On error the face is dropped.)
///
/// When added, the size of the face will be adjusted to match
/// `load_options`.
///
/// Returns the index for the added face.
pub fn add(&mut self, face: Face, opts: AddOptions) -> Result<Index, AddError> {
// We have some special indexes so we must never pass those.
let idx = self.faces[opts.style as usize].len();
if idx >= usize::from(Index::SPECIAL_START) - 1 {
return Err(AddError::CollectionFull);
}
let mut owned_face = face;
// Scale factor to adjust the size of the added face.
let scale_factor = self.scale_factor(&owned_face.get_metrics(), opts.size_adjustment);
// If we have load options, we update the size to ensure
// it's matches and is normalized to the primary if possible.
if let Some(load_opts) = &self.load_options {
let mut new_opts = load_opts.clone();
new_opts.size.points *= scale_factor as f32;
owned_face
.set_size(new_opts.face_options())
.map_err(|_| AddError::SetSizeFailed)?;
}
self.faces[opts.style as usize].push(EntryOrAlias::Entry(Entry {
face: AnyFace::Loaded(owned_face),
fallback: opts.fallback,
scale_factor: ScaleFactor::Scale(scale_factor),
}));
Ok(Index {
style: opts.style,
idx: idx as u16,
})
}
/// Add a deferred face to the collection.
///
/// Returns the index for the added face.
pub fn add_deferred(
&mut self,
face: DeferredFace,
opts: AddOptions,
) -> Result<Index, AddDeferredError> {
if self.load_options.is_none() {
return Err(AddDeferredError::DeferredLoadingUnavailable);
}
// We have some special indexes so we must never pass those.
let idx = self.faces[opts.style as usize].len();
if idx >= usize::from(Index::SPECIAL_START) - 1 {
return Err(AddDeferredError::CollectionFull);
}
self.faces[opts.style as usize].push(EntryOrAlias::Entry(Entry {
face: AnyFace::Deferred(face),
fallback: opts.fallback,
scale_factor: ScaleFactor::Adjustment(opts.size_adjustment),
}));
Ok(Index {
style: opts.style,
idx: idx as u16,
})
}
/// Return the Face represented by a given Index. The returned reference
/// is only valid as long as this collection is not modified.
///
/// This will initialize the face if it is deferred and not yet loaded,
/// which can fail.
pub fn get_face(&mut self, index: Index) -> Result<&mut Face, GetFaceError> {
let resolved = self.resolve(index)?;
self.get_face_from_entry(resolved)
}
/// Return the face for an index only if it is already loaded (never
/// triggers loading, so it only needs a shared reference). Returns `None`
/// for special, out-of-range or still-deferred entries.
pub fn get_loaded_face(&self, index: Index) -> Option<&Face> {
let resolved = self.resolve(index).ok()?;
match &self.entry_at(resolved).face {
AnyFace::Loaded(f) => Some(f),
AnyFace::Deferred(_) => None,
}
}
/// Resolve an index (which might point at an alias) to the index of the
/// non-alias entry.
fn resolve(&self, index: Index) -> Result<Index, EntryError> {
if index.special().is_some() {
return Err(EntryError::SpecialHasNoFace);
}
let list = &self.faces[index.style as usize];
match list.get(usize::from(index.idx)) {
None => Err(EntryError::IndexOutOfBounds),
Some(EntryOrAlias::Entry(_)) => Ok(index),
Some(EntryOrAlias::Alias(target)) => Ok(*target),
}
}
fn entry_at(&self, resolved: Index) -> &Entry {
match &self.faces[resolved.style as usize][usize::from(resolved.idx)] {
EntryOrAlias::Entry(e) => e,
EntryOrAlias::Alias(_) => unreachable!("alias must point to a non-alias entry"),
}
}
fn entry_at_mut(&mut self, resolved: Index) -> &mut Entry {
match &mut self.faces[resolved.style as usize][usize::from(resolved.idx)] {
EntryOrAlias::Entry(e) => e,
EntryOrAlias::Alias(_) => unreachable!("alias must point to a non-alias entry"),
}
}
/// Get the unaliased entry from an index
pub fn get_entry(&mut self, index: Index) -> Result<&mut Entry, EntryError> {
let resolved = self.resolve(index)?;
Ok(self.entry_at_mut(resolved))
}
/// Get the unaliased entry from an index (shared).
pub fn get_const_entry(&self, index: Index) -> Result<&Entry, EntryError> {
let resolved = self.resolve(index)?;
Ok(self.entry_at(resolved))
}
/// Get the face from an entry (loading it if deferred).
///
/// This entry must not be an alias.
fn get_face_from_entry(&mut self, entry: Index) -> Result<&mut Face, GetFaceError> {
if matches!(self.entry_at(entry).face, AnyFace::Deferred(_)) {
let mut opts = self
.load_options
.clone()
.ok_or(GetFaceError::DeferredLoadingUnavailable)?;
// Load the face.
let mut face = match &mut self.entry_at_mut(entry).face {
AnyFace::Deferred(d) => d.load(&opts.library, opts.face_options())?,
AnyFace::Loaded(_) => unreachable!(),
};
// Calculate the scale factor for this
// entry now that we have a loaded face.
let mut scale = self.entry_at(entry).scale_factor;
if let ScaleFactor::Adjustment(adj) = scale {
let factor = self.scale_factor(&face.get_metrics(), adj);
scale = ScaleFactor::Scale(factor);
self.entry_at_mut(entry).scale_factor = scale;
}
// If our scale factor is something other
// than 1.0 then we need to resize the face.
if let ScaleFactor::Scale(s) = scale {
if s != 1.0 {
opts.size.points *= s as f32;
face.set_size(opts.face_options())?;
}
}
// Set the loaded face on the entry (dropping the deferred face).
self.entry_at_mut(entry).face = AnyFace::Loaded(face);
}
match &mut self.entry_at_mut(entry).face {
AnyFace::Loaded(f) => Ok(f),
AnyFace::Deferred(_) => unreachable!(),
}
}
/// Return the index of the font in this collection that contains
/// the given codepoint, style, and presentation. If no font is found,
/// None is returned.
///
/// This does not trigger font loading; deferred fonts can be
/// searched for codepoints.
pub fn get_index(&self, cp: u32, style: Style, p_mode: PresentationMode) -> Option<Index> {
for (i, entry_or_alias) in self.faces[style as usize].iter().enumerate() {
if self.const_entry(entry_or_alias).has_codepoint(cp, p_mode) {
return Some(Index {
style,
idx: i as u16,
});
}
}
// Not found
None
}
fn const_entry<'a>(&'a self, e: &'a EntryOrAlias) -> &'a Entry {
match e {
EntryOrAlias::Entry(e) => e,
EntryOrAlias::Alias(i) => self.entry_at(*i),
}
}
/// Check if a specific font index has a specific codepoint. This does not
/// necessarily force the font to load. The presentation value "p" will
/// verify the Emoji representation matches if it is non-null. If "p" is
/// null then any presentation will be accepted.
pub fn has_codepoint(&self, index: Index, cp: u32, p_mode: PresentationMode) -> bool {
let list = &self.faces[index.style as usize];
match list.get(usize::from(index.idx)) {
None => false,
Some(e) => self.const_entry(e).has_codepoint(cp, p_mode),
}
}
/// Ensure we have an option for all styles in the collection, such
/// as italic and bold by synthesizing them if necessary from the
/// first regular face that has text glyphs.
///
/// If there is no regular face that has text glyphs, then this
/// does nothing.
pub fn complete_styles(
&mut self,
synthetic_config: FontSyntheticStyle,
) -> Result<(), CompleteError> {
// If every style has at least one entry then we're done!
// This is the most common case.
if self.faces.iter().all(|l| !l.is_empty()) {
return Ok(());
}
// Find the first regular face that has non-colorized text glyphs.
// This is the font we want to fallback to. This may not be index zero
// if a user configures something like an Emoji font first.
let regular_entry: Index = 'entry: {
let count = self.faces[Style::Regular as usize].len();
if count == 0 {
return Ok(());
}
// Find our first regular face that has text glyphs.
for i in 0..count {
// Load our face. If we fail to load it, we just skip it and
// continue on to try the next one.
let index = Index {
style: Style::Regular,
idx: i as u16,
};
let resolved = self.resolve(index).expect("index in range");
let face = match self.get_face_from_entry(resolved) {
Ok(f) => f,
Err(err) => {
log::warn!("error loading regular entry={i} err={err}");
continue;
}
};
// We have two conditionals here. The color check is obvious:
// we want to auto-italicize a normal text font. The second
// check is less obvious... for mixed color/non-color fonts, we
// accept the regular font if it has basic ASCII. This may not
// be strictly correct (especially with international fonts) but
// it's a reasonable heuristic and the first case will match 99%
// of the time.
if !face.has_color() || face.glyph_index('A' as u32).is_some() {
break 'entry resolved;
}
}
// No regular text face found. We can't provide any fallback.
return Err(CompleteError::DefaultUnavailable);
};
// If we don't have italic, attempt to create a synthetic italic face.
// If we can't create a synthetic italic face, we'll just use the regular
// face for italic.
let have_italic = !self.faces[Style::Italic as usize].is_empty();
if !have_italic {
if !synthetic_config.italic {
log::info!("italic style not available and synthetic italic disabled");
self.faces[Style::Italic as usize].push(EntryOrAlias::Alias(regular_entry));
} else {
match self.synthetic_italic(regular_entry) {
Ok(synthetic) => {
log::info!("synthetic italic face created");
self.faces[Style::Italic as usize].push(EntryOrAlias::Entry(Entry {
face: AnyFace::Loaded(synthetic),
fallback: false,
scale_factor: ScaleFactor::Scale(1.0),
}));
}
Err(err) => {
log::warn!(
"failed to create synthetic italic, italic style will not be available err={err}"
);
self.faces[Style::Italic as usize].push(EntryOrAlias::Alias(regular_entry));
}
}
}
}
// If we don't have bold, use the regular font.
let have_bold = !self.faces[Style::Bold as usize].is_empty();
if !have_bold {
if !synthetic_config.bold {
log::info!("bold style not available and synthetic bold disabled");
self.faces[Style::Bold as usize].push(EntryOrAlias::Alias(regular_entry));
} else {
match self.synthetic_bold(regular_entry) {
Ok(synthetic) => {
log::info!("synthetic bold face created");
self.faces[Style::Bold as usize].push(EntryOrAlias::Entry(Entry {
face: AnyFace::Loaded(synthetic),
fallback: false,
scale_factor: ScaleFactor::Scale(1.0),
}));
}
Err(err) => {
log::warn!(
"failed to create synthetic bold, bold style will not be available err={err}"
);
self.faces[Style::Bold as usize].push(EntryOrAlias::Alias(regular_entry));
}
}
}
}
// If we don't have bold italic, we attempt to synthesize a bold variant
// of the italic font. If we can't do that, we'll use the italic font.
if self.faces[Style::BoldItalic as usize].is_empty() {
self.complete_bold_italic(synthetic_config, regular_entry, have_bold);
}
Ok(())
}
fn complete_bold_italic(
&mut self,
synthetic_config: FontSyntheticStyle,
regular_entry: Index,
have_bold: bool,
) {
let bi = Style::BoldItalic as usize;
if !synthetic_config.bold_italic {
log::info!("bold italic style not available and synthetic bold italic disabled");
self.faces[bi].push(EntryOrAlias::Alias(regular_entry));
return;
}
// Prefer to synthesize on top of the face we already had. If we
// have bold then we try to synthesize italic on top of bold.
if have_bold {
let base_entry = self
.resolve(Index {
style: Style::Bold,
idx: 0,
})
.expect("bold entry exists");
if let Ok(synthetic) = self.synthetic_italic(base_entry) {
log::info!("synthetic bold italic face created from bold");
self.faces[bi].push(EntryOrAlias::Entry(Entry {
face: AnyFace::Loaded(synthetic),
fallback: false,
scale_factor: ScaleFactor::Scale(1.0),
}));
return;
}
// If synthesizing italic failed, then we try to synthesize
// bold on whatever italic font we have.
}
let base_entry = self
.resolve(Index {
style: Style::Italic,
idx: 0,
})
.expect("italic entry exists");
if let Ok(synthetic) = self.synthetic_bold(base_entry) {
log::info!("synthetic bold italic face created from italic");
self.faces[bi].push(EntryOrAlias::Entry(Entry {
face: AnyFace::Loaded(synthetic),
fallback: false,
scale_factor: ScaleFactor::Scale(1.0),
}));
return;
}
log::warn!("bold italic style not available, using italic font");
self.faces[bi].push(EntryOrAlias::Alias(base_entry));
}
/// Create a synthetic bold font face from the given entry and return it.
fn synthetic_bold(&mut self, entry: Index) -> Result<Face, SyntheticError> {
// We require loading options to create a synthetic bold face.
let opts = self
.load_options
.clone()
.ok_or(SyntheticError::DeferredLoadingUnavailable)?;
// Try to bold it.
let regular = self.get_face_from_entry(entry)?;
// NOTE: upstream computes a `face_opts` with the size inherited from
// the regular face but then (accidentally) passes `opts.faceOptions()`
// instead; we mirror that behaviour.
let face = regular.synthetic_bold(opts.face_options())?;
log::info!("font synthetic bold created family={}", face.name());
Ok(face)
}
/// Create a synthetic italic font face from the given entry and return it.
fn synthetic_italic(&mut self, entry: Index) -> Result<Face, SyntheticError> {
// We require loading options to create a synthetic italic face.
let opts = self
.load_options
.clone()
.ok_or(SyntheticError::DeferredLoadingUnavailable)?;
// Try to italicize it.
let regular = self.get_face_from_entry(entry)?;
// See the NOTE in `synthetic_bold`.
let face = regular.synthetic_italic(opts.face_options())?;
log::info!("font synthetic italic created family={}", face.name());
Ok(face)
}
/// Update the size of all faces in the collection. This will
/// also update the size in the load options for future deferred
/// face loading.
///
/// This requires load options to be set.
pub fn set_size(&mut self, size: DesiredSize) -> Result<(), SetSizeError> {
// Get our options so we can modify the size.
let opts = {
let opts = self
.load_options
.as_mut()
.ok_or(SetSizeError::DeferredLoadingUnavailable)?;
opts.size = size;
opts.clone()
};
let primary_entry = self
.resolve(Index {
style: Style::Regular,
idx: 0,
})
.ok();
// Resize all our faces that are loaded
for style in Style::ALL {
for i in 0..self.faces[style as usize].len() {
// Resize all faces. We skip entries that are aliases, since
// the underlying face will have a non-alias entry somewhere.
if matches!(self.faces[style as usize][i], EntryOrAlias::Alias(_)) {
continue;
}
let this = Index {
style,
idx: i as u16,
};
let entry = self.entry_at_mut(this);
let scale = entry.scale_factor;
if let AnyFace::Loaded(face) = &mut entry.face {
// If this isn't our primary face, we scale
// the size appropriately before setting it.
//
// If we don't have a primary face we also don't.
let mut new_opts = opts.clone();
if primary_entry.is_some() && Some(this) != primary_entry {
if let ScaleFactor::Scale(s) = scale {
new_opts.size.points *= s as f32;
}
}
face.set_size(new_opts.face_options())
.map_err(|_| SetSizeError::SetSizeFailed)?;
}
}
}
Ok(self.update_metrics()?)
}
/// Calculate a factor by which to scale the provided face to match
/// it with the primary face, depending on the specified adjustment.
///
/// If this encounters any problems loading the primary face or its
/// metrics then it just returns `1.0`.
///
/// This functions very much like the `font-size-adjust` CSS property.
/// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust
fn scale_factor(&mut self, face_metrics: &FaceMetrics, adjustment: SizeAdjustment) -> f64 {
// If there's no adjustment, the scale is 1.0
if adjustment == SizeAdjustment::None {
return 1.0;
}
// If we haven't calculated our primary face metrics yet, do so now.
if self.primary_face_metrics.is_none() {
// If we can't load the primary face, just use 1.0 as the scale factor.
let Ok(primary_face) = self.get_face(Index::default()) else {
return 1.0;
};
self.primary_face_metrics = Some(primary_face.get_metrics());
}
let primary_metrics = self.primary_face_metrics.expect("set above");
// We normalize the metrics values which are expressed in px to instead
// be in ems, so that it doesn't matter what size the faces actually are.
let primary_scale = 1.0 / primary_metrics.px_per_em;
let face_scale = 1.0 / face_metrics.px_per_em;
// We get the target metrics from the primary face and this face depending
// on the specified `adjustment`. If this face doesn't explicitly define a
// metric, or if the value it defines is invalid, we fall through to other
// options in the order below.
//
// In order to make sure the value is valid, we compare it with the result
// of the estimator function, which rules out both null and invalid values.
let mut adj = adjustment;
let (primary_metric, face_metric) = loop {
match adj {
SizeAdjustment::IcWidth => {
if face_metrics.ic_width != Some(face_metrics.ic_width()) {
adj = SizeAdjustment::ExHeight;
continue;
}
break (
primary_metrics.ic_width() * primary_scale,
face_metrics.ic_width() * face_scale,
);
}
SizeAdjustment::ExHeight => {
if face_metrics.ex_height != Some(face_metrics.ex_height()) {
adj = SizeAdjustment::CapHeight;
continue;
}
break (
primary_metrics.ex_height() * primary_scale,
face_metrics.ex_height() * face_scale,
);
}
SizeAdjustment::CapHeight => {
if face_metrics.cap_height != Some(face_metrics.cap_height()) {
adj = SizeAdjustment::LineHeight;
continue;
}
break (
primary_metrics.cap_height() * primary_scale,
face_metrics.cap_height() * face_scale,
);
}
SizeAdjustment::LineHeight => {
break (
primary_metrics.line_height() * primary_scale,
face_metrics.line_height() * face_scale,
);
}
SizeAdjustment::None => unreachable!(),
}
};
primary_metric / face_metric
}
/// Update the cell metrics for this collection, based on
/// the primary font and the modifiers in `metric_modifiers`.
///
/// This requires a primary font (index `0`) to be present.
pub fn update_metrics(&mut self) -> Result<(), UpdateMetricsError> {
let primary_face = self
.get_face(Index::default())
.map_err(|_| UpdateMetricsError::CannotLoadPrimaryFont)?;
let fm = primary_face.get_metrics();
self.primary_face_metrics = Some(fm);
let mut metrics = Metrics::calc(fm);
metrics.apply(&self.metric_modifiers);
self.metrics = Some(metrics);
Ok(())
}
}
/// Options for adjusting the size of a face relative to the primary face.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SizeAdjustment {
/// Don't adjust the size for this face, use the original point size.
None,
/// Match ideograph character width with the primary face.
IcWidth,
/// Match ex height with the primary face.
ExHeight,
/// Match cap height with the primary face.
CapHeight,
/// Match line height with the primary face.
LineHeight,
}
/// Load options are used to configure all the details a Collection
/// needs to load deferred faces.
#[derive(Clone)]
pub struct LoadOptions {
/// The library to use for loading faces. This is not owned by
/// the collection and can be used by multiple collections.
pub library: Library,
/// The desired font size for all loaded faces.
pub size: DesiredSize,
/// Freetype Load Flags to use when loading glyphs. This is a list of
/// bitfield constants that controls operations to perform during glyph
/// loading. Only a subset is exposed for configuration, for the whole set
/// of flags see `pkg.freetype.face.LoadFlags`.
pub freetype_load_flags: FreetypeLoadFlags,
}
impl LoadOptions {
/// Options with the default size (12 points) and load flags.
pub fn new(library: Library) -> LoadOptions {
LoadOptions {
library,
size: DesiredSize::new(12.0),
freetype_load_flags: face::FREETYPE_LOAD_FLAGS_DEFAULT,
}
}
/// The options to use for loading faces.
pub fn face_options(&self) -> face::Options {
face::Options {
size: self.size,
freetype_load_flags: self.freetype_load_flags,
}
}
}
pub enum AnyFace {
/// Not yet loaded.
Deferred(DeferredFace),
/// Loaded.
Loaded(Face),
}
/// Factor to multiply the collection size by for a face, or else the size
/// adjustment that should be used to calculate it once the face is loaded.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScaleFactor {
Adjustment(SizeAdjustment),
Scale(f64),
}
/// A entry in a collection can be deferred or loaded. A deferred face
/// is not yet fully loaded and only represents the font descriptor
/// and usually uses less resources. A loaded face is fully parsed,
/// ready to rasterize, and usually uses more resources than a
/// deferred version.
///
/// A face can also be a "fallback" variant that is still either
/// deferred or loaded. Today, there is only one difference between
/// fallback and non-fallback (or "explicit") faces: the handling
/// of emoji presentation.
///
/// For explicit faces, when an explicit emoji presentation is
/// not requested, we will use any glyph for that codepoint found
/// even if the font presentation does not match the UCD
/// (Unicode Character Database) value. When an explicit presentation
/// is requested (via either VS15/V16), that is always honored.
/// The reason we do this is because we assume that if a user
/// explicitly chosen a font face (hence it is "explicit" and
/// not "fallback"), they want to use any glyphs possible within that
/// font face. Fallback fonts on the other hand are picked as a
/// last resort, so we should prefer exactness if possible.
pub struct Entry {
pub face: AnyFace,
/// Whether this face is a fallback face, see
/// main doc comment on Entry for more info.
pub fallback: bool,
/// Factor to multiply the collection size by for this face, or
/// else the size adjustment that should be used to calculate
/// once the face is loaded.
///
/// This is computed when the face is loaded, based on a scale
/// factor computed for an adjustment from the primary face to
/// this one, which allows fallback fonts to be harmonized with
/// the primary font by matching one of the metrics between them.
pub scale_factor: ScaleFactor,
}
impl Entry {
/// If this face is loaded, then this returns the `Face`,
/// otherwise returns None.
pub fn get_loaded(&mut self) -> Option<&mut Face> {
match &mut self.face {
AnyFace::Deferred(_) => None,
AnyFace::Loaded(face) => Some(face),
}
}
/// True if the entry is deferred.
pub fn is_deferred(&self) -> bool {
matches!(self.face, AnyFace::Deferred(_))
}
/// True if this face satisfies the given codepoint and presentation.
pub fn has_codepoint(&self, cp: u32, p_mode: PresentationMode) -> bool {
let mut mode = p_mode;
loop {
match mode {
PresentationMode::Default(p) => {
mode = if self.fallback {
// Fallback fonts require explicit presentation matching.
PresentationMode::Explicit(p)
} else {
// Non-fallback fonts do not.
PresentationMode::Any
};
}
PresentationMode::Explicit(p) => {
return match &self.face {
AnyFace::Deferred(v) => v.has_codepoint(cp, Some(p)),
AnyFace::Loaded(face) => {
let Some(index) = face.glyph_index(cp) else {
return false;
};
match p {
Presentation::Text => !face.is_color_glyph(index),
Presentation::Emoji => face.is_color_glyph(index),
}
}
};
}
PresentationMode::Any => {
return match &self.face {
AnyFace::Deferred(v) => v.has_codepoint(cp, None),
AnyFace::Loaded(face) => face.glyph_index(cp).is_some(),
};
}
}
}
}
}
pub enum EntryOrAlias {
Entry(Entry),
/// An alias to another entry. This is used to share the same face,
/// avoid memory duplication. An alias must point to a non-alias entry.
Alias(Index),
}
/// The requested presentation for a codepoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PresentationMode {
/// The codepoint has an explicit presentation that is required,
/// i.e. VS15/V16.
Explicit(Presentation),
/// The codepoint has no explicit presentation and we should use
/// the presentation from the UCD.
Default(Presentation),
/// The codepoint can be any presentation.
Any,
}
/// The special-case fonts that we support.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u16)]
pub enum Special {
/// Sprite drawing, this is rendered JIT using 2D graphics APIs.
Sprite = Index::SPECIAL_START,
}
/// This represents a specific font in the collection.
///
/// The Zig version is a `packed struct(u16)` with a 3 bit style and 13 bit
/// index (up to 8192 fonts per style). Here the two parts are separate
/// fields, with [`Index::int`] producing the packed representation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Index {
pub style: Style,
pub idx: u16,
}
impl Default for Index {
fn default() -> Self {
Index {
style: Style::Regular,
idx: 0,
}
}
}
impl Index {
/// The number of bits we use for the index.
pub const IDX_BITS: u32 = 13;
/// We start all special fonts at this index so they can be detected
/// (`maxInt(IndexInt)`).
pub const SPECIAL_START: u16 = (1 << Self::IDX_BITS) - 1;
/// Initialize a special font index.
pub const fn init_special(v: Special) -> Index {
Index {
style: Style::Regular,
idx: v as u16,
}
}
/// Convert to the packed int (style in the low 3 bits).
pub fn int(self) -> u16 {
(self.style as u16) | (self.idx << 3)
}
/// Returns Some if this is a "special" index which doesn't map to
/// a real font face. We can still render it but there is no face for
/// this font.
pub fn special(self) -> Option<Special> {
if self.idx < Self::SPECIAL_START {
return None;
}
Some(Special::Sprite)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedded;
use crate::face::{DesiredSize, Options};
fn opts_96(points: f32) -> Options {
Options::new(DesiredSize {
points,
xdpi: 96,
ydpi: 96,
})
}
fn add_opts(style: Style, adj: SizeAdjustment) -> AddOptions {
AddOptions {
style,
fallback: false,
size_adjustment: adj,
}
}
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-3
}
fn assert_metrics_approx(expected: &Metrics, actual: &Metrics) {
assert_eq!(expected.cell_width, actual.cell_width);
assert_eq!(expected.cell_height, actual.cell_height);
assert_eq!(expected.cell_baseline, actual.cell_baseline);
assert_eq!(expected.underline_position, actual.underline_position);
assert_eq!(expected.underline_thickness, actual.underline_thickness);
assert_eq!(
expected.strikethrough_position,
actual.strikethrough_position
);
assert_eq!(
expected.strikethrough_thickness,
actual.strikethrough_thickness
);
assert_eq!(expected.overline_position, actual.overline_position);
assert_eq!(expected.overline_thickness, actual.overline_thickness);
assert_eq!(expected.box_thickness, actual.box_thickness);
assert_eq!(expected.cursor_height, actual.cursor_height);
assert!(
approx(expected.icon_height, actual.icon_height),
"icon_height"
);
assert!(
approx(expected.icon_height_single, actual.icon_height_single),
"icon_height_single"
);
assert!(approx(expected.face_width, actual.face_width), "face_width");
assert!(
approx(expected.face_height, actual.face_height),
"face_height"
);
assert!(approx(expected.face_y, actual.face_y), "face_y");
}
#[test]
fn index_layout() {
// We never want to take up more than a u16 since font indexes are
// everywhere. The maximum number of fonts for a style is 13 bits.
assert_eq!(2, std::mem::size_of::<u16>());
assert_eq!(13, Index::IDX_BITS);
assert_eq!(8191, Index::SPECIAL_START);
let sprite = Index::init_special(Special::Sprite);
assert_eq!(Some(Special::Sprite), sprite.special());
assert_eq!(None, Index::default().special());
assert_eq!(Index::SPECIAL_START << 3, sprite.int());
}
#[test]
fn init() {
let c = Collection::new();
drop(c);
}
#[test]
fn add_full() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
for _ in 0..(usize::from(Index::SPECIAL_START) - 1) {
c.add(
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
}
let face = Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap();
assert_eq!(
Err(AddError::CollectionFull),
c.add(face, add_opts(Style::Regular, SizeAdjustment::None))
);
}
#[test]
fn add_deferred_without_loading_options() {
let mut c = Collection::new();
// The deferred face can be anything since it should never be accessed.
let deferred = DeferredFace::default();
assert_eq!(
Err(AddDeferredError::DeferredLoadingUnavailable),
c.add_deferred(deferred, add_opts(Style::Regular, SizeAdjustment::None))
);
}
#[test]
fn get_face() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
let idx = c
.add(
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
let p1 = std::ptr::from_ref::<Face>(c.get_face(idx).unwrap());
let p2 = std::ptr::from_ref::<Face>(c.get_face(idx).unwrap());
assert_eq!(p1, p2);
}
#[test]
fn get_index() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.add(
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
// Should find all visible ASCII
for i in 32u32..127 {
assert!(
c.get_index(i, Style::Regular, PresentationMode::Any)
.is_some()
);
}
// Should not find emoji
assert!(
c.get_index('🥸' as u32, Style::Regular, PresentationMode::Any)
.is_none()
);
}
#[test]
fn complete_styles() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone()));
c.add(
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
let a = 'A' as u32;
assert!(c.get_index(a, Style::Bold, PresentationMode::Any).is_none());
assert!(
c.get_index(a, Style::Italic, PresentationMode::Any)
.is_none()
);
assert!(
c.get_index(a, Style::BoldItalic, PresentationMode::Any)
.is_none()
);
c.complete_styles(FontSyntheticStyle::default()).unwrap();
assert!(c.get_index(a, Style::Bold, PresentationMode::Any).is_some());
assert!(
c.get_index(a, Style::Italic, PresentationMode::Any)
.is_some()
);
assert!(
c.get_index(a, Style::BoldItalic, PresentationMode::Any)
.is_some()
);
}
#[test]
fn complete_styles_synthetic_disabled_uses_aliases() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone()));
c.add(
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
c.complete_styles(FontSyntheticStyle {
bold: false,
italic: false,
bold_italic: false,
})
.unwrap();
for style in [Style::Bold, Style::Italic, Style::BoldItalic] {
assert_eq!(1, c.count(style));
let idx = c
.get_index('A' as u32, style, PresentationMode::Any)
.unwrap();
// The alias resolves to the regular entry's face.
let p1 = std::ptr::from_ref::<Face>(c.get_face(idx).unwrap());
let p2 = std::ptr::from_ref::<Face>(c.get_face(Index::default()).unwrap());
assert_eq!(p1, p2);
}
}
#[test]
fn set_size() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone()));
c.add(
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
assert_eq!(12.0, c.load_options.as_ref().unwrap().size.points);
c.set_size(DesiredSize::new(24.0)).unwrap();
assert_eq!(24.0, c.load_options.as_ref().unwrap().size.points);
}
#[test]
fn has_codepoint() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone()));
let idx = c
.add(
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
assert!(c.has_codepoint(idx, 'A' as u32, PresentationMode::Any));
assert!(!c.has_codepoint(idx, '🥸' as u32, PresentationMode::Any));
}
#[test]
fn has_codepoint_emoji_default_graphical() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone()));
let idx = c
.add(
Face::init(&lib, embedded::EMOJI.into(), opts_96(12.0)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
assert!(!c.has_codepoint(idx, 'A' as u32, PresentationMode::Any));
assert!(c.has_codepoint(idx, '🥸' as u32, PresentationMode::Any));
// TODO(fontmem): test explicit/implicit
}
#[test]
fn metrics() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
let size = DesiredSize {
points: 12.0,
xdpi: 96,
ydpi: 96,
};
let mut lo = LoadOptions::new(lib.clone());
lo.size = size;
c.load_options = Some(lo);
c.add(
Face::init(&lib, embedded::INCONSOLATA.into(), Options::new(size)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
c.update_metrics().unwrap();
let base = Metrics {
cell_width: 8,
// The cell height is 17 px because the calculation is
//
// ascender - descender + gap
//
// which, for inconsolata is
//
// 859 - -190 + 0
//
// font units, at 1000 units per em that works out to 1.049 em,
// and 1em should be the point size * dpi scale, so 12 * (96/72)
// which is 16, and 16 * 1.049 = 16.784, which finally is rounded
// to 17.
//
// The icon height is (2 * cap_height + face_height) / 3
// = (2 * 623 + 1049) / 3 = 765, and 16 * 0.765 = 12.24.
cell_height: 17,
cell_baseline: 3,
underline_position: 17,
underline_thickness: 1,
strikethrough_position: 10,
strikethrough_thickness: 1,
overline_position: 0,
overline_thickness: 1,
box_thickness: 1,
cursor_height: 17,
icon_height: 16.784,
icon_height_single: 12.24,
face_width: 8.0,
face_height: 16.784,
face_y: -0.04,
cursor_thickness: 1,
};
assert_metrics_approx(&base, &c.metrics.unwrap());
// Resize should change metrics
c.set_size(DesiredSize {
points: 24.0,
xdpi: 96,
ydpi: 96,
})
.unwrap();
let big = Metrics {
cell_width: 16,
cell_height: 34,
cell_baseline: 6,
underline_position: 34,
underline_thickness: 2,
strikethrough_position: 19,
strikethrough_thickness: 2,
overline_position: 0,
overline_thickness: 2,
box_thickness: 2,
cursor_height: 34,
icon_height: 33.568,
icon_height_single: 24.48,
face_width: 16.0,
face_height: 33.568,
face_y: -0.08,
cursor_thickness: 1,
};
assert_metrics_approx(&big, &c.metrics.unwrap());
}
// TODO: Also test CJK fallback sizing, we don't currently have a CJK test font.
#[test]
fn adjusted_sizes() {
let lib = Library::init().unwrap();
let mut c = Collection::new();
let size = DesiredSize {
points: 12.0,
xdpi: 96,
ydpi: 96,
};
let mut lo = LoadOptions::new(lib.clone());
lo.size = size;
c.load_options = Some(lo);
// Add our primary face.
c.add(
Face::init(&lib, embedded::INCONSOLATA.into(), Options::new(size)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
c.update_metrics().unwrap();
for metric in [SizeAdjustment::ExHeight, SizeAdjustment::CapHeight] {
let pick = |m: &FaceMetrics| match metric {
SizeAdjustment::ExHeight => m.ex_height.unwrap(),
SizeAdjustment::CapHeight => m.cap_height.unwrap(),
_ => unreachable!(),
};
// Add the fallback face with the chosen adjustment metric.
let fallback_idx = c
.add(
Face::init(&lib, embedded::MONASPACE_NEON.into(), Options::new(size)).unwrap(),
add_opts(Style::Regular, metric),
)
.unwrap();
// The chosen metric should match.
{
let pm = c.get_face(Index::default()).unwrap().get_metrics();
let fm = c.get_face(fallback_idx).unwrap().get_metrics();
// We accept anything within half a pixel.
assert!((pick(&pm) - pick(&fm)).abs() <= 0.5);
}
// Resize should keep that relationship.
c.set_size(DesiredSize {
points: 37.0,
xdpi: 96,
ydpi: 96,
})
.unwrap();
{
let pm = c.get_face(Index::default()).unwrap().get_metrics();
let fm = c.get_face(fallback_idx).unwrap().get_metrics();
assert!((pick(&pm) - pick(&fm)).abs() <= 0.5);
}
// Reset size for the next iteration
c.set_size(size).unwrap();
}
{
// A reference metric of "none" should leave the size unchanged.
let fallback_idx = c
.add(
Face::init(&lib, embedded::MONASPACE_NEON.into(), Options::new(size)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::None),
)
.unwrap();
let primary_points = c.get_face(Index::default()).unwrap().size.points;
assert_eq!(
primary_points,
c.get_face(fallback_idx).unwrap().size.points
);
// Resize should keep that.
c.set_size(DesiredSize {
points: 37.0,
xdpi: 96,
ydpi: 96,
})
.unwrap();
let primary_points = c.get_face(Index::default()).unwrap().size.points;
assert_eq!(
primary_points,
c.get_face(fallback_idx).unwrap().size.points
);
// Reset collection size
c.set_size(size).unwrap();
}
// Add the symbol face.
let symbol_idx = c
.add(
Face::init(&lib, embedded::SYMBOLS_NERD_FONT.into(), Options::new(size)).unwrap(),
add_opts(Style::Regular, SizeAdjustment::ExHeight),
)
.unwrap();
// Test fallback to line_height() (ex_height and cap_height not defined
// in symbols font).
{
let pm = c.get_face(Index::default()).unwrap().get_metrics();
let sm = c.get_face(symbol_idx).unwrap().get_metrics();
// We accept anything within half a pixel.
assert!((pm.line_height() - sm.line_height()).abs() <= 0.5);
}
}
}
+59
View File
@@ -0,0 +1,59 @@
//! Small local mirrors of the `ghostty-config` types that the font engine
//! consumes.
//!
//! TODO(integration): `ghostty-config` should map its own
//! `FreetypeLoadFlags`, `FontSyntheticStyle` and `FontShapingBreak` onto
//! these (or this crate should re-export the config ones once the config
//! crate exists in the dependency graph).
/// See `freetype-load-flags` in the config. Defaults match upstream.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FreetypeLoadFlags {
pub hinting: bool,
pub force_autohint: bool,
pub monochrome: bool,
pub autohint: bool,
pub light: bool,
}
impl Default for FreetypeLoadFlags {
fn default() -> Self {
Self {
hinting: true,
force_autohint: false,
monochrome: false,
autohint: true,
light: true,
}
}
}
/// See `font-synthetic-style` in the config.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FontSyntheticStyle {
pub bold: bool,
pub italic: bool,
pub bold_italic: bool,
}
impl Default for FontSyntheticStyle {
fn default() -> Self {
Self {
bold: true,
italic: true,
bold_italic: true,
}
}
}
/// See `font-shaping-break` in the config.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FontShapingBreak {
pub cursor: bool,
}
impl Default for FontShapingBreak {
fn default() -> Self {
Self { cursor: true }
}
}
+170
View File
@@ -0,0 +1,170 @@
//! A deferred face represents a single font face with all the information
//! necessary to load it, but defers loading the full face until it is
//! needed.
//!
//! This allows us to have many fallback fonts to look for glyphs, but
//! only load them if they're really needed.
//!
//! Port of `font/DeferredFace.zig` (fontconfig variant only).
use crate::face::{self, Options, Variation};
use crate::fontconfig as fc;
use crate::{Face, Library, Presentation};
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error(transparent)]
Fontconfig(#[from] fc::Error),
#[error(transparent)]
Face(#[from] face::freetype::FaceError),
}
/// Fontconfig specific data. This is only present if building with fontconfig.
pub struct Fontconfig {
/// The pattern for this font. This must be the "render prepared" pattern.
/// (i.e. call FcFontRenderPrepare). The charset and langset used for
/// quick codepoint / presentation lookups are read from this pattern.
pub pattern: fc::Pattern,
/// Variations to apply to this font.
pub variations: Vec<Variation>,
}
#[derive(Default)]
pub struct DeferredFace {
/// Fontconfig
pub fc: Option<Fontconfig>,
}
impl DeferredFace {
pub fn from_fontconfig(fc: Fontconfig) -> DeferredFace {
DeferredFace { fc: Some(fc) }
}
/// Returns the family name of the font.
pub fn family_name(&self) -> Result<String, LoadError> {
if let Some(fc) = &self.fc {
return Ok(fc.pattern.get_string(fc::Property::Family, 0)?);
}
Ok(String::new())
}
/// Returns the name of this face.
pub fn name(&self) -> Result<String, LoadError> {
if let Some(fc) = &self.fc {
return Ok(fc.pattern.get_string(fc::Property::FullName, 0)?);
}
Ok(String::new())
}
/// Load the deferred font face.
pub fn load(&mut self, lib: &Library, opts: Options) -> Result<Face, LoadError> {
let fc = self
.fc
.as_ref()
.expect("deferred face has no fontconfig data");
// Filename and index for our face so we can load it
let filename = fc.pattern.get_string(fc::Property::File, 0)?;
let face_index = fc.pattern.get_integer(fc::Property::Index, 0)?;
let mut face = Face::init_file(lib, &filename, face_index, opts)?;
face.set_variations(&fc.variations, opts)?;
Ok(face)
}
/// Returns true if this face can satisfy the given codepoint and
/// presentation. If presentation is null, then it just checks if the
/// codepoint is present at all.
///
/// This does not require the face to be loaded (fontconfig metadata is
/// used).
pub fn has_codepoint(&self, cp: u32, p: Option<Presentation>) -> bool {
// If we are using fontconfig, use the fontconfig metadata to
// avoid loading the face.
let Some(fc) = &self.fc else {
// This is unreachable because discovery mechanisms terminate, and
// if we're not using a discovery mechanism, the face MUST be loaded.
unreachable!("deferred face without fontconfig data");
};
// Check if char exists
if !fc.pattern.charset_has_char(cp).unwrap_or(false) {
return false;
}
// If we have a presentation, check it matches
if let Some(desired) = p {
let emoji_lang = "und-zsye";
let actual = if fc.pattern.langset_has_lang(emoji_lang).unwrap_or(false) {
Presentation::Emoji
} else {
Presentation::Text
};
return desired == actual;
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::discovery::{Descriptor, Fontconfig};
// NOTE: this test needs the real FreeType `Face` (agent A); it cannot
// run while `Face` is a stub.
#[test]
fn fontconfig() {
// Load freetype
let lib = Library::init().unwrap();
// Get a deferred face from fontconfig
let mut def = {
let fc = Fontconfig::init();
let mut it = fc
.discover(&Descriptor {
family: Some("monospace".into()),
size: 12.0,
..Default::default()
})
.unwrap();
it.next().unwrap().unwrap()
};
// Verify we can get the name
let n = def.name().unwrap();
assert_ne!("", n);
// Load it and verify it works
let face = def
.load(&lib, Options::new(face::DesiredSize::new(12.0)))
.unwrap();
assert!(face.glyph_index(' ' as u32).is_some());
}
#[test]
fn family_name_and_presentation_from_fontconfig() {
let fc = Fontconfig::init();
let mut it = fc
.discover(&Descriptor {
family: Some("monospace".into()),
size: 12.0,
..Default::default()
})
.unwrap();
let def = it.next().unwrap().unwrap();
assert_ne!("", def.family_name().unwrap());
// A monospace text font is not an emoji-presentation font.
assert!(!def.has_codepoint('A' as u32, Some(Presentation::Emoji)));
}
#[test]
fn empty_deferred_face_has_empty_names() {
let def = DeferredFace::default();
assert_eq!("", def.family_name().unwrap());
assert_eq!("", def.name().unwrap());
}
}
+278
View File
@@ -0,0 +1,278 @@
//! Font discovery. Port of `font/discovery.zig` (fontconfig backend only;
//! the CoreText discoverer is macOS-only and out of scope).
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use crate::collection::Collection;
use crate::deferred_face::{DeferredFace, Fontconfig as DeferredFontconfig};
use crate::face::Variation;
use crate::fontconfig as fc;
/// Discover implementation for the compile options.
pub type Discover = Fontconfig;
/// Descriptor is used to search for fonts. The only required field
/// is "family". The rest are ignored unless they're set to a non-zero
/// value.
#[derive(Debug, Clone, Default)]
pub struct Descriptor {
/// Font family to search for. This can be a fully qualified font
/// name such as "Fira Code", "monospace", "serif", etc.
///
/// On systems that use fontconfig (Linux), this can be a full
/// fontconfig pattern, such as "Fira Code-14:bold".
pub family: Option<String>,
/// Specific font style to search for. This will filter the style
/// string the font advertises. The "bold/italic" booleans later in this
/// struct filter by the style trait the font has, not the string, so
/// these can be used in conjunction or not.
pub style: Option<String>,
/// A codepoint that this font must be able to render.
pub codepoint: u32,
/// Font size in points that the font should support. For conversion
/// to pixels, we will use 72 DPI for Mac and 96 DPI for everything else.
/// (If pixel conversion is necessary, i.e. emoji fonts)
pub size: f32,
/// True if we want to search specifically for a font that supports
/// specific styles.
pub bold: bool,
pub italic: bool,
pub monospace: bool,
/// Variation axes to apply to the font. This also impacts searching
/// for fonts since fonts with the ability to set these variations
/// will be preferred, but not guaranteed.
pub variations: Vec<Variation>,
}
impl Hash for Descriptor {
/// Hash the descriptor with the given hasher. Covers the same fields
/// as the Zig implementation.
fn hash<H: Hasher>(&self, state: &mut H) {
self.family.hash(state);
self.style.hash(state);
self.codepoint.hash(state);
self.size.to_bits().hash(state);
self.bold.hash(state);
self.italic.hash(state);
self.monospace.hash(state);
self.variations.len().hash(state);
for variation in &self.variations {
variation.id.hash(state);
// This is not correct, but we don't currently depend on the
// hash value being different based on decimal values of variations.
(variation.value as i64).hash(state);
}
}
}
impl Descriptor {
/// Returns a hash code that can be used to uniquely identify this
/// action.
pub fn hashcode(&self) -> u64 {
// `DefaultHasher::new()` uses fixed keys, so this is deterministic.
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
/// Convert to Fontconfig pattern to use for lookup. The pattern does
/// not have defaults filled/substituted (Fontconfig thing) so callers
/// must still do this.
pub fn to_fc_pattern(&self) -> fc::Pattern {
let mut pat = fc::Pattern::create();
if let Some(family) = &self.family {
assert!(pat.add_string(fc::Property::Family, family));
}
if let Some(style) = &self.style {
assert!(pat.add_string(fc::Property::Style, style));
}
if self.codepoint > 0 {
let mut cs = fc::CharSet::create();
assert!(cs.add_char(self.codepoint));
assert!(pat.add_charset(fc::Property::Charset, &cs));
}
if self.size > 0.0 {
assert!(pat.add_integer(fc::Property::Size, self.size.round() as i32));
}
if self.bold {
assert!(pat.add_integer(fc::Property::Weight, fc::Weight::Bold as i32));
}
if self.italic {
assert!(pat.add_integer(fc::Property::Slant, fc::Slant::Italic as i32));
}
// For fontconfig, we always add monospace in the pattern. Since
// fontconfig sorts by closeness to the pattern, this doesn't fully
// exclude non-monospace but helps prefer it.
assert!(pat.add_integer(fc::Property::Spacing, fc::Spacing::Mono as i32));
pat
}
}
pub struct Fontconfig {
fc_config: Arc<fc::Config>,
}
#[derive(Debug, thiserror::Error)]
pub enum DiscoverError {
#[error("fontconfig failed")]
FontConfigFailed,
#[error(transparent)]
Fontconfig(#[from] fc::Error),
}
impl Fontconfig {
pub fn init() -> Fontconfig {
// safe to call multiple times and concurrently
fc::init();
Fontconfig {
fc_config: Arc::new(
fc::Config::init_load_config_and_fonts()
.expect("FcInitLoadConfigAndFonts returned null"),
),
}
}
/// Discover fonts from a descriptor. This returns an iterator that can
/// be used to build up the deferred fonts.
pub fn discover(&self, desc: &Descriptor) -> Result<DiscoverIterator, DiscoverError> {
// Build our pattern that we'll search for
let mut pat = desc.to_fc_pattern();
assert!(self.fc_config.substitute_with_pat_pattern(&mut pat));
pat.default_substitute();
// Search
let set = self
.fc_config
.font_sort(&mut pat, false)
.map_err(|_| DiscoverError::FontConfigFailed)?;
Ok(DiscoverIterator {
config: Arc::clone(&self.fc_config),
pattern: pat,
set,
variations: desc.variations.clone(),
i: 0,
})
}
pub fn discover_fallback(
&self,
_collection: &Collection,
desc: &Descriptor,
) -> Result<DiscoverIterator, DiscoverError> {
self.discover(desc)
}
}
pub struct DiscoverIterator {
config: Arc<fc::Config>,
pattern: fc::Pattern,
set: fc::FontSet,
variations: Vec<Variation>,
i: usize,
}
impl DiscoverIterator {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<Option<DeferredFace>, fc::Error> {
if self.i >= self.set.len() {
return Ok(None);
}
// Get the copied pattern from our fontset that has the
// attributes configured for rendering.
let font_pattern = self
.config
.font_render_prepare(&mut self.pattern, &self.set, self.i)?;
// The charset and langset must exist (they are used for lookups).
font_pattern.charset_has_char(0)?;
font_pattern.langset_has_lang("")?;
// Increment after we return
self.i += 1;
Ok(Some(DeferredFace::from_fontconfig(DeferredFontconfig {
pattern: font_pattern,
variations: self.variations.clone(),
})))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn descriptor_hash() {
let d = Descriptor::default();
assert_ne!(d.hashcode(), 0);
}
#[test]
fn descriptor_hash_family_names() {
let d1 = Descriptor {
family: Some("A".into()),
..Default::default()
};
let d2 = Descriptor {
family: Some("B".into()),
..Default::default()
};
assert_ne!(d1.hashcode(), d2.hashcode());
}
#[test]
fn descriptor_hash_is_deterministic_and_covers_fields() {
let a = Descriptor {
family: Some("A".into()),
bold: true,
..Default::default()
};
assert_eq!(a.hashcode(), a.clone().hashcode());
let mut b = a.clone();
b.italic = true;
assert_ne!(a.hashcode(), b.hashcode());
}
#[test]
fn fontconfig() {
let fc = Fontconfig::init();
let _it = fc
.discover(&Descriptor {
family: Some("monospace".into()),
size: 12.0,
..Default::default()
})
.unwrap();
}
#[test]
fn fontconfig_codepoint() {
let fc = Fontconfig::init();
let mut it = fc
.discover(&Descriptor {
codepoint: 'A' as u32,
size: 12.0,
..Default::default()
})
.unwrap();
// The first result should have the codepoint. Later ones may not
// because fontconfig returns all fonts sorted.
let face = it.next().unwrap().unwrap();
assert!(face.has_codepoint('A' as u32, None));
// Should have other codepoints too
assert!(face.has_codepoint('B' as u32, None));
}
}
+55
View File
@@ -0,0 +1,55 @@
//! Fonts that can be embedded with Ghostty. Note they are only actually
//! embedded in the binary if they are referenced by the code, so fonts
//! used for tests will not result in the final binary being larger.
//!
//! Be careful to ensure that any fonts you embed are licensed for
//! redistribution and include their license as necessary (see `res/`).
/// Default fonts that we prefer for Ghostty.
pub static VARIABLE: &[u8] = include_bytes!("../res/JetBrainsMono-Variable.ttf");
pub static VARIABLE_ITALIC: &[u8] = include_bytes!("../res/JetBrainsMono-Italic-Variable.ttf");
/// Symbols-only nerd font.
pub static SYMBOLS_NERD_FONT: &[u8] = include_bytes!("../res/SymbolsNerdFont-Regular.ttf");
/// Static jetbrains mono faces, currently unused (except by tests).
pub static REGULAR: &[u8] = include_bytes!("../res/JetBrainsMono-Regular.ttf");
pub static BOLD: &[u8] = include_bytes!("../res/JetBrainsMono-Bold.ttf");
pub static ITALIC: &[u8] = include_bytes!("../res/JetBrainsMono-Italic.ttf");
pub static BOLD_ITALIC: &[u8] = include_bytes!("../res/JetBrainsMono-BoldItalic.ttf");
/// Emoji fonts
pub static EMOJI: &[u8] = include_bytes!("../res/NotoColorEmoji.ttf");
pub static EMOJI_TEXT: &[u8] = include_bytes!("../res/NotoEmoji-Regular.ttf");
// Fonts below are ONLY used for testing.
/// Fonts with general properties
pub static ARABIC: &[u8] = include_bytes!("../res/KawkabMono-Regular.ttf");
/// A font for testing which is patched with nerd font symbols.
pub static TEST_NERD_FONT: &[u8] = include_bytes!("../res/JetBrainsMonoNerdFont-Regular.ttf");
/// Specific font families below:
pub static CODE_NEW_ROMAN: &[u8] = include_bytes!("../res/CodeNewRoman-Regular.otf");
pub static INCONSOLATA: &[u8] = include_bytes!("../res/Inconsolata-Regular.ttf");
pub static GEIST_MONO: &[u8] = include_bytes!("../res/GeistMono-Regular.ttf");
pub static JETBRAINS_MONO: &[u8] = include_bytes!("../res/JetBrainsMonoNoNF-Regular.ttf");
pub static JULIA_MONO: &[u8] = include_bytes!("../res/JuliaMono-Regular.ttf");
/// Cozette is a unique font because it embeds some emoji characters
/// but has a text presentation.
pub static COZETTE: &[u8] = include_bytes!("../res/CozetteVector.ttf");
/// Monaspace has weird ligature behaviors we want to test in our shapers
/// so we embed it here.
pub static MONASPACE_NEON: &[u8] = include_bytes!("../res/MonaspaceNeon-Regular.otf");
/// Terminus TTF is a scalable font with bitmap glyphs at various sizes.
pub static TERMINUS_TTF: &[u8] = include_bytes!("../res/TerminusTTF-Regular.ttf");
/// Spleen is a monospaced bitmap font available in multiple formats.
/// Used for testing bitmap font support across different file formats.
pub static SPLEEN_BDF: &[u8] = include_bytes!("../res/spleen-8x16.bdf");
pub static SPLEEN_PCF: &[u8] = include_bytes!("../res/spleen-8x16.pcf");
pub static SPLEEN_OTB: &[u8] = include_bytes!("../res/spleen-8x16.otb");
+872
View File
@@ -0,0 +1,872 @@
//! Face types shared by all face implementations. Port of `font/face.zig`.
//!
//! The concrete FreeType face lives in [`freetype`] (`font/face/freetype.zig`).
// `(a + b) / 2` is kept verbatim from upstream (f64::midpoint rounds differently).
#![allow(clippy::manual_midpoint)]
use std::sync::Arc;
use crate::metrics::Metrics;
pub mod freetype;
pub use crate::config_types::FreetypeLoadFlags;
pub use freetype::Face;
/// If a DPI can't be calculated, this DPI is used. This is probably
/// wrong on modern devices so it is highly recommended you get the DPI
/// using whatever platform method you can.
pub const DEFAULT_DPI: u16 = 96;
pub const FREETYPE_LOAD_FLAGS_DEFAULT: FreetypeLoadFlags = FreetypeLoadFlags {
hinting: true,
force_autohint: false,
monochrome: false,
autohint: true,
light: true,
};
/// Font file bytes. FreeType does not copy memory faces, so whoever creates
/// a face must keep the data alive; this type makes that explicit
/// (cheap to clone, either static or reference counted).
#[derive(Clone)]
pub enum FontData {
Static(&'static [u8]),
Shared(Arc<[u8]>),
}
impl FontData {
pub fn as_slice(&self) -> &[u8] {
match self {
FontData::Static(s) => s,
FontData::Shared(s) => s,
}
}
}
impl AsRef<[u8]> for FontData {
fn as_ref(&self) -> &[u8] {
self.as_slice()
}
}
impl std::fmt::Debug for FontData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "FontData({} bytes)", self.as_slice().len())
}
}
impl From<&'static [u8]> for FontData {
fn from(v: &'static [u8]) -> Self {
FontData::Static(v)
}
}
impl From<Vec<u8>> for FontData {
fn from(v: Vec<u8>) -> Self {
FontData::Shared(v.into())
}
}
impl From<Arc<[u8]>> for FontData {
fn from(v: Arc<[u8]>) -> Self {
FontData::Shared(v)
}
}
/// Options for initializing a font face.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Options {
pub size: DesiredSize,
pub freetype_load_flags: FreetypeLoadFlags,
}
impl Options {
pub fn new(size: DesiredSize) -> Self {
Self {
size,
freetype_load_flags: FREETYPE_LOAD_FLAGS_DEFAULT,
}
}
}
/// The desired size for loading a font.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DesiredSize {
/// Desired size in points
pub points: f32,
/// The DPI of the screen so we can convert points to pixels.
pub xdpi: u16,
pub ydpi: u16,
}
impl DesiredSize {
pub fn new(points: f32) -> Self {
Self {
points,
xdpi: DEFAULT_DPI,
ydpi: DEFAULT_DPI,
}
}
/// Converts points to pixels
pub fn pixels(self) -> f32 {
// 1 point = 1/72 inch
(self.points * f32::from(self.ydpi)) / 72.0
}
}
/// A font variation setting. The best documentation for this I know of
/// is actually the CSS font-variation-settings property on MDN:
/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Variation {
pub id: VariationId,
pub value: f64,
}
/// A four byte OpenType variation axis tag (`wght`, `slnt`, ...). The
/// numeric value is the big-endian interpretation of the 4 characters
/// (Zig: `packed struct(u32) { d, c, b, a }`, i.e. `a` is the high byte).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VariationId {
pub a: u8,
pub b: u8,
pub c: u8,
pub d: u8,
}
impl VariationId {
pub fn new(v: &[u8; 4]) -> VariationId {
VariationId {
a: v[0],
b: v[1],
c: v[2],
d: v[3],
}
}
/// The raw 32-bit tag value.
pub fn as_u32(self) -> u32 {
u32::from_be_bytes([self.a, self.b, self.c, self.d])
}
pub fn from_u32(v: u32) -> VariationId {
let [a, b, c, d] = v.to_be_bytes();
VariationId { a, b, c, d }
}
/// Converts the ID to a string.
pub fn str(self) -> [u8; 4] {
[self.a, self.b, self.c, self.d]
}
}
/// The size and position of a glyph.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct GlyphSize {
pub width: f64,
pub height: f64,
pub x: f64,
pub y: f64,
}
/// Additional options for rendering glyphs.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RenderOptions {
/// The metrics that are defining the grid layout. These are usually
/// the metrics of the primary font face. The grid metrics are used
/// by the font face to better layout the glyph in situations where
/// the font is not exactly the same size as the grid.
pub grid_metrics: Metrics,
/// The number of grid cells this glyph will take up. This can be used
/// optionally by the rasterizer to better layout the glyph.
/// (`?u2` in Zig, so 0..=3.)
pub cell_width: Option<u8>,
/// Constraint and alignment properties for the glyph. The rasterizer
/// should call the `constrain` function on this with the original size
/// and bearings of the glyph to get remapped values that the glyph
/// should be scaled/moved to.
pub constraint: Constraint,
/// The number of cells, horizontally that the glyph is free to take up
/// when resized and aligned by `constraint`. This is usually 1, but if
/// there's whitespace to the right of the cell then it can be 2.
/// (`u2` in Zig.)
pub constraint_width: u8,
/// Thicken the glyph. This draws the glyph with a thicker stroke width.
/// This is purely an aesthetic setting.
///
/// This only works with CoreText currently.
pub thicken: bool,
/// "Strength" of the thickening, between `0` and `255`.
/// Only has an effect when `thicken` is enabled.
///
/// `0` does not correspond to *no* thickening,
/// just the *lightest* thickening available.
///
/// CoreText only.
pub thicken_strength: u8,
}
impl RenderOptions {
/// Options with the given grid metrics and all other fields defaulted
/// (`cell_width = null`, `constraint = .none`, `constraint_width = 1`,
/// `thicken = false`, `thicken_strength = 255`).
pub fn new(grid_metrics: Metrics) -> Self {
Self {
grid_metrics,
cell_width: None,
constraint: Constraint::NONE,
constraint_width: 1,
thicken: false,
thicken_strength: 255,
}
}
}
/// See the `constraint` field of [`RenderOptions`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Constraint {
/// Sizing rule.
pub size: ConstraintSize,
/// Vertical alignment rule.
pub align_vertical: Align,
/// Horizontal alignment rule.
pub align_horizontal: Align,
/// Top padding when resizing.
pub pad_top: f64,
/// Left padding when resizing.
pub pad_left: f64,
/// Right padding when resizing.
pub pad_right: f64,
/// Bottom padding when resizing.
pub pad_bottom: f64,
// Size and bearings of the glyph relative
// to the bounding box of its scale group.
pub relative_width: f64,
pub relative_height: f64,
pub relative_x: f64,
pub relative_y: f64,
/// Maximum aspect ratio (width/height) to allow when stretching.
pub max_xy_ratio: Option<f64>,
/// Maximum number of cells horizontally to use. (`u2` in Zig.)
pub max_constraint_width: u8,
/// What to use as the height metric when constraining the glyph and
/// the constraint width is 1,
pub height: Height,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConstraintSize {
/// Don't change the size of this glyph.
None,
/// Scale the glyph down if needed to fit within the bounds,
/// preserving aspect ratio.
Fit,
/// Scale the glyph up or down to exactly match the bounds,
/// preserving aspect ratio.
Cover,
/// Scale the glyph down if needed to fit within the bounds,
/// preserving aspect ratio. If the glyph doesn't cover a
/// single cell, scale up. If the glyph exceeds a single
/// cell but is within the bounds, do nothing.
/// (Nerd Font specific rule.)
FitCover1,
/// Stretch the glyph to exactly fit the bounds in both
/// directions, disregarding aspect ratio.
Stretch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Align {
/// Don't move the glyph on this axis.
None,
/// Move the glyph so that its leading (bottom/left)
/// edge aligns with the leading edge of the axis.
Start,
/// Move the glyph so that its trailing (top/right)
/// edge aligns with the trailing edge of the axis.
End,
/// Move the glyph so that it is centered on this axis.
Center,
/// Move the glyph so that it is centered on this axis,
/// but always with respect to the first cell even for
/// multi-cell constraints. (Nerd Font specific rule.)
Center1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Height {
/// Use the full line height of the primary face for
/// constraining this glyph.
Cell,
/// Use the icon height from the grid metrics for
/// constraining this glyph. Unlike `cell`, the value of
/// this height depends on both the constraint width and the
/// affected by the `adjust-icon-height` config option.
Icon,
}
impl Default for Constraint {
fn default() -> Self {
Self::NONE
}
}
impl Constraint {
/// Don't constrain the glyph in any way.
pub const NONE: Constraint = Constraint {
size: ConstraintSize::None,
align_vertical: Align::None,
align_horizontal: Align::None,
pad_top: 0.0,
pad_left: 0.0,
pad_right: 0.0,
pad_bottom: 0.0,
relative_width: 1.0,
relative_height: 1.0,
relative_x: 0.0,
relative_y: 0.0,
max_xy_ratio: None,
max_constraint_width: 2,
height: Height::Cell,
};
/// Returns true if the constraint does anything. If it doesn't,
/// because it neither sizes nor positions the glyph, then this
/// returns false.
#[inline]
pub fn does_anything(&self) -> bool {
self.size != ConstraintSize::None
|| self.align_horizontal != Align::None
|| self.align_vertical != Align::None
}
/// Apply this constraint to the provided glyph
/// size, given the available width and height.
///
/// `constraint_width` is the number of cells horizontally available for
/// this glyph.
pub fn constrain(
&self,
glyph: GlyphSize,
metrics: &Metrics,
constraint_width: u8,
) -> GlyphSize {
if !self.does_anything() {
return glyph;
}
if self.size == ConstraintSize::Stretch {
// Stretched glyphs are usually meant to align across cell
// boundaries, which works best if they're scaled and
// aligned to the grid rather than the face. This is most
// easily done by inserting this little fib in the metrics.
let mut m = *metrics;
m.face_width = f64::from(m.cell_width);
m.face_height = f64::from(m.cell_height);
m.face_y = 0.0;
// Negative padding for stretched glyphs is a band-aid to
// avoid gaps due to pixel rounding, but at the cost of
// unsightly overlap artifacts. Since we scale and align to
// the grid rather than the face, we don't need it.
let mut c = *self;
c.pad_bottom = c.pad_bottom.max(0.0);
c.pad_top = c.pad_top.max(0.0);
c.pad_left = c.pad_left.max(0.0);
c.pad_right = c.pad_right.max(0.0);
return c.constrain_inner(glyph, &m, constraint_width);
}
self.constrain_inner(glyph, metrics, constraint_width)
}
fn constrain_inner(
&self,
glyph: GlyphSize,
metrics: &Metrics,
constraint_width: u8,
) -> GlyphSize {
// For extra wide font faces, never stretch glyphs across two cells.
// This mirrors font_patcher.
let min_constraint_width: u8 = if self.size == ConstraintSize::Stretch
&& metrics.face_width > 0.9 * metrics.face_height
{
1
} else {
self.max_constraint_width.min(constraint_width)
};
// The bounding box for the glyph's scale group.
// Scaling and alignment rules are calculated for
// this box and then applied to the glyph.
let mut group = {
let group_width = glyph.width / self.relative_width;
let group_height = glyph.height / self.relative_height;
GlyphSize {
width: group_width,
height: group_height,
x: glyph.x - (group_width * self.relative_x),
y: glyph.y - (group_height * self.relative_y),
}
};
// Apply prescribed scaling, preserving the
// center bearings of the group bounding box
let (width_factor, height_factor) =
self.scale_factors(group, metrics, min_constraint_width);
let center_x = group.x + (group.width / 2.0);
let center_y = group.y + (group.height / 2.0);
group.width *= width_factor;
group.height *= height_factor;
group.x = center_x - (group.width / 2.0);
group.y = center_y - (group.height / 2.0);
// NOTE: font_patcher jumps through a lot of hoops at this
// point to ensure that the glyph remains within the target
// bounding box after rounding to font definition units.
// This is irrelevant here as we're not rounding, we're
// staying in f64 and heading straight to rendering.
// Apply prescribed alignment
group.y = self.aligned_y(group, metrics);
group.x = self.aligned_x(group, metrics, min_constraint_width);
// Transfer the scaling and alignment back to the glyph and return.
GlyphSize {
width: width_factor * glyph.width,
height: height_factor * glyph.height,
x: group.x + (group.width * self.relative_x),
y: group.y + (group.height * self.relative_y),
}
}
/// Return width and height scaling factors for this scaling group.
fn scale_factors(
&self,
group: GlyphSize,
metrics: &Metrics,
min_constraint_width: u8,
) -> (f64, f64) {
if self.size == ConstraintSize::None {
return (1.0, 1.0);
}
let multi_cell = min_constraint_width > 1;
let pad_width_factor = f64::from(min_constraint_width) - (self.pad_left + self.pad_right);
let pad_height_factor = 1.0 - (self.pad_bottom + self.pad_top);
let target_width = pad_width_factor * metrics.face_width;
let target_height = pad_height_factor
* match self.height {
Height::Cell => metrics.face_height,
// Like font-patcher, the icon constraint height depends on the
// constraint width. Unlike font-patcher, the multi-cell
// icon_height may be different from face_height due to the
// `adjust-icon-height` config option.
Height::Icon => {
if multi_cell {
metrics.icon_height
} else {
metrics.icon_height_single
}
}
};
let mut width_factor = target_width / group.width;
let mut height_factor = target_height / group.height;
match self.size {
ConstraintSize::None => unreachable!(),
ConstraintSize::Fit => {
// Scale down to fit if needed
height_factor = 1.0_f64.min(width_factor).min(height_factor);
width_factor = height_factor;
}
ConstraintSize::Cover => {
// Scale to cover
height_factor = width_factor.min(height_factor);
width_factor = height_factor;
}
ConstraintSize::FitCover1 => {
// Scale down to fit or up to cover at least one cell
// NOTE: This is similar to font_patcher's "pa" mode,
// however, font_patcher will only do the upscaling
// part if the constraint width is 1, resulting in
// some icons becoming smaller when the constraint
// width increases. You'd see icons shrinking when
// opening up a space after them. This makes no
// sense, so we've fixed the rule such that these
// icons are scaled to the same size for multi-cell
// constraints as they would be for single-cell.
height_factor = width_factor.min(height_factor);
if multi_cell && height_factor > 1.0 {
// Call back into this function with
// constraint width 1 to get single-cell scale
// factors. We use the height factor as width
// could have been modified by max_xy_ratio.
let (_, single_height_factor) = self.scale_factors(group, metrics, 1);
height_factor = 1.0_f64.max(single_height_factor);
}
width_factor = height_factor;
}
ConstraintSize::Stretch => {}
}
// Reduce aspect ratio if required
if let Some(ratio) = self.max_xy_ratio
&& group.width * width_factor > group.height * height_factor * ratio
{
width_factor = group.height * height_factor * ratio / group.width;
}
(width_factor, height_factor)
}
/// Return vertical bearing for aligning this group
fn aligned_y(&self, group: GlyphSize, metrics: &Metrics) -> f64 {
if self.size == ConstraintSize::None && self.align_vertical == Align::None {
// If we don't have any constraints affecting the vertical axis,
// we don't touch vertical alignment.
return group.y;
}
// We use face_height and offset by face_y, rather than
// using cell_height directly, to account for the asymmetry
// of the pixel cell around the face (a consequence of
// aligning the baseline with a pixel boundary rather than
// vertically centering the face).
let pad_bottom_dy = self.pad_bottom * metrics.face_height;
let pad_top_dy = self.pad_top * metrics.face_height;
let start_y = metrics.face_y + pad_bottom_dy;
let end_y = metrics.face_y + (metrics.face_height - group.height - pad_top_dy);
let center_y = (start_y + end_y) / 2.0;
match self.align_vertical {
// NOTE: Even if there is no prescribed alignment, we ensure
// that the group doesn't protrude outside the padded cell,
// since this is implied by every available size constraint. If
// the group is too high we fall back to centering, though if we
// hit the .none prong we always have self.size != .none, so
// this should never happen.
Align::None => {
if end_y < start_y {
center_y
} else {
start_y.max(group.y.min(end_y))
}
}
Align::Start => start_y,
Align::End => end_y,
Align::Center | Align::Center1 => center_y,
}
}
/// Return horizontal bearing for aligning this group
fn aligned_x(&self, group: GlyphSize, metrics: &Metrics, min_constraint_width: u8) -> f64 {
if self.size == ConstraintSize::None && self.align_horizontal == Align::None {
// If we don't have any constraints affecting the horizontal
// axis, we don't touch horizontal alignment.
return group.x;
}
// For multi-cell constraints, we align relative to the span
// from the left edge of the first cell to the right edge of
// the last face cell assuming it's left-aligned within the
// rounded and adjusted pixel cell. Any horizontal offset to
// center the face within the grid cell is the responsibility
// of the backend-specific rendering code, and should be done
// after applying constraints.
let full_face_span = metrics.face_width
+ f64::from((u32::from(min_constraint_width) - 1) * metrics.cell_width);
let pad_left_dx = self.pad_left * metrics.face_width;
let pad_right_dx = self.pad_right * metrics.face_width;
let start_x = pad_left_dx;
let end_x = full_face_span - group.width - pad_right_dx;
match self.align_horizontal {
// NOTE: Even if there is no prescribed alignment, we ensure
// that the glyph doesn't protrude outside the padded cell,
// since this is implied by every available size constraint. The
// left-side bound has priority if the group is too wide, though
// if we hit the .none prong we always have self.size != .none,
// so this should never happen.
Align::None => start_x.max(group.x.min(end_x)),
Align::Start => start_x,
Align::End => start_x.max(end_x),
Align::Center => start_x.max((start_x + end_x) / 2.0),
// NOTE: .center1 implements the font_patcher rule of centering
// in the first cell even for multi-cell constraints. Since glyphs
// are not allowed to protrude to the left, this results in the
// left-alignment like .start when the glyph is wider than a cell.
Align::Center1 => {
let end1_x = metrics.face_width - group.width - pad_right_dx;
start_x.max((start_x + end1_x) / 2.0)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn variation_id_wght_should_be_2003265652() {
let id = VariationId::new(b"wght");
assert_eq!(2003265652, id.as_u32());
assert_eq!(b"wght", &id.str());
}
#[test]
fn variation_id_slnt_should_be_1936486004() {
let id = VariationId {
a: b's',
b: b'l',
c: b'n',
d: b't',
};
assert_eq!(1936486004, id.as_u32());
assert_eq!(b"slnt", &id.str());
assert_eq!(VariationId::from_u32(1936486004), id);
}
#[test]
fn desired_size_pixels() {
let s = DesiredSize {
points: 12.0,
xdpi: 96,
ydpi: 144,
};
assert!((s.pixels() - 24.0).abs() < 1e-6);
assert_eq!(DesiredSize::new(10.0).ydpi, DEFAULT_DPI);
}
/// `comparison.expectApproxEqual` for f64: relative tolerance of
/// `sqrt(eps)` (Zig `std.math.approxEqRel`).
fn approx_eq(expected: f64, actual: f64) -> bool {
if expected == actual {
return true;
}
if expected.is_nan() || actual.is_nan() {
return false;
}
(expected - actual).abs() <= expected.abs().max(actual.abs()) * f64::EPSILON.sqrt()
}
fn expect_approx_equal(expected: GlyphSize, actual: GlyphSize) {
assert!(
approx_eq(expected.width, actual.width)
&& approx_eq(expected.height, actual.height)
&& approx_eq(expected.x, actual.x)
&& approx_eq(expected.y, actual.y),
"expected approximately {expected:?}, found {actual:?}"
);
}
#[test]
fn constraints() {
use crate::nerd_font_attributes::get_constraint;
// Hardcoded data matches metrics from CoreText at size 12 and DPI 96.
// Define grid metrics (matches font-family = JetBrains Mono)
let metrics = Metrics {
cell_width: 10,
cell_height: 22,
cell_baseline: 5,
underline_position: 19,
underline_thickness: 1,
strikethrough_position: 12,
strikethrough_thickness: 1,
overline_position: 0,
overline_thickness: 1,
box_thickness: 1,
cursor_thickness: 1,
cursor_height: 22,
icon_height: 21.12,
icon_height_single: 44.48 / 3.0,
face_width: 9.6,
face_height: 21.12,
face_y: 0.2,
};
// ASCII (no constraint).
{
let constraint = Constraint::NONE;
// BBox of 'x' from JetBrains Mono.
let glyph_x = GlyphSize {
width: 6.784,
height: 15.28,
x: 1.408,
y: 4.84,
};
// Any constraint width: do nothing.
for constraint_width in [1, 2] {
expect_approx_equal(
glyph_x,
constraint.constrain(glyph_x, &metrics, constraint_width),
);
}
}
// Symbol (same constraint as hardcoded in Renderer.addGlyph).
{
let constraint = Constraint {
size: ConstraintSize::Fit,
..Constraint::NONE
};
// BBox of '■' (0x25A0 black square) from Iosevka.
// NOTE: This glyph is designed to span two cells.
let glyph_25a0 = GlyphSize {
width: 10.272,
height: 10.272,
x: 2.864,
y: 5.304,
};
// Constraint width 1: scale down and shift to fit a single cell.
expect_approx_equal(
GlyphSize {
width: metrics.face_width,
height: metrics.face_width,
x: 0.0,
y: 5.64,
},
constraint.constrain(glyph_25a0, &metrics, 1),
);
// Constraint width 2: do nothing.
expect_approx_equal(glyph_25a0, constraint.constrain(glyph_25a0, &metrics, 2));
}
// Emoji (same constraint as hardcoded in SharedGrid.renderGlyph).
{
let constraint = Constraint {
size: ConstraintSize::Cover,
align_horizontal: Align::Center,
align_vertical: Align::Center,
pad_left: 0.025,
pad_right: 0.025,
..Constraint::NONE
};
// BBox of '🥸' (0x1F978) from Apple Color Emoji.
let glyph_1f978 = GlyphSize {
width: 20.0,
height: 20.0,
x: 0.46,
y: 1.0,
};
// Constraint width 2: scale to cover two cells with padding, center;
expect_approx_equal(
GlyphSize {
width: 18.72,
height: 18.72,
x: 0.44,
y: 1.4,
},
constraint.constrain(glyph_1f978, &metrics, 2),
);
}
// Nerd Font default.
{
let constraint = get_constraint(0xea61).unwrap();
// Verify that this is the constraint we expect.
assert_eq!(ConstraintSize::FitCover1, constraint.size);
assert_eq!(Height::Icon, constraint.height);
assert_eq!(Align::Center1, constraint.align_horizontal);
assert_eq!(Align::Center1, constraint.align_vertical);
// BBox of '' (0xEA61 nf-cod-lightbulb) from Symbols Only.
// NOTE: This icon is part of a group, so the
// constraint applies to a larger bounding box.
let glyph_ea61 = GlyphSize {
width: 9.015625,
height: 13.015625,
x: 3.015625,
y: 3.76525,
};
// Constraint width 1: scale and shift group to fit a single cell.
expect_approx_equal(
GlyphSize {
width: 7.2125,
height: 10.4125,
x: 0.8125,
y: 5.950695224719102,
},
constraint.constrain(glyph_ea61, &metrics, 1),
);
// Constraint width 2: no scaling; left-align and vertically center group.
expect_approx_equal(
GlyphSize {
width: glyph_ea61.width,
height: glyph_ea61.height,
x: 1.015625,
y: 4.7483690308988775,
},
constraint.constrain(glyph_ea61, &metrics, 2),
);
}
// Nerd Font stretch.
{
let constraint = get_constraint(0xe0c0).unwrap();
// Verify that this is the constraint we expect.
assert_eq!(ConstraintSize::Stretch, constraint.size);
assert_eq!(Height::Cell, constraint.height);
assert_eq!(Align::Start, constraint.align_horizontal);
assert_eq!(Align::Center1, constraint.align_vertical);
// BBox of ' ' (0xE0C0 nf-ple-flame_thick) from Symbols Only.
let glyph_e0c0 = GlyphSize {
width: 16.796875,
height: 16.46875,
x: -0.796875,
y: 1.7109375,
};
// Constraint width 1: stretch and position to exactly cover one cell.
expect_approx_equal(
GlyphSize {
width: f64::from(metrics.cell_width),
height: f64::from(metrics.cell_height),
x: 0.0,
y: 0.0,
},
constraint.constrain(glyph_e0c0, &metrics, 1),
);
// Constraint width 2: stretch and position to exactly cover two cells.
expect_approx_equal(
GlyphSize {
width: f64::from(2 * metrics.cell_width),
height: f64::from(metrics.cell_height),
x: 0.0,
y: 0.0,
},
constraint.constrain(glyph_e0c0, &metrics, 2),
);
}
}
}
+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]);
}
}
}
+438
View File
@@ -0,0 +1,438 @@
//! Minimal safe wrappers over the fontconfig C API (the subset used by
//! font discovery). Equivalent of `pkg/fontconfig` in the Zig tree.
//!
//! All raw FFI is confined to this module.
#![allow(unsafe_code)]
use std::ffi::{CStr, CString, c_int};
use std::ptr::{self, NonNull};
use std::sync::Arc;
use fontconfig_sys as sys;
use fontconfig_sys::constants as fc;
/// Errors from fontconfig calls (mirrors `fontconfig.Error`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("fontconfig: no match")]
NoMatch,
#[error("fontconfig: type mismatch")]
TypeMismatch,
#[error("fontconfig: no id")]
NoId,
#[error("fontconfig: out of memory")]
OutOfMemory,
#[error("fontconfig failed")]
FontconfigFailed,
}
fn result_to_error(r: u32) -> Result<(), Error> {
match r {
sys::FcResultMatch => Ok(()),
sys::FcResultNoMatch => Err(Error::NoMatch),
sys::FcResultTypeMismatch => Err(Error::TypeMismatch),
sys::FcResultNoId => Err(Error::NoId),
_ => Err(Error::OutOfMemory),
}
}
/// Font weight constants (`FC_WEIGHT_*`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Weight {
Bold = fc::FC_WEIGHT_BOLD,
}
/// Font slant constants (`FC_SLANT_*`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Slant {
Italic = fc::FC_SLANT_ITALIC,
}
/// Font spacing constants (`FC_*` spacing).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Spacing {
Mono = fc::FC_MONO,
}
/// Pattern property names.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Property {
Family,
Style,
FullName,
File,
Index,
Charset,
Lang,
Size,
Weight,
Slant,
Spacing,
}
impl Property {
fn cstr(self) -> &'static CStr {
match self {
Property::Family => fc::FC_FAMILY,
Property::Style => fc::FC_STYLE,
Property::FullName => fc::FC_FULLNAME,
Property::File => fc::FC_FILE,
Property::Index => fc::FC_INDEX,
Property::Charset => fc::FC_CHARSET,
Property::Lang => fc::FC_LANG,
Property::Size => fc::FC_SIZE,
Property::Weight => fc::FC_WEIGHT,
Property::Slant => fc::FC_SLANT,
Property::Spacing => fc::FC_SPACING,
}
}
}
/// Initialize fontconfig (safe to call multiple times and concurrently).
pub fn init() -> bool {
// SAFETY: FcInit has no preconditions.
unsafe { sys::FcInit() != 0 }
}
/// An owned `FcCharSet`.
pub struct CharSet(NonNull<sys::FcCharSet>);
impl CharSet {
pub fn create() -> CharSet {
// SAFETY: no preconditions; null (OOM) is handled.
let p = unsafe { sys::FcCharSetCreate() };
CharSet(NonNull::new(p).expect("FcCharSetCreate out of memory"))
}
pub fn add_char(&mut self, cp: u32) -> bool {
// SAFETY: self.0 is a valid, uniquely owned charset.
unsafe { sys::FcCharSetAddChar(self.0.as_ptr(), cp) != 0 }
}
pub fn has_char(&self, cp: u32) -> bool {
// SAFETY: self.0 is a valid charset.
unsafe { sys::FcCharSetHasChar(self.0.as_ptr(), cp) != 0 }
}
}
impl Drop for CharSet {
fn drop(&mut self) {
// SAFETY: we own the charset and destroy it once.
unsafe { sys::FcCharSetDestroy(self.0.as_ptr()) }
}
}
/// An owned `FcLangSet`.
pub struct LangSet(NonNull<sys::FcLangSet>);
impl LangSet {
pub fn create() -> LangSet {
// SAFETY: no preconditions.
let p = unsafe { sys::FcLangSetCreate() };
LangSet(NonNull::new(p).expect("FcLangSetCreate out of memory"))
}
pub fn add_lang(&mut self, lang: &str) -> bool {
let Ok(c) = CString::new(lang) else {
return false;
};
// SAFETY: valid langset and NUL-terminated string.
unsafe { sys::FcLangSetAdd(self.0.as_ptr(), c.as_ptr().cast()) != 0 }
}
pub fn has_lang(&self, lang: &str) -> bool {
has_lang_raw(self.0.as_ptr(), lang)
}
}
impl Drop for LangSet {
fn drop(&mut self) {
// SAFETY: we own the langset and destroy it once.
unsafe { sys::FcLangSetDestroy(self.0.as_ptr()) }
}
}
fn has_lang_raw(ls: *const sys::FcLangSet, lang: &str) -> bool {
let Ok(c) = CString::new(lang) else {
return false;
};
// SAFETY: `ls` is a valid langset (callers guarantee), string is NUL-terminated.
unsafe { sys::FcLangSetHasLang(ls, c.as_ptr().cast()) == sys::FcLangEqual }
}
/// An owned `FcPattern`.
pub struct Pattern(NonNull<sys::FcPattern>);
// SAFETY: patterns are never mutated after construction in this crate
// (only read through &self), and fontconfig patterns are plain data.
unsafe impl Send for Pattern {}
// SAFETY: see above; all &self methods are read-only.
unsafe impl Sync for Pattern {}
impl Pattern {
pub fn create() -> Pattern {
// SAFETY: no preconditions.
let p = unsafe { sys::FcPatternCreate() };
Pattern(NonNull::new(p).expect("FcPatternCreate out of memory"))
}
fn as_ptr(&self) -> *mut sys::FcPattern {
self.0.as_ptr()
}
pub fn add_string(&mut self, prop: Property, v: &str) -> bool {
let Ok(c) = CString::new(v) else {
return false;
};
// SAFETY: valid pattern; property and value are NUL-terminated (value is copied).
unsafe {
sys::FcPatternAddString(self.as_ptr(), prop.cstr().as_ptr(), c.as_ptr().cast()) != 0
}
}
pub fn add_integer(&mut self, prop: Property, v: i32) -> bool {
// SAFETY: valid pattern, NUL-terminated property.
unsafe { sys::FcPatternAddInteger(self.as_ptr(), prop.cstr().as_ptr(), v) != 0 }
}
pub fn add_charset(&mut self, prop: Property, cs: &CharSet) -> bool {
// SAFETY: valid pattern and charset (fontconfig takes its own reference).
unsafe { sys::FcPatternAddCharSet(self.as_ptr(), prop.cstr().as_ptr(), cs.0.as_ptr()) != 0 }
}
/// Get the string value at index `id`.
pub fn get_string(&self, prop: Property, id: u32) -> Result<String, Error> {
let mut out: *mut sys::FcChar8 = ptr::null_mut();
// SAFETY: valid pattern; out is a valid out pointer.
let r = unsafe {
sys::FcPatternGetString(
self.as_ptr(),
prop.cstr().as_ptr(),
id as c_int,
&raw mut out,
)
};
result_to_error(r)?;
if out.is_null() {
return Err(Error::NoMatch);
}
// SAFETY: fontconfig returned a NUL-terminated string owned by the pattern.
let s = unsafe { CStr::from_ptr(out.cast()) };
Ok(s.to_string_lossy().into_owned())
}
pub fn get_integer(&self, prop: Property, id: u32) -> Result<i32, Error> {
let mut out: c_int = 0;
// SAFETY: valid pattern; out is a valid out pointer.
let r = unsafe {
sys::FcPatternGetInteger(
self.as_ptr(),
prop.cstr().as_ptr(),
id as c_int,
&raw mut out,
)
};
result_to_error(r)?;
Ok(out)
}
/// Returns whether `cp` is in the charset property (index 0).
pub fn charset_has_char(&self, cp: u32) -> Result<bool, Error> {
let mut out: *mut sys::FcCharSet = ptr::null_mut();
// SAFETY: valid pattern; out is a valid out pointer.
let r = unsafe {
sys::FcPatternGetCharSet(
self.as_ptr(),
Property::Charset.cstr().as_ptr(),
0,
&raw mut out,
)
};
result_to_error(r)?;
if out.is_null() {
return Err(Error::NoMatch);
}
// SAFETY: the charset is owned by the pattern, which outlives this call.
Ok(unsafe { sys::FcCharSetHasChar(out, cp) != 0 })
}
/// Returns whether the langset property (index 0) equals `lang`.
pub fn langset_has_lang(&self, lang: &str) -> Result<bool, Error> {
let mut out: *mut sys::FcLangSet = ptr::null_mut();
// SAFETY: valid pattern; out is a valid out pointer.
let r = unsafe {
sys::FcPatternGetLangSet(
self.as_ptr(),
Property::Lang.cstr().as_ptr(),
0,
&raw mut out,
)
};
result_to_error(r)?;
if out.is_null() {
return Err(Error::NoMatch);
}
Ok(has_lang_raw(out, lang))
}
pub fn default_substitute(&mut self) {
// SAFETY: valid pattern.
unsafe { sys::FcDefaultSubstitute(self.as_ptr()) }
}
}
impl Drop for Pattern {
fn drop(&mut self) {
// SAFETY: we own the pattern reference and release it once.
unsafe { sys::FcPatternDestroy(self.as_ptr()) }
}
}
/// An owned `FcFontSet` as returned by `FcFontSort`.
pub struct FontSet(NonNull<sys::FcFontSet>);
impl FontSet {
pub fn len(&self) -> usize {
// SAFETY: valid font set.
let n = unsafe { (*self.0.as_ptr()).nfont };
usize::try_from(n).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Raw pattern pointer at `i`, borrowed from the set.
fn font(&self, i: usize) -> *mut sys::FcPattern {
assert!(i < self.len());
// SAFETY: bounds checked; `fonts` has `nfont` entries.
unsafe { *(*self.0.as_ptr()).fonts.add(i) }
}
}
impl Drop for FontSet {
fn drop(&mut self) {
// SAFETY: we own the set and destroy it once.
unsafe { sys::FcFontSetDestroy(self.0.as_ptr()) }
}
}
// SAFETY: the set is only read after construction.
unsafe impl Send for FontSet {}
// SAFETY: see above.
unsafe impl Sync for FontSet {}
/// An owned `FcConfig`.
pub struct Config(NonNull<sys::FcConfig>);
// SAFETY: fontconfig configs are internally reference counted and
// thread safe for the read operations we perform.
unsafe impl Send for Config {}
// SAFETY: see above.
unsafe impl Sync for Config {}
impl Config {
/// `FcInitLoadConfigAndFonts`.
pub fn init_load_config_and_fonts() -> Option<Config> {
// SAFETY: no preconditions.
NonNull::new(unsafe { sys::FcInitLoadConfigAndFonts() }).map(Config)
}
/// `FcConfigSubstituteWithPat(config, pat, NULL, FcMatchPattern)`.
pub fn substitute_with_pat_pattern(&self, pat: &mut Pattern) -> bool {
// SAFETY: valid config and pattern; null pattern-for-match is allowed.
unsafe {
sys::FcConfigSubstituteWithPat(
self.0.as_ptr(),
pat.as_ptr(),
ptr::null_mut(),
sys::FcMatchPattern,
) != 0
}
}
/// `FcFontSort`. Returns the sorted set when the result is a match.
pub fn font_sort(&self, pat: &mut Pattern, trim: bool) -> Result<FontSet, Error> {
let mut result: sys::FcResult = 0;
// SAFETY: valid config/pattern; null charset out-pointer is allowed.
let fs = unsafe {
sys::FcFontSort(
self.0.as_ptr(),
pat.as_ptr(),
c_int::from(trim),
ptr::null_mut(),
&raw mut result,
)
};
let fs = NonNull::new(fs).map(FontSet);
if result != sys::FcResultMatch {
return Err(Error::FontconfigFailed);
}
fs.ok_or(Error::FontconfigFailed)
}
/// `FcFontRenderPrepare(config, pat, set[i])`.
pub fn font_render_prepare(
&self,
pat: &mut Pattern,
set: &FontSet,
i: usize,
) -> Result<Pattern, Error> {
// SAFETY: valid config/pattern/font pattern; result is a new owned pattern.
let p = unsafe { sys::FcFontRenderPrepare(self.0.as_ptr(), pat.as_ptr(), set.font(i)) };
NonNull::new(p).map(Pattern).ok_or(Error::FontconfigFailed)
}
}
impl Drop for Config {
fn drop(&mut self) {
// SAFETY: we own the config reference and release it once.
unsafe { sys::FcConfigDestroy(self.0.as_ptr()) }
}
}
/// Shared handle to a config.
pub type SharedConfig = Arc<Config>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn langset_create() {
let fs = LangSet::create();
assert!(!fs.has_lang("und-zsye"));
}
#[test]
fn langset_has_lang_exact_match() {
let mut fs = LangSet::create();
assert!(fs.add_lang("en-US"));
assert!(fs.has_lang("en-US"));
let mut emoji = LangSet::create();
assert!(emoji.add_lang("und-zsye"));
assert!(emoji.has_lang("und-zsye"));
assert!(!emoji.has_lang("en-US"));
}
#[test]
fn charset_add_has() {
let mut cs = CharSet::create();
assert!(!cs.has_char('A' as u32));
assert!(cs.add_char('A' as u32));
assert!(cs.has_char('A' as u32));
}
#[test]
fn pattern_roundtrip() {
let mut p = Pattern::create();
assert!(p.add_string(Property::Family, "Foo"));
assert!(p.add_integer(Property::Index, 3));
assert_eq!("Foo", p.get_string(Property::Family, 0).unwrap());
assert_eq!(3, p.get_integer(Property::Index, 0).unwrap());
assert_eq!(Err(Error::NoMatch), p.get_string(Property::File, 0));
}
}
+71 -3
View File
@@ -1,14 +1,82 @@
//! Font engine. Currently contains the metrics, atlas, glyph and sprite-font
//! modules; the rest of the font engine (faces, shaping, collections, ...) is
//! ported later.
//! Font engine: metrics, atlas, sprite font, faces (FreeType), discovery
//! (fontconfig), collections, codepoint resolution, shaping (HarfBuzz) and
//! the shared glyph grid. Port of `ZIG/src/font/` for the Linux backend
//! (`fontconfig_freetype`).
#![cfg_attr(test, allow(clippy::unwrap_used))]
pub mod atlas;
pub mod backend;
pub mod codepoint_map;
pub mod codepoint_resolver;
pub mod collection;
pub mod config_types;
pub mod deferred_face;
pub mod discovery;
pub mod embedded;
pub mod face;
pub mod fontconfig;
pub mod glyph;
pub mod library;
pub mod metrics;
pub mod nerd_font_attributes;
pub mod opentype;
pub mod shape;
pub mod shaper;
pub mod shared_grid;
pub mod sprite;
pub use atlas::Atlas;
pub use backend::{Backend, OPTIONS as options};
pub use codepoint_map::CodepointMap;
pub use codepoint_resolver::CodepointResolver;
pub use collection::Collection;
pub use deferred_face::DeferredFace;
pub use discovery::{Descriptor, Discover};
pub use face::Face;
pub use glyph::Glyph;
pub use library::Library;
pub use metrics::Metrics;
pub use shared_grid::SharedGrid;
pub use sprite::{Face as SpriteFace, Sprite};
/// The styles that a family can take.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(u8)]
pub enum Style {
Regular = 0,
Bold = 1,
Italic = 2,
BoldItalic = 3,
}
impl Style {
/// All styles, in discriminant order (Zig: `std.meta.fields(Style)`).
pub const ALL: [Style; 4] = [
Style::Regular,
Style::Bold,
Style::Italic,
Style::BoldItalic,
];
}
/// The presentation for an emoji.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Presentation {
/// U+FE0E
Text = 0,
/// U+FE0F
Emoji = 1,
}
/// A FontIndex that can be used to use the sprite font directly.
pub const SPRITE_INDEX: collection::Index =
collection::Index::init_special(collection::Special::Sprite);
/// The default font size adjustment we use when loading fallback fonts.
///
/// TODO: Add user configuration for this instead of hard-coding it.
pub const DEFAULT_FALLBACK_ADJUSTMENT: collection::SizeAdjustment =
collection::SizeAdjustment::IcWidth;
+67
View File
@@ -0,0 +1,67 @@
//! A library represents the shared state that the underlying font
//! library implementation(s) require per-process. Port of `font/library.zig`
//! (the `FreetypeLibrary` variant).
use std::sync::{Arc, Mutex};
use crate::face::freetype::ft;
#[derive(Debug, thiserror::Error)]
pub enum LibraryError {
#[error("freetype error {0}")]
Freetype(i32),
}
/// Shared FreeType library state. Cheap to clone.
#[derive(Clone)]
pub struct Library {
pub(crate) inner: Arc<LibraryInner>,
}
pub(crate) struct LibraryInner {
pub(crate) lib: ft::Lib,
/// Mutex to be held any time the library is
/// being used to create or destroy a face.
pub(crate) mutex: Mutex<()>,
}
impl Library {
pub fn init() -> Result<Library, LibraryError> {
let lib = ft::Lib::new().map_err(LibraryError::Freetype)?;
Ok(Library {
inner: Arc::new(LibraryInner {
lib,
mutex: Mutex::new(()),
}),
})
}
/// Lock the library mutex (poisoning is ignored: the guarded state is
/// only FreeType's own).
pub(crate) fn lock(&self) -> std::sync::MutexGuard<'_, ()> {
self.inner
.mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
impl std::fmt::Debug for Library {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Library")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn init_and_clone() {
let lib = Library::init().unwrap();
let lib2 = lib.clone();
drop(lib);
drop(lib2.lock());
}
}
@@ -0,0 +1,3091 @@
//! This is a generated table, produced mechanically from
//! `font/nerd_font_attributes.zig` (itself generated by nerd_font_codegen.py).
//!
//! This file provides info extracted from the nerd fonts patcher script,
//! specifying the scaling/positioning attributes of various glyphs.
#![allow(clippy::excessive_precision)]
use crate::face::{Align, Constraint, ConstraintSize, Height};
/// Get the constraints for the provided codepoint.
pub fn get_constraint(cp: u32) -> Option<Constraint> {
let i = RANGES.partition_point(|r| r.1 < cp);
let r = RANGES.get(i)?;
if cp >= r.0 && cp <= r.1 {
Some(CONSTRAINTS[usize::from(r.2)])
} else {
None
}
}
/// Inclusive `(first, last, index into CONSTRAINTS)`, sorted and disjoint.
static RANGES: [(u32, u32, u16); 413] = [
(0x23fb, 0x23fe, 164),
(0x2630, 0x2630, 0),
(0x2665, 0x2665, 164),
(0x26a1, 0x26a1, 164),
(0x276c, 0x276d, 1),
(0x276e, 0x276f, 2),
(0x2770, 0x2771, 3),
(0x2b58, 0x2b58, 164),
(0xe000, 0xe00a, 164),
(0xe0a0, 0xe0a3, 4),
(0xe0b0, 0xe0b0, 5),
(0xe0b1, 0xe0b1, 6),
(0xe0b2, 0xe0b2, 7),
(0xe0b3, 0xe0b3, 8),
(0xe0b4, 0xe0b4, 9),
(0xe0b5, 0xe0b5, 10),
(0xe0b6, 0xe0b6, 11),
(0xe0b7, 0xe0b7, 12),
(0xe0b8, 0xe0b8, 13),
(0xe0b9, 0xe0b9, 14),
(0xe0ba, 0xe0ba, 15),
(0xe0bb, 0xe0bb, 16),
(0xe0bc, 0xe0bc, 13),
(0xe0bd, 0xe0bd, 14),
(0xe0be, 0xe0be, 15),
(0xe0bf, 0xe0bf, 16),
(0xe0c0, 0xe0c0, 17),
(0xe0c1, 0xe0c1, 18),
(0xe0c2, 0xe0c2, 19),
(0xe0c3, 0xe0c3, 20),
(0xe0c4, 0xe0c4, 21),
(0xe0c5, 0xe0c5, 22),
(0xe0c6, 0xe0c6, 23),
(0xe0c7, 0xe0c7, 24),
(0xe0c8, 0xe0c8, 17),
(0xe0ca, 0xe0ca, 19),
(0xe0cc, 0xe0cc, 25),
(0xe0cd, 0xe0cd, 26),
(0xe0ce, 0xe0ce, 27),
(0xe0cf, 0xe0cf, 4),
(0xe0d0, 0xe0d1, 27),
(0xe0d2, 0xe0d2, 28),
(0xe0d4, 0xe0d4, 29),
(0xe0d6, 0xe0d6, 30),
(0xe0d7, 0xe0d7, 31),
(0xe200, 0xe2a9, 164),
(0xe300, 0xe300, 32),
(0xe301, 0xe301, 33),
(0xe302, 0xe302, 34),
(0xe303, 0xe303, 35),
(0xe304, 0xe304, 36),
(0xe305, 0xe305, 37),
(0xe306, 0xe306, 38),
(0xe307, 0xe307, 39),
(0xe308, 0xe308, 40),
(0xe309, 0xe309, 41),
(0xe30a, 0xe30a, 42),
(0xe30b, 0xe30b, 43),
(0xe30c, 0xe30c, 44),
(0xe30d, 0xe30d, 45),
(0xe30e, 0xe30e, 46),
(0xe30f, 0xe30f, 47),
(0xe310, 0xe310, 48),
(0xe311, 0xe311, 49),
(0xe312, 0xe312, 50),
(0xe313, 0xe313, 51),
(0xe314, 0xe314, 52),
(0xe315, 0xe315, 53),
(0xe316, 0xe316, 54),
(0xe317, 0xe317, 55),
(0xe318, 0xe318, 56),
(0xe319, 0xe319, 57),
(0xe31a, 0xe31a, 58),
(0xe31b, 0xe31b, 59),
(0xe31c, 0xe31c, 60),
(0xe31d, 0xe31d, 61),
(0xe31e, 0xe31e, 62),
(0xe31f, 0xe31f, 63),
(0xe320, 0xe320, 64),
(0xe321, 0xe321, 65),
(0xe322, 0xe322, 66),
(0xe323, 0xe323, 67),
(0xe324, 0xe324, 68),
(0xe325, 0xe325, 69),
(0xe326, 0xe326, 70),
(0xe327, 0xe327, 71),
(0xe328, 0xe328, 72),
(0xe329, 0xe329, 73),
(0xe32a, 0xe32a, 74),
(0xe32b, 0xe32b, 75),
(0xe32c, 0xe32c, 76),
(0xe32d, 0xe32d, 77),
(0xe32e, 0xe32e, 78),
(0xe32f, 0xe32f, 79),
(0xe330, 0xe330, 80),
(0xe331, 0xe331, 81),
(0xe332, 0xe332, 82),
(0xe333, 0xe333, 83),
(0xe334, 0xe334, 84),
(0xe335, 0xe335, 85),
(0xe336, 0xe336, 86),
(0xe337, 0xe337, 87),
(0xe338, 0xe338, 88),
(0xe339, 0xe339, 89),
(0xe33a, 0xe33a, 90),
(0xe33b, 0xe33b, 91),
(0xe33c, 0xe33d, 92),
(0xe33e, 0xe33e, 93),
(0xe33f, 0xe33f, 94),
(0xe340, 0xe340, 95),
(0xe341, 0xe341, 96),
(0xe342, 0xe343, 164),
(0xe344, 0xe344, 97),
(0xe345, 0xe345, 98),
(0xe346, 0xe346, 164),
(0xe347, 0xe347, 99),
(0xe348, 0xe348, 164),
(0xe349, 0xe349, 100),
(0xe34a, 0xe34b, 164),
(0xe34c, 0xe34c, 101),
(0xe34d, 0xe34d, 102),
(0xe34e, 0xe34e, 164),
(0xe34f, 0xe34f, 103),
(0xe350, 0xe350, 164),
(0xe351, 0xe351, 104),
(0xe352, 0xe352, 105),
(0xe353, 0xe353, 106),
(0xe354, 0xe356, 107),
(0xe357, 0xe357, 108),
(0xe358, 0xe359, 107),
(0xe35a, 0xe35a, 109),
(0xe35b, 0xe35b, 110),
(0xe35c, 0xe35d, 164),
(0xe35e, 0xe35e, 58),
(0xe35f, 0xe35f, 42),
(0xe360, 0xe360, 111),
(0xe361, 0xe361, 71),
(0xe362, 0xe362, 112),
(0xe363, 0xe363, 113),
(0xe364, 0xe364, 114),
(0xe365, 0xe365, 46),
(0xe366, 0xe366, 115),
(0xe367, 0xe367, 73),
(0xe368, 0xe368, 164),
(0xe369, 0xe369, 116),
(0xe36a, 0xe36a, 164),
(0xe36b, 0xe36b, 117),
(0xe36c, 0xe36c, 118),
(0xe36d, 0xe36d, 119),
(0xe36e, 0xe36e, 120),
(0xe36f, 0xe36f, 121),
(0xe370, 0xe370, 122),
(0xe371, 0xe371, 123),
(0xe372, 0xe372, 124),
(0xe373, 0xe373, 125),
(0xe374, 0xe374, 126),
(0xe375, 0xe375, 127),
(0xe376, 0xe376, 128),
(0xe377, 0xe377, 129),
(0xe378, 0xe378, 130),
(0xe379, 0xe379, 131),
(0xe37a, 0xe37a, 132),
(0xe37b, 0xe37b, 133),
(0xe37c, 0xe37c, 164),
(0xe37d, 0xe37d, 134),
(0xe37e, 0xe37e, 135),
(0xe37f, 0xe37f, 136),
(0xe380, 0xe380, 137),
(0xe381, 0xe38d, 164),
(0xe38e, 0xe391, 138),
(0xe392, 0xe393, 139),
(0xe394, 0xe394, 138),
(0xe395, 0xe395, 140),
(0xe396, 0xe396, 141),
(0xe397, 0xe397, 142),
(0xe398, 0xe398, 143),
(0xe399, 0xe399, 144),
(0xe39a, 0xe39a, 145),
(0xe39b, 0xe39b, 146),
(0xe39c, 0xe39c, 147),
(0xe39d, 0xe39d, 148),
(0xe39e, 0xe39e, 149),
(0xe39f, 0xe39f, 150),
(0xe3a0, 0xe3a0, 151),
(0xe3a1, 0xe3a1, 152),
(0xe3a2, 0xe3a3, 153),
(0xe3a4, 0xe3a4, 154),
(0xe3a5, 0xe3a5, 153),
(0xe3a6, 0xe3a6, 154),
(0xe3a7, 0xe3a8, 153),
(0xe3a9, 0xe3a9, 108),
(0xe3aa, 0xe3aa, 155),
(0xe3ab, 0xe3ab, 156),
(0xe3ac, 0xe3ac, 157),
(0xe3ad, 0xe3ad, 158),
(0xe3ae, 0xe3ae, 159),
(0xe3af, 0xe3af, 160),
(0xe3b0, 0xe3b2, 161),
(0xe3b3, 0xe3b3, 160),
(0xe3b4, 0xe3b4, 164),
(0xe3b5, 0xe3bb, 160),
(0xe3bc, 0xe3c0, 164),
(0xe3c1, 0xe3c1, 162),
(0xe3c2, 0xe3c2, 163),
(0xe3c3, 0xe3e3, 164),
(0xe5fa, 0xe6b8, 164),
(0xe700, 0xe8ef, 164),
(0xea60, 0xea60, 164),
(0xea61, 0xea61, 165),
(0xea62, 0xea7c, 164),
(0xea7d, 0xea7d, 166),
(0xea7e, 0xea88, 164),
(0xea8a, 0xea8c, 164),
(0xea8f, 0xea98, 164),
(0xea99, 0xea99, 167),
(0xea9a, 0xea9a, 168),
(0xea9b, 0xea9b, 169),
(0xea9c, 0xea9c, 170),
(0xea9d, 0xea9d, 171),
(0xea9e, 0xea9f, 172),
(0xeaa0, 0xeaa0, 171),
(0xeaa1, 0xeaa1, 168),
(0xeaa2, 0xeaa2, 173),
(0xeaa3, 0xeab3, 164),
(0xeab4, 0xeab4, 174),
(0xeab5, 0xeab5, 175),
(0xeab6, 0xeab6, 176),
(0xeab7, 0xeab7, 177),
(0xeab8, 0xeac7, 164),
(0xeac9, 0xeac9, 164),
(0xeacc, 0xead3, 164),
(0xead4, 0xead5, 178),
(0xead6, 0xead6, 179),
(0xead7, 0xeb09, 164),
(0xeb0b, 0xeb42, 164),
(0xeb43, 0xeb43, 180),
(0xeb44, 0xeb4e, 164),
(0xeb50, 0xeb6d, 164),
(0xeb6e, 0xeb6e, 181),
(0xeb6f, 0xeb6f, 182),
(0xeb70, 0xeb70, 183),
(0xeb71, 0xeb71, 181),
(0xeb72, 0xeb89, 164),
(0xeb8a, 0xeb8a, 184),
(0xeb8b, 0xeb99, 164),
(0xeb9a, 0xeb9a, 185),
(0xeb9b, 0xebd4, 164),
(0xebd5, 0xebd5, 186),
(0xebd6, 0xebd6, 187),
(0xebd7, 0xec06, 164),
(0xec07, 0xec07, 188),
(0xec08, 0xec0a, 164),
(0xec0b, 0xec0b, 189),
(0xec0c, 0xec0c, 190),
(0xec0d, 0xec1e, 164),
(0xed00, 0xedff, 164),
(0xee00, 0xee00, 191),
(0xee01, 0xee01, 192),
(0xee02, 0xee02, 193),
(0xee03, 0xee03, 191),
(0xee04, 0xee04, 192),
(0xee05, 0xee05, 193),
(0xee06, 0xee06, 194),
(0xee07, 0xee07, 195),
(0xee08, 0xee08, 196),
(0xee09, 0xee09, 197),
(0xee0a, 0xee0a, 198),
(0xee0b, 0xee0b, 199),
(0xee0c, 0xefce, 164),
(0xf000, 0xf004, 164),
(0xf005, 0xf005, 200),
(0xf006, 0xf025, 164),
(0xf026, 0xf027, 201),
(0xf028, 0xf02a, 164),
(0xf02b, 0xf02b, 202),
(0xf02c, 0xf030, 164),
(0xf031, 0xf033, 203),
(0xf034, 0xf034, 164),
(0xf035, 0xf035, 204),
(0xf036, 0xf043, 164),
(0xf044, 0xf044, 205),
(0xf045, 0xf045, 164),
(0xf046, 0xf046, 206),
(0xf047, 0xf047, 164),
(0xf048, 0xf048, 207),
(0xf049, 0xf049, 208),
(0xf04a, 0xf04a, 207),
(0xf04b, 0xf04b, 209),
(0xf04c, 0xf04d, 210),
(0xf04e, 0xf04e, 207),
(0xf04f, 0xf04f, 211),
(0xf050, 0xf050, 208),
(0xf051, 0xf051, 207),
(0xf052, 0xf052, 212),
(0xf053, 0xf05f, 164),
(0xf060, 0xf061, 213),
(0xf062, 0xf062, 164),
(0xf063, 0xf063, 214),
(0xf064, 0xf076, 164),
(0xf077, 0xf077, 215),
(0xf078, 0xf078, 216),
(0xf079, 0xf07d, 164),
(0xf07e, 0xf07e, 217),
(0xf07f, 0xf088, 164),
(0xf089, 0xf089, 218),
(0xf08a, 0xf0a3, 164),
(0xf0a4, 0xf0a5, 219),
(0xf0a6, 0xf0d6, 164),
(0xf0d7, 0xf0d7, 220),
(0xf0d8, 0xf0d8, 221),
(0xf0d9, 0xf0d9, 222),
(0xf0da, 0xf0da, 223),
(0xf0db, 0xf0db, 164),
(0xf0dc, 0xf0dc, 224),
(0xf0dd, 0xf0dd, 225),
(0xf0de, 0xf0de, 226),
(0xf0df, 0xf0ff, 164),
(0xf100, 0xf101, 227),
(0xf102, 0xf102, 228),
(0xf103, 0xf103, 229),
(0xf104, 0xf105, 227),
(0xf106, 0xf107, 230),
(0xf108, 0xf12f, 164),
(0xf130, 0xf130, 231),
(0xf131, 0xf140, 164),
(0xf141, 0xf141, 232),
(0xf142, 0xf152, 164),
(0xf153, 0xf154, 206),
(0xf155, 0xf155, 164),
(0xf156, 0xf156, 233),
(0xf157, 0xf157, 234),
(0xf158, 0xf158, 206),
(0xf159, 0xf159, 235),
(0xf15a, 0xf174, 164),
(0xf175, 0xf175, 236),
(0xf176, 0xf176, 164),
(0xf177, 0xf178, 237),
(0xf179, 0xf181, 164),
(0xf182, 0xf182, 238),
(0xf183, 0xf220, 164),
(0xf221, 0xf221, 239),
(0xf222, 0xf222, 240),
(0xf223, 0xf223, 164),
(0xf224, 0xf226, 239),
(0xf227, 0xf227, 240),
(0xf228, 0xf228, 239),
(0xf229, 0xf229, 241),
(0xf22a, 0xf22a, 239),
(0xf22b, 0xf22b, 242),
(0xf22c, 0xf22c, 239),
(0xf22d, 0xf22d, 242),
(0xf22e, 0xf254, 164),
(0xf255, 0xf256, 243),
(0xf257, 0xf257, 244),
(0xf258, 0xf258, 245),
(0xf259, 0xf259, 164),
(0xf25a, 0xf25a, 243),
(0xf25b, 0xf25b, 246),
(0xf25c, 0xf381, 164),
(0xf400, 0xf415, 164),
(0xf416, 0xf416, 247),
(0xf417, 0xf423, 164),
(0xf424, 0xf424, 248),
(0xf425, 0xf430, 164),
(0xf431, 0xf431, 249),
(0xf432, 0xf432, 250),
(0xf433, 0xf433, 251),
(0xf434, 0xf434, 252),
(0xf435, 0xf437, 164),
(0xf438, 0xf438, 253),
(0xf439, 0xf43d, 164),
(0xf43e, 0xf43e, 254),
(0xf43f, 0xf442, 164),
(0xf443, 0xf443, 255),
(0xf444, 0xf445, 256),
(0xf446, 0xf449, 164),
(0xf44a, 0xf44a, 257),
(0xf44b, 0xf44b, 258),
(0xf44c, 0xf45b, 164),
(0xf45c, 0xf45c, 259),
(0xf45d, 0xf45f, 164),
(0xf460, 0xf460, 260),
(0xf461, 0xf461, 261),
(0xf462, 0xf466, 164),
(0xf467, 0xf467, 262),
(0xf468, 0xf46b, 164),
(0xf46c, 0xf46c, 263),
(0xf46d, 0xf46f, 164),
(0xf470, 0xf470, 264),
(0xf471, 0xf475, 164),
(0xf476, 0xf476, 265),
(0xf477, 0xf479, 164),
(0xf47a, 0xf47a, 266),
(0xf47b, 0xf47c, 267),
(0xf47d, 0xf47d, 268),
(0xf47e, 0xf47e, 269),
(0xf47f, 0xf48a, 164),
(0xf48b, 0xf48b, 270),
(0xf48c, 0xf492, 164),
(0xf493, 0xf493, 271),
(0xf494, 0xf499, 164),
(0xf49a, 0xf49a, 272),
(0xf49b, 0xf4c2, 164),
(0xf4c3, 0xf4c3, 256),
(0xf4c4, 0xf4ee, 164),
(0xf4ef, 0xf4ef, 273),
(0xf4f0, 0xf4f0, 274),
(0xf4f1, 0xf4f1, 275),
(0xf4f2, 0xf4f2, 273),
(0xf4f3, 0xf51c, 164),
(0xf51d, 0xf51d, 256),
(0xf51e, 0xf533, 164),
(0xf0001, 0xf1af0, 164),
];
static CONSTRAINTS: [Constraint; 276] = [
Constraint {
size: ConstraintSize::Cover,
height: Height::Icon,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
pad_left: 0.05,
pad_right: 0.05,
pad_top: 0.05,
pad_bottom: 0.05,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Cover,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7142857142857143,
relative_height: 0.8910614525139665,
relative_x: 0.1428571428571428,
relative_y: 0.0349162011173184,
pad_top: 0.15,
pad_bottom: 0.15,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Cover,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9885714285714285,
relative_height: 0.8910614525139665,
relative_x: 0.0057142857142857,
relative_y: 0.0125698324022346,
pad_top: 0.15,
pad_bottom: 0.15,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Cover,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
pad_top: 0.15,
pad_bottom: 0.15,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
pad_left: -0.03,
pad_right: -0.03,
pad_top: -0.005,
pad_bottom: -0.005,
max_xy_ratio: Some(0.7),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
max_xy_ratio: Some(0.7),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::End,
align_vertical: Align::Center1,
pad_left: -0.03,
pad_right: -0.03,
pad_top: -0.005,
pad_bottom: -0.005,
max_xy_ratio: Some(0.7),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::End,
align_vertical: Align::Center1,
max_xy_ratio: Some(0.7),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
pad_left: -0.03,
pad_right: -0.03,
pad_top: -0.005,
pad_bottom: -0.005,
max_xy_ratio: Some(0.59),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
max_xy_ratio: Some(0.5),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::End,
align_vertical: Align::Center1,
pad_left: -0.03,
pad_right: -0.03,
pad_top: -0.005,
pad_bottom: -0.005,
max_xy_ratio: Some(0.59),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::End,
align_vertical: Align::Center1,
max_xy_ratio: Some(0.5),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
pad_left: -0.025,
pad_right: -0.025,
pad_top: -0.005,
pad_bottom: -0.005,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::End,
align_vertical: Align::Center1,
pad_left: -0.025,
pad_right: -0.025,
pad_top: -0.005,
pad_bottom: -0.005,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::End,
align_vertical: Align::Center1,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
pad_left: -0.025,
pad_right: -0.025,
pad_top: -0.005,
pad_bottom: -0.005,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::End,
align_vertical: Align::Center1,
pad_left: -0.025,
pad_right: -0.025,
pad_top: -0.005,
pad_bottom: -0.005,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::End,
align_vertical: Align::Center1,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
max_xy_ratio: Some(0.86),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::End,
align_vertical: Align::Center1,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
max_xy_ratio: Some(0.86),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
max_xy_ratio: Some(0.78),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::End,
align_vertical: Align::Center1,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
max_xy_ratio: Some(0.78),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
pad_left: -0.01,
pad_right: -0.01,
pad_top: -0.005,
pad_bottom: -0.005,
max_xy_ratio: Some(0.85),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
max_xy_ratio: Some(0.865),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
pad_left: -0.01,
pad_right: -0.01,
pad_top: -0.005,
pad_bottom: -0.005,
max_xy_ratio: Some(0.7),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::End,
align_vertical: Align::Center1,
pad_left: -0.01,
pad_right: -0.01,
pad_top: -0.005,
pad_bottom: -0.005,
max_xy_ratio: Some(0.7),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
pad_left: -0.025,
pad_right: -0.025,
pad_top: -0.005,
pad_bottom: -0.005,
max_xy_ratio: Some(0.7),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::End,
align_vertical: Align::Center1,
pad_left: -0.025,
pad_right: -0.025,
pad_top: -0.005,
pad_bottom: -0.005,
max_xy_ratio: Some(0.7),
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8984375000000000,
relative_y: 0.0986328125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8798828125000000,
relative_y: 0.1171875000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7646484375000000,
relative_y: 0.2314453125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8789062500000000,
relative_y: 0.1171875000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9755859375000000,
relative_y: 0.0244140625000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9960937500000000,
relative_y: 0.0019531250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9863281250000000,
relative_y: 0.0097656250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9951171875000000,
relative_y: 0.0039062500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9785156250000000,
relative_y: 0.0195312500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9736328125000000,
relative_y: 0.0214843750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9648437500000000,
relative_y: 0.0302734375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8437500000000000,
relative_y: 0.1513671875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8027343750000000,
relative_y: 0.1835937500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7753906250000000,
relative_y: 0.1083984375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9833984375000000,
relative_y: 0.0166015625000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9716796875000000,
relative_y: 0.0263671875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6621093750000000,
relative_y: 0.0986328125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6425781250000000,
relative_y: 0.1171875000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5322265625000000,
relative_y: 0.2314453125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6416015625000000,
relative_y: 0.1181640625000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7382812500000000,
relative_y: 0.0195312500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6787109375000000,
relative_y: 0.1357421875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7480468750000000,
relative_y: 0.0097656250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7529296875000000,
relative_y: 0.0048828125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7314453125000000,
relative_y: 0.0263671875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7402343750000000,
relative_y: 0.0195312500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7294921875000000,
relative_y: 0.0283203125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6074218750000000,
relative_y: 0.1503906250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7363281250000000,
relative_y: 0.0224609375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7460937500000000,
relative_y: 0.0126953125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.2675781250000000,
relative_y: 0.3310546875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7363281250000000,
relative_y: 0.0986328125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7177734375000000,
relative_y: 0.1171875000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8085937500000000,
relative_y: 0.0253906250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7509765625000000,
relative_y: 0.0839843750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8281250000000000,
relative_y: 0.0097656250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8349609375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8154296875000000,
relative_y: 0.0214843750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8144531250000000,
relative_y: 0.0195312500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8076171875000000,
relative_y: 0.0273437500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6845703125000000,
relative_y: 0.1503906250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8173828125000000,
relative_y: 0.0175781250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8105468750000000,
relative_y: 0.0263671875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5175781250000000,
relative_y: 0.2421875000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6992187500000000,
relative_y: 0.1005859375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6787109375000000,
relative_y: 0.1201171875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5654296875000000,
relative_y: 0.2324218750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7714843750000000,
relative_y: 0.0273437500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7148437500000000,
relative_y: 0.0830078125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7919921875000000,
relative_y: 0.0097656250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7871093750000000,
relative_y: 0.0126953125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7714843750000000,
relative_y: 0.0263671875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7773437500000000,
relative_y: 0.0195312500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7714843750000000,
relative_y: 0.0283203125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6503906250000000,
relative_y: 0.1503906250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7753906250000000,
relative_y: 0.0234375000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7792968750000000,
relative_y: 0.0185546875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8445945945945946,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5283203125000000,
relative_y: 0.2324218750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5449218750000000,
relative_y: 0.2148437500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5273437500000000,
relative_y: 0.2324218750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.3293918918918919,
relative_y: 0.6706081081081081,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5200000000000000,
relative_y: 0.2707692307692308,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8307692307692308,
relative_y: 0.0861538461538462,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8327702702702703,
relative_y: 0.0050675675675676,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5307692307692308,
relative_y: 0.2092307692307692,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5332112630208333,
relative_y: 0.2040934244791667,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8307692307692308,
relative_y: 0.1246153846153846,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5307967032967034,
relative_y: 0.2615384615384616,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8659995118379302,
relative_y: 0.1340004881620698,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9890163534293386,
relative_y: 0.0002440810349036,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5751953125000000,
relative_y: 0.1142578125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6533203125000000,
relative_y: 0.1328125000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5215384615384615,
relative_y: 0.2846153846153846,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8308012820512821,
relative_y: 0.1230448717948718,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9935233160621761,
relative_y: 0.0025906735751295,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9961139896373057,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9935233160621761,
relative_y: 0.0012953367875648,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9987046632124352,
relative_y: 0.0012953367875648,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7695312500000000,
relative_y: 0.0302734375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9902343750000000,
relative_y: 0.0097656250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7900390625000000,
relative_y: 0.0097656250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8251953125000000,
relative_y: 0.0097656250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7832031250000000,
relative_y: 0.0166015625000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.4902343750000000,
relative_y: 0.2548828125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9333658774713205,
relative_y: 0.0266048328044911,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7076171875000000,
relative_y: 0.1083984375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8427734375000000,
relative_y: 0.0625000000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7529721467391304,
relative_y: 0.0956606657608696,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6835937500000000,
relative_y: 0.1250000000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8642578125000000,
relative_y: 0.0625000000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6103515625000000,
relative_y: 0.1933593750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7949218750000000,
relative_y: 0.0576171875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8652343750000000,
relative_y: 0.0058593750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.3154296875000000,
relative_y: 0.2861328125000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6772460937500000,
relative_y: 0.1303710937500000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6992187500000000,
relative_y: 0.1337890625000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7314453125000000,
relative_y: 0.1552734375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7314453125000000,
relative_y: 0.1542968750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5751953125000000,
relative_y: 0.1826171875000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5263671875000000,
relative_y: 0.2285156250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5751953125000000,
relative_y: 0.1835937500000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9003906250000000,
relative_y: 0.0957031250000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6015625000000000,
relative_y: 0.2324218750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5200000000000000,
relative_y: 0.2784615384615385,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5200000000000000,
relative_y: 0.2630769230769231,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.4990253411306043,
relative_height: 0.9987012987012988,
relative_x: 0.4996751137102014,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.4996751137102014,
relative_height: 0.9987012987012988,
relative_x: 0.4990253411306043,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5471085120207927,
relative_height: 0.9987012987012988,
relative_x: 0.4515919428200130,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5945419103313840,
relative_height: 0.9987012987012988,
relative_x: 0.4041585445094217,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6426250812215725,
relative_x: 0.3573749187784275,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6900584795321637,
relative_x: 0.3099415204678362,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7381416504223521,
relative_x: 0.2618583495776478,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7855750487329435,
relative_x: 0.2144249512670565,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9987004548408057,
relative_height: 0.9987012987012988,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8323586744639376,
relative_height: 0.9935064935064936,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7855750487329435,
relative_height: 0.9948051948051948,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7381416504223521,
relative_height: 0.9961038961038962,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6907082521117609,
relative_height: 0.9961038961038962,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6426250812215725,
relative_height: 0.9961038961038962,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5945419103313840,
relative_height: 0.9974025974025974,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.4990253411306043,
relative_height: 0.9987012987012988,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.4996751137102014,
relative_height: 0.9987012987012988,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9902343750000000,
relative_y: 0.0078125000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7900390625000000,
relative_y: 0.0058593750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8251953125000000,
relative_y: 0.0078125000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7519531250000000,
relative_y: 0.0068359375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6152343750000000,
relative_y: 0.2324218750000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9986072423398329,
relative_y: 0.0013927576601671,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9958217270194986,
relative_y: 0.0041782729805014,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6590187942396876,
relative_y: 0.1349768123016842,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7939956065413717,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7513020833333334,
relative_height: 0.9291573452647278,
relative_x: 0.0846354166666667,
relative_y: 0.0708426547352722,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8394854586129754,
relative_height: 0.8751387347391787,
relative_x: 0.0917225950782998,
relative_y: 0.0416204217536071,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9395973154362416,
relative_height: 0.4778024417314096,
relative_x: 0.0302013422818792,
relative_y: 0.2269700332963374,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7673378076062640,
relative_height: 0.8523862375138734,
relative_x: 0.1526845637583893,
relative_y: 0.0754716981132075,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8590604026845637,
relative_height: 0.7613762486126526,
relative_x: 0.0721476510067114,
relative_y: 0.0871254162042175,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8590604026845637,
relative_height: 0.7574916759156493,
relative_x: 0.0721476510067114,
relative_y: 0.0832408435072142,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.4082774049217002,
relative_height: 0.5077691453940066,
relative_x: 0.2863534675615212,
relative_y: 0.2763596004439512,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5117449664429530,
relative_height: 0.4051054384017758,
relative_x: 0.2136465324384788,
relative_y: 0.3068812430632630,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8061116965226555,
relative_height: 0.9438247156716689,
relative_x: 0.0679662802950474,
relative_y: 0.0147523709167545,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9945482866043613,
relative_height: 0.5264797507788161,
relative_y: 0.2024922118380062,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5264797507788161,
relative_height: 0.9945482866043613,
relative_x: 0.2024922118380062,
relative_y: 0.0054517133956386,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5264797507788161,
relative_height: 0.9945482866043613,
relative_x: 0.2710280373831775,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9945482866043613,
relative_height: 0.5264797507788161,
relative_x: 0.0054517133956386,
relative_y: 0.2710280373831775,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7069825436408977,
relative_x: 0.1483790523690773,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8780760626398211,
relative_y: 0.0687919463087248,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7335766423357665,
relative_height: 0.9996188152778837,
relative_x: 0.1991657977059437,
relative_y: 0.0003811847221163,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.4954604409857328,
relative_y: 0.2522697795071336,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.4973958333333333,
relative_x: 0.2493489583333333,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.4973958333333333,
relative_height: 0.9961089494163424,
relative_x: 0.2493489583333333,
relative_y: 0.0038910505836576,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.3468834688346883,
relative_height: 0.3353615785256410,
relative_x: 0.2642276422764228,
relative_y: 0.3313050881410256,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8740779768177028,
relative_height: 0.9438247156716689,
relative_x: 0.0679662802950474,
relative_y: 0.0147523709167545,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9322210636079249,
relative_height: 0.9318897917604415,
relative_y: 0.0681102082395584,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9996446423917936,
relative_y: 0.0003553576082064,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.3495911047345768,
relative_height: 0.3355179398148149,
relative_x: 0.2615335565120357,
relative_y: 0.3311487268518519,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9327424400417101,
relative_height: 0.9996188152778837,
relative_y: 0.0003811847221163,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8008342022940563,
relative_x: 0.1991657977059437,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::End,
align_vertical: Align::Center1,
relative_width: 0.8681172291296625,
relative_height: 0.8626692456479691,
relative_x: 0.1314387211367673,
relative_y: 0.0686653771760155,
pad_left: -0.025,
pad_right: -0.025,
pad_top: -0.005,
pad_bottom: -0.005,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8626692456479691,
relative_y: 0.0686653771760155,
pad_left: -0.05,
pad_right: -0.05,
pad_top: -0.005,
pad_bottom: -0.005,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Stretch,
max_constraint_width: 1,
align_horizontal: Align::Start,
align_vertical: Align::Center1,
relative_width: 0.8685612788632326,
relative_height: 0.8626692456479691,
relative_y: 0.0686653771760155,
pad_left: -0.025,
pad_right: -0.025,
pad_top: -0.005,
pad_bottom: -0.005,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Cover,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7059415911379657,
relative_height: 0.2234524408656266,
relative_x: 0.1470292044310171,
relative_y: 0.7765475591343735,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Cover,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5000000000000000,
relative_height: 0.7498741821841973,
relative_x: 0.5000000000000000,
relative_y: 0.2501258178158027,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Cover,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6299093655589124,
relative_height: 0.8535480624056366,
relative_x: 0.3700906344410876,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Cover,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.4997483643683945,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Cover,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6299093655589124,
relative_height: 0.8535480624056366,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::Cover,
max_constraint_width: 1,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5000000000000000,
relative_height: 0.7498741821841973,
relative_y: 0.2501258178158027,
pad_left: 0.015,
pad_right: 0.015,
pad_top: 0.015,
pad_bottom: 0.015,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9999664113932554,
relative_y: 0.0000335886067446,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9786184354605580,
relative_y: 0.0103951316192896,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9758052740827267,
relative_y: 0.0238869355863696,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9987922705314010,
relative_y: 0.0006038647342995,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9989935587761675,
relative_y: 0.0004025764895330,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9925925925925926,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8751322751322751,
relative_y: 0.0624338624338624,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8577706898990622,
relative_y: 0.0711892586341537,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8579450878868969,
relative_y: 0.0710148606463189,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9997041418532618,
relative_y: 0.0002958581467381,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8572940020656472,
relative_y: 0.0713404035569438,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7138835298072554,
relative_y: 0.1433479295317200,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9999748091795350,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8567975830815709,
relative_y: 0.0719033232628399,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9987915407854985,
relative_y: 0.0006042296072508,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5700483091787439,
relative_y: 0.2862318840579710,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5700483091787439,
relative_y: 0.1437198067632850,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.4989429175475687,
relative_y: 0.2505285412262157,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9998488512696494,
relative_y: 0.0001511487303507,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7502645502645503,
relative_y: 0.1248677248677249,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.4281400966183575,
relative_y: 0.2053140096618357,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.4281400966183575,
relative_y: 0.3472222222222222,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7140772371750631,
relative_y: 0.1333462732919255,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7140396210163651,
relative_y: 0.1333838894506235,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
relative_height: 0.4275362318840580,
relative_y: 0.0012077294685990,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
relative_height: 0.4287439613526570,
relative_y: 0.5712560386473430,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8573155985489722,
relative_y: 0.0713422007255139,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9286577992744861,
relative_y: 0.0713422007255139,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9286577992744861,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.5000000000000000,
relative_y: 0.2853688029020556,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9998602571268865,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.2593984962406015,
relative_y: 0.3696741854636592,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8752505446623093,
relative_y: 0.0623155929038282,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8756468797564688,
relative_y: 0.0624338624338624,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8756067947646895,
relative_y: 0.0623492063492063,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9989423585404548,
relative_y: 0.0005288207297726,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6250661025912215,
relative_y: 0.1877313590692755,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9998046921689268,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9994854643684076,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.8746819883943630,
relative_y: 0.0624017379870223,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9370837263813853,
relative_y: 0.0624017379870223,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6874767744332962,
relative_y: 0.1560043449675557,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9993997599039616,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7810124049619848,
relative_y: 0.0935945806894186,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.7498142113988452,
relative_y: 0.1247927742525582,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.9975006099019084,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_height: 0.6090604026845637,
relative_y: 0.2119686800894855,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5019531250000000,
relative_height: 0.5755033557046980,
relative_x: 0.2480468750000000,
relative_y: 0.2108501118568233,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6240234375000000,
relative_height: 0.7695749440715883,
relative_x: 0.2031250000000000,
relative_y: 0.1420581655480984,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6718750000000000,
relative_height: 0.7147651006711410,
relative_x: 0.1875000000000000,
relative_y: 0.1610738255033557,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6240234375000000,
relative_height: 0.7695749440715883,
relative_x: 0.2041015625000000,
relative_y: 0.0883668903803132,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6718750000000000,
relative_height: 0.7147651006711410,
relative_x: 0.1406250000000000,
relative_y: 0.1599552572706935,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.2436523437500000,
relative_height: 0.4560546875000000,
relative_x: 0.3813476562500000,
relative_y: 0.2719726562500000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5029296875000000,
relative_height: 0.5755033557046980,
relative_x: 0.2500000000000000,
relative_y: 0.2136465324384788,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7500000000000000,
relative_x: 0.1250000000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5000000000000000,
relative_height: 0.5000000000000000,
relative_x: 0.2500000000000000,
relative_y: 0.2500000000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.2436523437500000,
relative_height: 0.4560546875000000,
relative_x: 0.3750000000000000,
relative_y: 0.2719726562500000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.4560546875000000,
relative_height: 0.2436523437500000,
relative_x: 0.2719726562500000,
relative_y: 0.3188476562500000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5019531250000000,
relative_height: 0.5749440715883669,
relative_x: 0.2480468750000000,
relative_y: 0.2114093959731544,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.3593750000000000,
relative_height: 0.6240234375000000,
relative_x: 0.3750000000000000,
relative_y: 0.1884765625000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6237816764132553,
relative_height: 0.9988851727982163,
relative_x: 0.1881091617933723,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5639648437500000,
relative_height: 0.5649414062500000,
relative_x: 0.2187500000000000,
relative_y: 0.2177734375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5039062500000000,
relative_height: 0.5771812080536913,
relative_x: 0.2490234375000000,
relative_y: 0.2091722595078300,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9926757812500000,
relative_height: 0.2690429687500000,
relative_y: 0.6865234375000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8732325694783033,
relative_x: 0.0633837152608484,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.5843079922027290,
relative_height: 0.9509476031215162,
relative_x: 0.2066276803118908,
relative_y: 0.0234113712374582,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.6250000000000000,
relative_height: 0.3593750000000000,
relative_x: 0.1875000000000000,
relative_y: 0.3281250000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.3593750000000000,
relative_height: 0.6240234375000000,
relative_x: 0.2656250000000000,
relative_y: 0.1875000000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.4560546875000000,
relative_height: 0.2436523437500000,
relative_x: 0.2719726562500000,
relative_y: 0.3750000000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7187500000000000,
relative_height: 0.0937500000000000,
relative_x: 0.1250000000000000,
relative_y: 0.4687500000000000,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8313840155945419,
relative_height: 0.9509476031215162,
relative_x: 0.0843079922027290,
relative_y: 0.0234113712374582,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.8727450024378351,
relative_x: 0.0633837152608484,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.7142857142857143,
relative_x: 0.1428571428571428,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9642857142857143,
relative_height: 0.7407407407407407,
relative_y: 0.1111111111111111,
..Constraint::NONE
},
Constraint {
size: ConstraintSize::FitCover1,
height: Height::Icon,
align_horizontal: Align::Center1,
align_vertical: Align::Center1,
relative_width: 0.9642857142857143,
relative_height: 0.7407407407407407,
relative_x: 0.0357142857142857,
relative_y: 0.1111111111111111,
..Constraint::NONE
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_is_sorted_and_disjoint() {
for w in RANGES.windows(2) {
assert!(w[0].1 < w[1].0);
}
for r in &RANGES {
assert!(r.0 <= r.1);
assert!(usize::from(r.2) < CONSTRAINTS.len());
}
}
#[test]
fn lookups() {
let c = get_constraint(0xea61).unwrap();
assert_eq!(c.size, ConstraintSize::FitCover1);
assert!(get_constraint(0x41).is_none());
assert!(get_constraint(0x2630).is_some());
assert!(get_constraint(0x276d).is_some());
assert!(get_constraint(0x276e - 0x100).is_none());
assert!(get_constraint(0xf4f1).is_some());
assert!(get_constraint(0xf4f3).is_some());
}
}
+121
View File
@@ -0,0 +1,121 @@
//! Font Header Table (`head`). Port of `font/opentype/head.zig`.
//!
//! References:
//! - https://learn.microsoft.com/en-us/typography/opentype/spec/head
//!
//! Field names are in camelCase to match names in the spec.
#![allow(non_snake_case)]
use super::sfnt::Fixed;
use super::{EndOfStream, Reader};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Head {
/// Major version number of the font header table — set to 1.
pub majorVersion: u16,
/// Minor version number of the font header table — set to 0.
pub minorVersion: u16,
/// Set by font manufacturer.
pub fontRevision: Fixed,
/// To compute: set it to 0, sum the entire font as uint32, then store
/// 0xB1B0AFBA - sum.
pub checksumAdjustment: u32,
/// Set to 0x5F0F3CF5.
pub magicNumber: u32,
/// Bit flags (see the OpenType spec for bit meanings).
pub flags: u16,
/// Set to a value from 16 to 16384.
pub unitsPerEm: u16,
/// Seconds since 12:00 midnight, January 1st, 1904, UTC.
pub created: i64,
/// Seconds since 12:00 midnight, January 1st, 1904, UTC.
pub modified: i64,
/// Minimum x coordinate across all glyph bounding boxes.
pub xMin: i16,
/// Minimum y coordinate across all glyph bounding boxes.
pub yMin: i16,
/// Maximum x coordinate across all glyph bounding boxes.
pub xMax: i16,
/// Maximum y coordinate across all glyph bounding boxes.
pub yMax: i16,
/// Bit 0: Bold; 1: Italic; 2: Underline; 3: Outline; 4: Shadow;
/// 5: Condensed; 6: Extended.
pub macStyle: u16,
/// Smallest readable size in pixels.
pub lowestRecPPEM: u16,
/// Deprecated (Set to 2).
pub fontDirectionHint: i16,
/// 0 for short offsets (Offset16), 1 for long (Offset32).
pub indexToLocFormat: i16,
/// 0 for current format.
pub glyphDataFormat: i16,
}
impl Head {
/// Parse the table from raw data.
pub fn init(data: &[u8]) -> Result<Head, EndOfStream> {
let mut r = Reader::new(data);
Ok(Head {
majorVersion: r.u16()?,
minorVersion: r.u16()?,
fontRevision: Fixed(r.i32()?),
checksumAdjustment: r.u32()?,
magicNumber: r.u32()?,
flags: r.u16()?,
unitsPerEm: r.u16()?,
created: r.i64()?,
modified: r.i64()?,
xMin: r.i16()?,
yMin: r.i16()?,
xMax: r.i16()?,
yMax: r.i16()?,
macStyle: r.u16()?,
lowestRecPPEM: r.u16()?,
fontDirectionHint: r.i16()?,
indexToLocFormat: r.i16()?,
glyphDataFormat: r.i16()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedded;
use crate::opentype::sfnt::Sfnt;
#[test]
fn head() {
let font = Sfnt::init(embedded::JULIA_MONO).unwrap();
let table = font.get_table(b"head").unwrap();
let head = Head::init(table).unwrap();
assert_eq!(
Head {
majorVersion: 1,
minorVersion: 0,
fontRevision: Fixed::from_f64(0.05499267578125),
checksumAdjustment: 1007668681,
magicNumber: 1594834165,
flags: 7,
unitsPerEm: 2000,
created: 3797757830,
modified: 3797760444,
xMin: -1000,
yMin: -1058,
xMax: 3089,
yMax: 2400,
macStyle: 0,
lowestRecPPEM: 7,
fontDirectionHint: 2,
indexToLocFormat: 1,
glyphDataFormat: 0,
},
head
);
}
#[test]
fn head_too_short() {
assert_eq!(Head::init(&[0; 10]), Err(EndOfStream));
}
}
+105
View File
@@ -0,0 +1,105 @@
//! Horizontal Header Table (`hhea`). Port of `font/opentype/hhea.zig`.
//!
//! References:
//! - https://learn.microsoft.com/en-us/typography/opentype/spec/hhea
#![allow(non_snake_case)]
use super::{EndOfStream, Reader};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Hhea {
/// Major version number of the horizontal header table — set to 1.
pub majorVersion: u16,
/// Minor version number of the horizontal header table — set to 0.
pub minorVersion: u16,
/// Typographic ascent (design units).
pub ascender: i16,
/// Typographic descent (design units).
pub descender: i16,
/// Typographic line gap (design units).
pub lineGap: i16,
/// Maximum advance width value in 'hmtx' table.
pub advanceWidthMax: u16,
/// Minimum left sidebearing value in 'hmtx' table.
pub minLeftSideBearing: i16,
/// Minimum right sidebearing value.
pub minRightSideBearing: i16,
/// Max(lsb + (xMax - xMin)).
pub xMaxExtent: i16,
/// Used to calculate the slope of the cursor (rise/run).
pub caretSlopeRise: i16,
pub caretSlopeRun: i16,
/// Set to 0 for non-slanted fonts.
pub caretOffset: i16,
pub reserved0: i16,
pub reserved1: i16,
pub reserved2: i16,
pub reserved3: i16,
/// 0 for current format.
pub metricDataFormat: i16,
/// Number of hMetric entries in 'hmtx' table.
pub numberOfHMetrics: u16,
}
impl Hhea {
pub fn init(data: &[u8]) -> Result<Hhea, EndOfStream> {
let mut r = Reader::new(data);
Ok(Hhea {
majorVersion: r.u16()?,
minorVersion: r.u16()?,
ascender: r.i16()?,
descender: r.i16()?,
lineGap: r.i16()?,
advanceWidthMax: r.u16()?,
minLeftSideBearing: r.i16()?,
minRightSideBearing: r.i16()?,
xMaxExtent: r.i16()?,
caretSlopeRise: r.i16()?,
caretSlopeRun: r.i16()?,
caretOffset: r.i16()?,
reserved0: r.i16()?,
reserved1: r.i16()?,
reserved2: r.i16()?,
reserved3: r.i16()?,
metricDataFormat: r.i16()?,
numberOfHMetrics: r.u16()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedded;
use crate::opentype::sfnt::Sfnt;
#[test]
fn hhea() {
let font = Sfnt::init(embedded::JULIA_MONO).unwrap();
let table = font.get_table(b"hhea").unwrap();
let hhea = Hhea::init(table).unwrap();
assert_eq!(
Hhea {
majorVersion: 1,
minorVersion: 0,
ascender: 1900,
descender: -450,
lineGap: 0,
advanceWidthMax: 1200,
minLeftSideBearing: -1000,
minRightSideBearing: -1889,
xMaxExtent: 3089,
caretSlopeRise: 1,
caretSlopeRun: 0,
caretOffset: 0,
reserved0: 0,
reserved1: 0,
reserved2: 0,
reserved3: 0,
metricDataFormat: 0,
numberOfHMetrics: 2,
},
hhea
);
}
}
+57
View File
@@ -0,0 +1,57 @@
//! OpenType table parsing. Port of `font/opentype.zig` and `font/opentype/*`.
pub mod head;
pub mod hhea;
pub mod os2;
pub mod post;
pub mod sfnt;
pub mod svg;
pub use head::Head;
pub use hhea::Hhea;
pub use os2::OS2;
pub use post::Post;
pub use svg::SVG;
/// Error returned when a table is too short (Zig: `error.EndOfStream`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("end of stream")]
pub struct EndOfStream;
/// Minimal big-endian cursor used by the table parsers.
#[derive(Debug, Clone)]
pub(crate) struct Reader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
pub(crate) fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
pub(crate) fn bytes<const N: usize>(&mut self) -> Result<[u8; N], EndOfStream> {
let end = self.pos.checked_add(N).ok_or(EndOfStream)?;
let s = self.data.get(self.pos..end).ok_or(EndOfStream)?;
self.pos = end;
let mut out = [0u8; N];
out.copy_from_slice(s);
Ok(out)
}
pub(crate) fn u16(&mut self) -> Result<u16, EndOfStream> {
Ok(u16::from_be_bytes(self.bytes()?))
}
pub(crate) fn i16(&mut self) -> Result<i16, EndOfStream> {
Ok(i16::from_be_bytes(self.bytes()?))
}
pub(crate) fn u32(&mut self) -> Result<u32, EndOfStream> {
Ok(u32::from_be_bytes(self.bytes()?))
}
pub(crate) fn i32(&mut self) -> Result<i32, EndOfStream> {
Ok(i32::from_be_bytes(self.bytes()?))
}
pub(crate) fn i64(&mut self) -> Result<i64, EndOfStream> {
Ok(i64::from_be_bytes(self.bytes()?))
}
}
+286
View File
@@ -0,0 +1,286 @@
//! OS/2 and Windows Metrics Table. Port of `font/opentype/os2.zig`.
//!
//! The Zig version has one extern struct per table version and a merged
//! `OS2` struct with optional trailing fields; here the per-version structs
//! are replaced by a single sequential parser that stops at the end of the
//! version-specific layout.
//!
//! References:
//! - https://learn.microsoft.com/en-us/typography/opentype/spec/os2
#![allow(non_snake_case)]
use super::{EndOfStream, Reader};
/// Font selection flags (`fsSelection`), bit 0 is the LSB.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FSSelection {
/// Font contains italic or oblique glyphs.
pub italic: bool,
/// Glyphs are underscored.
pub underscore: bool,
/// Glyphs have their foreground and background reversed.
pub negative: bool,
/// Outline (hollow) glyphs.
pub outlined: bool,
/// Glyphs are overstruck.
pub strikeout: bool,
/// Glyphs are emboldened.
pub bold: bool,
/// Glyphs are in the standard weight/style for the font.
pub regular: bool,
/// If set, it is strongly recommended that applications use
/// OS/2.sTypoAscender - OS/2.sTypoDescender + OS/2.sTypoLineGap as the
/// default line spacing for this font.
pub use_typo_metrics: bool,
/// The font has 'name' table strings consistent with a weight/width/slope
/// family without requiring use of name IDs 21 and 22.
pub wws: bool,
/// Font contains oblique glyphs.
pub oblique: bool,
}
impl FSSelection {
pub fn from_u16(v: u16) -> Self {
let b = |n: u32| v & (1 << n) != 0;
Self {
italic: b(0),
underscore: b(1),
negative: b(2),
outlined: b(3),
strikeout: b(4),
bold: b(5),
regular: b(6),
use_typo_metrics: b(7),
wws: b(8),
oblique: b(9),
}
}
pub fn to_u16(self) -> u16 {
let bits = [
self.italic,
self.underscore,
self.negative,
self.outlined,
self.strikeout,
self.bold,
self.regular,
self.use_typo_metrics,
self.wws,
self.oblique,
];
bits.iter()
.enumerate()
.fold(0u16, |acc, (i, &set)| acc | (u16::from(set) << i))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum Os2Error {
#[error("end of stream")]
EndOfStream,
#[error("OS/2 version not supported")]
OS2VersionNotSupported,
}
impl From<EndOfStream> for Os2Error {
fn from(_: EndOfStream) -> Self {
Os2Error::EndOfStream
}
}
/// Merged view over all OS/2 table versions; fields that only exist in
/// later versions are `Option`s.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OS2 {
pub version: u16,
pub xAvgCharWidth: i16,
pub usWeightClass: u16,
pub usWidthClass: u16,
pub fsType: u16,
pub ySubscriptXSize: i16,
pub ySubscriptYSize: i16,
pub ySubscriptXOffset: i16,
pub ySubscriptYOffset: i16,
pub ySuperscriptXSize: i16,
pub ySuperscriptYSize: i16,
pub ySuperscriptXOffset: i16,
pub ySuperscriptYOffset: i16,
pub yStrikeoutSize: i16,
pub yStrikeoutPosition: i16,
pub sFamilyClass: i16,
pub panose: [u8; 10],
pub ulUnicodeRange1: u32,
pub ulUnicodeRange2: u32,
pub ulUnicodeRange3: u32,
pub ulUnicodeRange4: u32,
pub achVendID: [u8; 4],
pub fsSelection: FSSelection,
pub usFirstCharIndex: u16,
pub usLastCharIndex: u16,
pub sTypoAscender: i16,
pub sTypoDescender: i16,
pub sTypoLineGap: i16,
pub usWinAscent: u16,
pub usWinDescent: u16,
// Version 1+
pub ulCodePageRange1: Option<u32>,
pub ulCodePageRange2: Option<u32>,
// Version 2+
pub sxHeight: Option<i16>,
pub sCapHeight: Option<i16>,
pub usDefaultChar: Option<u16>,
pub usBreakChar: Option<u16>,
pub usMaxContext: Option<u16>,
// Version 5
pub usLowerOpticalPointSize: Option<u16>,
pub usUpperOpticalPointSize: Option<u16>,
}
impl OS2 {
pub fn init(data: &[u8]) -> Result<OS2, Os2Error> {
let mut r = Reader::new(data);
let version = r.u16()?;
if version > 5 {
return Err(Os2Error::OS2VersionNotSupported);
}
let mut t = OS2 {
version,
xAvgCharWidth: r.i16()?,
usWeightClass: r.u16()?,
usWidthClass: r.u16()?,
fsType: r.u16()?,
ySubscriptXSize: r.i16()?,
ySubscriptYSize: r.i16()?,
ySubscriptXOffset: r.i16()?,
ySubscriptYOffset: r.i16()?,
ySuperscriptXSize: r.i16()?,
ySuperscriptYSize: r.i16()?,
ySuperscriptXOffset: r.i16()?,
ySuperscriptYOffset: r.i16()?,
yStrikeoutSize: r.i16()?,
yStrikeoutPosition: r.i16()?,
sFamilyClass: r.i16()?,
panose: r.bytes()?,
ulUnicodeRange1: r.u32()?,
ulUnicodeRange2: r.u32()?,
ulUnicodeRange3: r.u32()?,
ulUnicodeRange4: r.u32()?,
achVendID: r.bytes()?,
fsSelection: FSSelection::from_u16(r.u16()?),
usFirstCharIndex: r.u16()?,
usLastCharIndex: r.u16()?,
sTypoAscender: r.i16()?,
sTypoDescender: r.i16()?,
sTypoLineGap: r.i16()?,
usWinAscent: r.u16()?,
usWinDescent: r.u16()?,
ulCodePageRange1: None,
ulCodePageRange2: None,
sxHeight: None,
sCapHeight: None,
usDefaultChar: None,
usBreakChar: None,
usMaxContext: None,
usLowerOpticalPointSize: None,
usUpperOpticalPointSize: None,
};
if version >= 1 {
t.ulCodePageRange1 = Some(r.u32()?);
t.ulCodePageRange2 = Some(r.u32()?);
}
if version >= 2 {
t.sxHeight = Some(r.i16()?);
t.sCapHeight = Some(r.i16()?);
t.usDefaultChar = Some(r.u16()?);
t.usBreakChar = Some(r.u16()?);
t.usMaxContext = Some(r.u16()?);
}
if version == 5 {
t.usLowerOpticalPointSize = Some(r.u16()?);
t.usUpperOpticalPointSize = Some(r.u16()?);
}
Ok(t)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedded;
use crate::opentype::sfnt::Sfnt;
#[test]
fn os_2() {
let font = Sfnt::init(embedded::JULIA_MONO).unwrap();
let table = font.get_table(b"OS/2").unwrap();
let os2 = OS2::init(table).unwrap();
assert_eq!(
OS2 {
version: 4,
xAvgCharWidth: 1200,
usWeightClass: 400,
usWidthClass: 5,
fsType: 0,
ySubscriptXSize: 1300,
ySubscriptYSize: 1200,
ySubscriptXOffset: 0,
ySubscriptYOffset: 150,
ySuperscriptXSize: 1300,
ySuperscriptYSize: 1200,
ySuperscriptXOffset: 0,
ySuperscriptYOffset: 700,
yStrikeoutSize: 100,
yStrikeoutPosition: 550,
sFamilyClass: 0,
panose: [2, 11, 6, 9, 6, 3, 0, 2, 0, 4],
ulUnicodeRange1: 3843162111,
ulUnicodeRange2: 3603300351,
ulUnicodeRange3: 117760229,
ulUnicodeRange4: 96510060,
achVendID: *b"corm",
fsSelection: FSSelection {
regular: true,
use_typo_metrics: true,
..Default::default()
},
usFirstCharIndex: 13,
usLastCharIndex: 65535,
sTypoAscender: 1900,
sTypoDescender: -450,
sTypoLineGap: 0,
usWinAscent: 2400,
usWinDescent: 450,
ulCodePageRange1: Some(1613234687),
ulCodePageRange2: Some(0),
sxHeight: Some(1100),
sCapHeight: Some(1450),
usDefaultChar: Some(0),
usBreakChar: Some(32),
usMaxContext: Some(126),
usLowerOpticalPointSize: None,
usUpperOpticalPointSize: None,
},
os2
);
}
#[test]
fn fs_selection_roundtrip() {
for v in 0..(1u16 << 10) {
assert_eq!(FSSelection::from_u16(v).to_u16(), v);
}
}
#[test]
fn unsupported_version() {
assert_eq!(OS2::init(&[0, 9]), Err(Os2Error::OS2VersionNotSupported));
assert_eq!(OS2::init(&[0, 0, 1]), Err(Os2Error::EndOfStream));
}
}
+70
View File
@@ -0,0 +1,70 @@
//! PostScript Table (`post`). Port of `font/opentype/post.zig`.
//!
//! References:
//! - https://learn.microsoft.com/en-us/typography/opentype/spec/post
#![allow(non_snake_case)]
use super::sfnt::{Fixed, Version16Dot16};
use super::{EndOfStream, Reader};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Post {
pub version: Version16Dot16,
/// Italic angle in counter-clockwise degrees from the vertical.
pub italicAngle: Fixed,
/// Suggested y offset of the top of the underline stroke (design units).
pub underlinePosition: i16,
/// Suggested underline thickness (design units).
pub underlineThickness: i16,
/// Set to 0 if the font is proportionally spaced, non-zero if monospaced.
pub isFixedPitch: u32,
pub minMemType42: u32,
pub maxMemType42: u32,
pub minMemType1: u32,
pub maxMemType1: u32,
}
impl Post {
pub fn init(data: &[u8]) -> Result<Post, EndOfStream> {
let mut r = Reader::new(data);
Ok(Post {
version: Version16Dot16::from_u32(r.u32()?),
italicAngle: Fixed(r.i32()?),
underlinePosition: r.i16()?,
underlineThickness: r.i16()?,
isFixedPitch: r.u32()?,
minMemType42: r.u32()?,
maxMemType42: r.u32()?,
minMemType1: r.u32()?,
maxMemType1: r.u32()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedded;
use crate::opentype::sfnt::Sfnt;
#[test]
fn post() {
let font = Sfnt::init(embedded::JULIA_MONO).unwrap();
let table = font.get_table(b"post").unwrap();
let post = Post::init(table).unwrap();
assert_eq!(
Post {
version: Version16Dot16 { minor: 0, major: 2 },
italicAngle: Fixed::from_f64(0.0),
underlinePosition: -200,
underlineThickness: 100,
isFixedPitch: 1,
minMemType42: 0,
maxMemType42: 0,
minMemType1: 0,
maxMemType1: 0,
},
post
);
}
}
+230
View File
@@ -0,0 +1,230 @@
//! SFNT container and basic OpenType data types. Port of
//! `font/opentype/sfnt.zig`.
use super::{EndOfStream, Reader};
pub type Tag = [u8; 4];
/// 32-bit signed fixed-point number (16.16). Stored as the raw `i32`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Fixed(pub i32);
impl Fixed {
pub fn from_f64(float: f64) -> Self {
Fixed((float * 65536.0).round() as i32)
}
pub fn to_f64(self) -> f64 {
f64::from(self.0) / 65536.0
}
/// Fractional bits (low 16).
pub fn frac(self) -> u16 {
(self.0 as u32 & 0xFFFF) as u16
}
/// Integer part (high 16, signed).
pub fn int(self) -> i16 {
(self.0 >> 16) as i16
}
}
/// 16-bit signed fixed number with the low 14 bits of fraction (2.14).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct F2Dot14(pub i16);
impl F2Dot14 {
pub fn from_f64(float: f64) -> Self {
F2Dot14((float * 16384.0).round() as i16)
}
pub fn to_f64(self) -> f64 {
f64::from(self.0) / 16384.0
}
}
/// Packed 32-bit value with major and minor version numbers.
/// (Zig `packed struct(u32) { minor: u16, major: u16 }`: `minor` is the low
/// half, so reading a big-endian u32 yields `major` in the high half.)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Version16Dot16 {
pub minor: u16,
pub major: u16,
}
impl Version16Dot16 {
pub fn from_u32(v: u32) -> Self {
Self {
minor: (v & 0xFFFF) as u16,
major: (v >> 16) as u16,
}
}
}
/// 32-bit signed 26.6 fixed point numbers. Stored as the raw `i32`
/// (the 26.6 bit pattern, i.e. what FreeType calls `FT_F26Dot6`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct F26Dot6(pub i32);
impl F26Dot6 {
const FRAC_FACTOR: f64 = 64.0;
const HALF: i32 = 1 << 5;
pub fn from_f64(float: f64) -> Self {
F26Dot6((float * Self::FRAC_FACTOR).round() as i32)
}
pub fn to_f64(self) -> f64 {
f64::from(self.0) / Self::FRAC_FACTOR
}
pub fn to_f32(self) -> f32 {
self.0 as f32 / 64.0
}
/// The fractional bits (low 6, unsigned).
pub fn frac(self) -> u8 {
(self.0 & 0x3F) as u8
}
/// The integer part (floor).
pub fn int(self) -> i32 {
self.0 >> 6
}
/// Round to the nearest integer, .5 rounds away from 0.
pub fn round(self) -> i32 {
if self.0 & Self::HALF != 0 {
self.int() + 1
} else {
self.int()
}
}
}
impl std::fmt::Display for F26Dot6 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_f64())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum SfntError {
#[error("end of stream")]
EndOfStream,
}
impl From<EndOfStream> for SfntError {
fn from(_: EndOfStream) -> Self {
SfntError::EndOfStream
}
}
/// The static portion of the table directory (the "offset subtable").
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OffsetSubtable {
/// Indicates the type of font file we're reading.
/// - 0x00_01_00_00 ---- TrueType
/// - 0x74_72_75_65 'true' TrueType
/// - 0x4F_54_54_4F 'OTTO' OpenType
/// - 0x74_79_70_31 'typ1' PostScript
pub sfnt_version: u32,
pub num_tables: u16,
pub search_range: u16,
pub entry_selector: u16,
pub range_shift: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TableRecord {
pub tag: Tag,
pub checksum: u32,
/// Offset from beginning of font file.
pub offset: u32,
pub length: u32,
}
#[derive(Debug, Clone)]
pub struct Directory {
pub offset: OffsetSubtable,
pub records: Vec<TableRecord>,
}
/// Wrapper for parsing a SFNT font and accessing its tables.
#[derive(Debug, Clone)]
pub struct Sfnt<'a> {
pub directory: Directory,
data: &'a [u8],
}
impl<'a> Sfnt<'a> {
/// Parse a font from raw data. The struct keeps a reference to `data`.
pub fn init(data: &'a [u8]) -> Result<Sfnt<'a>, SfntError> {
let mut r = Reader::new(data);
let offset = OffsetSubtable {
sfnt_version: r.u32()?,
num_tables: r.u16()?,
search_range: r.u16()?,
entry_selector: r.u16()?,
range_shift: r.u16()?,
};
let mut records = Vec::with_capacity(usize::from(offset.num_tables));
for _ in 0..offset.num_tables {
records.push(TableRecord {
tag: r.bytes()?,
checksum: r.u32()?,
offset: r.u32()?,
length: r.u32()?,
});
}
Ok(Sfnt {
directory: Directory { offset, records },
data,
})
}
/// Returns the bytes of the table with the provided tag if present.
/// (None as well if the record points outside of the data.)
pub fn get_table(&self, tag: &[u8; 4]) -> Option<&'a [u8]> {
let record = self.directory.records.iter().find(|r| &r.tag == tag)?;
let start = record.offset as usize;
let end = start.checked_add(record.length as usize)?;
self.data.get(start..end)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedded;
#[test]
fn fixed_point() {
let p = F26Dot6::from_f64(26.6);
assert_eq!(p.int(), 26);
assert_eq!(p.frac(), 38);
assert_eq!(p.to_f64(), 26.59375);
assert_eq!(p.round(), 27);
let n = F26Dot6::from_f64(-26.6);
assert_eq!(n.int(), -27);
assert_eq!(n.frac(), 26);
assert_eq!(n.to_f64(), -26.59375);
assert_eq!(n.round(), -27);
}
#[test]
fn parse_font() {
let sfnt = Sfnt::init(embedded::JULIA_MONO).unwrap();
assert_eq!(19, sfnt.directory.offset.num_tables);
assert_eq!(b"prep", &sfnt.directory.records[18].tag);
}
#[test]
fn get_table() {
let sfnt = Sfnt::init(embedded::JULIA_MONO).unwrap();
let svg = sfnt.get_table(b"SVG ").unwrap();
assert_eq!(430, svg.len());
}
#[test]
fn truncated_data_is_an_error() {
assert!(Sfnt::init(&[0, 1, 0, 0, 0, 5]).is_err());
}
}
+146
View File
@@ -0,0 +1,146 @@
//! SVG glyphs description table. Port of `font/opentype/svg.zig`.
//!
//! This struct is focused purely on the operations we need for Ghostty,
//! namely to be able to look up whether an glyph ID is present in the SVG
//! table or not. It isn't meant to be a general purpose SVG table reader.
//!
//! References:
//! - https://www.w3.org/2013/10/SVG_in_OpenType/#thesvg
//! - https://learn.microsoft.com/en-us/typography/opentype/spec/svg
use super::EndOfStream;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum SvgError {
#[error("end of stream")]
EndOfStream,
#[error("SVG table version not supported")]
SVGVersionNotSupported,
}
impl From<EndOfStream> for SvgError {
fn from(_: EndOfStream) -> Self {
SvgError::EndOfStream
}
}
/// Size of one document record in bytes.
const RECORD_SIZE: usize = 12;
#[derive(Debug, Clone)]
pub struct SVG<'a> {
/// The start and end glyph IDs (inclusive) that are present in the
/// table. This is used to very quickly include/exclude a glyph from
/// the table.
pub start_glyph_id: u16,
pub end_glyph_id: u16,
/// All records in the table (`len * 12` bytes).
records: &'a [u8],
}
fn glyph_range(record: &[u8]) -> Result<(u16, u16), SvgError> {
if record.len() < 4 {
return Err(SvgError::EndOfStream);
}
Ok((
u16::from_be_bytes([record[0], record[1]]),
u16::from_be_bytes([record[2], record[3]]),
))
}
impl<'a> SVG<'a> {
pub fn init(data: &'a [u8]) -> Result<SVG<'a>, SvgError> {
let get = |pos: usize, n: usize| data.get(pos..pos.checked_add(n)?);
// Version
let version = get(0, 2).ok_or(SvgError::EndOfStream)?;
if u16::from_be_bytes([version[0], version[1]]) != 0 {
return Err(SvgError::SVGVersionNotSupported);
}
// Offset
let off = get(2, 4).ok_or(SvgError::EndOfStream)?;
let offset = u32::from_be_bytes([off[0], off[1], off[2], off[3]]) as usize;
// Document list: count followed by records.
let len_b = get(offset, 2).ok_or(SvgError::EndOfStream)?;
let len = usize::from(u16::from_be_bytes([len_b[0], len_b[1]]));
let start = offset + 2;
// Zig reads records[0] (and the last) unconditionally.
let records = get(start, len * RECORD_SIZE).ok_or(SvgError::EndOfStream)?;
if len == 0 {
return Err(SvgError::EndOfStream);
}
let start_range = glyph_range(&records[..RECORD_SIZE])?;
let end_range = if len == 1 {
start_range
} else {
glyph_range(&records[(len - 1) * RECORD_SIZE..])?
};
Ok(SVG {
start_glyph_id: start_range.0,
end_glyph_id: end_range.1,
records,
})
}
pub fn has_glyph(&self, glyph_id: u16) -> bool {
// Fast path: outside the table range
if glyph_id < self.start_glyph_id || glyph_id > self.end_glyph_id {
return false;
}
// Fast path, matches the start/end glyph IDs
if glyph_id == self.start_glyph_id || glyph_id == self.end_glyph_id {
return true;
}
// Slow path: binary search our records
let (mut lo, mut hi) = (0usize, self.records.len() / RECORD_SIZE);
while lo < hi {
let mid = lo + (hi - lo) / 2;
let rec = &self.records[mid * RECORD_SIZE..(mid + 1) * RECORD_SIZE];
let (start, end) = glyph_range(rec).unwrap_or((u16::MAX, 0));
if glyph_id < start {
hi = mid;
} else if glyph_id > end {
lo = mid + 1;
} else {
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embedded;
use crate::opentype::sfnt::Sfnt;
// NOTE: the Zig test obtains the table through `Face.copyTable`; we read
// the same bytes through the SFNT directory (Face-based variant lives
// with the freetype face tests).
#[test]
fn svg() {
let font = Sfnt::init(embedded::JULIA_MONO).unwrap();
let table = font.get_table(b"SVG ").unwrap();
let svg = SVG::init(table).unwrap();
assert_eq!(11482, svg.start_glyph_id);
assert_eq!(11482, svg.end_glyph_id);
assert!(svg.has_glyph(11482));
assert!(!svg.has_glyph(11483));
}
#[test]
fn svg_bad_version() {
assert_eq!(
SVG::init(&[0, 1, 0, 0, 0, 0]).unwrap_err(),
SvgError::SVGVersionNotSupported
);
}
}
+71
View File
@@ -0,0 +1,71 @@
//! Shaping types shared by all shapers. Port of `font/shape.zig`.
//!
//! The shaper implementation for the Linux backend is HarfBuzz
//! ([`crate::shaper::harfbuzz::Shaper`]).
use crate::collection::Index;
pub use crate::shaper::cache::Cache;
pub use crate::shaper::feature::{DEFAULT_FEATURES, Feature, FeatureList};
pub use crate::shaper::{harfbuzz, noop};
/// Shaper implementation for our compile options (backend
/// `fontconfig_freetype` => HarfBuzz).
pub use crate::shaper::harfbuzz::Shaper;
/// A cell is a single single within a terminal that should be rendered
/// for a shaping call. Note all terminal cells may be present; only
/// cells that have a glyph that needs to be rendered.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Cell {
/// The X position of this shaper cell relative to the offset of the
/// run. Because runs are always within a single row, it is expected
/// that the caller can reconstruct the full position of the cell by
/// using the known Y position of the cell and adding the X position
/// to the run offset.
pub x: u16,
/// An additional offset to apply to the rendering.
pub x_offset: i16,
pub y_offset: i16,
/// The glyph index for this cell. The font index to use alongside
/// this cell is available in the text run. This glyph index is only
/// valid for a given GroupCache and FontIndex that was used to create
/// the runs.
pub glyph_index: u32,
}
/// Options for shapers.
#[derive(Debug, Clone, Default)]
pub struct Options {
/// Font features to use when shaping.
///
/// Note: eventually, this will move to font.Face probably as we may
/// want to support per-face feature configuration. For now, we only
/// support applying features globally.
pub features: Vec<String>,
}
/// A single text run. Port of `shaper/run.zig` `TextRun`.
///
/// TODO(integration): `run.zig` (RunIterator over terminal rows) is deferred
/// to F2; the `grid: *SharedGrid` field is omitted here and should be added
/// when `SharedGrid` and the run iterator are wired in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextRun {
/// A unique hash for this run. This can be used to cache the shaping
/// results.
pub hash: u64,
/// The offset in the row where this run started. This is added to the
/// X position of the final shaped cells to get the absolute position
/// in the row where they belong.
pub offset: u16,
/// The total number of cells produced by this run.
pub cells: u16,
/// The font index to use for the glyphs of this run.
pub font_index: Index,
}
+111
View File
@@ -0,0 +1,111 @@
//! This structure caches the shaped cells for a given text run.
//!
//! At one point, shaping was the most expensive part of rendering text
//! (accounting for 96% of frame time on my machine). To speed it up, this
//! was introduced so that shaping results can be cached depending on the
//! run.
//!
//! The cache key is the text run. The text run builds its own hash value
//! based on the font, style, codepoint, etc. This just utilizes the hash that
//! the text run provides.
use ghostty_datastruct::cache_table::{CacheContext, CacheTable};
use crate::shape::{Cell, TextRun};
/// Context for cache table.
#[derive(Debug, Default)]
pub struct CellCacheTableContext;
impl CacheContext<u64, Vec<Cell>> for CellCacheTableContext {
fn hash(&mut self, key: &u64) -> u64 {
*key
}
fn eql(&mut self, a: &u64, b: &u64) -> bool {
a == b
}
}
/// Cache table for run hash -> shaped cells.
///
/// Capacity is slightly arbitrary. These numbers are guesses: I'd expect an
/// average of 256 frequently cached runs is a safe guess for most terminal
/// screens, with 8 items per bucket to give decent resiliency to important
/// runs.
type CellCacheTable = CacheTable<u64, Vec<Cell>, CellCacheTableContext, 256, 8>;
/// The cache table of shaped cells.
#[derive(Debug)]
pub struct Cache {
map: CellCacheTable,
}
impl Default for Cache {
fn default() -> Self {
Self::new()
}
}
impl Cache {
pub fn new() -> Cache {
Cache {
map: CellCacheTable::new(CellCacheTableContext),
}
}
/// Get the shaped cells for the given text run,
/// or None if they are not in the cache.
pub fn get(&mut self, run: &TextRun) -> Option<&[Cell]> {
self.map.get(&run.hash).map(Vec::as_slice)
}
/// Insert the shaped cells for the given text run into the cache.
///
/// The cells will be duplicated.
pub fn put(&mut self, run: &TextRun, cells: &[Cell]) {
// An evicted entry is simply dropped.
let _evicted = self.map.put(run.hash, cells.to_vec());
}
/// Remove everything from the cache.
pub fn clear(&mut self) {
self.map.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collection::Index;
#[test]
fn cache() {
let mut c = Cache::new();
let run = TextRun {
hash: 1,
offset: 0,
cells: 0,
font_index: Index::default(),
};
assert!(c.get(&run).is_none());
c.put(
&run,
&[
Cell {
x: 0,
glyph_index: 0,
..Cell::default()
},
Cell {
x: 1,
glyph_index: 1,
..Cell::default()
},
],
);
let actual = c.get(&run).unwrap();
assert_eq!(actual.len(), 2);
}
}
+474
View File
@@ -0,0 +1,474 @@
//! OpenType font feature settings. Port of `font/shaper/feature.zig`.
// The explicit `continue`s mirror the Zig state machine one-to-one.
#![allow(clippy::needless_continue, clippy::single_match_else)]
use std::fmt;
/// A minimal byte reader mirroring the `std.io.fixedBufferStream` reader
/// semantics that the Zig parser relies on: reading past the end yields a
/// `,` (the parser uses `reader.readByte() catch ','`).
pub struct Reader<'a> {
buf: &'a [u8],
pub pos: usize,
}
impl<'a> Reader<'a> {
pub fn new(buf: &'a [u8]) -> Self {
Reader { buf, pos: 0 }
}
/// Read a byte; at end of stream returns `,` without advancing.
fn read_byte(&mut self) -> u8 {
match self.buf.get(self.pos) {
Some(&b) => {
self.pos += 1;
b
}
None => b',',
}
}
/// Skip until after the next `,` or end of stream.
fn skip_until_delimiter_or_eof(&mut self) {
while let Some(&b) = self.buf.get(self.pos) {
self.pos += 1;
if b == b',' {
break;
}
}
}
pub fn at_end(&self) -> bool {
self.pos >= self.buf.len()
}
}
/// Represents an OpenType font feature setting, which consists of a tag and
/// a numeric parameter >= 0. Most features are boolean, so only parameters
/// of 0 and 1 make sense for them, but some (e.g. 'cv01'..'cv99') can take
/// parameters to choose between multiple variants of a given character or
/// characters.
///
/// Ref:
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#features-and-lookups
/// - https://harfbuzz.github.io/shaping-opentype-features.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Feature {
pub tag: [u8; 4],
pub value: u32,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
/// Initial state.
Start,
/// Parsing the tag.
Tag,
/// In the space between the tag and the value.
Space,
/// Parsing an integer parameter directly in to `value`.
Int,
/// Parsing a boolean keyword parameter ("on"/"off").
Bool,
/// Encountered an unrecoverable syntax error, advancing to boundary.
Err,
/// Done parsing feature, skip whitespace until end.
Done,
/// Successfully finished.
Finished,
}
impl Feature {
pub fn from_string(s: &str) -> Option<Feature> {
Self::from_bytes(s.as_bytes())
}
pub fn from_bytes(s: &[u8]) -> Option<Feature> {
Self::from_reader(&mut Reader::new(s))
}
/// Parse a single font feature setting from a reader, with a version
/// of the syntax of HarfBuzz's font feature strings. Stops at end of stream
/// or when a ',' is encountered.
///
/// This parsing aims to be as error-tolerant as possible while avoiding any
/// assumptions in ambiguous scenarios. When invalid syntax is encountered,
/// the reader is advanced to the next boundary (end-of-stream or ',') so
/// that further features may be read.
///
/// Ref: https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string
pub fn from_reader(reader: &mut Reader<'_>) -> Option<Feature> {
let mut tag = [0u8; 4];
let mut tag_len = 0usize;
let mut value: Option<u32> = None;
let mut state = State::Start;
loop {
match state {
// If we're done then we skip whitespace until we see a ','.
State::Finished => break,
State::Done => loop {
match reader.read_byte() {
b' ' | b'\t' => continue,
b',' => {
state = State::Finished;
break;
}
// If we see something other than whitespace or a ','
// then this is an error since the intent is unclear.
_ => {
state = State::Err;
break;
}
}
},
// If we're fast-forwarding from an error we just wanna
// stop at the first boundary and ignore all other bytes.
State::Err => {
reader.skip_until_delimiter_or_eof();
return None;
}
State::Start => loop {
match reader.read_byte() {
// Ignore leading whitespace.
b' ' | b'\t' => continue,
// Empty feature string.
b',' => return None,
// '+' prefix to explicitly enable feature.
b'+' => {
value = Some(1);
state = State::Tag;
break;
}
// '-' prefix to explicitly disable feature.
b'-' => {
value = Some(0);
state = State::Tag;
break;
}
// Quote mark introducing a tag.
b'"' | b'\'' => {
state = State::Tag;
break;
}
// First letter of tag.
byte => {
tag_len = 1;
tag[0] = byte;
state = State::Tag;
break;
}
}
},
State::Tag => loop {
match reader.read_byte() {
// If the tag is interrupted by a comma it's invalid.
b',' => return None,
// Ignore quote marks. This does technically ignore cases like
// "'k'e'r'n' = 0", but it's unambiguous so if someone really
// wants to do that in their config then... sure why not.
b'"' | b'\'' => continue,
// In all other cases we add the byte to our tag.
byte => {
tag[tag_len] = byte;
tag_len += 1;
if tag_len == 4 {
state = State::Space;
break;
}
}
}
},
State::Space => {
loop {
match reader.read_byte() {
b' ' | b'\t' => continue,
// Ignore quote marks since we might have a
// closing quote from the tag still ahead.
b'"' | b'\'' => continue,
// Allow an '=' (which we can safely ignore)
// only if we don't already have a value due
// to a '+' or '-' prefix.
b'=' => {
if value.is_some() {
state = State::Err;
break;
}
}
b',' => {
// Specifying only a tag turns a feature on.
if value.is_none() {
value = Some(1);
}
state = State::Finished;
break;
}
byte @ b'0'..=b'9' => {
// If we already have value because of a
// '+' or '-' prefix then this is an error.
if value.is_some() {
state = State::Err;
} else {
value = Some(u32::from(byte - b'0'));
state = State::Int;
}
break;
}
b'o' | b'O' => {
// If we already have value because of a
// '+' or '-' prefix then this is an error.
if value.is_some() {
state = State::Err;
} else {
state = State::Bool;
}
break;
}
_ => {
state = State::Err;
break;
}
}
}
}
State::Int => {
loop {
match reader.read_byte() {
b',' => {
state = State::Finished;
break;
}
byte @ b'0'..=b'9' => {
// If our value gets too big while
// parsing we consider it an error.
let v = value.unwrap_or(0);
match v
.checked_mul(10)
.and_then(|v| v.checked_add(u32::from(byte - b'0')))
{
Some(n) => value = Some(n),
None => {
state = State::Err;
break;
}
}
}
_ => {
state = State::Err;
break;
}
}
}
}
State::Bool => loop {
match reader.read_byte() {
b',' => return None,
b'n' | b'N' => {
// "ofn"
if value.is_some() {
debug_assert_eq!(value, Some(0));
state = State::Err;
break;
}
value = Some(1);
state = State::Done;
break;
}
b'f' | b'F' => {
// To make sure we consume two 'f's.
if value.is_none() {
value = Some(0);
} else {
debug_assert_eq!(value, Some(0));
state = State::Done;
break;
}
}
_ => {
state = State::Err;
break;
}
}
},
}
if state == State::Finished {
break;
}
}
debug_assert!(value.is_some());
debug_assert_eq!(tag_len, 4);
Some(Feature { tag, value: value? })
}
}
impl fmt::Display for Feature {
/// Serialize this feature. The string that this produces should be valid
/// to parse.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let tag = String::from_utf8_lossy(&self.tag);
if self.value <= 1 {
// Format boolean options as "+tag" for on and "-tag" for off.
write!(f, "{}{}", if self.value == 1 { '+' } else { '-' }, tag)
} else {
// Format non-boolean tags as "tag=value".
write!(f, "{tag}={}", self.value)
}
}
}
/// A list of font feature settings (see `Feature` for more documentation).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FeatureList {
pub features: Vec<Feature>,
}
impl FeatureList {
/// Parse a comma separated list of features.
/// See `Feature::from_reader` for more docs.
pub fn from_string(s: &str) -> FeatureList {
let mut this = FeatureList::default();
this.append_from_string(s);
this
}
/// Append features to this list from a string with a comma separated list.
/// See `Feature::from_reader` for more docs.
pub fn append_from_string(&mut self, s: &str) {
let mut reader = Reader::new(s.as_bytes());
while !reader.at_end() {
let i = reader.pos;
if let Some(feature) = Feature::from_reader(&mut reader) {
self.features.push(feature);
} else {
log::warn!(
"failed to parse font feature setting: \"{}\"",
String::from_utf8_lossy(&s.as_bytes()[i..reader.pos])
);
}
}
}
}
impl fmt::Display for FeatureList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, feature) in self.features.iter().enumerate() {
if i != 0 {
f.write_str(", ")?;
}
write!(f, "{feature}")?;
}
Ok(())
}
}
/// These features are hardcoded to always be on by default. Users
/// can turn them off by setting the features to "-liga" for example.
pub const DEFAULT_FEATURES: [Feature; 1] = [Feature {
tag: *b"liga",
value: 1,
}];
#[cfg(test)]
mod tests {
use super::*;
fn f(tag: [u8; 4], value: u32) -> Feature {
Feature { tag, value }
}
#[test]
fn feature_from_string() {
// This is not *complete* coverage of every possible
// combination of syntax, but it covers quite a few.
// Boolean settings (on)
let kern_on = Some(f(*b"kern", 1));
for s in [
"kern",
"kern, ",
"kern on",
"kern on, ",
"+kern",
"+kern, ",
"\"kern\" = 1",
"\"kern\" = 1, ",
] {
assert_eq!(kern_on, Feature::from_string(s), "{s}");
}
// Boolean settings (off)
let kern_off = Some(f(*b"kern", 0));
for s in [
"kern off",
"kern off, ",
"-'kern'",
"-'kern', ",
"\"kern\" = 0",
"\"kern\" = 0, ",
] {
assert_eq!(kern_off, Feature::from_string(s), "{s}");
}
// Non-boolean settings
let aalt_2 = Some(f(*b"aalt", 2));
for s in ["aalt=2", "aalt=2, ", "'aalt' 2", "'aalt' 2, "] {
assert_eq!(aalt_2, Feature::from_string(s), "{s}");
}
// Various ambiguous/error cases which should be null
for s in [
"aalt=2x", // bad number
"toolong", // tag too long
"sht", // tag too short
"-kern 1", // redundant/conflicting
"-kern on", // redundant/conflicting
"aalt=o,", // bad keyword
"aalt=ofn,", // bad keyword
] {
assert_eq!(None, Feature::from_string(s), "{s}");
}
}
#[test]
fn feature_list_from_string() {
let s = concat!(
" kern, kern on , +kern, \"kern\" = 1,", // Boolean settings (on)
"kern off, -'kern' , \"kern\"=0,", // Boolean settings (off)
"aalt=2, 'aalt'\t2,", // Non-boolean settings
"aalt=2x, toolong, sht, -kern 1, -kern on, aalt=o, aalt=ofn,", // Invalid cases
"last", // To ensure final element is included correctly.
);
let feats = FeatureList::from_string(s);
let mut expected = vec![f(*b"kern", 1); 4];
expected.extend(vec![f(*b"kern", 0); 3]);
expected.extend(vec![f(*b"aalt", 2); 2]);
expected.push(f(*b"last", 1));
assert_eq!(expected, feats.features);
}
#[test]
fn feature_display() {
assert_eq!("+kern", f(*b"kern", 1).to_string());
assert_eq!("-kern", f(*b"kern", 0).to_string());
assert_eq!("aalt=3", f(*b"aalt", 3).to_string());
assert_eq!(
"+liga, ss01=2",
FeatureList::from_string("liga, ss01=2").to_string()
);
}
#[test]
fn default_features_has_liga() {
assert_eq!(DEFAULT_FEATURES, [f(*b"liga", 1)]);
}
}
+400
View File
@@ -0,0 +1,400 @@
//! HarfBuzz shaper. Port of `font/shaper/harfbuzz.zig`.
//!
//! The Zig shaper is driven by `RunIterator` (shaper/run.zig, deferred to F2)
//! through the hooks `prepare` / `addCodepoint` / `finalize`; those hooks are
//! exposed here as [`Shaper::prepare`], [`Shaper::add_codepoint`] and
//! [`Shaper::finalize`]. [`Shaper::shape`] takes the face of the run's font
//! (or `None` for the special sprite font, whose codepoint == glyph index)
//! instead of a `TextRun` + `SharedGrid`; the caller is responsible for
//! holding whatever lock protects the face (FreeType faces are not thread
//! safe). [`Shaper::shape_text`] is a convenience that does the whole
//! prepare/add/finalize/shape sequence.
use super::hb;
use crate::face::freetype::Face;
use crate::shape::{self, Cell, DEFAULT_FEATURES, FeatureList};
#[derive(Debug, thiserror::Error)]
pub enum ShapeError {
#[error("failed to create harfbuzz buffer")]
BufferCreate,
#[error("harfbuzz failed")]
HarfbuzzFailed,
}
#[derive(Debug, Clone, Copy)]
struct Codepoint {
cluster: u32,
}
#[derive(Debug, Clone, Copy, Default)]
struct RunOffset {
cluster: u32,
x: i32,
y: i32,
}
#[derive(Debug, Clone, Copy, Default)]
struct CellOffset {
cluster: u32,
x: i32,
}
/// Shaper that uses Harfbuzz.
pub struct Shaper {
/// The buffer used for text shaping. We reuse it across multiple shaping
/// calls to prevent allocations.
hb_buf: hb::Buffer,
/// The shared memory used for shaping results.
cell_buf: Vec<Cell>,
/// The features to use for shaping.
hb_feats: Vec<hb::Feature>,
/// The codepoints added to the buffer before shaping. We need to keep
/// these separately because after shaping, HarfBuzz replaces codepoints
/// with glyph indices in the buffer.
codepoints: Vec<Codepoint>,
}
impl Shaper {
pub fn new(opts: &shape::Options) -> Result<Shaper, ShapeError> {
// Parse all the features we want to use.
let mut feature_list = FeatureList::default();
feature_list.features.extend_from_slice(&DEFAULT_FEATURES);
for feature_str in &opts.features {
feature_list.append_from_string(feature_str);
}
let hb_feats = feature_list
.features
.iter()
.map(|f| hb::global_feature(f.tag, f.value))
.collect();
Ok(Shaper {
hb_buf: hb::Buffer::new().ok_or(ShapeError::BufferCreate)?,
cell_buf: Vec::new(),
hb_feats,
codepoints: Vec::new(),
})
}
pub fn end_frame(&self) {}
/// Number of codepoints currently in the HarfBuzz buffer (before
/// shaping) / glyphs (after shaping).
pub fn hb_buf_len(&self) -> usize {
self.hb_buf.len()
}
// ---- RunIterator hooks ----
/// Reset the buffer for the current run.
pub fn prepare(&mut self) {
self.hb_buf.reset();
self.hb_buf.set_content_type_unicode();
// We set the cluster level to `characters` to give us the most
// granularity, matching the CoreText shaper, and allowing us
// to use our same ligature detection heuristics.
self.hb_buf.set_cluster_level_characters();
self.codepoints.clear();
// We don't support RTL text because RTL in terminals is messy.
// Its something we want to improve. For now, we force LTR because
// our renderers assume a strictly increasing X value.
self.hb_buf.set_direction_ltr();
}
pub fn add_codepoint(&mut self, cp: u32, cluster: u32) {
// We pass the index into codepoints as the cluster value to HarfBuzz.
// After shaping, we use info.cluster to get back the index, which
// lets us look up the original cluster value from codepoints.
let index = self.codepoints.len() as u32;
self.hb_buf.add(cp, index);
self.codepoints.push(Codepoint { cluster });
}
pub fn finalize(&mut self) {
self.hb_buf.guess_segment_properties();
}
/// Convenience: shape `text` as a single run where every item is a
/// `(codepoint, cluster)` pair (cluster = cell x position).
pub fn shape_text(
&mut self,
face: Option<&Face>,
text: &[(u32, u32)],
) -> Result<&[Cell], ShapeError> {
self.prepare();
for &(cp, cluster) in text {
self.add_codepoint(cp, cluster);
}
self.finalize();
self.shape(face)
}
/// Shape the current run (as built through the hooks). `face` is the face
/// for the run's font index, or `None` if the font is a special-case
/// font (sprite), for which codepoint == glyph_index.
///
/// The return value is only valid until the next shape call is called.
pub fn shape(&mut self, face: Option<&Face>) -> Result<&[Cell], ShapeError> {
// We only do shaping if the font is not a special-case. For special-case
// fonts, the codepoint == glyph_index so we don't need to run any shaping.
if let Some(face) = face {
let i = if face.quirks_disable_default_font_features {
// If we are disabling default font features we just offset
// our features by the hardcoded items because always
// add those at the beginning.
DEFAULT_FEATURES.len()
} else {
0
};
hb::shape(face.hb_font(), &mut self.hb_buf, &self.hb_feats[i..]);
}
self.cell_buf.clear();
// If our buffer is empty, we short-circuit the rest of the work
// return nothing.
if self.hb_buf.is_empty() {
return Ok(&self.cell_buf);
}
let info = self.hb_buf.glyph_infos();
let pos = self
.hb_buf
.glyph_positions()
.ok_or(ShapeError::HarfbuzzFailed)?;
// This is perhaps not true somewhere, but we currently assume it is true.
// If it isn't true, I'd like to catch it and learn more.
debug_assert_eq!(info.len(), pos.len());
// This keeps track of the current x and y offsets (sum of advances)
// and the furthest cluster we've seen so far (max).
let mut run_offset = RunOffset::default();
// This keeps track of the cell starting x and cluster.
let mut cell_offset = CellOffset::default();
// Convert all our info/pos to cells and set it.
for (info_v, pos_v) in info.iter().zip(pos.iter()) {
// info_v.cluster is the index into our codepoints array. We use it
// to get the original cluster.
let index = info_v.cluster as usize;
// Our cluster is also our cell X position. If the cluster changes
// then we need to reset our current cell offsets.
let cluster = self.codepoints[index].cluster;
if cell_offset.cluster != cluster {
let is_after_glyph_from_current_or_next_clusters = cluster <= run_offset.cluster;
let is_first_codepoint_in_cluster = if index == 0 {
true
} else {
self.codepoints[index - 1].cluster != cluster
};
// We need to reset the `cell_offset` at the start of a new
// cluster, but we do that conditionally if the codepoint
// `is_first_codepoint_in_cluster` and the cluster is not
// `is_after_glyph_from_current_or_next_clusters`, which is
// a heuristic to detect ligatures and avoid positioning
// glyphs that mark ligatures incorrectly. The idea is that
// if the first codepoint in a cluster doesn't appear in
// the stream, it's very likely that it combined with
// codepoints from a previous cluster into a ligature.
// Then, the subsequent codepoints are very likely marking
// glyphs that are placed relative to that ligature, so if
// we were to reset the `cell_offset` to align it with the
// grid, the positions would be off. The
// `!is_after_glyph_from_current_or_next_clusters` check is
// needed in case these marking glyphs come from a later
// cluster but are rendered first (see the Chakma and
// Bengali tests). In that case when we get to the
// codepoint that `is_first_codepoint_in_cluster`, but in a
// cluster that
// `is_after_glyph_from_current_or_next_clusters`, we don't
// want to reset to the grid and cause the positions to be
// off. (Note that we could go back and align the cells to
// the grid starting from the one from the cluster that
// rendered out of order, but that is more complicated so
// we don't do that for now. Also, it's TBD if there are
// exceptions to this heuristic for detecting ligatures,
// but using the logging below seems to show it works
// well.)
if is_first_codepoint_in_cluster && !is_after_glyph_from_current_or_next_clusters {
cell_offset = CellOffset {
cluster,
x: run_offset.x,
};
}
}
// Under both FreeType and CoreText the harfbuzz scale is
// in 26.6 fixed point units, so we round to the nearest
// whole value here.
let x_offset = run_offset.x - cell_offset.x + ((pos_v.x_offset + 0b100_000) >> 6);
let y_offset = run_offset.y + ((pos_v.y_offset + 0b100_000) >> 6);
self.cell_buf.push(Cell {
x: cell_offset.cluster as u16,
x_offset: x_offset as i16,
y_offset: y_offset as i16,
glyph_index: info_v.codepoint,
});
// Add our advances to keep track of our run offsets.
// Advances apply to the NEXT cell.
// Under both FreeType and CoreText the harfbuzz scale is
// in 26.6 fixed point units, so we round to the nearest
// whole value here.
run_offset.x += (pos_v.x_advance + 0b100_000) >> 6;
run_offset.y += (pos_v.y_advance + 0b100_000) >> 6;
run_offset.cluster = run_offset.cluster.max(cluster);
}
Ok(&self.cell_buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::face::{DesiredSize, FontData, Options};
use crate::{Library, embedded};
fn text(s: &str) -> Vec<(u32, u32)> {
s.chars()
.enumerate()
.map(|(i, c)| (c as u32, i as u32))
.collect()
}
fn face(data: &'static [u8]) -> Face {
let lib = Library::init().unwrap();
Face::init(
&lib,
FontData::Static(data),
Options::new(DesiredSize::new(12.0)),
)
.unwrap()
}
#[test]
fn shape_emoji_run_buffer_length() {
// "shape": thumbs up, thumbs up, skin tone modifier => 3 codepoints.
let f = face(embedded::INCONSOLATA);
let mut s = Shaper::new(&shape::Options::default()).unwrap();
s.prepare();
s.add_codepoint(0x1F44D, 0);
s.add_codepoint(0x1F44D, 1);
s.add_codepoint(0x1F3FD, 1);
s.finalize();
assert_eq!(3, s.hb_buf_len());
s.shape(Some(&f)).unwrap();
}
#[test]
fn shape_inconsolata_ligs() {
let f = face(embedded::INCONSOLATA);
// Some of our tests rely on dlig being enabled by default.
let mut s = Shaper::new(&shape::Options {
features: vec!["dlig".to_string()],
})
.unwrap();
let cells = s.shape_text(Some(&f), &text(">=")).unwrap();
assert_eq!(1, cells.len());
let cells = s.shape_text(Some(&f), &text("===")).unwrap();
assert_eq!(1, cells.len());
}
#[test]
fn shape_monaspace_ligs() {
let f = face(embedded::MONASPACE_NEON);
// Some of our tests rely on dlig being enabled by default.
let mut s = Shaper::new(&shape::Options {
features: vec!["dlig".to_string()],
})
.unwrap();
let cells = s.shape_text(Some(&f), &text("===")).unwrap();
assert_eq!(1, cells.len());
}
#[test]
fn shape_ligatures_disabled_by_feature() {
let f = face(embedded::INCONSOLATA);
let mut s = Shaper::new(&shape::Options {
features: vec!["-liga".to_string()],
})
.unwrap();
let cells = s.shape_text(Some(&f), &text("===")).unwrap();
assert_eq!(3, cells.len());
}
#[test]
fn shape_arabic_forced_ltr() {
let f = face(embedded::ARABIC);
let mut s = Shaper::new(&shape::Options::default()).unwrap();
let arabic = include_str!("../../testdata/arabic.txt");
let line = arabic.lines().next().unwrap();
let t = text(line);
let n = t.len();
let cells = s.shape_text(Some(&f), &t).unwrap();
assert_eq!(n, cells.len());
let mut x = cells[0].x;
for cell in &cells[1..] {
assert_eq!(x + 1, cell.x);
x = cell.x;
}
}
#[test]
fn shape_combining_characters() {
let f = face(embedded::INCONSOLATA);
let mut s = Shaper::new(&shape::Options::default()).unwrap();
// n + two combining diaeresis in cell 0, then 'a' in cell 1.
let t = [('n' as u32, 0), (0x0308, 0), (0x0308, 0), ('a' as u32, 1)];
let cells = s.shape_text(Some(&f), &t).unwrap();
assert_eq!(4, cells.len());
assert_eq!(0, cells[0].x);
assert_eq!(0, cells[1].x);
assert_eq!(0, cells[2].x);
assert_eq!(1, cells[3].x);
}
#[test]
fn shape_special_font_passthrough() {
// Special fonts (sprite) are not shaped: glyph index == codepoint.
let mut s = Shaper::new(&shape::Options::default()).unwrap();
let cells = s.shape_text(None, &[(0x2500, 0), (0x2502, 1)]).unwrap();
assert_eq!(2, cells.len());
assert_eq!(0x2500, cells[0].glyph_index);
assert_eq!(1, cells[1].x);
assert_eq!(0, cells[1].x_offset);
}
#[test]
fn shape_empty_run() {
let f = face(embedded::INCONSOLATA);
let mut s = Shaper::new(&shape::Options::default()).unwrap();
assert_eq!(0, s.shape_text(Some(&f), &[]).unwrap().len());
}
#[test]
fn shape_with_empty_cells_in_between_clusters() {
let f = face(embedded::INCONSOLATA);
let mut s = Shaper::new(&shape::Options::default()).unwrap();
// 'A' at cell 0, 'B' at cell 3.
let cells = s
.shape_text(Some(&f), &[('A' as u32, 0), ('B' as u32, 3)])
.unwrap();
assert_eq!(2, cells.len());
assert_eq!(0, cells[0].x);
assert_eq!(3, cells[1].x);
}
}
+165
View File
@@ -0,0 +1,165 @@
//! Minimal safe wrapper over the HarfBuzz buffer/shape API (the bits of
//! the Zig `harfbuzz` package that the shaper uses).
#![allow(unsafe_code)]
use std::ptr::NonNull;
use harfbuzz_sys as sys;
use crate::face::freetype::HbFont;
/// A HarfBuzz feature (`hb_feature_t`) applied globally.
pub type Feature = sys::hb_feature_t;
pub const FEATURE_GLOBAL_START: u32 = 0;
pub const FEATURE_GLOBAL_END: u32 = u32::MAX;
/// Build a global feature from a 4-byte tag and value.
pub fn global_feature(tag: [u8; 4], value: u32) -> Feature {
Feature {
tag: u32::from_be_bytes(tag),
value,
start: FEATURE_GLOBAL_START,
end: FEATURE_GLOBAL_END,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct GlyphInfo {
pub codepoint: u32,
pub cluster: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct GlyphPosition {
pub x_advance: i32,
pub y_advance: i32,
pub x_offset: i32,
pub y_offset: i32,
}
/// An owned `hb_buffer_t`.
pub struct Buffer(NonNull<sys::hb_buffer_t>);
// SAFETY: the buffer is uniquely owned and HarfBuzz buffers have no thread
// affinity; all access goes through `&self`/`&mut self`.
unsafe impl Send for Buffer {}
impl Buffer {
pub fn new() -> Option<Buffer> {
// SAFETY: hb_buffer_create always returns a valid (possibly the
// inert "empty") buffer; we check for null anyway.
NonNull::new(unsafe { sys::hb_buffer_create() }).map(Buffer)
}
pub fn reset(&mut self) {
// SAFETY: self.0 is a valid buffer.
unsafe { sys::hb_buffer_reset(self.0.as_ptr()) }
}
pub fn set_content_type_unicode(&mut self) {
// SAFETY: self.0 is a valid buffer.
unsafe {
sys::hb_buffer_set_content_type(self.0.as_ptr(), sys::HB_BUFFER_CONTENT_TYPE_UNICODE);
}
}
pub fn set_cluster_level_characters(&mut self) {
// SAFETY: self.0 is a valid buffer.
unsafe {
sys::hb_buffer_set_cluster_level(
self.0.as_ptr(),
sys::HB_BUFFER_CLUSTER_LEVEL_CHARACTERS,
);
}
}
pub fn set_direction_ltr(&mut self) {
// SAFETY: self.0 is a valid buffer.
unsafe { sys::hb_buffer_set_direction(self.0.as_ptr(), sys::HB_DIRECTION_LTR) }
}
pub fn add(&mut self, cp: u32, cluster: u32) {
// SAFETY: self.0 is a valid buffer.
unsafe { sys::hb_buffer_add(self.0.as_ptr(), cp, cluster) }
}
pub fn guess_segment_properties(&mut self) {
// SAFETY: self.0 is a valid buffer.
unsafe { sys::hb_buffer_guess_segment_properties(self.0.as_ptr()) }
}
pub fn len(&self) -> usize {
// SAFETY: self.0 is a valid buffer.
unsafe { sys::hb_buffer_get_length(self.0.as_ptr()) as usize }
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Glyph infos (valid after shaping).
pub fn glyph_infos(&mut self) -> Vec<GlyphInfo> {
let mut len: u32 = 0;
// SAFETY: self.0 is valid; the returned pointer addresses `len`
// initialized infos that live until the buffer is next mutated, and
// we copy them out immediately.
unsafe {
let ptr = sys::hb_buffer_get_glyph_infos(self.0.as_ptr(), &raw mut len);
if ptr.is_null() {
return Vec::new();
}
std::slice::from_raw_parts(ptr, len as usize)
.iter()
.map(|i| GlyphInfo {
codepoint: i.codepoint,
cluster: i.cluster,
})
.collect()
}
}
/// Glyph positions (valid after shaping); None if unavailable.
pub fn glyph_positions(&mut self) -> Option<Vec<GlyphPosition>> {
let mut len: u32 = 0;
// SAFETY: as for `glyph_infos`.
unsafe {
let ptr = sys::hb_buffer_get_glyph_positions(self.0.as_ptr(), &raw mut len);
if ptr.is_null() {
return None;
}
Some(
std::slice::from_raw_parts(ptr, len as usize)
.iter()
.map(|p| GlyphPosition {
x_advance: p.x_advance,
y_advance: p.y_advance,
x_offset: p.x_offset,
y_offset: p.y_offset,
})
.collect(),
)
}
}
}
impl Drop for Buffer {
fn drop(&mut self) {
// SAFETY: we own the buffer reference.
unsafe { sys::hb_buffer_destroy(self.0.as_ptr()) }
}
}
/// Shape the buffer with the given font and features.
pub fn shape(font: &HbFont, buf: &mut Buffer, features: &[Feature]) {
// SAFETY: font and buffer pointers are valid; the feature slice is valid
// for `features.len()` entries.
unsafe {
sys::hb_shape(
font.as_ptr(),
buf.0.as_ptr(),
features.as_ptr(),
features.len() as u32,
);
}
}
+9
View File
@@ -0,0 +1,9 @@
//! Text shapers. Port of `font/shaper/`.
//!
//! `run.zig` (RunIterator over terminal rows) is deferred (F2).
pub mod cache;
pub mod feature;
pub mod harfbuzz;
mod hb;
pub mod noop;
+87
View File
@@ -0,0 +1,87 @@
//! Shaper that doesn't do any shaping. Port of `font/shaper/noop.zig`.
//! Each individual codepoint is mapped directly to the detected text run
//! font's glyph index.
use crate::face::freetype::Face;
use crate::shape::{self, Cell};
#[derive(Debug, Clone, Copy)]
struct Codepoint {
codepoint: u32,
cluster: u32,
}
/// Shaper that doesn't do any shaping.
#[derive(Debug, Default)]
pub struct Shaper {
/// The codepoints of the current run.
codepoints: Vec<Codepoint>,
/// The shared memory used for shaping results.
cell_buf: Vec<Cell>,
}
impl Shaper {
pub fn new(_opts: &shape::Options) -> Shaper {
Shaper::default()
}
pub fn end_frame(&self) {}
// ---- RunIterator hooks ----
pub fn prepare(&mut self) {
self.codepoints.clear();
}
pub fn add_codepoint(&mut self, cp: u32, cluster: u32) {
self.codepoints.push(Codepoint {
codepoint: cp,
cluster,
});
}
pub fn finalize(&mut self) {}
/// Shape the current run. `face` is `None` for special fonts (sprite),
/// whose codepoint == glyph index.
///
/// Panics if a codepoint has no glyph in `face` (the run iterator
/// guarantees this never happens).
pub fn shape(&mut self, face: Option<&Face>) -> &[Cell] {
self.cell_buf.clear();
for entry in &self.codepoints {
let glyph_index = match face {
// Special fonts aren't shaped and their codepoint == glyph.
None => entry.codepoint,
Some(face) => face
.glyph_index(entry.codepoint)
.expect("run iterator guarantees a glyph for every codepoint"),
};
self.cell_buf.push(Cell {
x: entry.cluster as u16,
glyph_index,
..Cell::default()
});
}
&self.cell_buf
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noop_special_passthrough() {
let mut s = Shaper::new(&shape::Options::default());
s.prepare();
s.add_codepoint(0x41, 0);
s.add_codepoint(0x42, 2);
s.finalize();
let cells = s.shape(None);
assert_eq!(2, cells.len());
assert_eq!(0x41, cells[0].glyph_index);
assert_eq!(2, cells[1].x);
}
}
+539
View File
@@ -0,0 +1,539 @@
//! This structure represents the state required to render a terminal
//! grid using the font subsystem. It is "shared" because it is able to
//! be shared across multiple surfaces.
//!
//! It is desirable for the grid state to be shared because the font
//! configuration for a set of surfaces is almost always the same and
//! font data is relatively memory intensive. Further, the font subsystem
//! should be read-heavy compared to write-heavy, so it handles concurrent
//! reads well. Going even further, the font subsystem should be very rarely
//! read at all since it should only be necessary when the grid actively
//! changes.
//!
//! SharedGrid does NOT support resizing, font-family changes, font removals
//! in collections, etc. Because the Grid is shared this would cause a
//! major disruption in the rendering of multiple surfaces (i.e. increasing
//! the font size in one would increase it in all). In many cases this isn't
//! desirable so to implement configuration changes the grid should be
//! reinitialized and all surfaces should switch over to using that one.
#![allow(clippy::collapsible_if, clippy::if_not_else, clippy::float_cmp)]
use std::collections::HashMap;
use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::atlas::{self, Atlas};
use crate::codepoint_resolver::{CodepointResolver, RenderGlyphError};
use crate::collection::{self, Collection, GetFaceError, Index, PresentationMode};
use crate::face::{Constraint, ConstraintSize, RenderOptions};
use crate::{Glyph, Metrics, Presentation, SpriteFace, Style};
/// The size of a grid cell in pixels.
// TODO(integration): replace with `renderer::CellSize` once the renderer
// crate exists.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellSize {
pub width: u32,
pub height: u32,
}
#[derive(Debug, thiserror::Error)]
pub enum InitError {
#[error(transparent)]
UpdateMetrics(#[from] collection::UpdateMetricsError),
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
GetFace(#[from] GetFaceError),
#[error(transparent)]
Render(#[from] RenderGlyphError),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Render {
pub glyph: Glyph,
pub presentation: Presentation,
}
/// The glyph cache key. Like upstream (`GlyphKey.Packed`), only the render
/// option fields that affect the identity of the render are part of the key:
/// the grid metrics and the constraint are NOT (the constraint is a function
/// of the codepoint/font, and the metrics are fixed for the grid's lifetime).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct GlyphKey {
index: Index,
glyph: u32,
cell_width: u8,
thicken: bool,
thicken_strength: u8,
constraint_width: u8,
}
impl GlyphKey {
fn new(index: Index, glyph: u32, opts: &RenderOptions) -> GlyphKey {
GlyphKey {
index,
glyph,
cell_width: opts.cell_width.unwrap_or(0) & 0b11,
thicken: opts.thicken,
thicken_strength: opts.thicken_strength,
constraint_width: opts.constraint_width & 0b11,
}
}
}
/// The lock-protected state of the grid. Callers that need direct access
/// (for example the renderer uploading the atlases) take the guard from
/// [`SharedGrid::read`] / [`SharedGrid::write`].
pub struct State {
/// Cache for codepoints to font indexes in a group.
pub codepoints: HashMap<(Style, u32, Option<Presentation>), Option<Index>>,
/// Cache for glyph renders into the atlas.
glyphs: HashMap<GlyphKey, Render>,
/// The texture atlas to store renders in. The Glyph data in the glyphs
/// cache is dependent on the atlas matching.
pub atlas_grayscale: Atlas,
pub atlas_color: Atlas,
/// The underlying resolver for font data, fallbacks, etc. The shared
/// grid takes ownership of the resolver.
pub resolver: CodepointResolver,
/// The currently active grid metrics dictating the layout of the grid.
/// This is calculated based on the resolver and current fonts.
pub metrics: Metrics,
}
pub struct SharedGrid {
/// The RwLock used to protect the shared grid. Callers are expected to use
/// this (through `read`/`write`) if they need to i.e. access the atlas
/// directly. Because callers can use this lock directly, maintainers need
/// to be extra careful to review call sites to ensure they are using the
/// lock correctly.
lock: RwLock<State>,
}
impl SharedGrid {
/// Initialize the grid.
///
/// The resolver must have a collection that supports deferred loading
/// (`collection.load_options != None`). This is because we need the load
/// options data to determine grid metrics and setup our sprite font.
///
/// SharedGrid always configures the sprite font. This struct is expected
/// to be used with a terminal grid and therefore the sprite font is
/// always necessary for correct rendering.
pub fn new(resolver: CodepointResolver) -> Result<SharedGrid, InitError> {
// We need to support loading options since we use the size data
assert!(resolver.collection.load_options.is_some());
let mut state = State {
// We set an initial capacity that can fit a good number of
// characters. This number was picked empirically based on my own
// terminal usage.
codepoints: HashMap::with_capacity(128),
glyphs: HashMap::with_capacity(128),
atlas_grayscale: Atlas::new(512, atlas::Format::Grayscale),
atlas_color: Atlas::new(512, atlas::Format::Bgra),
resolver,
// Loaded below
metrics: Metrics::calc(crate::metrics::FaceMetrics::default()),
};
// Initialize our metrics.
state.reload_metrics()?;
Ok(SharedGrid {
lock: RwLock::new(state),
})
}
/// Take the shared (read) lock.
pub fn read(&self) -> RwLockReadGuard<'_, State> {
self.lock
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Take the exclusive (write) lock.
pub fn write(&self) -> RwLockWriteGuard<'_, State> {
self.lock
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Returns the grid cell size.
pub fn cell_size(&self) -> CellSize {
let st = self.read();
CellSize {
width: st.metrics.cell_width,
height: st.metrics.cell_height,
}
}
/// Returns a copy of the current grid metrics.
pub fn metrics(&self) -> Metrics {
self.read().metrics
}
/// Get the font index for a given codepoint. This is cached.
///
/// This always forces loading any deferred fonts since we assume that if
/// you're looking up an index that the caller plans to use the font. By
/// loading the font in this function we can ensure thread-safety on the
/// load without complicating future calls.
pub fn get_index(
&self,
cp: u32,
style: Style,
p: Option<Presentation>,
) -> Result<Option<Index>, Error> {
let key = (style, cp, p);
// Fast path: the cache has the value. This is almost always true and
// only requires a read lock.
{
let st = self.read();
if let Some(v) = st.codepoints.get(&key) {
return Ok(*v);
}
}
// Slow path: we need to search this codepoint
let mut guard = self.write();
let st = &mut *guard;
// Try to get it, if it is now in the cache another thread beat us to it.
if let Some(v) = st.codepoints.get(&key) {
return Ok(*v);
}
// Load a value and cache it. This even caches negative matches.
let value = st.resolver.get_index(cp, style, p);
st.codepoints.insert(key, value);
if let Some(idx) = value {
// If the font is a sprite font then we don't need to preload
// because get_face doesn't work with special fonts.
if idx.special().is_none() {
// Load the face in case its deferred. If this fails then we
// would've failed to load it in the future anyways so we want
// to undo all the caching we did.
if let Err(err) = st.resolver.collection.get_face(idx) {
st.codepoints.remove(&key);
return Err(err.into());
}
}
}
Ok(value)
}
/// Returns true if the given font index has the codepoint and presentation.
pub fn has_codepoint(&self, idx: Index, cp: u32, p: Option<Presentation>) -> bool {
let st = self.read();
st.resolver.collection.has_codepoint(
idx,
cp,
match p {
Some(v) => PresentationMode::Explicit(v),
None => PresentationMode::Any,
},
)
}
/// Render a codepoint. This uses the first font index that has the codepoint
/// and matches the presentation requested. If the codepoint cannot be found
/// in any font, an null render is returned.
pub fn render_codepoint(
&self,
cp: u32,
style: Style,
p: Option<Presentation>,
opts: RenderOptions,
) -> Result<Option<Render>, Error> {
// Note: we could optimize the below to use way less locking, but
// at the time of writing this codepath is only called for preedit
// text which is relatively rare and almost non-existent in multiple
// surfaces at the same time.
// Get the font that has the codepoint
let Some(index) = self.get_index(cp, style, p)? else {
return Ok(None);
};
// Get the glyph for the font
let glyph_index = {
let mut guard = self.write();
// (The face was preloaded by `get_index`; `get_face` is a no-op
// lookup then. We need `&mut` for the possible lazy load.)
let face = guard.resolver.collection.get_face(index)?;
match face.glyph_index(cp) {
Some(g) => g,
None => return Ok(None),
}
};
// Render
Ok(Some(self.render_glyph(index, glyph_index, opts)?))
}
/// Render a glyph index. This automatically determines the correct texture
/// atlas to use and caches the result.
pub fn render_glyph(
&self,
index: Index,
glyph_index: u32,
opts: RenderOptions,
) -> Result<Render, Error> {
let key = GlyphKey::new(index, glyph_index, &opts);
// Fast path: the cache has the value. This is almost always true and
// only requires a read lock.
{
let st = self.read();
if let Some(v) = st.glyphs.get(&key) {
return Ok(*v);
}
}
// Slow path: we need to search this codepoint
let mut guard = self.write();
let st = &mut *guard;
if let Some(v) = st.glyphs.get(&key) {
return Ok(*v);
}
// Get the presentation to determine what atlas to use. (Nothing is
// inserted into the cache until the render succeeds, which gives the
// same roll-back behaviour as upstream's errdefer.)
let p = st.resolver.get_presentation(index, glyph_index)?;
let atlas: &mut Atlas = match p {
Presentation::Text => &mut st.atlas_grayscale,
Presentation::Emoji => &mut st.atlas_color,
};
let mut render_opts = opts;
// Always use these constraints for emoji.
if p == Presentation::Emoji {
render_opts.constraint = Constraint {
// Scale emoji to be as large as possible
// while preserving their aspect ratio.
size: ConstraintSize::Cover,
// Center the emoji in its cells.
align_horizontal: crate::face::Align::Center,
align_vertical: crate::face::Align::Center,
// Add a small bit of padding so the emoji
// doesn't quite touch the edges of the cells.
pad_left: 0.025,
pad_right: 0.025,
..Constraint::NONE
};
}
// Render into the atlas
let glyph = match st
.resolver
.render_glyph(atlas, index, glyph_index, render_opts)
{
Ok(g) => g,
// If the atlas is full, we resize it
Err(err) if err.is_atlas_full() => {
atlas.grow(atlas.size * 2);
st.resolver
.render_glyph(atlas, index, glyph_index, render_opts)?
}
Err(err) => return Err(err.into()),
};
// Cache and return
let render = Render {
glyph,
presentation: p,
};
st.glyphs.insert(key, render);
Ok(render)
}
}
impl State {
fn reload_metrics(&mut self) -> Result<(), collection::UpdateMetricsError> {
let collection: &mut Collection = &mut self.resolver.collection;
collection.update_metrics()?;
self.metrics = collection.metrics.expect("set by update_metrics");
// Setup our sprite font.
self.resolver.sprite = Some(SpriteFace::new(self.metrics));
Ok(())
}
/// Number of cached glyph renders (test/diagnostic helper).
pub fn glyph_cache_len(&self) -> usize {
self.glyphs.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collection::{AddOptions, LoadOptions, SizeAdjustment};
use crate::face::{DesiredSize, Options};
use crate::{Face, Library, embedded};
fn test_grid(lib: &Library) -> SharedGrid {
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone()));
c.add(
Face::init(
lib,
embedded::REGULAR.into(),
Options::new(DesiredSize {
points: 12.0,
xdpi: 96,
ydpi: 96,
}),
)
.unwrap(),
AddOptions {
style: Style::Regular,
fallback: false,
size_adjustment: SizeAdjustment::None,
},
)
.unwrap();
SharedGrid::new(CodepointResolver::new(c)).unwrap()
}
#[test]
fn get_index() {
let lib = Library::init().unwrap();
let grid = test_grid(&lib);
// Visible ASCII.
for i in 32u32..127 {
let idx = grid.get_index(i, Style::Regular, None).unwrap().unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(0, idx.idx);
assert!(grid.has_codepoint(idx, i, None));
}
// Do it again without a resolver set to ensure we only hit the cache
// (upstream sets the resolver to `undefined`; we swap in an empty one).
let old_resolver = std::mem::replace(
&mut grid.write().resolver,
CodepointResolver::new(Collection::new()),
);
for i in 32u32..127 {
let idx = grid.get_index(i, Style::Regular, None).unwrap().unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(0, idx.idx);
}
grid.write().resolver = old_resolver;
}
#[test]
fn render_glyph_error_after_cache_insert_rolls_back_cache_entry() {
// Upstream uses a tripwire to fail `renderGlyph` after the cache entry
// was inserted. Here a failure is provoked with an invalid index
// (`get_presentation` fails); the cache must stay clean.
let lib = Library::init().unwrap();
let grid = test_grid(&lib);
let idx = grid
.get_index('A' as u32, Style::Regular, None)
.unwrap()
.unwrap();
let glyph_index = {
let st = grid.read();
st.resolver
.collection
.get_loaded_face(idx)
.unwrap()
.glyph_index('A' as u32)
.unwrap()
};
let render_opts = RenderOptions::new(grid.metrics());
assert_eq!(0, grid.read().glyph_cache_len());
let bad = Index {
style: Style::Regular,
idx: 100,
};
assert!(grid.render_glyph(bad, glyph_index, render_opts).is_err());
assert_eq!(0, grid.read().glyph_cache_len());
// And a good render is cached.
let r = grid.render_glyph(idx, glyph_index, render_opts).unwrap();
assert_eq!(Presentation::Text, r.presentation);
assert_eq!(1, grid.read().glyph_cache_len());
assert_eq!(r, grid.render_glyph(idx, glyph_index, render_opts).unwrap());
assert_eq!(1, grid.read().glyph_cache_len());
}
#[test]
fn render_codepoint_and_cell_size() {
let lib = Library::init().unwrap();
let grid = test_grid(&lib);
let size = grid.cell_size();
assert!(size.width > 0 && size.height > 0);
let r = grid
.render_codepoint(
'A' as u32,
Style::Regular,
None,
RenderOptions::new(grid.metrics()),
)
.unwrap()
.unwrap();
assert!(r.glyph.width > 0);
// Not in any font and no discovery.
assert!(
grid.render_codepoint(
0x10FFFE,
Style::Regular,
None,
RenderOptions::new(grid.metrics())
)
.unwrap()
.is_none()
);
}
#[test]
fn atlas_grows_when_full() {
let lib = Library::init().unwrap();
let grid = test_grid(&lib);
let metrics = grid.metrics();
// Render sprite glyphs (box drawing) until the 512 atlas must grow.
for cp in 0x2500u32..0x2580 {
let idx = grid.get_index(cp, Style::Regular, None).unwrap().unwrap();
grid.render_glyph(idx, cp, RenderOptions::new(metrics))
.unwrap();
}
assert!(grid.read().atlas_grayscale.size >= 512);
}
#[test]
fn init_error_without_primary_face() {
// Analogue of upstream "init error": failure in `init` must surface
// as an error (no primary font to compute metrics from).
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib));
assert!(SharedGrid::new(CodepointResolver::new(c)).is_err());
}
}
+11 -18
View File
@@ -26,22 +26,8 @@ use crate::atlas::{self, Atlas};
use crate::glyph::Glyph;
use crate::metrics::Metrics;
/// Text/emoji presentation.
// TODO(integration): replace with the real `font::Presentation` once the
// rest of the font engine (face.zig / main.zig) is ported.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Presentation {
Text,
Emoji,
}
/// The subset of `font.face.RenderOptions` that the sprite face uses.
// TODO(integration): replace with the real `font::face::RenderOptions`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RenderOptions {
/// The number of grid cells this glyph will take up (0..=3, a `u2` in Zig).
pub cell_width: Option<u8>,
}
pub use crate::Presentation;
pub use crate::face::RenderOptions;
/// Errors from [`Face::render_glyph`].
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
@@ -469,7 +455,14 @@ mod tests {
for cp in cps {
for cell_width in [None, Some(1), Some(2)] {
let g = face
.render_glyph(&mut atlas, cp, RenderOptions { cell_width })
.render_glyph(
&mut atlas,
cp,
RenderOptions {
cell_width,
..RenderOptions::new(metrics)
},
)
.unwrap_or_else(|e| panic!("cp {cp:X} {w}x{}: {e}", asc + desc));
// The glyph must stay within the padded canvas.
let cw = metrics.cell_width * u32::from(cell_width.unwrap_or(1).max(1));
@@ -538,7 +531,7 @@ mod tests {
.render_glyph(
&mut atlas,
Sprite::CursorBar as u32,
RenderOptions::default(),
RenderOptions::new(face.metrics),
)
.unwrap();
// 1px bar placed at x = -1 (half its thickness rounded up).