use rustybuzz::ttf_parser;
use rustybuzz::ttf_parser::name_id;
use zenith_core::FontStyle;
use crate::error::LayoutError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FaceMetadata {
pub family: String,
pub weight: u16,
pub style: FontStyle,
}
pub fn face_metadata(bytes: &[u8], index: u32) -> Result<FaceMetadata, LayoutError> {
let face = ttf_parser::Face::parse(bytes, index)
.map_err(|e| LayoutError::new(format!("font parse failed: {e:?}")))?;
let family =
best_family_name(&face).ok_or_else(|| LayoutError::new("font has no family name"))?;
let weight = face.weight().to_number();
let style = if face.is_italic() {
FontStyle::Italic
} else {
FontStyle::Normal
};
Ok(FaceMetadata {
family,
weight,
style,
})
}
fn best_family_name(face: &ttf_parser::Face<'_>) -> Option<String> {
let mut typo_family: Option<String> = None;
let mut family: Option<String> = None;
for name in face.names() {
if name.name_id == name_id::TYPOGRAPHIC_FAMILY
&& typo_family.is_none()
&& let Some(s) = name.to_string()
{
typo_family = Some(s);
} else if name.name_id == name_id::FAMILY
&& family.is_none()
&& let Some(s) = name.to_string()
{
family = Some(s);
}
}
typo_family.or(family)
}
#[cfg(test)]
mod tests {
use super::*;
use zenith_core::font::embedded;
const REGULAR: &[u8] = embedded::NOTO_SANS_REGULAR;
const BOLD: &[u8] = embedded::NOTO_SANS_BOLD;
const ITALIC: &[u8] = embedded::NOTO_SANS_ITALIC;
const BOLD_ITALIC: &[u8] = embedded::NOTO_SANS_BOLD_ITALIC;
const MONO: &[u8] = embedded::NOTO_SANS_MONO_REGULAR;
#[test]
fn noto_sans_regular_family_weight_style() {
let m = face_metadata(REGULAR, 0).expect("regular must parse");
assert!(
m.family.contains("Noto Sans"),
"family should contain 'Noto Sans', got '{}'",
m.family
);
assert_eq!(m.weight, 400, "Regular weight must be 400");
assert_eq!(m.style, FontStyle::Normal, "Regular must be Normal");
}
#[test]
fn noto_sans_bold_weight() {
let m = face_metadata(BOLD, 0).expect("bold must parse");
assert!(
m.family.contains("Noto Sans"),
"family should contain 'Noto Sans', got '{}'",
m.family
);
assert_eq!(m.weight, 700, "Bold weight must be 700");
assert_eq!(m.style, FontStyle::Normal, "Bold (upright) must be Normal");
}
#[test]
fn noto_sans_italic_style() {
let m = face_metadata(ITALIC, 0).expect("italic must parse");
assert!(
m.family.contains("Noto Sans"),
"family should contain 'Noto Sans', got '{}'",
m.family
);
assert_eq!(m.style, FontStyle::Italic, "Italic must be Italic");
}
#[test]
fn noto_sans_bold_italic_weight_and_style() {
let m = face_metadata(BOLD_ITALIC, 0).expect("bold-italic must parse");
assert!(
m.family.contains("Noto Sans"),
"family should contain 'Noto Sans', got '{}'",
m.family
);
assert_eq!(m.weight, 700, "Bold-Italic weight must be 700");
assert_eq!(m.style, FontStyle::Italic, "Bold-Italic must be Italic");
}
#[test]
fn noto_sans_mono_family() {
let m = face_metadata(MONO, 0).expect("mono must parse");
assert!(
m.family.contains("Noto Sans Mono") || m.family.contains("Noto Sans"),
"mono family should contain 'Noto Sans', got '{}'",
m.family
);
assert_eq!(m.weight, 400, "Mono Regular weight must be 400");
assert_eq!(m.style, FontStyle::Normal, "Mono Regular must be Normal");
}
#[test]
fn invalid_bytes_return_err() {
let result = face_metadata(b"not a font", 0);
assert!(result.is_err(), "invalid bytes must return Err");
let msg = result.unwrap_err().message;
assert!(
msg.contains("font parse failed"),
"error should mention 'font parse failed', got: {msg}"
);
}
}