diff --git a/Cargo.lock b/Cargo.lock index e9024a4..379b7ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,6 +261,7 @@ dependencies = [ "rustybuzz", "skrifa", "thiserror", + "unicode-script", ] [[package]] diff --git a/fontheight-core/Cargo.toml b/fontheight-core/Cargo.toml index 3cc8cde..a68907d 100644 --- a/fontheight-core/Cargo.toml +++ b/fontheight-core/Cargo.toml @@ -17,3 +17,4 @@ ordered-float = "4.6" rustybuzz = "0.20.1" skrifa = "0.26.5" thiserror = "2" +unicode-script = "0.5.7" diff --git a/fontheight-core/src/lib.rs b/fontheight-core/src/lib.rs index e946538..d039fc9 100644 --- a/fontheight-core/src/lib.rs +++ b/fontheight-core/src/lib.rs @@ -8,6 +8,8 @@ use skrifa::{ MetadataProvider, }; use thiserror::Error; +use unicode_script::UnicodeScript; +pub use unicode_script::{Script, ScriptExtension}; use crate::{locations::interesting_locations, pens::BezierPen}; @@ -166,3 +168,32 @@ pub enum FontHeightError { #[error(transparent)] Drawing(#[from] SkrifaDrawError), } + +pub fn discover_font_scripts( + font: &skrifa::FontRef, +) -> Result { + let mut empty_cmap = true; + let scripts = font.charmap().mappings().try_fold( + ScriptExtension::default(), + |acc, (codepoint, _)| { + empty_cmap = false; + let script_extension = char::from_u32(codepoint) + .ok_or(ScriptDiscoveryError::InvalidCodepoint(codepoint))? + .script_extension(); + Ok(acc.union(script_extension)) + }, + )?; + if empty_cmap { + Err(ScriptDiscoveryError::EmptyCmap) + } else { + Ok(scripts) + } +} + +#[derive(Debug, Error)] +pub enum ScriptDiscoveryError { + #[error("invalid codepoint in cmap: {0:#x}")] + InvalidCodepoint(u32), + #[error("empty cmap")] + EmptyCmap, +} diff --git a/fontheight-core/src/word_lists.rs b/fontheight-core/src/word_lists.rs index 8d6c558..6c101a5 100644 --- a/fontheight-core/src/word_lists.rs +++ b/fontheight-core/src/word_lists.rs @@ -7,16 +7,20 @@ use std::{ use thiserror::Error; +use crate::{Script, ScriptExtension}; + #[derive(Debug)] pub struct WordList { name: String, words: Vec, + scripts: ScriptExtension, } impl WordList { pub fn load( name: impl Into, path: impl AsRef, + scripts: ScriptExtension, ) -> Result { let path = path.as_ref(); let file_content = fs::read_to_string(path).map_err(|io_err| { @@ -29,29 +33,43 @@ impl WordList { .filter(|word| !word.is_empty()) .map(String::from) .collect(), + scripts, }) } pub fn define( name: impl Into, words: impl IntoIterator>, + scripts: ScriptExtension, ) -> Self { WordList { name: name.into(), words: words.into_iter().map(Into::into).collect(), + scripts, } } // Private API used by static-lang-word-lists #[doc(hidden)] - pub fn new(name: String, words: Vec) -> Self { - WordList { name, words } + #[inline] + pub fn new( + name: String, + words: Vec, + scripts: ScriptExtension, + ) -> Self { + WordList { + name, + words, + scripts, + } } + #[inline] pub fn name(&self) -> &str { &self.name } + #[inline] pub fn iter(&self) -> WordListIter { WordListIter(self.words.iter()) } @@ -70,6 +88,16 @@ impl WordList { pub fn get(&self, index: usize) -> Option<&str> { self.words.get(index).map(|word| word.as_str()) } + + #[inline] + pub fn covers(&self, script: Script) -> bool { + self.scripts.contains_script(script) + } + + #[inline] + pub fn covers_all(&self, scripts: ScriptExtension) -> bool { + self.scripts.intersection(scripts) == self.scripts + } } impl Index for WordList {