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` always
3//! resolves exactly the two weights `FontKey::SANS_REGULAR`/`SANS_BOLD` —
4//! either the bundled default (`with_defaults()`, needs the `default-fonts`
5//! feature) or caller-supplied bytes (`with_fonts()`, always available). An
6//! arbitrary-weight/arbitrary-`FontKey` registry beyond this fixed pair is
7//! out of scope here (see the tracking issue linked from `with_fonts`).
8
9use lightweight_pdf_core::FontKey;
10use lightweight_pdf_fonts::{EmbeddedFontMetrics, FontData, FontError};
11use lightweight_pdf_layout::{FontMetrics, FontResolver};
12
13// Crate-local copy (not the repo-root `assets/fonts/`): `cargo package`
14// only bundles files inside the crate's own directory, so a path reaching
15// outside it (`../../../assets/...`) silently drops the font files from
16// the published tarball — verified missing via `cargo publish --dry-run`,
17// which fails the packaged crate's own build with a "file not found" once
18// it's extracted and compiled in isolation. The repo-root copy stays too
19// (used by `lightweight-pdf-fonts`' own tests and referenced from
20// `README.md`), so this does duplicate ~860KB — the accepted cost of a
21// crate that must be self-contained once published.
22#[cfg(feature = "default-fonts")]
23const SANS_REGULAR_BYTES: &[u8] = include_bytes!("../assets/fonts/SourceSans3-Regular.ttf");
24#[cfg(feature = "default-fonts")]
25const SANS_BOLD_BYTES: &[u8] = include_bytes!("../assets/fonts/SourceSans3-Bold.ttf");
26
27/// Local newtype so `lightweight-pdf-layout`'s `FontMetrics` trait (foreign to this
28/// crate) can be implemented for `lightweight-pdf-fonts`' metrics type (also
29/// foreign) — Rust's orphan rules require the trait *or* the type to be
30/// local, so a thin local wrapper is the standard way to bridge two
31/// external crates without either depending on the other.
32struct MetricsAdapter(pub(crate) EmbeddedFontMetrics);
33
34impl FontMetrics for MetricsAdapter {
35    fn advance(&self, ch: char) -> f32 {
36        // Fallback width for characters the font has no glyph for: roughly
37        // a notdef-box width, keeps wrapping usable rather than panicking.
38        self.0.advance_1000(ch).unwrap_or(500.0)
39    }
40
41    fn ascent(&self) -> f32 {
42        self.0.ascent
43    }
44
45    fn descent(&self) -> f32 {
46        self.0.descent
47    }
48}
49
50pub struct RegisteredFont {
51    pub font_data: FontData,
52    pub base_font_name: &'static str,
53    adapter: MetricsAdapter,
54}
55
56impl RegisteredFont {
57    fn new(bytes: &[u8], base_font_name: &'static str) -> Result<Self, FontError> {
58        let font_data = FontData::load(bytes.to_vec())?;
59        let metrics = EmbeddedFontMetrics::from_font_data(&font_data)?;
60        Ok(RegisteredFont {
61            font_data,
62            base_font_name,
63            adapter: MetricsAdapter(metrics),
64        })
65    }
66
67    /// FontDescriptor fields (ascent/descent/cap height/bbox/...) come from
68    /// here — the same metrics used for layout, not recomputed separately.
69    pub fn metrics(&self) -> &EmbeddedFontMetrics {
70        &self.adapter.0
71    }
72}
73
74pub struct FontRegistry {
75    pub regular: RegisteredFont,
76    pub bold: RegisteredFont,
77}
78
79impl FontRegistry {
80    #[cfg(feature = "default-fonts")]
81    pub fn with_defaults() -> Result<Self, FontError> {
82        Ok(FontRegistry {
83            regular: RegisteredFont::new(SANS_REGULAR_BYTES, "SourceSans3-Subset")?,
84            bold: RegisteredFont::new(SANS_BOLD_BYTES, "SourceSans3-Bold-Subset")?,
85        })
86    }
87
88    /// Builds a registry from caller-supplied static TrueType `glyf` fonts
89    /// (ADR-012, same constraint as the bundled defaults) instead of Source
90    /// Sans 3 — always available, independent of the `default-fonts`
91    /// feature. Still exactly the two-weight `SANS_REGULAR`/`SANS_BOLD`
92    /// model; an arbitrary-weight/arbitrary-`FontKey` registry is tracked
93    /// separately (github.com/casoon/lightweight-pdf/issues/1).
94    pub fn with_fonts(regular_bytes: &[u8], bold_bytes: &[u8]) -> Result<Self, FontError> {
95        Ok(FontRegistry {
96            regular: RegisteredFont::new(regular_bytes, "CustomFont-Regular-Subset")?,
97            bold: RegisteredFont::new(bold_bytes, "CustomFont-Bold-Subset")?,
98        })
99    }
100
101    /// Order matches how the facade registers PDF fonts — used to build
102    /// resource names (`F1`, `F2`, ...) consistently between PDF font
103    /// registration and content-stream references.
104    pub fn font_entries(&self) -> [(FontKey, &RegisteredFont); 2] {
105        [(FontKey::SANS_REGULAR, &self.regular), (FontKey::SANS_BOLD, &self.bold)]
106    }
107
108    pub fn entry(&self, key: FontKey) -> &RegisteredFont {
109        if key == FontKey::SANS_BOLD {
110            &self.bold
111        } else {
112            &self.regular
113        }
114    }
115}
116
117impl FontResolver for FontRegistry {
118    fn metrics(&self, key: FontKey) -> &dyn FontMetrics {
119        &self.entry(key).adapter
120    }
121}