Skip to main content

latex_rust/font/
mod.rs

1//! OpenType math font metrics. Integer font units → [`Dim`](crate::Dim).
2
3use ttf_parser::Face;
4
5use crate::dim::Dim;
6use crate::error::{Error, FontError};
7
8/// Embedded STIX Two Math Regular 2.13 (SIL OFL 1.1).
9///
10/// A `static` rather than a `const`, so that the font's bytes are placed in the binary once and every use refers
11/// to that one copy. A `const` is inlined at each use site, and a crate that reads these bytes as well as calling
12/// [`MathFont::stix_two_math`] carries the 839 KB font twice.
13pub static STIX_TWO_MATH_OTF: &[u8] =
14    include_bytes!("../../fonts/stix-two-math/STIXTwoMath-Regular.otf");
15
16/// SHA-256 (hex) of [`STIX_TWO_MATH_OTF`]. Locked by gold.
17pub const STIX_TWO_MATH_SHA256: &str =
18    "f2076b9f1676438439dd41e23676f5ab99056e83d6b8f8c27841591ef2ccfa72";
19
20/// Face name as shipped.
21pub const STIX_TWO_MATH_NAME: &str = "STIX Two Math";
22
23/// Horizontal glyph metrics in font units and em.
24///
25/// # Examples
26///
27/// ```
28/// use latex_rust::MathFont;
29///
30/// let font = MathFont::stix_two_math().unwrap();
31/// let g = font.glyph('x').unwrap();
32/// assert_eq!(g.ch, 'x');
33/// assert!(!g.advance.is_zero());
34/// ```
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct GlyphMetrics {
37    /// Character requested.
38    pub ch: char,
39    /// OpenType glyph id.
40    pub glyph_id: u16,
41    /// Horizontal advance, font units.
42    pub advance_fu: u16,
43    /// Advance in em.
44    pub advance: Dim,
45    /// Height above baseline in em (`max(y_max, 0)`).
46    pub height: Dim,
47    /// Depth below baseline in em (`max(-y_min, 0)`).
48    pub depth: Dim,
49}
50
51/// Loaded math face.
52///
53/// # Examples
54///
55/// ```
56/// use latex_rust::MathFont;
57///
58/// let font = MathFont::stix_two_math().unwrap();
59/// assert_eq!(font.units_per_em(), 1000);
60/// ```
61pub struct MathFont {
62    raw: &'static [u8],
63    face: Face<'static>,
64    units_per_em: u16,
65    ascender_fu: i16,
66    descender_fu: i16,
67}
68
69impl MathFont {
70    /// Load the embedded STIX Two Math Regular face.
71    ///
72    /// # Errors
73    ///
74    /// [`crate::FontError::InvalidFace`] if the embedded bytes are not a usable OpenType face.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use latex_rust::MathFont;
80    /// assert!(MathFont::stix_two_math().is_ok());
81    /// ```
82    pub fn stix_two_math() -> Result<Self, Error> {
83        Self::from_bytes(STIX_TWO_MATH_OTF)
84    }
85
86    /// Parse OpenType bytes. Lifetime is `'static` for the embedded font only;
87    /// this constructor requires a static buffer so the face can be rebuilt.
88    pub fn from_bytes(raw: &'static [u8]) -> Result<Self, Error> {
89        let face = Face::parse(raw, 0).map_err(|_| FontError::InvalidFace)?;
90        let units_per_em = face.units_per_em();
91        if units_per_em == 0 {
92            return Err(FontError::InvalidFace.into());
93        }
94        let ascender_fu = face.ascender();
95        let descender_fu = face.descender();
96        Ok(Self {
97            raw,
98            face,
99            units_per_em,
100            ascender_fu,
101            descender_fu,
102        })
103    }
104
105    /// The parsed OpenType face.
106    ///
107    /// An external render backend needs glyph outlines and bounding boxes,
108    /// which this crate does not otherwise expose. Reaching the face here
109    /// rather than re-parsing [`Self::bytes`] guarantees that the glyph ids in
110    /// [`BoxContent::Glyph`](crate::BoxContent::Glyph) are resolved against the
111    /// same face, parsed by the same version of `ttf-parser`, that produced
112    /// them. The crate re-exports [`ttf_parser`] so that a
113    /// consumer can name this type without pinning the version itself.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use latex_rust::{ttf_parser, MathFont};
119    ///
120    /// let font = MathFont::stix_two_math().expect("STIX Two Math");
121    /// let metrics = font.glyph('x').expect("x");
122    /// let id = ttf_parser::GlyphId(metrics.glyph_id);
123    /// assert!(font.face().glyph_bounding_box(id).is_some());
124    /// ```
125    #[must_use]
126    pub fn face(&self) -> &Face<'static> {
127        &self.face
128    }
129
130    /// OpenType bytes this face was parsed from.
131    #[must_use]
132    pub fn bytes(&self) -> &'static [u8] {
133        self.raw
134    }
135
136    /// `unitsPerEm` from the `head` table.
137    #[must_use]
138    pub fn units_per_em(&self) -> u16 {
139        self.units_per_em
140    }
141
142    /// `hhea` ascender in font units.
143    #[must_use]
144    pub fn ascender_fu(&self) -> i16 {
145        self.ascender_fu
146    }
147
148    /// `hhea` descender in font units (typically negative).
149    #[must_use]
150    pub fn descender_fu(&self) -> i16 {
151        self.descender_fu
152    }
153
154    /// Ascender in em.
155    #[must_use]
156    pub fn ascender(&self) -> Dim {
157        Dim::from_font_units(i64::from(self.ascender_fu), self.units_per_em)
158    }
159
160    /// Depth below baseline from `hhea` descender, in em (non-negative).
161    #[must_use]
162    pub fn descender(&self) -> Dim {
163        let d = i64::from(self.descender_fu);
164        Dim::from_font_units(-d, self.units_per_em)
165    }
166
167    /// Metrics for `ch`, or [`FontError::MissingGlyph`].
168    pub fn glyph(&self, ch: char) -> Result<GlyphMetrics, Error> {
169        let face = self.face();
170        let gid = face.glyph_index(ch).ok_or(FontError::MissingGlyph { ch })?;
171        let advance_fu = face
172            .glyph_hor_advance(gid)
173            .ok_or(FontError::MissingGlyph { ch })?;
174        let mut height_fu = 0i64;
175        let mut depth_fu = 0i64;
176        if let Some(bbox) = face.glyph_bounding_box(gid) {
177            height_fu = i64::from(bbox.y_max).max(0);
178            depth_fu = i64::from(-bbox.y_min).max(0);
179        }
180        let upem = self.units_per_em;
181        Ok(GlyphMetrics {
182            ch,
183            glyph_id: gid.0,
184            advance_fu,
185            advance: Dim::from_font_units(i64::from(advance_fu), upem),
186            height: Dim::from_font_units(height_fu, upem),
187            depth: Dim::from_font_units(depth_fu, upem),
188        })
189    }
190
191    /// Metrics for OpenType glyph id `gid`, tagged with `ch` for the box payload.
192    pub fn glyph_id(&self, ch: char, gid: u16) -> Result<GlyphMetrics, Error> {
193        let face = self.face();
194        let gid = ttf_parser::GlyphId(gid);
195        let advance_fu = face
196            .glyph_hor_advance(gid)
197            .ok_or(FontError::MissingGlyph { ch })?;
198        let mut height_fu = 0i64;
199        let mut depth_fu = 0i64;
200        if let Some(bbox) = face.glyph_bounding_box(gid) {
201            height_fu = i64::from(bbox.y_max).max(0);
202            depth_fu = i64::from(-bbox.y_min).max(0);
203        }
204        let upem = self.units_per_em;
205        Ok(GlyphMetrics {
206            ch,
207            glyph_id: gid.0,
208            advance_fu,
209            advance: Dim::from_font_units(i64::from(advance_fu), upem),
210            height: Dim::from_font_units(height_fu, upem),
211            depth: Dim::from_font_units(depth_fu, upem),
212        })
213    }
214
215    /// MATH italic correction for `glyph_id`, or zero.
216    pub fn italic_correction(&self, glyph_id: u16) -> Dim {
217        let face = self.face();
218        let Some(math) = face.tables().math else {
219            return Dim::zero();
220        };
221        let Some(info) = math.glyph_info else {
222            return Dim::zero();
223        };
224        let Some(table) = info.italic_corrections else {
225            return Dim::zero();
226        };
227        match table.get(ttf_parser::GlyphId(glyph_id)) {
228            Some(v) => Dim::from_font_units(i64::from(v.value), self.units_per_em),
229            None => Dim::zero(),
230        }
231    }
232
233    /// MATH top-accent attachment (em from glyph left), if present.
234    pub fn top_accent_attachment(&self, glyph_id: u16) -> Option<Dim> {
235        let face = self.face();
236        let math = face.tables().math?;
237        let info = math.glyph_info?;
238        let table = info.top_accent_attachments?;
239        let v = table.get(ttf_parser::GlyphId(glyph_id))?;
240        Some(Dim::from_font_units(i64::from(v.value), self.units_per_em))
241    }
242
243    /// Horizontal glyph-assembly parts: `(gid, start_connector, end_connector, advance, extender)`.
244    /// Lengths are font units.
245    pub fn horizontal_assembly_parts(&self, glyph_id: u16) -> Vec<(u16, u16, u16, u16, bool)> {
246        let mut out = Vec::new();
247        let face = self.face();
248        let Some(math) = face.tables().math else {
249            return out;
250        };
251        let Some(variants) = math.variants else {
252            return out;
253        };
254        let Some(cons) = variants
255            .horizontal_constructions
256            .get(ttf_parser::GlyphId(glyph_id))
257        else {
258            return out;
259        };
260        let Some(assembly) = cons.assembly else {
261            return out;
262        };
263        for i in 0..assembly.parts.len() {
264            if let Some(p) = assembly.parts.get(i) {
265                out.push((
266                    p.glyph_id.0,
267                    p.start_connector_length,
268                    p.end_connector_length,
269                    p.full_advance,
270                    p.part_flags.extender(),
271                ));
272            }
273        }
274        out
275    }
276
277    /// Horizontal MATH variants of `glyph_id`, including the base glyph first.
278    pub fn horizontal_variants(&self, glyph_id: u16) -> Vec<u16> {
279        let mut out = vec![glyph_id];
280        let face = self.face();
281        let Some(math) = face.tables().math else {
282            return out;
283        };
284        let Some(variants) = math.variants else {
285            return out;
286        };
287        let Some(cons) = variants
288            .horizontal_constructions
289            .get(ttf_parser::GlyphId(glyph_id))
290        else {
291            return out;
292        };
293        for i in 0..cons.variants.len() {
294            if let Some(v) = cons.variants.get(i) {
295                out.push(v.variant_glyph.0);
296            }
297        }
298        out
299    }
300
301    /// Vertical MATH variants of `glyph_id`, including the base glyph first.
302    pub fn vertical_variants(&self, glyph_id: u16) -> Vec<u16> {
303        let mut out = vec![glyph_id];
304        let face = self.face();
305        let Some(math) = face.tables().math else {
306            return out;
307        };
308        let Some(variants) = math.variants else {
309            return out;
310        };
311        let Some(cons) = variants
312            .vertical_constructions
313            .get(ttf_parser::GlyphId(glyph_id))
314        else {
315            return out;
316        };
317        for i in 0..cons.variants.len() {
318            if let Some(v) = cons.variants.get(i) {
319                out.push(v.variant_glyph.0);
320            }
321        }
322        out
323    }
324
325    /// SHA-256 hex of the raw face bytes.
326    #[must_use]
327    pub fn sha256_hex(bytes: &[u8]) -> String {
328        let d = crate::hash::sha256(bytes);
329        let mut s = String::with_capacity(64);
330        for b in d {
331            s.push_str(&hex_byte(b));
332        }
333        s
334    }
335}
336
337fn hex_byte(b: u8) -> String {
338    const H: &[u8; 16] = b"0123456789abcdef";
339    let hi = H[(b >> 4) as usize];
340    let lo = H[(b & 0xf) as usize];
341    let mut out = String::with_capacity(2);
342    out.push(hi as char);
343    out.push(lo as char);
344    out
345}