Skip to main content

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}