Skip to main content

pdfrum_edit/
svg_text.rs

1//! The faces an ingested SVG's `<text>` is set in.
2//!
3//! An SVG
4//! names fonts by *family* — `font-family: Inter, sans-serif` — and a PDF
5//! carries font *programs*. Nothing bridges those two without a font
6//! database, so the caller supplies one: [`SvgFonts`] is a set of face
7//! programs, each registered under the family its own name table declares.
8//!
9//! # Why the caller supplies them
10//!
11//! `usvg`'s own defaults would scan the host — `system-fonts` and
12//! `memmap-fonts` — and pdfrum does not take a dependency's defaults.
13//! More to the point, a document whose appearance depends on which
14//! fonts a build machine happens to have installed is not reproducible, and
15//! reproducibility is the whole reason `SaveOptions::id_source` exists. A
16//! caller who *wants* the host's fonts reads them and registers them, and
17//! that is then a decision in their code rather than an accident in ours.
18//!
19//! # What text becomes
20//!
21//! **Outlines.** `usvg` lays the text out and flattens each span to filled
22//! paths, and the ingestion walk draws those paths like any others — so text
23//! goes into the page as vectors with no font embedded and no encoding to get
24//! wrong, and it renders identically everywhere. Outlines are the default
25//! because embedding would need a font program and an encoding this pass
26//! does not carry.
27//!
28//! An SVG whose `<text>` names a family this set does not carry draws
29//! nothing, and that is reported as [`Unsupported::Text`](crate::Unsupported)
30//! — a missing face is a reported gap, never a silent one.
31
32use std::sync::Arc;
33
34/// The font faces an ingested SVG's `<text>` may be set in.
35///
36/// Each face is registered under the family names its own `name` table
37/// declares, which is how an SVG's `font-family` finds it. Empty by default:
38/// a session that registers nothing renders no text, and says so in the
39/// report.
40///
41/// Cheap to clone — the registered faces are shared, not copied — so one set
42/// built at start-up serves every document.
43///
44/// ```no_run
45/// use pdfrum::{Document, SvgFonts};
46///
47/// let mut fonts = SvgFonts::new();
48/// fonts.register(std::fs::read("Inter.ttf")?);
49/// assert_eq!(fonts.families(), ["Inter"]);
50///
51/// let doc = Document::open("in.pdf")?;
52/// let mut edit = doc.edit();
53/// edit.set_svg_fonts(fonts);
54/// # Ok::<(), Box<dyn std::error::Error>>(())
55/// ```
56#[derive(Debug, Clone, Default)]
57pub struct SvgFonts {
58    /// `usvg`'s database, behind an `Arc` because that is the shape
59    /// [`usvg::Options`] wants and it makes a clone free.
60    db: Arc<usvg::fontdb::Database>,
61    /// The family every `<text>` with no `font-family` of its own is set in,
62    /// and the one `usvg` falls back to for a family it does not know. The
63    /// first registered face's, because that is the answer in every case a
64    /// second knob would have been set to.
65    default_family: String,
66}
67
68impl SvgFonts {
69    /// An empty set: no faces, so no text draws.
70    #[must_use]
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Register one font program — a TTF, OTF or TTC — under the families its
76    /// own name table declares.
77    ///
78    /// Returns whether the face was usable. A `false` is a font this build
79    /// cannot parse, not an error: registering a directory of faces should
80    /// not fail on the one that is a README, and a family that never arrives
81    /// shows up as a reported [`Unsupported::Text`](crate::Unsupported) when
82    /// a document asks for it.
83    ///
84    /// The **first** face registered also becomes the default family, so the
85    /// common case — one face, text with no `font-family` — needs no second
86    /// call.
87    pub fn register(&mut self, program: impl Into<Vec<u8>>) -> bool {
88        let db = Arc::make_mut(&mut self.db);
89        let before = db.len();
90        db.load_font_data(program.into());
91        if db.len() == before {
92            return false;
93        }
94        if self.default_family.is_empty() {
95            // The last face is the one just loaded: `load_font_data` appends,
96            // and this branch only runs when the set was empty before.
97            self.default_family = db
98                .faces()
99                .last()
100                .and_then(|face| face.families.first().map(|(name, _)| name.clone()))
101                .unwrap_or_default();
102        }
103        true
104    }
105
106    /// Every family this set can serve, sorted and without duplicates.
107    ///
108    /// What an SVG's `font-family` is matched against, so it is also the
109    /// answer to "why did that text not draw".
110    #[must_use]
111    pub fn families(&self) -> Vec<String> {
112        let mut families: Vec<String> = self
113            .db
114            .faces()
115            .flat_map(|face| face.families.iter().map(|(name, _)| name.clone()))
116            .collect();
117        families.sort_unstable();
118        families.dedup();
119        families
120    }
121
122    /// Whether any face is registered.
123    ///
124    /// `true` means every `<text>` in every document is reported rather than
125    /// drawn, which is what this crate did before the feature existed.
126    #[must_use]
127    pub fn is_empty(&self) -> bool {
128        self.db.is_empty()
129    }
130
131    /// The database and default family, for the parse options.
132    pub(crate) fn parts(&self) -> (Arc<usvg::fontdb::Database>, &str) {
133        (Arc::clone(&self.db), &self.default_family)
134    }
135}