1use std::collections::HashMap;
5use std::hash::{DefaultHasher, Hash, Hasher};
6
7use crate::Warning;
8use crate::style::FontLoader;
9
10use super::{Session, Stale};
11
12pub(super) struct FontFiles {
18 files: HashMap<String, (u64, Vec<u8>)>,
19 base: usize,
21 print: Option<u64>,
24 warnings: Vec<Warning>,
26}
27
28impl FontFiles {
29 pub(super) fn over(base: usize) -> FontFiles {
30 FontFiles {
31 files: HashMap::new(),
32 base,
33 print: None,
34 warnings: Vec::new(),
35 }
36 }
37
38 fn insert(&mut self, url: &str, bytes: Vec<u8>) -> bool {
40 let mut h = DefaultHasher::new();
41 bytes.hash(&mut h);
42 let hash = h.finish();
43 if self.files.get(url).is_some_and(|(known, _)| *known == hash) {
44 return false;
45 }
46 self.files.insert(url.to_string(), (hash, bytes));
47 true
48 }
49
50 fn hash(&self, url: &str) -> Option<u64> {
51 self.files.get(url).map(|(hash, _)| *hash)
52 }
53}
54
55impl FontLoader for FontFiles {
56 fn load(&self, url: &str) -> Option<Vec<u8>> {
57 self.files.get(url).map(|(_, bytes)| bytes.clone())
58 }
59}
60
61impl Session<'_> {
62 pub fn add_font_file(&mut self, url: &str, bytes: Vec<u8>) -> Result<(), super::AddFontError> {
69 if self.registry.get_mut().is_none() {
70 return Err(super::AddFontError::Borrowed);
71 }
72 if self.fonts.insert(url, bytes) && self.load_faces() {
73 self.recompile();
74 }
75 Ok(())
76 }
77
78 pub(super) fn register_font(
81 &mut self,
82 source: crate::fonts::FontSource,
83 ) -> Result<Vec<u16>, super::AddFontError> {
84 let registry = self
85 .registry
86 .get_mut()
87 .ok_or(super::AddFontError::Borrowed)?;
88 registry.truncate(self.fonts.base);
89 let added = registry.add(source);
90 self.fonts.base = registry.len();
91 self.fonts.print = None;
92 self.load_faces();
93 Ok(added?)
94 }
95
96 pub(super) fn load_faces(&mut self) -> bool {
100 let (Some(sheets), Some(registry)) = (&mut self.sheets, self.registry.get_mut()) else {
101 return false;
102 };
103 let mut h = DefaultHasher::new();
104 sheets.hash_faces(&mut h, |url| self.fonts.hash(url));
105 let print = h.finish();
106 if self.fonts.print == Some(print) {
107 sheets.set_font_warnings(self.fonts.warnings.clone());
108 return false;
109 }
110 registry.truncate(self.fonts.base);
111 sheets.load_fonts(registry, &self.fonts);
112 self.fonts.warnings = sheets.font_warnings().to_vec();
113 self.fonts.print = Some(print);
114 self.output = None;
117 self.lines.clear();
118 self.settled.clear();
119 self.stale = self.stale.max(Stale::Break);
120 true
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use std::collections::BTreeSet;
127
128 use crate::LayoutOutput;
129 use crate::fonts::{FaceAttributes, FontSource, bundled_registry};
130 use crate::pages::DrawItem;
131 use crate::session::testing::{FELL, book, prose, registry, section, sheets};
132 use crate::session::{AddFontError, Session};
133 use crate::style::{NoFonts, Source, Stylesheets};
134
135 const FELL_CAPS: &str = "@font-face { font-family: 'Fell Caps'; src: url(fell.ttf); \
136 font-weight: 700; font-style: italic }\n\
137 p { font-family: 'Fell Caps', serif; font-weight: 700; font-style: italic }";
138
139 fn session() -> Session<'static> {
140 let mut session = Session::owning(bundled_registry().unwrap());
141 session.set_content(book(vec![section("one.md", prose("alpha", 4))]));
142 session
143 }
144
145 fn used(output: &LayoutOutput) -> BTreeSet<u16> {
147 output
148 .pages
149 .iter()
150 .flat_map(|page| &page.items)
151 .filter_map(|item| match item {
152 DrawItem::Text { font_id, .. } => Some(*font_id),
153 _ => None,
154 })
155 .collect()
156 }
157
158 fn family(output: &LayoutOutput, family: &str) -> Vec<u16> {
159 (0..output.fonts.len() as u16)
160 .filter(|id| output.fonts[usize::from(*id)].family == family)
161 .collect()
162 }
163
164 #[test]
167 fn a_font_face_with_bytes_registers_what_the_rule_declares() {
168 let mut session = session();
169 session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
170 session.set_style(sheets(FELL_CAPS));
171 let output = session.preview();
172 let fell = family(output, "fell caps");
173 assert_eq!(fell.len(), 1, "{:?}", output.fonts);
174 assert_eq!(
175 output.fonts[usize::from(fell[0])].attributes,
176 FaceAttributes {
177 italic: true,
178 weight: 700
179 },
180 );
181 assert!(output.warnings.is_empty(), "{:?}", output.warnings);
182 }
183
184 #[test]
187 fn text_that_asks_for_the_family_sets_in_it() {
188 let mut session = session();
189 session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
190 session.set_style(sheets(FELL_CAPS));
191 let output = session.preview();
192 let fell = family(output, "fell caps");
193 assert_eq!(used(output), fell.into_iter().collect());
194 assert!(session.export().is_ok());
195 }
196
197 #[test]
200 fn a_font_face_without_bytes_warns_as_a_loader_does() {
201 let mut session = session();
202 session.set_style(sheets(FELL_CAPS));
203 let output = session.preview();
204
205 let mut loaded = sheets(FELL_CAPS);
206 loaded.load_fonts(&mut bundled_registry().unwrap(), &NoFonts);
207 assert!(!loaded.warnings().is_empty());
208 for warning in loaded.warnings() {
209 assert!(output.warnings.contains(warning), "{warning:?} is missing");
210 }
211 }
212
213 #[test]
216 fn a_face_whose_bytes_arrive_after_the_sheet_sets_in_it() {
217 let mut session = session();
218 session.set_style(sheets(FELL_CAPS));
219 let before = session.preview();
220 assert!(family(before, "fell caps").is_empty());
221 assert!(!before.warnings.is_empty());
222
223 session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
224 let after = session.preview();
225 let fell = family(after, "fell caps");
226 assert_eq!(used(after), fell.into_iter().collect());
227 assert!(after.warnings.is_empty(), "{:?}", after.warnings);
228 }
229
230 #[test]
233 fn the_same_sheet_again_registers_nothing_again() {
234 let css =
235 format!("{FELL_CAPS}\n@font-face {{ font-family: Absent; src: url(absent.ttf) }}");
236 let mut session = session();
237 session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
238 session.set_style(sheets(&css));
239 let warnings = session.preview().warnings.clone();
240 let faces = session.fonts().len();
241 let before = session.stages();
242
243 session.set_style(sheets(&css));
244 session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
245 let output = session.preview();
246 assert_eq!(output.warnings, warnings);
247 assert_eq!(session.fonts().len(), faces);
248 assert_eq!(session.stages().lines, before.lines);
249 }
250
251 #[test]
254 fn changing_or_removing_a_rule_changes_the_faces_used() {
255 let mut session = session();
256 session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
257 session.set_style(sheets(FELL_CAPS));
258 let bundled = session.preview().fonts.len() - 1;
259
260 session.set_style(sheets(&FELL_CAPS.replace("Fell Caps", "Oxford")));
261 let renamed = session.preview();
262 assert!(family(renamed, "fell caps").is_empty());
263 let oxford = family(renamed, "oxford");
264 assert_eq!(used(renamed), oxford.into_iter().collect());
265
266 session.set_style(sheets("p { font-family: 'Fell Caps', serif }"));
267 let removed = session.preview();
268 assert_eq!(removed.fonts.len(), bundled);
269 assert!(used(removed).iter().all(|id| usize::from(*id) < bundled));
270 }
271
272 #[test]
276 fn a_face_the_host_adds_goes_under_the_sheets_faces() {
277 let mut session = session();
278 let bundled = session.fonts().len() as u16;
279 session.add_font_file("fell.ttf", FELL.to_vec()).unwrap();
280 session.set_style(sheets(FELL_CAPS));
281 session.preview();
282
283 let ids = session
284 .add_font(FontSource::from_bytes(FELL.to_vec()).unwrap())
285 .unwrap();
286 assert_eq!(ids, vec![bundled]);
287 let output = session.preview();
288 let fell = family(output, "fell caps");
289 assert_eq!(fell, vec![bundled + 1]);
290 assert_eq!(used(output), fell.into_iter().collect());
291 }
292
293 #[test]
296 fn a_borrowed_registry_refuses_a_font_file() {
297 let mut session = Session::new(registry());
298 session.set_style(Stylesheets::parse(&[Source::author("a.css", FELL_CAPS)]));
299 assert!(matches!(
300 session.add_font_file("fell.ttf", FELL.to_vec()),
301 Err(AddFontError::Borrowed)
302 ));
303 }
304}