term_transcript/svg/
font.rs

1//! Font-related functionality.
2
3use std::{collections::BTreeSet, fmt};
4
5use base64::{prelude::BASE64_STANDARD, Engine};
6use serde::{Serialize, Serializer};
7
8use crate::BoxedError;
9
10/// Representation of a font that can be embedded into SVG via `@font-face` CSS with a data URL `src`.
11#[derive(Debug, Serialize)]
12pub struct EmbeddedFont {
13    /// Family name of the font.
14    pub family_name: String,
15    /// Font metrics.
16    pub metrics: FontMetrics,
17    /// Font faces. Must have at least 1 entry.
18    pub faces: Vec<EmbeddedFontFace>,
19}
20
21/// Representation of a single face of an [`EmbeddedFont`]. Corresponds to a single `@font-face` CSS rule.
22#[derive(Serialize)]
23pub struct EmbeddedFontFace {
24    /// MIME type for the font, e.g. `font/woff2`.
25    pub mime_type: String,
26    /// Font data. Encoded in base64 when serialized.
27    #[serde(serialize_with = "base64_encode")]
28    pub base64_data: Vec<u8>,
29    /// Determines the `font-weight` selector for the `@font-face` rule.
30    pub is_bold: Option<bool>,
31    /// Determines the `font-style` selector for the `@font-face` rule.
32    pub is_italic: Option<bool>,
33}
34
35// Make `Debug` representation shorter.
36impl fmt::Debug for EmbeddedFontFace {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        formatter
39            .debug_struct("EmbeddedFontFace")
40            .field("mime_type", &self.mime_type)
41            .field("data.len", &self.base64_data.len())
42            .field("is_bold", &self.is_bold)
43            .field("is_italic", &self.is_italic)
44            .finish()
45    }
46}
47
48impl EmbeddedFontFace {
49    /// Creates a face based on the provided WOFF2 font data. All selectors are set to `None`.
50    pub fn woff2(data: Vec<u8>) -> Self {
51        Self {
52            mime_type: "font/woff2".to_owned(),
53            base64_data: data,
54            is_bold: None,
55            is_italic: None,
56        }
57    }
58}
59
60fn base64_encode<S: Serializer>(data: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
61    let encoded = BASE64_STANDARD.encode(data);
62    encoded.serialize(serializer)
63}
64
65/// Font metrics used in SVG layout.
66#[derive(Debug, Clone, Copy, Serialize)]
67pub struct FontMetrics {
68    /// Font design units per em. Usually 1,000 or a power of 2 (e.g., 2,048).
69    pub units_per_em: u16,
70    /// Horizontal advance in font design units.
71    pub advance_width: u16,
72    /// Typographic ascent in font design units. Usually positive.
73    pub ascent: i16,
74    /// Typographic descent in font design units. Usually negative.
75    pub descent: i16,
76    /// `letter-spacing` adjustment for the bold font face in em units.
77    pub bold_spacing: f64,
78    /// `letter-spacing` adjustment for the italic font face in em units. Accounts for font advance width
79    /// not matching between the regular and italic faces (e.g., in Roboto Mono), which can lead
80    /// to misaligned terminal columns.
81    pub italic_spacing: f64,
82}
83
84/// Produces an [`EmbeddedFont`] for SVG.
85pub trait FontEmbedder: 'static + fmt::Debug + Send + Sync {
86    /// Errors produced by the embedder.
87    type Error: Into<BoxedError>;
88
89    /// Performs embedding. This can involve subsetting the font based on the specified chars used in the transcript.
90    ///
91    /// # Errors
92    ///
93    /// May return errors if embedding / subsetting fails.
94    fn embed_font(&self, used_chars: BTreeSet<char>) -> Result<EmbeddedFont, Self::Error>;
95}
96
97#[derive(Debug)]
98pub(super) struct BoxedErrorEmbedder<T>(pub(super) T);
99
100impl<T: FontEmbedder> FontEmbedder for BoxedErrorEmbedder<T> {
101    type Error = BoxedError;
102
103    fn embed_font(&self, used_chars: BTreeSet<char>) -> Result<EmbeddedFont, Self::Error> {
104        self.0.embed_font(used_chars).map_err(Into::into)
105    }
106}