Skip to main content

typst_pack/
font_catalog.rs

1//! Validated Font Containers and the ordered Font Catalog.
2
3#[cfg(feature = "embedded-fonts")]
4use typst::foundations::Bytes;
5use typst::text::{Font, FontBook, FontInfo};
6use typst::utils::LazyHash;
7use typst_kit::fonts::FontStore;
8
9use crate::CanonicalIdentity;
10use crate::pack::{FontFaceIdentity, font_container_identity};
11use crate::payload::SharedBytes;
12
13/// Whether a Font Container's bytes travel inside the Pack or must be
14/// fulfilled externally when the Pack is compiled.
15///
16/// A caller declares the disposition of every catalog position; it is never
17/// inferred from container bytes, so identical inputs produce identical Packs
18/// across build configurations.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum FontDisposition {
21    /// The container's exact bytes are stored in the Pack.
22    Embedded,
23    /// The container is declared and must be supplied at compilation.
24    External,
25}
26
27impl FontDisposition {
28    /// [`Embedded`](Self::Embedded) when `embed` is set, otherwise
29    /// [`External`](Self::External).
30    pub fn embedded_if(embed: bool) -> Self {
31        if embed {
32            Self::Embedded
33        } else {
34            Self::External
35        }
36    }
37
38    /// Whether the container's exact bytes are stored in the Pack.
39    pub fn is_embedded(self) -> bool {
40        matches!(self, Self::Embedded)
41    }
42}
43
44/// A failure to construct a validated Font Container.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
46#[non_exhaustive]
47pub enum FontContainerError {
48    /// The exact bytes contain no face the embedded Typst engine can read.
49    #[error("font container has no readable face")]
50    NoReadableFace,
51}
52
53/// The exact validated bytes of one standalone font file or multi-face
54/// collection.
55#[derive(Clone, Debug)]
56pub struct FontContainer {
57    data: SharedBytes,
58    identity: CanonicalIdentity,
59    faces: Vec<FontContainerFace>,
60}
61
62impl FontContainer {
63    /// Validates exact owned container bytes.
64    pub fn new(data: impl Into<Vec<u8>>) -> Result<Self, FontContainerError> {
65        Self::from_shared(SharedBytes::new(data.into()))
66    }
67
68    #[cfg(feature = "embedded-fonts")]
69    fn from_bytes(data: Bytes) -> Result<Self, FontContainerError> {
70        Self::from_shared(SharedBytes::from_typst(data))
71    }
72
73    pub(crate) fn from_shared(data: SharedBytes) -> Result<Self, FontContainerError> {
74        let identity = font_container_identity(data.as_slice());
75        let faces = Font::iter(data.to_typst())
76            .map(|font| FontContainerFace {
77                identity: FontFaceIdentity::new(identity, font.index()),
78                font,
79            })
80            .collect::<Vec<_>>();
81        if faces.is_empty() {
82            return Err(FontContainerError::NoReadableFace);
83        }
84        Ok(Self {
85            data,
86            identity,
87            faces,
88        })
89    }
90
91    /// The exact container bytes.
92    pub fn data(&self) -> &[u8] {
93        self.data.as_slice()
94    }
95
96    /// The Canonical Identity of the container bytes.
97    pub fn identity(&self) -> CanonicalIdentity {
98        self.identity
99    }
100
101    /// The readable faces in container-local index order.
102    pub fn faces(&self) -> &[FontContainerFace] {
103        &self.faces
104    }
105
106    pub(crate) fn font(&self, index: u32) -> Option<Font> {
107        self.faces
108            .iter()
109            .find(|face| face.identity.index() == index)
110            .map(|face| face.font.clone())
111    }
112}
113
114impl PartialEq for FontContainer {
115    fn eq(&self, other: &Self) -> bool {
116        self.data == other.data
117    }
118}
119
120impl Eq for FontContainer {}
121
122/// One readable face of a validated Font Container.
123#[derive(Clone, Debug)]
124pub struct FontContainerFace {
125    identity: FontFaceIdentity,
126    font: Font,
127}
128
129impl FontContainerFace {
130    /// The exact container and container-local face index.
131    pub fn identity(&self) -> FontFaceIdentity {
132        self.identity
133    }
134
135    /// The shared exact container bytes this face was parsed from.
136    pub fn data(&self) -> &[u8] {
137        self.font.data().as_slice()
138    }
139
140    /// Official selection metadata derived from the verified container bytes.
141    pub fn info(&self) -> &FontInfo {
142        self.font.info()
143    }
144}
145
146impl PartialEq for FontContainerFace {
147    fn eq(&self, other: &Self) -> bool {
148        self.identity == other.identity && self.data() == other.data()
149    }
150}
151
152impl Eq for FontContainerFace {}
153
154/// One position in a Font Catalog.
155#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct FontCatalogEntry {
157    container: FontContainer,
158    disposition: FontDisposition,
159}
160
161impl FontCatalogEntry {
162    /// Pairs one validated container with its explicit disposition.
163    pub fn new(container: FontContainer, disposition: FontDisposition) -> Self {
164        Self {
165            container,
166            disposition,
167        }
168    }
169
170    /// The validated Font Container at this position.
171    pub fn container(&self) -> &FontContainer {
172        &self.container
173    }
174
175    /// Whether this position embeds or externally fulfills its container.
176    pub fn disposition(&self) -> FontDisposition {
177        self.disposition
178    }
179}
180
181/// Exactly the Font Containers Pack Creation may select faces from, in the
182/// order the caller chose.
183///
184/// Face selection is attributable to that order alone. Faces are expanded in
185/// container-local index order, and nothing joins a supplied catalog
186/// implicitly, so Pack contents are not a function of which crate features a
187/// build enabled or which font sources a host offers.
188#[derive(Clone, Debug, Default, PartialEq, Eq)]
189pub struct FontCatalog {
190    entries: Vec<FontCatalogEntry>,
191}
192
193impl FontCatalog {
194    /// An empty catalog, offering no face at all.
195    pub fn new() -> Self {
196        Self::default()
197    }
198
199    /// Appends one explicitly disposed container after every existing entry.
200    pub fn push(&mut self, entry: FontCatalogEntry) {
201        self.entries.push(entry);
202    }
203
204    /// The entries in insertion order.
205    pub fn entries(&self) -> &[FontCatalogEntry] {
206        &self.entries
207    }
208
209    /// The faces this catalog offers, in catalog order: every face
210    /// of the first container in container-local index order, then those of
211    /// the second, and so on.
212    pub fn faces(&self) -> Vec<FontCatalogFace> {
213        self.expand().faces
214    }
215
216    /// Expands the catalog into the faces creation compiles against.
217    pub(crate) fn expand(&self) -> CatalogFonts {
218        let mut store = FontStore::new();
219        let mut faces = Vec::new();
220        for entry in &self.entries {
221            for face in entry.container.faces() {
222                let font = face.font.clone();
223                let info = font.info().clone();
224                faces.push(FontCatalogFace {
225                    identity: face.identity(),
226                    disposition: entry.disposition,
227                });
228                store.push((font, info));
229            }
230        }
231        CatalogFonts { store, faces }
232    }
233}
234
235impl Extend<FontCatalogEntry> for FontCatalog {
236    fn extend<T: IntoIterator<Item = FontCatalogEntry>>(&mut self, entries: T) {
237        self.entries.extend(entries);
238    }
239}
240
241impl FromIterator<FontCatalogEntry> for FontCatalog {
242    fn from_iter<T: IntoIterator<Item = FontCatalogEntry>>(entries: T) -> Self {
243        let mut catalog = Self::new();
244        catalog.extend(entries);
245        catalog
246    }
247}
248
249/// One face a catalog offers to Pack Creation at one explicit position.
250#[derive(Clone, Copy, Debug, PartialEq, Eq)]
251pub struct FontCatalogFace {
252    identity: FontFaceIdentity,
253    disposition: FontDisposition,
254}
255
256impl FontCatalogFace {
257    /// The exact container and container-local face index.
258    pub fn identity(&self) -> FontFaceIdentity {
259        self.identity
260    }
261
262    /// The disposition the face's container carries.
263    pub fn disposition(&self) -> FontDisposition {
264        self.disposition
265    }
266}
267
268/// The compile-time projection of one Font Catalog, indexed exactly as the
269/// representative compile sees it.
270pub(crate) struct CatalogFonts {
271    store: FontStore,
272    faces: Vec<FontCatalogFace>,
273}
274
275// Only creation compiles against catalog faces.
276impl CatalogFonts {
277    /// The selection metadata official Typst chooses faces from.
278    pub(crate) fn book(&self) -> &LazyHash<FontBook> {
279        self.store.book()
280    }
281
282    /// The face at the given catalog position.
283    pub(crate) fn font(&self, index: usize) -> Option<Font> {
284        self.store.font(index)
285    }
286
287    /// The disposition carried by the container of the face at the given
288    /// catalog position.
289    pub(crate) fn disposition(&self, index: usize) -> Option<FontDisposition> {
290        self.faces.get(index).map(FontCatalogFace::disposition)
291    }
292}
293
294/// Typst's embedded fonts as validated containers, in Typst's own order.
295///
296/// A caller splices them into its catalog at the position it wants; they never
297/// join a catalog implicitly. Their disposition is the caller's choice, like
298/// that of any other container.
299#[cfg(feature = "embedded-fonts")]
300pub fn typst_embedded_font_containers() -> impl Iterator<Item = FontContainer> {
301    // Typst exposes its embedded fonts one face at a time. Every face of one
302    // container carries that container's exact bytes, so first-seen order over
303    // the faces recovers the containers Typst ships, in Typst's own order.
304    let mut containers: Vec<Bytes> = Vec::new();
305    for (font, _) in typst_kit::fonts::embedded() {
306        if !containers.iter().any(|data| data == font.data()) {
307            containers.push(font.data().clone());
308        }
309    }
310    containers
311        .into_iter()
312        .map(|data| FontContainer::from_bytes(data).expect("embedded Font Container is readable"))
313}