appcore_filemaker/
font.rs1use std::collections::{BTreeMap, BTreeSet};
14use std::sync::Arc;
15
16use sha2::{Digest, Sha256};
17use skrifa::{instance::Size, FontRef, MetadataProvider};
18
19use crate::{ErrorCode, FileMakerError, Result};
20
21#[derive(Clone, Debug)]
23pub struct FontAsset {
24 pub name: String,
26 pub bytes: Arc<[u8]>,
28 pub face_index: u32,
30 pub digest: [u8; 32],
32}
33
34impl FontAsset {
35 pub fn new(name: impl Into<String>, bytes: Vec<u8>, face_index: u32) -> Result<Self> {
37 let name = name.into();
38 if name.is_empty() || name.len() > 128 {
39 return Err(font_error("font name is empty or too long"));
40 }
41 FontRef::from_index(&bytes, face_index)
42 .map_err(|_| font_error("font bytes or face index are invalid"))?;
43 let digest = Sha256::digest(&bytes).into();
44 Ok(Self {
45 name,
46 bytes: bytes.into(),
47 face_index,
48 digest,
49 })
50 }
51
52 #[must_use]
54 pub fn covers(&self, text: &str) -> bool {
55 let Ok(face) = FontRef::from_index(&self.bytes, self.face_index) else {
56 return false;
57 };
58 let charmap = face.charmap();
59 text.chars()
60 .all(|character| character.is_control() || charmap.map(character).is_some())
61 }
62
63 pub fn units_per_em(&self) -> Result<u16> {
65 let face = FontRef::from_index(&self.bytes, self.face_index)
66 .map_err(|_| font_error("registered font became invalid"))?;
67 let units_per_em = face
68 .metrics(Size::unscaled(), skrifa::instance::LocationRef::default())
69 .units_per_em;
70 if units_per_em == 0 {
71 return Err(font_error("registered font has no units-per-em"));
72 }
73 Ok(units_per_em)
74 }
75}
76
77pub trait FontResolver: Send + Sync {
79 fn resolve_font(&self, name: &str, max_bytes: usize) -> Result<FontAsset>;
81}
82
83#[derive(Clone, Debug, Default)]
85pub struct FontManager {
86 fonts: BTreeMap<String, FontAsset>,
87 fallback: Vec<String>,
88}
89
90impl FontManager {
91 pub fn register_from(
93 &mut self,
94 resolver: &dyn FontResolver,
95 name: &str,
96 max_bytes: usize,
97 ) -> Result<()> {
98 self.register(resolver.resolve_font(name, max_bytes)?)
99 }
100
101 pub fn register(&mut self, font: FontAsset) -> Result<()> {
103 if self.fonts.contains_key(&font.name) {
104 return Err(font_error(format!(
105 "font `{}` is already registered",
106 font.name
107 )));
108 }
109 self.fonts.insert(font.name.clone(), font);
110 Ok(())
111 }
112
113 pub fn set_fallback(&mut self, fallback: Vec<String>) -> Result<()> {
115 if fallback.len() > 64 || fallback.iter().any(|name| !self.fonts.contains_key(name)) {
116 return Err(font_error(
117 "fallback contains too many fonts or an unknown name",
118 ));
119 }
120 self.fallback = fallback;
121 Ok(())
122 }
123
124 pub fn get(&self, name: &str) -> Result<&FontAsset> {
126 self.fonts
127 .get(name)
128 .ok_or_else(|| font_error(format!("font `{name}` is not registered")))
129 }
130
131 pub fn select_for_grapheme<'a>(
133 &'a self,
134 primary: &str,
135 grapheme: &str,
136 ) -> Result<&'a FontAsset> {
137 let primary = self.get(primary)?;
138 if primary.covers(grapheme) {
139 return Ok(primary);
140 }
141 for name in &self.fallback {
142 let font = self.get(name)?;
143 if font.covers(grapheme) {
144 return Ok(font);
145 }
146 }
147 let scalar = grapheme
148 .chars()
149 .find(|value| !value.is_control())
150 .map_or(0, u32::from);
151 Err(font_error(format!(
152 "no explicit font contains glyph U+{scalar:04X}"
153 )))
154 }
155
156 pub fn digests(&self) -> impl Iterator<Item = (&str, &[u8; 32])> {
158 self.fonts
159 .iter()
160 .map(|(name, font)| (name.as_str(), &font.digest))
161 }
162
163 pub fn fallback_names(&self) -> impl Iterator<Item = &str> {
165 self.fallback.iter().map(String::as_str)
166 }
167}
168
169#[derive(Clone, Debug, Eq, PartialEq)]
171pub struct FontSubset {
172 pub font: String,
174 pub glyph_ids: BTreeSet<u16>,
176}
177
178impl FontSubset {
179 #[must_use]
181 pub fn new(font: impl Into<String>) -> Self {
182 Self {
183 font: font.into(),
184 glyph_ids: BTreeSet::new(),
185 }
186 }
187
188 pub fn record(&mut self, glyph_id: u16) {
190 self.glyph_ids.insert(glyph_id);
191 }
192}
193
194fn font_error(message: impl Into<String>) -> FileMakerError {
195 FileMakerError::new(ErrorCode::FontMissing, message)
196}