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}