1use std::collections::{BTreeMap, BTreeSet};
29
30use ttf_parser::{GlyphId, name_id};
31
32mod glyph;
33mod pbf;
34
35const FONT_SIZE_PX: u32 = 24;
36
37#[derive(Debug, thiserror::Error)]
39pub enum Error {
40 #[error("invalid font: {0}")]
42 InvalidFont(String),
43 #[error("unsupported font: {0}")]
45 UnsupportedFont(String),
46 #[error("range start {0} is not a multiple of 256")]
48 InvalidRangeStart(u32),
49}
50
51pub struct FontFace {
53 font: fontdue::Font,
54 family_name: String,
55 style_name: String,
56 fontstack_name: String,
57 ascender_round: i32,
58 charmap: BTreeMap<u32, u16>,
59 advances: Vec<u32>,
60}
61
62impl FontFace {
63 pub fn parse(bytes: &[u8]) -> Result<FontFace, Error> {
71 let face = ttf_parser::Face::parse(bytes, 0)
72 .map_err(|error| Error::InvalidFont(error.to_string()))?;
73
74 if face.tables().colr.is_some() {
75 return Err(Error::UnsupportedFont(
76 "COLR color fonts are not supported".to_owned(),
77 ));
78 }
79
80 let family_name = preferred_name(
81 &face,
82 name_id::TYPOGRAPHIC_FAMILY,
83 name_id::FAMILY,
84 "family",
85 )?;
86 let style_name = preferred_name(
87 &face,
88 name_id::TYPOGRAPHIC_SUBFAMILY,
89 name_id::SUBFAMILY,
90 "style",
91 )?;
92 let fontstack_name = format!("{family_name} {style_name}");
93
94 let units_per_em = face.units_per_em();
95 let ascender_round = (f64::from(face.ascender()) * f64::from(FONT_SIZE_PX)
96 / f64::from(units_per_em))
97 .round() as i32;
98 let charmap = extract_charmap(&face);
99 let advances = (0..face.number_of_glyphs())
100 .map(|glyph_index| {
101 face.glyph_hor_advance(GlyphId(glyph_index))
102 .map(|advance| {
103 glyph::freetype_advance(u32::from(advance), units_per_em, FONT_SIZE_PX)
104 })
105 .unwrap_or(0)
106 })
107 .collect();
108
109 let font = fontdue::Font::from_bytes(
110 bytes,
111 fontdue::FontSettings {
112 scale: FONT_SIZE_PX as f32,
113 collection_index: 0,
114 ..fontdue::FontSettings::default()
115 },
116 )
117 .map_err(|message| Error::InvalidFont(message.to_owned()))?;
118
119 Ok(FontFace {
120 font,
121 family_name,
122 style_name,
123 fontstack_name,
124 ascender_round,
125 charmap,
126 advances,
127 })
128 }
129
130 pub fn family_name(&self) -> &str {
132 &self.family_name
133 }
134
135 pub fn style_name(&self) -> &str {
137 &self.style_name
138 }
139
140 pub fn fontstack_name(&self) -> &str {
142 &self.fontstack_name
143 }
144
145 pub fn covered_ranges(&self) -> Vec<u32> {
147 self.charmap
148 .keys()
149 .map(|codepoint| codepoint & !0xff)
150 .collect::<BTreeSet<_>>()
151 .into_iter()
152 .collect()
153 }
154
155 pub fn glyph_count(&self) -> usize {
157 self.charmap.len()
158 }
159}
160
161pub fn generate_range(face: &FontFace, start: u32) -> Result<Vec<u8>, Error> {
170 if !start.is_multiple_of(256) {
171 return Err(Error::InvalidRangeStart(start));
172 }
173
174 let end = start + 255;
175 let glyphs = face
176 .charmap
177 .range(start..=end)
178 .map(|(&codepoint, &glyph_index)| glyph::generate(face, codepoint, glyph_index))
179 .collect::<Vec<_>>();
180 let range = format!("{start}-{end}");
181 Ok(pbf::encode_fontstack(
182 face.fontstack_name(),
183 &range,
184 &glyphs,
185 ))
186}
187
188fn preferred_name(
189 face: &ttf_parser::Face<'_>,
190 preferred_id: u16,
191 fallback_id: u16,
192 kind: &str,
193) -> Result<String, Error> {
194 for name_id in [preferred_id, fallback_id] {
195 if let Some(value) = face
196 .names()
197 .into_iter()
198 .filter(|name| name.name_id == name_id && name.is_unicode())
199 .filter_map(|name| name.to_string())
200 .find(|value| !value.is_empty())
201 {
202 return Ok(value);
203 }
204 }
205
206 Err(Error::UnsupportedFont(format!(
207 "missing Unicode {kind} name (name IDs {preferred_id} and {fallback_id})"
208 )))
209}
210
211fn extract_charmap(face: &ttf_parser::Face<'_>) -> BTreeMap<u32, u16> {
212 let mut charmap = BTreeMap::new();
213 let Some(cmap) = face.tables().cmap else {
214 return charmap;
215 };
216
217 for subtable in cmap
218 .subtables
219 .into_iter()
220 .filter(|table| table.is_unicode())
221 {
222 subtable.codepoints(|codepoint| {
223 if char::from_u32(codepoint).is_none() {
224 return;
225 }
226 if let Some(glyph_id) = subtable.glyph_index(codepoint)
227 && glyph_id.0 != 0
228 {
229 charmap.insert(codepoint, glyph_id.0);
230 }
231 });
232 }
233
234 charmap
235}
236
237#[cfg(test)]
238mod tests;