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
75pub struct FontFace {
77 inner: OwnedFont,
78 advance_width: u16,
79}
80
81impl fmt::Debug for FontFace {
83 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84 let font = self.inner.get();
85 formatter
86 .debug_struct("FontFace")
87 .field("family_name", &self.family_name())
88 .field("category", &font.category())
89 .field("variation_axes", &font.variation_axes())
90 .field("metrics", &font.metrics())
91 .finish_non_exhaustive()
92 }
93}
94
95impl FontFace {
96 pub fn new(bytes: Box<[u8]>) -> Result<Self, SubsettingError> {
103 let inner = OwnedFont::new(bytes)?;
104 let advance_width = Self::check(inner.get())?;
105 Ok(Self {
106 inner,
107 advance_width,
108 })
109 }
110
111 fn check(font: &Font<'_>) -> Result<u16, SubsettingError> {
112 let permissions = font.permissions();
113 if !permissions.allow_subsetting {
114 return Err(SubsettingError::NoSubsetting);
115 }
116 if permissions.embed_only_bitmaps {
117 return Err(SubsettingError::NoEmbedding);
118 }
119
120 font.naming()
121 .family
122 .ok_or(SubsettingError::NoFontFamilyName)?;
123 font.metrics()
124 .monospace_advance_width
125 .ok_or(SubsettingError::NotMonospace)
126 }
127
128 fn family_name(&self) -> &str {
129 self.inner.get().naming().family.unwrap()
131 }
132
133 fn category(&self) -> FontCategory {
134 self.inner.get().category()
135 }
136
137 fn metrics(&self) -> FontMetrics {
138 let metrics = self.inner.get().metrics();
139 FontMetrics {
140 units_per_em: metrics.units_per_em,
141 advance_width: self.advance_width,
142 ascent: metrics.ascent,
143 descent: metrics.descent,
144 bold_spacing: 0.0,
145 italic_spacing: 0.0,
146 }
147 }
148
149 fn letter_spacing(&self, base_metrics: &FontMetrics) -> f64 {
150 (f64::from(base_metrics.advance_width) - f64::from(self.advance_width))
151 / f64::from(base_metrics.units_per_em)
152 }
153
154 fn checked_subset(&self, chars: &BTreeSet<char>) -> Result<Font<'_>, SubsettingError> {
155 let font = self.inner.get();
156 let missing_chars: String = chars
157 .iter()
158 .copied()
159 .filter(|ch| !font.contains_char(*ch))
160 .collect();
161 if !missing_chars.is_empty() {
162 return Err(SubsettingError::MissingChars(missing_chars));
163 }
164 font.subset(chars).map_err(Into::into)
165 }
166}
167
168#[derive(Debug)]
169enum AuxFontFaces {
170 Bold(FontFace),
171 Italic(FontFace),
172}
173
174#[derive(Debug)]
176pub struct FontSubsetter {
177 family_name: String,
178 metrics: FontMetrics,
179 regular_face: FontFace,
180 additional_faces: Option<AuxFontFaces>,
181}
182
183impl FontSubsetter {
184 pub fn new(font: FontFace) -> Result<Self, SubsettingError> {
194 Self::from_faces(font, None)
195 }
196
197 #[cfg_attr(feature = "tracing", tracing::instrument(ret, err))]
209 pub fn from_faces(
210 first_face: FontFace,
211 second_face: Option<FontFace>,
212 ) -> Result<Self, SubsettingError> {
213 use self::FontCategory::{Bold, Italic, Regular};
214
215 let first_cat = first_face.category();
216 let (regular, aux) = if let Some(second) = second_face {
217 let second_cat = second.category();
218 match (first_cat, second_cat) {
219 (Regular, Bold) => (first_face, Some(AuxFontFaces::Bold(second))),
220 (Regular, Italic) => (first_face, Some(AuxFontFaces::Italic(second))),
221 (Bold, Regular) => (second, Some(AuxFontFaces::Bold(first_face))),
222 (Italic, Regular) => (second, Some(AuxFontFaces::Italic(first_face))),
223 _ => {
224 return Err(SubsettingError::UnsupportedFontCategories(vec![
225 first_cat, second_cat,
226 ]))
227 }
228 }
229 } else {
230 if first_cat != Regular {
231 return Err(SubsettingError::UnsupportedFontCategories(vec![first_cat]));
232 }
233 (first_face, None)
234 };
235
236 let family_name = regular.family_name().to_owned();
237 let mut metrics = regular.metrics();
238 match &aux {
239 Some(AuxFontFaces::Bold(font)) => {
240 metrics.bold_spacing = font.letter_spacing(&metrics);
241 }
242 Some(AuxFontFaces::Italic(font)) => {
243 metrics.italic_spacing = font.letter_spacing(&metrics);
244 }
245 None => { }
246 }
247
248 #[cfg(feature = "tracing")]
249 tracing::info!(?metrics, "using font metrics");
250
251 Ok(Self {
252 family_name,
253 metrics,
254 regular_face: regular,
255 additional_faces: aux,
256 })
257 }
258}
259
260impl FontEmbedder for FontSubsetter {
261 type Error = SubsettingError;
262
263 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), ret, err))]
264 fn embed_font(&self, mut used_chars: BTreeSet<char>) -> Result<EmbeddedFont, Self::Error> {
265 used_chars.remove(&'\n');
266 let subset = self.regular_face.checked_subset(&used_chars)?;
267 let mut faces = vec![EmbeddedFontFace::woff2(subset.to_woff2())];
268 match &self.additional_faces {
269 Some(AuxFontFaces::Bold(face)) => {
270 let subset = face.checked_subset(&used_chars)?;
271 faces.push(EmbeddedFontFace {
272 is_bold: Some(true),
273 ..EmbeddedFontFace::woff2(subset.to_woff2())
274 });
275 faces[0].is_bold = Some(false);
276 }
277 Some(AuxFontFaces::Italic(face)) => {
278 let subset = face.checked_subset(&used_chars)?;
279 faces.push(EmbeddedFontFace {
280 is_italic: Some(true),
281 ..EmbeddedFontFace::woff2(subset.to_woff2())
282 });
283 faces[0].is_italic = Some(false);
284 }
285 None => { }
286 }
287
288 Ok(EmbeddedFont {
289 family_name: self.family_name.clone(),
290 metrics: self.metrics,
291 faces,
292 })
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use std::{fs, path::Path};
299
300 use assert_matches::assert_matches;
301 use test_casing::test_casing;
302
303 use super::*;
304 use crate::{
305 svg::{Template, TemplateOptions},
306 Transcript, UserInput,
307 };
308
309 fn read_font(path: &str) -> FontFace {
310 const FONTS_DIR: &str = "../examples/fonts";
311
312 let bytes = fs::read(Path::new(FONTS_DIR).join(path)).unwrap();
313 FontFace::new(bytes.into()).unwrap()
314 }
315
316 fn roboto_mono() -> FontFace {
317 read_font("RobotoMono.ttf")
318 }
319
320 fn roboto_mono_italic() -> FontFace {
321 read_font("RobotoMono-Italic.ttf")
322 }
323
324 fn fira_mono() -> FontFace {
325 read_font("FiraMono-Regular.ttf")
326 }
327
328 fn fira_mono_bold() -> FontFace {
329 read_font("FiraMono-Bold.ttf")
330 }
331
332 fn test_subsetting_font(subsetter: FontSubsetter, pure_svg: bool) -> String {
333 let font_family = subsetter.family_name.clone();
334 let mut transcript = Transcript::new();
335 transcript.add_interaction(
336 UserInput::command("test"),
337 "\u{1b}[44m\u{1b}[1mH\u{1b}[0mello, \u{1b}[32m\u{1b}[3mworld\u{1b}[0m! ".repeat(10),
338 );
339
340 let options = TemplateOptions::default().with_font_subsetting(subsetter);
341 let options = options.validated().unwrap();
342 let mut buffer = vec![];
343 let template = if pure_svg {
344 Template::pure_svg(options)
345 } else {
346 Template::new(options)
347 };
348 template.render(&transcript, &mut buffer).unwrap();
349 let buffer = String::from_utf8(buffer).unwrap();
350
351 assert!(buffer.contains("@font-face"), "{buffer}");
352 assert!(
353 buffer.contains(&format!("font-family: \"{font_family}\";")),
354 "{buffer}"
355 );
356 assert!(
357 buffer.contains("src: url(\"data:font/woff2;base64,"),
358 "{buffer}"
359 );
360 assert!(
361 buffer.contains(&format!("font: 14px \"{font_family}\", monospace;")),
362 "{buffer}"
363 );
364
365 buffer
366 }
367
368 #[test_casing(2, [false, true])]
369 fn subsetting_font(pure_svg: bool) {
370 let subsetter = FontSubsetter::new(roboto_mono()).unwrap();
371 assert_eq!(subsetter.family_name, "Roboto Mono");
372 let buffer = test_subsetting_font(subsetter, pure_svg);
373
374 if pure_svg {
375 assert!(
377 buffer.contains(r#"<rect x="10" y="28.5" width="8.4" height="18.5" class="fg4"/>"#),
378 "{buffer}"
379 );
380 assert!(
381 buffer.contains(
382 r#"<rect x="127.62" y="28.5" width="8.4" height="18.5" class="fg4"/>"#
383 ),
384 "{buffer}"
385 );
386 }
387 }
388
389 #[test_casing(2, [false, true])]
390 #[allow(clippy::float_cmp)] fn subsetting_font_with_aux_italic_font(pure_svg: bool) {
392 let subsetter =
393 FontSubsetter::from_faces(roboto_mono(), Some(roboto_mono_italic())).unwrap();
394 assert_eq!(subsetter.family_name, "Roboto Mono");
395 assert_matches!(&subsetter.additional_faces, Some(AuxFontFaces::Italic(_)));
396 assert_eq!(subsetter.metrics.bold_spacing, 0.0);
397 assert_ne!(subsetter.metrics.italic_spacing, 0.0);
398
399 let buffer = test_subsetting_font(subsetter, pure_svg);
400 let font_faces = buffer
401 .lines()
402 .filter(|line| line.trim_start().starts_with("@font-face"))
403 .count();
404 assert_eq!(font_faces, 2, "{buffer}");
405
406 assert!(
407 buffer.contains(".bold,.prompt { font-weight: bold; }"),
408 "{buffer}"
409 );
410 assert!(
411 buffer.contains(".italic { font-style: italic; letter-spacing: 0.0132em; }"),
412 "{buffer}"
413 );
414 }
415
416 #[test_casing(2, [false, true])]
417 #[allow(clippy::float_cmp)] fn subsetting_font_with_aux_bold_font(pure_svg: bool) {
419 let subsetter = FontSubsetter::from_faces(fira_mono(), Some(fira_mono_bold())).unwrap();
420 assert_eq!(subsetter.family_name, "Fira Mono");
421 assert_matches!(&subsetter.additional_faces, Some(AuxFontFaces::Bold(_)));
422 assert_eq!(subsetter.metrics.bold_spacing, 0.0);
424 assert_eq!(subsetter.metrics.italic_spacing, 0.0);
425
426 let buffer = test_subsetting_font(subsetter, pure_svg);
427 let font_faces = buffer
428 .lines()
429 .filter(|line| line.trim_start().starts_with("@font-face"))
430 .count();
431 assert_eq!(font_faces, 2, "{buffer}");
432
433 assert!(
434 buffer.contains(".bold,.prompt { font-weight: bold; }"),
435 "{buffer}"
436 );
437 assert!(
438 buffer.contains(".italic { font-style: italic; }"),
439 "{buffer}"
440 );
441 }
442}