1use std::collections::HashSet;
2use std::hash::{Hash, Hasher};
3use std::io::Cursor;
4
5use std::sync::Arc;
6use swash::text::{Codepoint, Script};
7
8use swash::scale::ScaleContext;
9use swash::{FontRef, StringId, Tag};
10
11#[cfg(feature = "woff2")]
12use woff2::convert_woff2_to_ttf;
13
14#[derive(Clone)]
15#[allow(clippy::upper_case_acronyms)]
16pub(crate) enum FontType {
17 OTF,
18 TTF,
19 WOFF,
20 WOFF2,
21}
22
23impl FontType {
24 pub(crate) fn embed_tag(&self) -> &'static str {
25 match self {
26 FontType::OTF => "application/font-otf",
27 FontType::TTF => "application/font-ttf",
28 FontType::WOFF => "application/font-woff",
29 FontType::WOFF2 => "application/font-woff2",
30 }
31 }
32}
33
34pub type FontLoadingError = String;
38
39pub type FontLoadingResult<T> = Result<T, FontLoadingError>;
43
44pub struct Font<'a> {
49 name: String,
50 re: FontRef<'a>,
51 font_type: FontType,
52 supported_scripts: HashSet<CScript>,
53 packed_font_data: Option<Vec<u8>>,
54 approximate_pixel_width: f32,
55}
56
57impl<'a> Font<'a> {
58 fn identify_scripts_in_font(fr: &FontRef) -> HashSet<CScript> {
59 let mut scripts_in_specs = fr
60 .writing_systems()
61 .filter_map(|s| s.script())
62 .map(CScript::try_from)
63 .filter_map(|s| s.ok())
64 .collect::<HashSet<CScript>>();
65
66 if scripts_in_specs.is_empty() {
67 fr.charmap().enumerate(|i, _g| {
68 if let Ok(c) = <u32 as TryInto<char>>::try_into(i) {
69 match CScript::try_from(c.script()) {
70 Ok(cs) => {
71 scripts_in_specs.insert(cs);
72 }
73 Err(_e) => {}
74 };
75 }
76 });
77 }
78
79 #[allow(clippy::unwrap_used)]
80 scripts_in_specs.insert(CScript::try_from(Script::Common).unwrap());
81
82 scripts_in_specs
83 }
84
85 pub fn from_data(data: &'a mut Vec<u8>) -> FontLoadingResult<Self> {
90 assert!(data.len() >= 4);
91 let (font_type, re, packed_data) = if &data[0..4] == b"\x00\x01\x00\x00" {
92 (FontType::TTF, FontRef::from_index(data, 0), None)
93 } else if &data[0..4] == b"OTTO" {
94 (FontType::OTF, FontRef::from_index(data, 0), None)
95 } else if &data[0..4] == b"wOF2" {
96 #[cfg(feature = "woff2")]
97 {
98 let cv = match convert_woff2_to_ttf(&mut data.as_slice()) {
99 Ok(c) => c,
100 Err(e) => return Err(e.to_string()),
101 };
102 let pack = data.clone();
103
104 data.clear();
105 data.extend_from_slice(cv.as_slice());
106
107 (FontType::WOFF2, FontRef::from_index(data, 0), Some(pack))
108 }
109 #[cfg(not(feature = "woff2"))]
110 unimplemented!("activate the woff2 feature for this font")
111 } else if &data[0..4] == b"wOFF" {
112 let mut inp_cur = Cursor::new(&data);
113 let mut out_cur = Cursor::new(Vec::new());
114 rs_woff::woff2otf(&mut inp_cur, &mut out_cur)
115 .expect("font conversion from woff1 unsuccessful");
116
117 let pack = data.clone();
118
119 data.clear();
120 data.extend_from_slice(out_cur.get_ref().as_slice());
121
122 (FontType::WOFF, FontRef::from_index(data, 0), Some(pack))
123 } else {
124 unimplemented!("unrecognized font magic {:?}", &data[0..4]);
125 };
126
127 let re = match re {
128 None => return Err(FontLoadingError::from("loading font failed")),
129 Some(e) => e,
130 };
131
132 let font_name = match re
133 .localized_strings()
134 .find(|s| s.id() == StringId::PostScript)
135 {
136 None => "FontNameNotFound".to_string(),
137 Some(locale) => locale.to_string(),
138 };
139
140 let mut scale_context = ScaleContext::new();
141 let mut scaler = scale_context.builder(re).size(20_f32).build();
142 let glyph_id = re.charmap().map('a');
143 let outline = scaler.scale_outline(glyph_id).unwrap();
144
145 Ok(Font {
146 name: font_name,
147 re,
148 font_type,
149 supported_scripts: Font::identify_scripts_in_font(&re),
150 packed_font_data: packed_data,
151 approximate_pixel_width: outline.bounds().width() / 20.,
152 })
153 }
154
155 pub(crate) fn reference(&self) -> &FontRef<'a> {
156 &self.re
157 }
158
159 pub(crate) fn font_type(&self) -> &FontType {
160 &self.font_type
161 }
162
163 pub(crate) fn name(&self) -> &str {
164 &self.name
165 }
166
167 pub(crate) fn packed(&self) -> &Option<Vec<u8>> {
168 &self.packed_font_data
169 }
170
171 pub(crate) fn approximate_pixel_width(&self) -> f32 {
172 self.approximate_pixel_width
173 }
174 #[allow(dead_code)]
175 pub(crate) fn supported_features(&self) -> impl IntoIterator<Item = (Tag, u16)> + '_ {
176 self.reference().features().map(|f| (f.tag(), 1))
177 }
178}
179
180impl<'a> PartialEq<Self> for Font<'a> {
181 fn eq(&self, other: &Self) -> bool {
182 self.name.eq(&other.name)
183 }
184}
185
186impl<'a> Eq for Font<'a> {}
187
188impl<'a> Hash for Font<'a> {
189 fn hash<H: Hasher>(&self, state: &mut H) {
190 self.name.hash(state)
191 }
192}
193
194#[derive(Clone)]
199pub struct FontSet<'a> {
200 inner: Arc<Vec<Font<'a>>>,
201}
202
203impl<'a> FontSet<'a> {
204 pub(crate) fn get_font_for_script(&self, script: &CScript) -> Option<&Font> {
205 self.inner
206 .iter()
207 .find(|f| f.supported_scripts.contains(script))
208 }
209}
210
211#[derive(Default)]
225pub struct FontSetBuilder<'a> {
226 fonts: Vec<Font<'a>>,
227}
228
229impl<'a> FontSetBuilder<'a> {
230 pub fn new() -> Self {
234 Self::default()
235 }
236
237 pub fn push(mut self, font: Font<'a>) -> Self {
241 if self.fonts.iter().any(|x| x.name == font.name) {
242 eprintln!(
243 "Skipped duplicate font / second font with duplicate name: {}",
244 font.name
245 )
246 } else {
247 self.fonts.push(font);
248 }
249 self
250 }
251
252 pub fn extend(mut self, fonts: Vec<Font<'a>>) -> Self {
256 for font in fonts {
257 self = self.push(font);
258 }
259 self
260 }
261
262 pub fn build(self) -> FontSet<'a> {
266 if self.fonts.is_empty() {
267 panic!("At least one font needs to be provided.");
268 }
269 FontSet {
270 inner: Arc::new(self.fonts),
271 }
272 }
273}
274
275#[derive(Hash, PartialEq, Eq, Debug)]
276pub(crate) struct CScript {
277 u: unicode_script::Script,
278 s: swash::text::Script,
279}
280
281impl CScript {
282 #[allow(dead_code)]
283 pub fn u(&self) -> unicode_script::Script {
284 self.u
285 }
286
287 pub fn s(&self) -> swash::text::Script {
288 self.s
289 }
290}
291
292impl TryFrom<swash::text::Script> for CScript {
293 type Error = ();
294
295 fn try_from(value: swash::text::Script) -> Result<CScript, ()> {
296 match unicode_script::Script::from_full_name(value.name().replace(' ', "_").as_str()) {
297 None => Err(()),
298 Some(u) => Ok(CScript { u, s: value }),
299 }
300 }
301}
302
303impl Default for CScript {
304 fn default() -> Self {
305 CScript {
306 u: unicode_script::Script::Unknown,
307 s: swash::text::Script::Unknown,
308 }
309 }
310}
311
312pub(crate) trait GuessScript {
313 fn guess_script(&self) -> CScript;
314}
315
316impl GuessScript for String {
317 fn guess_script(&self) -> CScript {
318 match self.chars().next() {
319 None => CScript::default(),
320 Some(cr) => CScript {
321 u: unicode_script::UnicodeScript::script(&cr),
322 s: Codepoint::script(cr),
323 },
324 }
325 }
326}
327
328impl GuessScript for &str {
329 fn guess_script(&self) -> CScript {
330 match self.chars().next() {
331 None => CScript::default(),
332 Some(cr) => CScript {
333 u: unicode_script::UnicodeScript::script(&cr),
334 s: Codepoint::script(cr),
335 },
336 }
337 }
338}