embedded-typst 0.1.0

A typst::World for embedding the Typst compiler: lazily loaded fonts, a virtual filesystem over several roots, Typst packages and reproducible dates. Native and Wasm.
Documentation
//! Fonts for [`EmbeddedWorld`](crate::EmbeddedWorld).

#[cfg(not(target_arch = "wasm32"))]
use std::collections::HashSet;
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
use std::sync::OnceLock;

use typst::foundations::Bytes;
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;

/// Font metadata for Typst plus the fonts themselves. Fonts given as data are
/// loaded up front; fonts found on disk are only read into memory when Typst
/// actually uses them.
///
/// Build once and share it between compilations with an `Arc`.
pub struct Fonts {
    book: LazyHash<FontBook>,
    slots: Vec<FontSlot>,
}

/// A single font face, either already loaded or located in a file on disk.
struct FontSlot {
    #[cfg(not(target_arch = "wasm32"))]
    path: Option<PathBuf>,
    #[cfg(not(target_arch = "wasm32"))]
    index: u32,
    font: OnceLock<Option<Font>>,
}

impl FontSlot {
    fn loaded(font: Font) -> Self {
        Self {
            #[cfg(not(target_arch = "wasm32"))]
            path: None,
            #[cfg(not(target_arch = "wasm32"))]
            index: 0,
            font: OnceLock::from(Some(font)),
        }
    }

    fn get(&self) -> Option<Font> {
        self.font
            .get_or_init(|| {
                #[cfg(not(target_arch = "wasm32"))]
                {
                    let data = std::fs::read(self.path.as_ref()?).ok()?;
                    Font::new(Bytes::new(data), self.index)
                }
                #[cfg(target_arch = "wasm32")]
                {
                    None
                }
            })
            .clone()
    }
}

impl Fonts {
    /// Start with no fonts. Fonts are registered in the order the builder
    /// methods are called.
    pub fn builder() -> FontsBuilder {
        FontsBuilder {
            book: FontBook::new(),
            slots: Vec::new(),
            #[cfg(not(target_arch = "wasm32"))]
            seen: HashSet::new(),
        }
    }

    /// The metadata of all fonts, for [`World::book`](typst::World::book).
    pub fn book(&self) -> &LazyHash<FontBook> {
        &self.book
    }

    /// The font at `index` in the [book](Self::book), read from disk on first
    /// use.
    pub fn font(&self, index: usize) -> Option<Font> {
        self.slots.get(index)?.get()
    }

    /// Font files from disk that have been loaded so far, with their data.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn loaded_files(&self) -> Vec<(PathBuf, Bytes)> {
        self.slots
            .iter()
            .filter_map(|slot| {
                let path = slot.path.clone()?;
                let font = slot.font.get()?.as_ref()?;
                Some((path, font.data().clone()))
            })
            .collect()
    }
}

/// Collects fonts for [`Fonts`].
pub struct FontsBuilder {
    book: FontBook,
    slots: Vec<FontSlot>,
    /// Font files already registered, so a file found twice (as a system
    /// font and in a font path, say) is only listed once.
    #[cfg(not(target_arch = "wasm32"))]
    seen: HashSet<PathBuf>,
}

impl FontsBuilder {
    /// Add Typst's bundled fonts, so every target, especially Wasm, can render
    /// a document without host-installed fonts.
    #[cfg(feature = "typst-fonts")]
    pub fn typst_fonts(mut self) -> Self {
        for data in typst_assets::fonts() {
            self.push_data(Bytes::new(data));
        }
        self
    }

    /// Add every face of a font file or collection held in memory, such as
    /// an `include_bytes!` font.
    pub fn data(mut self, data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self {
        self.push_data(Bytes::new(data));
        self
    }

    /// Add the fonts installed on the system.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn system_fonts(mut self) -> Self {
        let mut database = fontdb::Database::new();
        database.load_system_fonts();
        self.push_database(&database);
        self
    }

    /// Add a font file or every font file in a directory (recursively).
    /// Paths that do not exist or hold no fonts add nothing.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn path(mut self, path: impl AsRef<Path>) -> Self {
        let path = path.as_ref();
        let mut database = fontdb::Database::new();
        if path.is_dir() {
            database.load_fonts_dir(path);
        } else if path.is_file() {
            let _ = database.load_font_file(path);
        }
        self.push_database(&database);
        self
    }

    pub fn build(self) -> Fonts {
        Fonts {
            book: LazyHash::new(self.book),
            slots: self.slots,
        }
    }

    fn push_data(&mut self, data: Bytes) {
        for font in Font::iter(data) {
            self.book.push(font.info().clone());
            self.slots.push(FontSlot::loaded(font));
        }
    }

    /// Register the faces of every file in `database` without keeping the
    /// data: fontdb lists every face of a collection (.ttc/.otc) separately,
    /// so each file is read once from fontdb's (memory-mapped) data.
    #[cfg(not(target_arch = "wasm32"))]
    fn push_database(&mut self, database: &fontdb::Database) {
        let files: Vec<_> = database
            .faces()
            .filter_map(|face| match &face.source {
                fontdb::Source::File(path) | fontdb::Source::SharedFile(path, _) => self
                    .seen
                    .insert(path.clone())
                    .then(|| (face.id, path.clone())),
                fontdb::Source::Binary(_) => None,
            })
            .collect();

        for (id, path) in files {
            database.with_face_data(id, |data, _| {
                for (index, info) in typst::text::FontInfo::iter(data).enumerate() {
                    self.book.push(info);
                    self.slots.push(FontSlot {
                        path: Some(path.clone()),
                        index: index as u32,
                        font: OnceLock::new(),
                    });
                }
            });
        }
    }
}