From 7b917093b675877762654b9363c1de6ea2f6e8e6 Mon Sep 17 00:00:00 2001 From: Ricky Atkins Date: Thu, 27 Feb 2025 14:12:20 +0000 Subject: [PATCH 1/2] Add support for discovering a font's supported scripts Read the cmap and use unicode script categories --- Cargo.lock | 1 + fontheight-core/Cargo.toml | 1 + fontheight-core/src/lib.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) 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..2cc52ba 100644 --- a/fontheight-core/src/lib.rs +++ b/fontheight-core/src/lib.rs @@ -8,6 +8,7 @@ use skrifa::{ MetadataProvider, }; use thiserror::Error; +use unicode_script::{ScriptExtension, UnicodeScript}; use crate::{locations::interesting_locations, pens::BezierPen}; @@ -166,3 +167,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, +} From bdc049eb51aef07bfbf4033d41b1e57d1e6ade34 Mon Sep 17 00:00:00 2001 From: Ricky Atkins Date: Thu, 27 Feb 2025 14:23:48 +0000 Subject: [PATCH 2/2] Add scripts to word lists Expose unicode_script types through our public API --- fontheight-core/src/lib.rs | 3 ++- fontheight-core/src/word_lists.rs | 32 +++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/fontheight-core/src/lib.rs b/fontheight-core/src/lib.rs index 2cc52ba..d039fc9 100644 --- a/fontheight-core/src/lib.rs +++ b/fontheight-core/src/lib.rs @@ -8,7 +8,8 @@ use skrifa::{ MetadataProvider, }; use thiserror::Error; -use unicode_script::{ScriptExtension, UnicodeScript}; +use unicode_script::UnicodeScript; +pub use unicode_script::{Script, ScriptExtension}; use crate::{locations::interesting_locations, pens::BezierPen}; 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 {