1use std::{collections::BTreeSet, fmt};
4
5use font_subset::{Font, FontCategory, OwnedFont, ParseError};
6
7use super::{EmbeddedFont, EmbeddedFontFace, FontEmbedder, FontMetrics};
8
9#[derive(Debug)]
11#[non_exhaustive]
12pub enum SubsettingError {
13 Parse(ParseError),
15 NoFontFamilyName,
17 NoSubsetting,
19 NoEmbedding,
21 NotMonospace,
23 UnsupportedFontCategories(Vec<FontCategory>),
25 MissingChars(String),
27}
28
29impl From<ParseError> for SubsettingError {
30 fn from(err: ParseError) -> Self {
31 Self::Parse(err)
32 }
33}
34
35impl fmt::Display for SubsettingError {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::Parse(err) => write!(formatter, "error parsing font: {err}"),
39 Self::NoFontFamilyName => formatter.write_str("no family name in font file"),
40 Self::NoSubsetting => {
41 formatter.write_str("subsetting is disallowed by font permissions")
42 }
43 Self::NoEmbedding => formatter.write_str("embedding is disallowed by font permissions"),
44 Self::NotMonospace => formatter.write_str("provided font is not monospace"),
45 Self::UnsupportedFontCategories(categories) => {
46 write!(
47 formatter,
48 "unsupported font categories in the provided font faces: {categories:?}"
49 )
50 }
51 Self::MissingChars(chars) => {
52 write!(
53 formatter,
54 "font misses glyphs for chars used in transcript: {chars}"
55 )
56 }
57 }
58 }
59}
60
61impl std::error::Error for SubsettingError {
62 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
63 match self {
64 Self::Parse(err) => Some(err),
65 Self::NoFontFamilyName
66 | Self::NoSubsetting
67 | Self::NoEmbedding
68 | Self::NotMonospace
69 | Self::UnsupportedFontCategories(_)
70 | Self::MissingChars(_) => None,
71 }
72 }
73}
74
75#[derive(Debug)]
76enum AuxFontFaces {
77 Bold(OwnedFont),
78 Italic(OwnedFont),
79}
80
81#[derive(Debug)]
83pub struct FontSubsetter {
84 family_name: String,
85 metrics: FontMetrics,
86 regular_face: OwnedFont,
87 additional_faces: Option<AuxFontFaces>,
88}
89
90impl FontSubsetter {
91 pub fn new(
97 font_bytes: Box<[u8]>,
98 second_face: Option<Box<[u8]>>,
99 ) -> Result<Self, SubsettingError> {
100 use self::FontCategory::{Bold, Italic, Regular};
101
102 let first_face = OwnedFont::new(font_bytes)?;
103 let family_name = Self::check(first_face.get())?.to_owned();
104 let mut metrics = Self::convert_metrics(&first_face.get().metrics())?;
105
106 let second_face = second_face
107 .map(|bytes| {
108 let font = OwnedFont::new(bytes)?;
109 Self::check(font.get())?;
110 Ok::<_, SubsettingError>(font)
111 })
112 .transpose()?;
113
114 let first_cat = first_face.get().category();
115 let (regular, aux) = if let Some(second) = second_face {
116 let second_cat = second.get().category();
117 match (first_cat, second_cat) {
118 (Regular, Bold) => (first_face, Some(AuxFontFaces::Bold(second))),
119 (Regular, Italic) => (first_face, Some(AuxFontFaces::Italic(second))),
120 (Bold, Regular) => (second, Some(AuxFontFaces::Bold(first_face))),
121 (Italic, Regular) => (second, Some(AuxFontFaces::Italic(first_face))),
122 _ => {
123 return Err(SubsettingError::UnsupportedFontCategories(vec![
124 first_cat, second_cat,
125 ]))
126 }
127 }
128 } else {
129 if first_cat != Regular {
130 return Err(SubsettingError::UnsupportedFontCategories(vec![first_cat]));
131 }
132 (first_face, None)
133 };
134
135 match &aux {
136 Some(AuxFontFaces::Bold(font)) => {
137 metrics.bold_spacing = Self::letter_spacing(&metrics, font.get())?;
138 }
139 Some(AuxFontFaces::Italic(font)) => {
140 metrics.italic_spacing = Self::letter_spacing(&metrics, font.get())?;
141 }
142 None => { }
143 }
144
145 Ok(Self {
146 family_name,
147 metrics,
148 regular_face: regular,
149 additional_faces: aux,
150 })
151 }
152
153 fn convert_metrics(metrics: &font_subset::FontMetrics) -> Result<FontMetrics, SubsettingError> {
154 Ok(FontMetrics {
155 units_per_em: metrics.units_per_em,
156 advance_width: metrics
157 .monospace_advance_width
158 .ok_or(SubsettingError::NotMonospace)?,
159 ascent: metrics.ascent,
160 descent: metrics.descent,
161 bold_spacing: 0.0,
162 italic_spacing: 0.0,
163 })
164 }
165
166 fn letter_spacing(base_metrics: &FontMetrics, font: &Font<'_>) -> Result<f64, SubsettingError> {
167 let aux_advance_width = font
168 .metrics()
169 .monospace_advance_width
170 .ok_or(SubsettingError::NotMonospace)?;
171 let aux_advance_width = f64::from(aux_advance_width);
172 Ok((f64::from(base_metrics.advance_width) - aux_advance_width)
173 / f64::from(base_metrics.units_per_em))
174 }
175
176 fn check<'font>(font: &'font Font<'_>) -> Result<&'font str, SubsettingError> {
178 let permissions = font.permissions();
179 if !permissions.allow_subsetting {
180 return Err(SubsettingError::NoSubsetting);
181 }
182 if permissions.embed_only_bitmaps {
183 return Err(SubsettingError::NoEmbedding);
184 }
185
186 font.naming()
187 .family
188 .ok_or(SubsettingError::NoFontFamilyName)
189 }
190
191 fn checked_subset<'a>(
192 font: &Font<'a>,
193 chars: &BTreeSet<char>,
194 ) -> Result<Font<'a>, SubsettingError> {
195 let missing_chars: String = chars
196 .iter()
197 .copied()
198 .filter(|ch| !font.contains_char(*ch))
199 .collect();
200 if !missing_chars.is_empty() {
201 return Err(SubsettingError::MissingChars(missing_chars));
202 }
203 font.subset(chars).map_err(Into::into)
204 }
205}
206
207impl FontEmbedder for FontSubsetter {
208 type Error = SubsettingError;
209
210 fn embed_font(&self, mut used_chars: BTreeSet<char>) -> Result<EmbeddedFont, Self::Error> {
211 used_chars.remove(&'\n');
212 let subset = Self::checked_subset(self.regular_face.get(), &used_chars)?;
213 let mut faces = vec![EmbeddedFontFace::woff2(subset.to_woff2())];
214 match &self.additional_faces {
215 Some(AuxFontFaces::Bold(face)) => {
216 let subset = Self::checked_subset(face.get(), &used_chars)?;
217 faces.push(EmbeddedFontFace {
218 is_bold: Some(true),
219 ..EmbeddedFontFace::woff2(subset.to_woff2())
220 });
221 faces[0].is_bold = Some(false);
222 }
223 Some(AuxFontFaces::Italic(face)) => {
224 let subset = Self::checked_subset(face.get(), &used_chars)?;
225 faces.push(EmbeddedFontFace {
226 is_italic: Some(true),
227 ..EmbeddedFontFace::woff2(subset.to_woff2())
228 });
229 faces[0].is_italic = Some(false);
230 }
231 None => { }
232 }
233
234 Ok(EmbeddedFont {
235 family_name: self.family_name.clone(),
236 metrics: self.metrics,
237 faces,
238 })
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use std::fs;
245
246 use assert_matches::assert_matches;
247 use test_casing::test_casing;
248
249 use super::*;
250 use crate::{
251 svg::{Template, TemplateOptions},
252 Transcript, UserInput,
253 };
254
255 fn roboto_mono() -> Box<[u8]> {
256 fs::read("../examples/fonts/RobotoMono-VariableFont_wght.ttf")
257 .unwrap()
258 .into()
259 }
260
261 fn roboto_mono_italic() -> Box<[u8]> {
262 fs::read("../examples/fonts/RobotoMono-Italic-VariableFont_wght.ttf")
263 .unwrap()
264 .into()
265 }
266
267 fn fira_mono() -> Box<[u8]> {
268 fs::read("../examples/fonts/FiraMono-Regular.ttf")
269 .unwrap()
270 .into()
271 }
272
273 fn fira_mono_bold() -> Box<[u8]> {
274 fs::read("../examples/fonts/FiraMono-Bold.ttf")
275 .unwrap()
276 .into()
277 }
278
279 fn test_subsetting_font(subsetter: FontSubsetter, pure_svg: bool) -> String {
280 let font_family = subsetter.family_name.clone();
281 let mut transcript = Transcript::new();
282 transcript.add_interaction(
283 UserInput::command("test"),
284 "\u{1b}[44m\u{1b}[1mH\u{1b}[0mello, \u{1b}[32m\u{1b}[3mworld\u{1b}[0m! ".repeat(10),
285 );
286
287 let options = TemplateOptions {
288 ..TemplateOptions::default().with_font_subsetting(subsetter)
289 };
290 let mut buffer = vec![];
291 let template = if pure_svg {
292 Template::pure_svg(options)
293 } else {
294 Template::new(options)
295 };
296 template.render(&transcript, &mut buffer).unwrap();
297 let buffer = String::from_utf8(buffer).unwrap();
298
299 assert!(buffer.contains("@font-face"), "{buffer}");
300 assert!(
301 buffer.contains(&format!("font-family: \"{font_family}\";")),
302 "{buffer}"
303 );
304 assert!(
305 buffer.contains("src: url(\"data:font/woff2;base64,"),
306 "{buffer}"
307 );
308 assert!(
309 buffer.contains(&format!("font: 14px \"{font_family}\", monospace;")),
310 "{buffer}"
311 );
312
313 buffer
314 }
315
316 #[test_casing(2, [false, true])]
317 fn subsetting_font(pure_svg: bool) {
318 let subsetter = FontSubsetter::new(roboto_mono(), None).unwrap();
319 assert_eq!(subsetter.family_name, "Roboto Mono");
320 let buffer = test_subsetting_font(subsetter, pure_svg);
321
322 if pure_svg {
323 assert!(
325 buffer.contains(
326 r#"<rect x="10.0" y="27.33" width="8.4" height="18.46" class="fg4"/>"#
327 ),
328 "{buffer}"
329 );
330 assert!(
331 buffer.contains(
332 r#"<rect x="127.62" y="27.33" width="8.4" height="18.46" class="fg4"/>"#
333 ),
334 "{buffer}"
335 );
336 }
337 }
338
339 #[test_casing(2, [false, true])]
340 #[allow(clippy::float_cmp)] fn subsetting_font_with_aux_italic_font(pure_svg: bool) {
342 let subsetter = FontSubsetter::new(roboto_mono(), Some(roboto_mono_italic())).unwrap();
343 assert_eq!(subsetter.family_name, "Roboto Mono");
344 assert_matches!(&subsetter.additional_faces, Some(AuxFontFaces::Italic(_)));
345 assert_eq!(subsetter.metrics.bold_spacing, 0.0);
346 assert_ne!(subsetter.metrics.italic_spacing, 0.0);
347
348 let buffer = test_subsetting_font(subsetter, pure_svg);
349 let font_faces = buffer
350 .lines()
351 .filter(|line| line.trim_start().starts_with("@font-face"))
352 .count();
353 assert_eq!(font_faces, 2, "{buffer}");
354
355 assert!(
356 buffer.contains(".bold,.prompt { font-weight: bold; }"),
357 "{buffer}"
358 );
359 assert!(
360 buffer.contains(".italic { font-style: italic; letter-spacing: 0.0132em; }"),
361 "{buffer}"
362 );
363 }
364
365 #[test_casing(2, [false, true])]
366 #[allow(clippy::float_cmp)] fn subsetting_font_with_aux_bold_font(pure_svg: bool) {
368 let subsetter = FontSubsetter::new(fira_mono(), Some(fira_mono_bold())).unwrap();
369 assert_eq!(subsetter.family_name, "Fira Mono");
370 assert_matches!(&subsetter.additional_faces, Some(AuxFontFaces::Bold(_)));
371 assert_eq!(subsetter.metrics.bold_spacing, 0.0);
373 assert_eq!(subsetter.metrics.italic_spacing, 0.0);
374
375 let buffer = test_subsetting_font(subsetter, pure_svg);
376 let font_faces = buffer
377 .lines()
378 .filter(|line| line.trim_start().starts_with("@font-face"))
379 .count();
380 assert_eq!(font_faces, 2, "{buffer}");
381
382 assert!(
383 buffer.contains(".bold,.prompt { font-weight: bold; }"),
384 "{buffer}"
385 );
386 assert!(
387 buffer.contains(".italic { font-style: italic; }"),
388 "{buffer}"
389 );
390 }
391}