verovioxide_data/lib.rs
1//! Bundled SMuFL fonts and resources for verovioxide.
2//!
3//! This crate embeds the SMuFL fonts and related resources from the Verovio
4//! project for use at runtime. The data includes glyph definitions, bounding
5//! boxes, and optional CSS files with embedded WOFF2 fonts for web output.
6//!
7//! # Font Features
8//!
9//! By default, only the Leipzig font is included. Additional fonts can be
10//! enabled via feature flags:
11//!
12//! - `font-leipzig` (default) - Leipzig SMuFL font
13//! - `font-bravura` - Bravura SMuFL font
14//! - `font-gootville` - Gootville SMuFL font
15//! - `font-leland` - Leland SMuFL font
16//! - `font-petaluma` - Petaluma SMuFL font
17//! - `all-fonts` - Enable all fonts
18//!
19//! Note: Bravura is always included as baseline because it's used to build
20//! the glyph name table in Verovio.
21//!
22//! # Example
23//!
24//! ```no_run
25//! use verovioxide_data::{resource_dir, extract_resources};
26//!
27//! // Access embedded resources directly (in-memory)
28//! let dir = resource_dir();
29//! if let Some(bravura) = dir.get_file("Bravura.xml") {
30//! let contents = bravura.contents_utf8().unwrap();
31//! println!("Bravura bounding boxes loaded: {} bytes", contents.len());
32//! }
33//!
34//! // Or extract to a temporary directory for file-based access
35//! let temp_dir = extract_resources().expect("Failed to extract resources");
36//! let path = temp_dir.path().join("Bravura.xml");
37//! assert!(path.exists());
38//! ```
39
40use include_dir::{Dir, include_dir};
41use std::io;
42use std::path::Path;
43use tempfile::TempDir;
44use thiserror::Error;
45
46/// The embedded verovio data directory.
47///
48/// This contains all bundled SMuFL fonts and resources compiled into the binary.
49/// The actual contents depend on which font features are enabled at compile time.
50static VEROVIO_DATA: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/data");
51
52/// Errors that can occur when working with verovioxide data resources.
53#[derive(Debug, Error)]
54pub enum DataError {
55 /// Failed to create the temporary directory.
56 #[error("failed to create temporary directory: {0}")]
57 TempDirCreation(#[source] io::Error),
58
59 /// Failed to create a directory during extraction.
60 #[error("failed to create directory '{path}': {source}")]
61 DirectoryCreation {
62 /// The path that failed to be created.
63 path: String,
64 /// The underlying I/O error.
65 #[source]
66 source: io::Error,
67 },
68
69 /// Failed to write a file during extraction.
70 #[error("failed to write file '{path}': {source}")]
71 FileWrite {
72 /// The path that failed to be written.
73 path: String,
74 /// The underlying I/O error.
75 #[source]
76 source: io::Error,
77 },
78}
79
80/// Returns a reference to the embedded verovio data directory.
81///
82/// This provides in-memory access to all bundled resources without extraction.
83/// Use this when you need to read resources directly from the embedded data.
84///
85/// # Example
86///
87/// ```
88/// use verovioxide_data::resource_dir;
89///
90/// let dir = resource_dir();
91///
92/// // List all top-level files
93/// for file in dir.files() {
94/// println!("File: {}", file.path().display());
95/// }
96///
97/// // Access a specific file - Bravura.xml is always included
98/// let file = dir.get_file("Bravura.xml").expect("Bravura.xml should exist");
99/// let contents = file.contents_utf8().expect("Should be valid UTF-8");
100/// assert!(contents.len() > 0);
101/// ```
102#[must_use]
103pub fn resource_dir() -> &'static Dir<'static> {
104 &VEROVIO_DATA
105}
106
107/// Extracts all embedded resources to a temporary directory.
108///
109/// This creates a temporary directory and writes all embedded resources to it,
110/// preserving the directory structure. The returned `TempDir` will automatically
111/// clean up when dropped.
112///
113/// Use this when you need file-system access to the resources, for example when
114/// interfacing with C libraries that expect file paths.
115///
116/// # Errors
117///
118/// Returns a [`DataError`] if:
119/// - The temporary directory cannot be created
120/// - A subdirectory cannot be created
121/// - A file cannot be written
122///
123/// # Example
124///
125/// ```no_run
126/// use verovioxide_data::extract_resources;
127///
128/// let temp_dir = extract_resources().expect("Failed to extract resources");
129/// let bravura_path = temp_dir.path().join("Bravura.xml");
130/// assert!(bravura_path.exists());
131///
132/// // Use the resources...
133///
134/// // TempDir is automatically cleaned up when dropped
135/// ```
136pub fn extract_resources() -> Result<TempDir, DataError> {
137 let temp_dir = TempDir::new().map_err(DataError::TempDirCreation)?;
138 extract_dir_contents(&VEROVIO_DATA, temp_dir.path())?;
139 Ok(temp_dir)
140}
141
142/// Recursively extracts directory contents to the target path.
143fn extract_dir_contents(dir: &Dir<'_>, target: &Path) -> Result<(), DataError> {
144 // Extract all files in this directory
145 for file in dir.files() {
146 let file_path = target.join(file.path());
147 std::fs::write(&file_path, file.contents()).map_err(|source| DataError::FileWrite {
148 path: file_path.display().to_string(),
149 source,
150 })?;
151 }
152
153 // Recursively extract subdirectories
154 for subdir in dir.dirs() {
155 let subdir_path = target.join(subdir.path());
156 std::fs::create_dir_all(&subdir_path).map_err(|source| DataError::DirectoryCreation {
157 path: subdir_path.display().to_string(),
158 source,
159 })?;
160 extract_dir_contents(subdir, target)?;
161 }
162
163 Ok(())
164}
165
166/// Returns `true` if the Leipzig font is available.
167///
168/// Leipzig is the default font and is always included unless explicitly disabled.
169#[must_use]
170pub const fn has_leipzig() -> bool {
171 cfg!(feature = "font-leipzig")
172}
173
174/// Returns `true` if the Bravura font is available.
175///
176/// Note: The Bravura baseline data (Bravura.xml and Bravura/) is always included
177/// because it's required for building the glyph name table. This function returns
178/// `true` only when the full Bravura feature is enabled.
179#[must_use]
180pub const fn has_bravura() -> bool {
181 cfg!(feature = "font-bravura")
182}
183
184/// Returns `true` if the Gootville font is available.
185#[must_use]
186pub const fn has_gootville() -> bool {
187 cfg!(feature = "font-gootville")
188}
189
190/// Returns `true` if the Leland font is available.
191#[must_use]
192pub const fn has_leland() -> bool {
193 cfg!(feature = "font-leland")
194}
195
196/// Returns `true` if the Petaluma font is available.
197#[must_use]
198pub const fn has_petaluma() -> bool {
199 cfg!(feature = "font-petaluma")
200}
201
202/// Lists all available SMuFL font names based on enabled features.
203///
204/// This always includes "Bravura" as it's required for baseline functionality.
205/// Additional fonts are included based on which feature flags are enabled.
206///
207/// # Example
208///
209/// ```
210/// use verovioxide_data::available_fonts;
211///
212/// let fonts = available_fonts();
213/// // Bravura is always included as the baseline font
214/// assert!(fonts.contains(&"Bravura"));
215/// // Should have at least one font
216/// assert!(!fonts.is_empty());
217/// ```
218#[must_use]
219pub fn available_fonts() -> Vec<&'static str> {
220 let mut fonts = vec!["Bravura"]; // Always included as baseline
221
222 if has_leipzig() {
223 fonts.push("Leipzig");
224 }
225 if has_bravura() && !fonts.contains(&"Bravura") {
226 fonts.push("Bravura");
227 }
228 if has_gootville() {
229 fonts.push("Gootville");
230 }
231 if has_leland() {
232 fonts.push("Leland");
233 }
234 if has_petaluma() {
235 fonts.push("Petaluma");
236 }
237
238 fonts
239}
240
241/// Returns the default font name.
242///
243/// The default font is Leipzig when the `font-leipzig` feature is enabled,
244/// otherwise falls back to Bravura (which is always available).
245#[must_use]
246pub const fn default_font() -> &'static str {
247 if cfg!(feature = "font-leipzig") {
248 "Leipzig"
249 } else {
250 "Bravura"
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_resource_dir_contains_bravura_xml() {
260 let dir = resource_dir();
261 let bravura = dir.get_file("Bravura.xml");
262 assert!(
263 bravura.is_some(),
264 "Bravura.xml should exist in embedded data"
265 );
266 }
267
268 #[test]
269 fn test_resource_dir_contains_bravura_directory() {
270 let dir = resource_dir();
271 let bravura_dir = dir.get_dir("Bravura");
272 assert!(
273 bravura_dir.is_some(),
274 "Bravura/ directory should exist in embedded data"
275 );
276 }
277
278 #[test]
279 fn test_resource_dir_contains_text_directory() {
280 let dir = resource_dir();
281 let text_dir = dir.get_dir("text");
282 assert!(
283 text_dir.is_some(),
284 "text/ directory should exist in embedded data"
285 );
286 }
287
288 #[test]
289 fn test_extract_resources_creates_files() {
290 let temp_dir = extract_resources().expect("Failed to extract resources");
291 let bravura_path = temp_dir.path().join("Bravura.xml");
292 assert!(bravura_path.exists(), "Bravura.xml should be extracted");
293 }
294
295 #[test]
296 fn test_extract_resources_creates_subdirectories() {
297 let temp_dir = extract_resources().expect("Failed to extract resources");
298 let text_path = temp_dir.path().join("text");
299 assert!(text_path.exists(), "text/ directory should be extracted");
300 assert!(text_path.is_dir(), "text should be a directory");
301 }
302
303 #[test]
304 fn test_available_fonts_includes_bravura() {
305 let fonts = available_fonts();
306 assert!(
307 fonts.contains(&"Bravura"),
308 "Bravura should always be available"
309 );
310 }
311
312 #[test]
313 #[cfg(feature = "font-leipzig")]
314 fn test_available_fonts_includes_leipzig_when_feature_enabled() {
315 let fonts = available_fonts();
316 assert!(
317 fonts.contains(&"Leipzig"),
318 "Leipzig should be available when feature is enabled"
319 );
320 }
321
322 #[test]
323 #[cfg(feature = "font-leipzig")]
324 fn test_default_font_is_leipzig_when_feature_enabled() {
325 assert_eq!(default_font(), "Leipzig");
326 }
327
328 #[test]
329 fn test_has_leipzig_matches_feature() {
330 // This test always passes - it just verifies the function works
331 let _ = has_leipzig();
332 }
333
334 #[test]
335 fn test_has_bravura_matches_feature() {
336 let _ = has_bravura();
337 }
338
339 #[test]
340 fn test_has_gootville_matches_feature() {
341 let _ = has_gootville();
342 }
343
344 #[test]
345 fn test_has_leland_matches_feature() {
346 let _ = has_leland();
347 }
348
349 #[test]
350 fn test_has_petaluma_matches_feature() {
351 let _ = has_petaluma();
352 }
353
354 #[test]
355 fn test_bravura_xml_has_content() {
356 let dir = resource_dir();
357 let bravura = dir
358 .get_file("Bravura.xml")
359 .expect("Bravura.xml should exist");
360 let contents = bravura.contents_utf8().expect("Should be valid UTF-8");
361 assert!(
362 contents.contains("bounding-boxes"),
363 "Bravura.xml should contain bounding-boxes element"
364 );
365 }
366
367 #[test]
368 fn test_text_directory_contains_times_font() {
369 let dir = resource_dir();
370 let text_dir = dir.get_dir("text").expect("text/ should exist");
371
372 // include_dir stores full paths from the included directory root
373 // So we need to check for "text/Times.xml" not just "Times.xml"
374 let times = text_dir.get_file("text/Times.xml");
375 assert!(
376 times.is_some(),
377 "text/Times.xml should exist in text directory"
378 );
379 }
380}