zenith_layout/
font_meta.rs1use rustybuzz::ttf_parser;
8use rustybuzz::ttf_parser::name_id;
9use zenith_core::FontStyle;
10
11use crate::error::LayoutError;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct FaceMetadata {
16 pub family: String,
18 pub weight: u16,
20 pub style: FontStyle,
22}
23
24pub fn face_metadata(bytes: &[u8], index: u32) -> Result<FaceMetadata, LayoutError> {
35 let face = ttf_parser::Face::parse(bytes, index)
36 .map_err(|e| LayoutError::new(format!("font parse failed: {e:?}")))?;
37
38 let family =
39 best_family_name(&face).ok_or_else(|| LayoutError::new("font has no family name"))?;
40
41 let weight = face.weight().to_number();
42 let style = if face.is_italic() {
43 FontStyle::Italic
44 } else {
45 FontStyle::Normal
46 };
47
48 Ok(FaceMetadata {
49 family,
50 weight,
51 style,
52 })
53}
54
55fn best_family_name(face: &ttf_parser::Face<'_>) -> Option<String> {
62 let mut typo_family: Option<String> = None;
63 let mut family: Option<String> = None;
64
65 for name in face.names() {
66 if name.name_id == name_id::TYPOGRAPHIC_FAMILY
67 && typo_family.is_none()
68 && let Some(s) = name.to_string()
69 {
70 typo_family = Some(s);
71 } else if name.name_id == name_id::FAMILY
72 && family.is_none()
73 && let Some(s) = name.to_string()
74 {
75 family = Some(s);
76 }
77 }
78
79 typo_family.or(family)
80}
81
82#[cfg(test)]
85mod tests {
86 use super::*;
87
88 use zenith_core::font::embedded;
91 const REGULAR: &[u8] = embedded::NOTO_SANS_REGULAR;
92 const BOLD: &[u8] = embedded::NOTO_SANS_BOLD;
93 const ITALIC: &[u8] = embedded::NOTO_SANS_ITALIC;
94 const BOLD_ITALIC: &[u8] = embedded::NOTO_SANS_BOLD_ITALIC;
95 const MONO: &[u8] = embedded::NOTO_SANS_MONO_REGULAR;
96
97 #[test]
98 fn noto_sans_regular_family_weight_style() {
99 let m = face_metadata(REGULAR, 0).expect("regular must parse");
100 assert!(
101 m.family.contains("Noto Sans"),
102 "family should contain 'Noto Sans', got '{}'",
103 m.family
104 );
105 assert_eq!(m.weight, 400, "Regular weight must be 400");
106 assert_eq!(m.style, FontStyle::Normal, "Regular must be Normal");
107 }
108
109 #[test]
110 fn noto_sans_bold_weight() {
111 let m = face_metadata(BOLD, 0).expect("bold must parse");
112 assert!(
113 m.family.contains("Noto Sans"),
114 "family should contain 'Noto Sans', got '{}'",
115 m.family
116 );
117 assert_eq!(m.weight, 700, "Bold weight must be 700");
118 assert_eq!(m.style, FontStyle::Normal, "Bold (upright) must be Normal");
119 }
120
121 #[test]
122 fn noto_sans_italic_style() {
123 let m = face_metadata(ITALIC, 0).expect("italic must parse");
124 assert!(
125 m.family.contains("Noto Sans"),
126 "family should contain 'Noto Sans', got '{}'",
127 m.family
128 );
129 assert_eq!(m.style, FontStyle::Italic, "Italic must be Italic");
130 }
131
132 #[test]
133 fn noto_sans_bold_italic_weight_and_style() {
134 let m = face_metadata(BOLD_ITALIC, 0).expect("bold-italic must parse");
135 assert!(
136 m.family.contains("Noto Sans"),
137 "family should contain 'Noto Sans', got '{}'",
138 m.family
139 );
140 assert_eq!(m.weight, 700, "Bold-Italic weight must be 700");
141 assert_eq!(m.style, FontStyle::Italic, "Bold-Italic must be Italic");
142 }
143
144 #[test]
145 fn noto_sans_mono_family() {
146 let m = face_metadata(MONO, 0).expect("mono must parse");
147 assert!(
148 m.family.contains("Noto Sans Mono") || m.family.contains("Noto Sans"),
149 "mono family should contain 'Noto Sans', got '{}'",
150 m.family
151 );
152 assert_eq!(m.weight, 400, "Mono Regular weight must be 400");
153 assert_eq!(m.style, FontStyle::Normal, "Mono Regular must be Normal");
154 }
155
156 #[test]
157 fn invalid_bytes_return_err() {
158 let result = face_metadata(b"not a font", 0);
159 assert!(result.is_err(), "invalid bytes must return Err");
160 let msg = result.unwrap_err().message;
161 assert!(
162 msg.contains("font parse failed"),
163 "error should mention 'font parse failed', got: {msg}"
164 );
165 }
166}