Skip to main content

mos_fonts/
font.rs

1use crate::resources;
2use crate::{Base14Font, EmbeddedFont};
3
4/// A renderable font -- either one of the Adobe Core 14 (no data
5/// embedded; outlines from the PDF reader) or a bundled TrueType
6/// cut (data embedded, subset per-document).
7///
8/// # Examples
9///
10/// ```
11/// use mos_fonts::{Base14Font, Font};
12///
13/// let font = Font::Base14(Base14Font::Helvetica);
14///
15/// assert_eq!(font.pdf_base_name(), "Helvetica");
16/// ```
17#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
18pub enum Font {
19    /// A Base14 face. Layout uses AFM metrics; PDF emit uses
20    /// `WinAnsiEncoding` + per-document `/Differences`.
21    Base14(Base14Font),
22    /// A bundled embedded face. Layout uses `rustybuzz` shaping;
23    /// PDF emit produces a Type 0 CID font with `/ToUnicode`.
24    Embedded(EmbeddedFontId),
25}
26
27/// Stable identifier for each bundled embedded cut. Used as the enum
28/// payload of [`Font::Embedded`] so [`Font`] stays `Copy`/`Hash`/`Eq`
29/// without resorting to pointer identity.
30///
31/// The crate ships three bundled families today: Noto Sans (four style
32/// cuts -- `Regular`/`Bold`/`Italic`/`BoldItalic`) for proportional
33/// body text, Noto Sans Mono (one cut -- `Mono`) for `` `raw` `` runs,
34/// and Noto Sans Math (one cut -- `Math`) as the per-glyph fallback for
35/// math operators when the primary face doesn't cover them. The
36/// flat-enum shape is deliberate at this scale -- when a fourth family
37/// lands the right move is restructuring to `{ family, cut }` rather
38/// than expanding this enum variant-by-variant further.
39///
40/// # Examples
41///
42/// ```
43/// use mos_fonts::EmbeddedFontId;
44///
45/// let id = EmbeddedFontId::Regular;
46///
47/// assert_eq!(id.pdf_resource_index(), 15);
48/// ```
49#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Ord, PartialOrd)]
50pub enum EmbeddedFontId {
51    /// Noto Sans Regular.
52    Regular,
53    /// Noto Sans Bold.
54    Bold,
55    /// Noto Sans Italic.
56    Italic,
57    /// Noto Sans Bold Italic.
58    BoldItalic,
59    /// Noto Sans Mono Regular. The crate's monospace face for raw runs.
60    Mono,
61    /// Noto Sans Math Regular. The crate's math face -- covers
62    /// `<= >= != sqrt partial sum delta diamond minus` and broader
63    /// Unicode math. Used as the default fallback target on
64    /// [`crate::FontFamily::noto_sans`] so math codepoints route through
65    /// it when shaping yields `.notdef` against the primary face.
66    Math,
67}
68
69impl EmbeddedFontId {
70    /// All bundled embedded cuts in a stable order. Used by the PDF
71    /// backend to enumerate `/Font` resource entries deterministically.
72    pub const ALL: [Self; 6] = [
73        Self::Regular,
74        Self::Bold,
75        Self::Italic,
76        Self::BoldItalic,
77        Self::Mono,
78        Self::Math,
79    ];
80
81    /// Resolve to the bundled [`EmbeddedFont`] data. Initialised on
82    /// first use; subsequent calls return the same `&'static` reference.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use mos_fonts::EmbeddedFontId;
88    ///
89    /// let data = EmbeddedFontId::Regular.data();
90    ///
91    /// assert_eq!(data.postscript_name, "NotoSans");
92    /// ```
93    #[must_use]
94    pub fn data(self) -> &'static EmbeddedFont {
95        resources::embedded_font(self)
96    }
97
98    /// PDF resource-name index `n` for `Fn`. F1..F14 are reserved for
99    /// the 14 Base14 fonts (see `mos-pdf`'s Base14 resource layout);
100    /// embedded cuts use F15..=F255. The number is **stable per
101    /// variant forever** -- never re-number for byte-stability.
102    /// Adding a new bundled cut is one match arm here plus a
103    /// `RESOURCE_NAMES`-table lookup; the 256-entry table in
104    /// `$OUT_DIR/resource_names.rs` is generated by `build.rs` and
105    /// already accommodates F20..=F255 with no per-variant churn.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use mos_fonts::EmbeddedFontId;
111    ///
112    /// assert_eq!(EmbeddedFontId::Math.pdf_resource_index(), 20);
113    /// ```
114    #[must_use]
115    pub const fn pdf_resource_index(self) -> u8 {
116        match self {
117            Self::Regular => 15,
118            Self::Bold => 16,
119            Self::Italic => 17,
120            Self::BoldItalic => 18,
121            Self::Mono => 19,
122            Self::Math => 20,
123        }
124    }
125
126    /// PDF resource name (`b"F<n>"`) for this cut. Backed by a baked
127    /// `[&[u8]; 256]` LUT generated at build time so future variants
128    /// only need to pick an unused index above and the name resolves
129    /// automatically.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use mos_fonts::EmbeddedFontId;
135    ///
136    /// assert_eq!(EmbeddedFontId::Regular.pdf_resource_name(), b"F15");
137    /// ```
138    #[must_use]
139    pub fn pdf_resource_name(self) -> &'static [u8] {
140        resources::pdf_resource_name(self)
141    }
142}
143
144impl Font {
145    /// All 14 Base14 faces in Mosaic's PDF-resource order (`F1`..`F14`).
146    ///
147    /// Page resource dictionaries always enumerate these -- even when
148    /// unused -- so a Base14-only document's byte output stays stable
149    /// across runs. Embedded faces are added on top, per-document.
150    ///
151    /// The ordering is **decoupled** from [`Base14Font::ALL`]: the
152    /// four pre-existing layout faces keep their historical `F1`..`F4`
153    /// resource numbers so existing integration goldens don't shift.
154    pub const ALL_BASE14: [Self; 14] = [
155        Self::Base14(Base14Font::Helvetica),
156        Self::Base14(Base14Font::HelveticaBold),
157        Self::Base14(Base14Font::HelveticaOblique),
158        Self::Base14(Base14Font::Courier),
159        Self::Base14(Base14Font::HelveticaBoldOblique),
160        Self::Base14(Base14Font::TimesRoman),
161        Self::Base14(Base14Font::TimesBold),
162        Self::Base14(Base14Font::TimesItalic),
163        Self::Base14(Base14Font::TimesBoldItalic),
164        Self::Base14(Base14Font::CourierBold),
165        Self::Base14(Base14Font::CourierOblique),
166        Self::Base14(Base14Font::CourierBoldOblique),
167        Self::Base14(Base14Font::Symbol),
168        Self::Base14(Base14Font::ZapfDingbats),
169    ];
170
171    /// If this is a Base14 face, return the underlying variant.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use mos_fonts::{Base14Font, Font};
177    ///
178    /// let font = Font::Base14(Base14Font::Courier);
179    ///
180    /// assert_eq!(font.base14(), Some(Base14Font::Courier));
181    /// ```
182    #[must_use]
183    pub const fn base14(self) -> Option<Base14Font> {
184        match self {
185            Self::Base14(f) => Some(f),
186            Self::Embedded(_) => None,
187        }
188    }
189
190    /// If this is an embedded face, return its bundled id.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use mos_fonts::{EmbeddedFontId, Font};
196    ///
197    /// let font = Font::Embedded(EmbeddedFontId::Bold);
198    ///
199    /// assert_eq!(font.embedded(), Some(EmbeddedFontId::Bold));
200    /// ```
201    #[must_use]
202    pub const fn embedded(self) -> Option<EmbeddedFontId> {
203        match self {
204            Self::Embedded(id) => Some(id),
205            Self::Base14(_) => None,
206        }
207    }
208
209    /// PDF `/BaseFont` name for Base14 (e.g. `"Helvetica-BoldOblique"`)
210    /// or the embedded face's PostScript name. The embedded path also
211    /// gets a six-letter subset tag in the actual PDF emission, but that
212    /// belongs to the per-document subset, not the bundled cut.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use mos_fonts::{Base14Font, Font};
218    ///
219    /// let font = Font::Base14(Base14Font::HelveticaBoldOblique);
220    ///
221    /// assert_eq!(font.pdf_base_name(), "Helvetica-BoldOblique");
222    /// ```
223    #[must_use]
224    pub fn pdf_base_name(self) -> &'static str {
225        match self {
226            Self::Base14(f) => f.pdf_base_name(),
227            Self::Embedded(id) => id.data().postscript_name,
228        }
229    }
230
231    /// Stable per-resource name (`F1`..`F14` for Base14, `F15`..`F20`
232    /// for embedded). Page font dictionaries map these to indirect
233    /// font refs.
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use mos_fonts::{Base14Font, Font};
239    ///
240    /// let font = Font::Base14(Base14Font::Helvetica);
241    ///
242    /// assert_eq!(font.pdf_resource_name(), b"F1");
243    /// ```
244    #[must_use]
245    pub fn pdf_resource_name(self) -> &'static [u8] {
246        match self {
247            Self::Base14(f) => Self::Base14(f).base14_resource_name(),
248            Self::Embedded(id) => id.pdf_resource_name(),
249        }
250    }
251
252    /// Internal: Base14-only resource name table (`F1`..`F14`). Kept as
253    /// a method on `Font` rather than on `Base14Font` so the lookup
254    /// reads the same way the enum dispatch does. The `Embedded` arm
255    /// is handled at the caller (`pdf_resource_name`) so this private
256    /// helper never sees it.
257    fn base14_resource_name(self) -> &'static [u8] {
258        let Self::Base14(f) = self else {
259            return b"F0";
260        };
261        match f {
262            Base14Font::Helvetica => b"F1",
263            Base14Font::HelveticaBold => b"F2",
264            Base14Font::HelveticaOblique => b"F3",
265            Base14Font::Courier => b"F4",
266            Base14Font::HelveticaBoldOblique => b"F5",
267            Base14Font::TimesRoman => b"F6",
268            Base14Font::TimesBold => b"F7",
269            Base14Font::TimesItalic => b"F8",
270            Base14Font::TimesBoldItalic => b"F9",
271            Base14Font::CourierBold => b"F10",
272            Base14Font::CourierOblique => b"F11",
273            Base14Font::CourierBoldOblique => b"F12",
274            Base14Font::Symbol => b"F13",
275            Base14Font::ZapfDingbats => b"F14",
276        }
277    }
278}
279
280impl From<Base14Font> for Font {
281    fn from(f: Base14Font) -> Self {
282        Self::Base14(f)
283    }
284}
285
286impl From<EmbeddedFontId> for Font {
287    fn from(id: EmbeddedFontId) -> Self {
288        Self::Embedded(id)
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn pdf_resource_name_is_f1_through_f20() {
298        for (i, font) in Font::ALL_BASE14.iter().enumerate() {
299            let expected = format!("F{}", i + 1);
300            assert_eq!(font.pdf_resource_name(), expected.as_bytes());
301        }
302        for (i, id) in EmbeddedFontId::ALL.iter().enumerate() {
303            let expected = format!("F{}", 15 + i);
304            assert_eq!(id.pdf_resource_name(), expected.as_bytes());
305        }
306    }
307
308    #[test]
309    fn font_all_base14_preserves_historical_resource_numbers() {
310        assert_eq!(Font::ALL_BASE14[0], Font::Base14(Base14Font::Helvetica));
311        assert_eq!(Font::ALL_BASE14[1], Font::Base14(Base14Font::HelveticaBold));
312        assert_eq!(
313            Font::ALL_BASE14[2],
314            Font::Base14(Base14Font::HelveticaOblique)
315        );
316        assert_eq!(Font::ALL_BASE14[3], Font::Base14(Base14Font::Courier));
317    }
318}