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}