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}