Skip to main content

lightweight_pdf/
fonts.rs

1//! Bridges `lightweight-pdf-fonts::FontData` to `lightweight-pdf-layout::FontResolver`
2//! (ADR-010: "Font-Bridge liegt an der Facade"). `FontRegistry` is a
3//! dynamic `FontKey -> RegisteredFont` map (`register()`/`register_named()`
4//! register arbitrary keys, not just `SANS_REGULAR`/`SANS_BOLD`). Two
5//! convenience constructors cover the common case: `with_defaults()`
6//! (bundled Source Sans 3 regular/bold, needs the `default-fonts` feature)
7//! and `with_fonts()` (caller-supplied regular/bold bytes, always
8//! available).
9//!
10//! Two lookups, two different jobs: `entry()` (used by the `FontResolver`
11//! impl below, i.e. every width/wrap calculation during layout) falls
12//! back to the registry's default key when `key` was never registered —
13//! layout has no error channel, and a fallback width is the only way it
14//! can keep going at all. `get()` (used by `render::text::embed_fonts`,
15//! once layout is done and every font actually referenced is known) does
16//! *not* fall back: a key nothing was ever registered under there turns
17//! into `RenderError::MissingFont`, not a silently wrong embedded font.
18
19use lightweight_pdf_core::FontKey;
20use lightweight_pdf_fonts::{EmbeddedFontMetrics, FontData, FontError};
21use lightweight_pdf_layout::{FontMetrics, FontResolver};
22
23// Crate-local copy (not the repo-root `assets/fonts/`): `cargo package`
24// only bundles files inside the crate's own directory, so a path reaching
25// outside it (`../../../assets/...`) silently drops the font files from
26// the published tarball — verified missing via `cargo publish --dry-run`,
27// which fails the packaged crate's own build with a "file not found" once
28// it's extracted and compiled in isolation. The repo-root copy stays too
29// (used by `lightweight-pdf-fonts`' own tests and referenced from
30// `README.md`), so this does duplicate ~860KB — the accepted cost of a
31// crate that must be self-contained once published.
32#[cfg(feature = "default-fonts")]
33const SANS_REGULAR_BYTES: &[u8] = include_bytes!("../assets/fonts/SourceSans3-Regular.ttf");
34#[cfg(feature = "default-fonts")]
35const SANS_BOLD_BYTES: &[u8] = include_bytes!("../assets/fonts/SourceSans3-Bold.ttf");
36
37/// Local newtype so `lightweight-pdf-layout`'s `FontMetrics` trait (foreign to this
38/// crate) can be implemented for `lightweight-pdf-fonts`' metrics type (also
39/// foreign) — Rust's orphan rules require the trait *or* the type to be
40/// local, so a thin local wrapper is the standard way to bridge two
41/// external crates without either depending on the other.
42struct MetricsAdapter(pub(crate) EmbeddedFontMetrics);
43
44impl FontMetrics for MetricsAdapter {
45    fn advance(&self, ch: char) -> f32 {
46        // Fallback width for characters the font has no glyph for: roughly
47        // a notdef-box width, keeps wrapping usable rather than panicking.
48        // The layout crate surfaces the miss itself via `has_glyph()` /
49        // `LayoutWarningKind::MissingGlyph`, so this no longer fails silently.
50        self.0.advance_1000(ch).unwrap_or(500.0)
51    }
52
53    fn ascent(&self) -> f32 {
54        self.0.ascent
55    }
56
57    fn descent(&self) -> f32 {
58        self.0.descent
59    }
60
61    fn has_glyph(&self, ch: char) -> bool {
62        self.0.advance_1000(ch).is_some()
63    }
64}
65
66pub struct RegisteredFont {
67    pub font_data: FontData,
68    pub base_font_name: String,
69    adapter: MetricsAdapter,
70}
71
72impl RegisteredFont {
73    fn new(bytes: &[u8], base_font_name: impl Into<String>) -> Result<Self, FontError> {
74        let font_data = FontData::load(bytes.to_vec())?;
75        let metrics = EmbeddedFontMetrics::from_font_data(&font_data)?;
76        Ok(RegisteredFont {
77            font_data,
78            base_font_name: base_font_name.into(),
79            adapter: MetricsAdapter(metrics),
80        })
81    }
82
83    /// FontDescriptor fields (ascent/descent/cap height/bbox/...) come from
84    /// here — the same metrics used for layout, not recomputed separately.
85    pub fn metrics(&self) -> &EmbeddedFontMetrics {
86        &self.adapter.0
87    }
88}
89
90pub struct FontRegistry {
91    fonts: std::collections::HashMap<FontKey, RegisteredFont>,
92    default_key: FontKey,
93}
94
95impl FontRegistry {
96    pub fn empty() -> Self {
97        FontRegistry {
98            fonts: std::collections::HashMap::new(),
99            default_key: FontKey::SANS_REGULAR,
100        }
101    }
102
103    #[cfg(feature = "default-fonts")]
104    pub fn with_defaults() -> Result<Self, FontError> {
105        let mut reg = Self::empty();
106        reg.register_named(FontKey::SANS_REGULAR, "SourceSans3-Subset", SANS_REGULAR_BYTES)?;
107        reg.register_named(FontKey::SANS_BOLD, "SourceSans3-Bold-Subset", SANS_BOLD_BYTES)?;
108        Ok(reg)
109    }
110
111    /// Builds a registry from caller-supplied static TrueType `glyf` fonts
112    /// (ADR-012, same constraint as the bundled defaults) instead of Source
113    /// Sans 3 — always available, independent of the `default-fonts`
114    /// feature.
115    pub fn with_fonts(regular_bytes: &[u8], bold_bytes: &[u8]) -> Result<Self, FontError> {
116        let mut reg = Self::empty();
117        reg.register_named(FontKey::SANS_REGULAR, "CustomFont-Regular-Subset", regular_bytes)?;
118        reg.register_named(FontKey::SANS_BOLD, "CustomFont-Bold-Subset", bold_bytes)?;
119        Ok(reg)
120    }
121
122    pub fn register(&mut self, key: FontKey, bytes: &[u8]) -> Result<(), FontError> {
123        let name = format!("CustomFont-{}-Subset", key.0);
124        self.register_named(key, name, bytes)
125    }
126
127    pub fn register_named(&mut self, key: FontKey, name: impl Into<String>, bytes: &[u8]) -> Result<(), FontError> {
128        let font = RegisteredFont::new(bytes, name)?;
129        self.fonts.insert(key, font);
130        Ok(())
131    }
132
133    /// Order matches how the facade registers PDF fonts — used to build
134    /// resource names (`F1`, `F2`, ...) consistently between PDF font
135    /// registration and content-stream references.
136    pub fn font_entries(&self) -> Vec<(FontKey, &RegisteredFont)> {
137        self.fonts.iter().map(|(&k, v)| (k, v)).collect()
138    }
139
140    /// `true` if nothing was ever registered — checked by
141    /// `render::render_document` before layout starts, so an empty
142    /// registry becomes `RenderError::NoFontsRegistered` instead of
143    /// `entry()`'s fallback panicking with nothing to fall back to.
144    pub fn is_empty(&self) -> bool {
145        self.fonts.is_empty()
146    }
147
148    /// Panics if `self` is empty. Safe everywhere it's actually called
149    /// (every width/wrap calculation during layout, via `FontResolver`
150    /// below) because `render::render_document` rejects an empty
151    /// registry up front with `RenderError::NoFontsRegistered`, before
152    /// layout — the only caller of `entry()` — ever runs.
153    pub fn entry(&self, key: FontKey) -> &RegisteredFont {
154        self.fonts
155            .get(&key)
156            .or_else(|| self.fonts.get(&self.default_key))
157            .expect("FontRegistry must contain at least one registered font")
158    }
159
160    /// The exact, unfallen-back-to entry for `key` — `None` if nothing was
161    /// ever registered under it. Used at embed time
162    /// (`render::text::embed_fonts`) to turn a missing weight/style into
163    /// `RenderError::MissingFont` instead of silently embedding whatever
164    /// `entry()`'s fallback resolved to under the requested key's name.
165    pub fn get(&self, key: FontKey) -> Option<&RegisteredFont> {
166        self.fonts.get(&key)
167    }
168}
169
170impl FontResolver for FontRegistry {
171    fn metrics(&self, key: FontKey) -> &dyn FontMetrics {
172        &self.entry(key).adapter
173    }
174}