tinymist_world/font/web/
mod.rs

1use js_sys::ArrayBuffer;
2use tinymist_std::error::prelude::*;
3use typst::foundations::Bytes;
4use typst::text::{
5    Coverage, Font, FontBook, FontFlags, FontInfo, FontStretch, FontStyle, FontVariant, FontWeight,
6};
7use wasm_bindgen::prelude::*;
8
9use super::{BufferFontLoader, FontLoader, FontResolverImpl, FontSlot};
10use crate::font::cache::FontInfoCache;
11use crate::font::info::typst_typographic_family;
12
13/// Destructures a JS `[key, value]` pair into a tuple of [`Deserializer`]s.
14pub(crate) fn convert_pair(pair: JsValue) -> (JsValue, JsValue) {
15    let pair = pair.unchecked_into::<js_sys::Array>();
16    (pair.get(0), pair.get(1))
17}
18struct FontBuilder {}
19
20fn font_family_web_to_typst(family: &str, full_name: &str) -> Result<String> {
21    let mut family = family;
22    if family.starts_with("Noto")
23        || family.starts_with("NewCM")
24        || family.starts_with("NewComputerModern")
25    {
26        family = full_name;
27    }
28
29    if family.is_empty() {
30        return Err(error_once!("font_family_web_to_typst.empty_family"));
31    }
32
33    Ok(typst_typographic_family(family).to_string())
34}
35
36struct WebFontInfo {
37    family: String,
38    full_name: String,
39    postscript_name: String,
40    style: String,
41}
42
43fn infer_info_from_web_font(
44    WebFontInfo {
45        family,
46        full_name,
47        postscript_name,
48        style,
49    }: WebFontInfo,
50) -> Result<FontInfo> {
51    let family = font_family_web_to_typst(&family, &full_name)?;
52
53    let mut full = full_name;
54    full.make_ascii_lowercase();
55
56    let mut postscript = postscript_name;
57    postscript.make_ascii_lowercase();
58
59    let mut style = style;
60    style.make_ascii_lowercase();
61
62    let search_scopes = [style.as_str(), postscript.as_str(), full.as_str()];
63
64    let variant = {
65        // Some fonts miss the relevant bits for italic or oblique, so
66        // we also try to infer that from the full name.
67        let italic = full.contains("italic");
68        let oblique = full.contains("oblique") || full.contains("slanted");
69
70        let style = match (italic, oblique) {
71            (false, false) => FontStyle::Normal,
72            (true, _) => FontStyle::Italic,
73            (_, true) => FontStyle::Oblique,
74        };
75
76        let weight = {
77            let mut weight = None;
78            let mut secondary_weight = None;
79            'searchLoop: for &search_style in &[
80                "thin",
81                "extralight",
82                "extra light",
83                "extra-light",
84                "light",
85                "regular",
86                "medium",
87                "semibold",
88                "semi bold",
89                "semi-bold",
90                "bold",
91                "extrabold",
92                "extra bold",
93                "extra-bold",
94                "black",
95            ] {
96                for (idx, &search_scope) in search_scopes.iter().enumerate() {
97                    if search_scope.contains(search_style) {
98                        let guess_weight = match search_style {
99                            "thin" => Some(FontWeight::THIN),
100                            "extralight" => Some(FontWeight::EXTRALIGHT),
101                            "extra light" => Some(FontWeight::EXTRALIGHT),
102                            "extra-light" => Some(FontWeight::EXTRALIGHT),
103                            "light" => Some(FontWeight::LIGHT),
104                            "regular" => Some(FontWeight::REGULAR),
105                            "medium" => Some(FontWeight::MEDIUM),
106                            "semibold" => Some(FontWeight::SEMIBOLD),
107                            "semi bold" => Some(FontWeight::SEMIBOLD),
108                            "semi-bold" => Some(FontWeight::SEMIBOLD),
109                            "bold" => Some(FontWeight::BOLD),
110                            "extrabold" => Some(FontWeight::EXTRABOLD),
111                            "extra bold" => Some(FontWeight::EXTRABOLD),
112                            "extra-bold" => Some(FontWeight::EXTRABOLD),
113                            "black" => Some(FontWeight::BLACK),
114                            _ => unreachable!(),
115                        };
116
117                        if let Some(guess_weight) = guess_weight {
118                            if idx == 0 {
119                                weight = Some(guess_weight);
120                                break 'searchLoop;
121                            } else {
122                                secondary_weight = Some(guess_weight);
123                            }
124                        }
125                    }
126                }
127            }
128
129            weight.unwrap_or(secondary_weight.unwrap_or(FontWeight::REGULAR))
130        };
131
132        let stretch = {
133            let mut stretch = None;
134            'searchLoop: for &search_style in &[
135                "ultracondensed",
136                "ultra_condensed",
137                "ultra-condensed",
138                "extracondensed",
139                "extra_condensed",
140                "extra-condensed",
141                "condensed",
142                "semicondensed",
143                "semi_condensed",
144                "semi-condensed",
145                "normal",
146                "semiexpanded",
147                "semi_expanded",
148                "semi-expanded",
149                "expanded",
150                "extraexpanded",
151                "extra_expanded",
152                "extra-expanded",
153                "ultraexpanded",
154                "ultra_expanded",
155                "ultra-expanded",
156            ] {
157                for (idx, &search_scope) in search_scopes.iter().enumerate() {
158                    if search_scope.contains(search_style) {
159                        let guess_stretch = match search_style {
160                            "ultracondensed" => Some(FontStretch::ULTRA_CONDENSED),
161                            "ultra_condensed" => Some(FontStretch::ULTRA_CONDENSED),
162                            "ultra-condensed" => Some(FontStretch::ULTRA_CONDENSED),
163                            "extracondensed" => Some(FontStretch::EXTRA_CONDENSED),
164                            "extra_condensed" => Some(FontStretch::EXTRA_CONDENSED),
165                            "extra-condensed" => Some(FontStretch::EXTRA_CONDENSED),
166                            "condensed" => Some(FontStretch::CONDENSED),
167                            "semicondensed" => Some(FontStretch::SEMI_CONDENSED),
168                            "semi_condensed" => Some(FontStretch::SEMI_CONDENSED),
169                            "semi-condensed" => Some(FontStretch::SEMI_CONDENSED),
170                            "normal" => Some(FontStretch::NORMAL),
171                            "semiexpanded" => Some(FontStretch::SEMI_EXPANDED),
172                            "semi_expanded" => Some(FontStretch::SEMI_EXPANDED),
173                            "semi-expanded" => Some(FontStretch::SEMI_EXPANDED),
174                            "expanded" => Some(FontStretch::EXPANDED),
175                            "extraexpanded" => Some(FontStretch::EXTRA_EXPANDED),
176                            "extra_expanded" => Some(FontStretch::EXTRA_EXPANDED),
177                            "extra-expanded" => Some(FontStretch::EXTRA_EXPANDED),
178                            "ultraexpanded" => Some(FontStretch::ULTRA_EXPANDED),
179                            "ultra_expanded" => Some(FontStretch::ULTRA_EXPANDED),
180                            "ultra-expanded" => Some(FontStretch::ULTRA_EXPANDED),
181                            _ => None,
182                        };
183
184                        if let Some(guess_stretch) = guess_stretch {
185                            if idx == 0 {
186                                stretch = Some(guess_stretch);
187                                break 'searchLoop;
188                            }
189                        }
190                    }
191                }
192            }
193
194            stretch.unwrap_or(FontStretch::NORMAL)
195        };
196
197        FontVariant {
198            style,
199            weight,
200            stretch,
201        }
202    };
203
204    let flags = {
205        // guess mono and serif
206        let mut flags = FontFlags::empty();
207
208        for search_scope in search_scopes {
209            if search_scope.contains("mono") {
210                flags |= FontFlags::MONOSPACE;
211            } else if search_scope.contains("serif") {
212                flags |= FontFlags::SERIF;
213            }
214        }
215
216        flags
217    };
218    let coverage = Coverage::from_vec(vec![0, 4294967295]);
219
220    Ok(FontInfo {
221        family,
222        variant,
223        flags,
224        coverage,
225    })
226}
227
228impl FontBuilder {
229    fn to_string(&self, field: &str, val: &JsValue) -> Result<String> {
230        Ok(val
231            .as_string()
232            .ok_or_else(|| JsValue::from_str(&format!("expected string for {field}, got {val:?}")))
233            .unwrap())
234    }
235
236    fn font_web_to_typst(
237        &self,
238        val: &JsValue,
239    ) -> Result<(JsValue, js_sys::Function, Vec<typst::text::FontInfo>)> {
240        let mut postscript_name = String::new();
241        let mut family = String::new();
242        let mut full_name = String::new();
243        let mut style = String::new();
244        let mut font_ref = None;
245        let mut font_blob_loader = None;
246        let mut font_cache: Option<FontInfoCache> = None;
247
248        for (k, v) in
249            js_sys::Object::entries(val.dyn_ref().ok_or_else(
250                || error_once!("WebFontToTypstFont.entries", val: format!("{:?}", val)),
251            )?)
252            .iter()
253            .map(convert_pair)
254        {
255            let k = self.to_string("web_font.key", &k)?;
256            match k.as_str() {
257                "postscriptName" => {
258                    postscript_name = self.to_string("web_font.postscriptName", &v)?;
259                }
260                "family" => {
261                    family = self.to_string("web_font.family", &v)?;
262                }
263                "fullName" => {
264                    full_name = self.to_string("web_font.fullName", &v)?;
265                }
266                "style" => {
267                    style = self.to_string("web_font.style", &v)?;
268                }
269                "ref" => {
270                    font_ref = Some(v);
271                }
272                "info" => {
273                    // a previous calculated font info
274                    font_cache = serde_wasm_bindgen::from_value(v).ok();
275                }
276                "blob" => {
277                    font_blob_loader = Some(v.clone().dyn_into().map_err(error_once_map!(
278                        "web_font.blob_builder",
279                        v: format!("{:?}", v)
280                    ))?);
281                }
282                _ => panic!("unknown key for {}: {}", "web_font", k),
283            }
284        }
285
286        let font_info = match font_cache {
287            Some(font_cache) => Some(
288                // todo cache invalidatio: font_cache.conditions.iter()
289                font_cache.info,
290            ),
291            None => None,
292        };
293
294        let font_info: Vec<FontInfo> = match font_info {
295            Some(font_info) => font_info,
296            None => {
297                vec![infer_info_from_web_font(WebFontInfo {
298                    family: family.clone(),
299                    full_name,
300                    postscript_name,
301                    style,
302                })?]
303            }
304        };
305
306        Ok((
307            font_ref.ok_or_else(|| error_once!("WebFontToTypstFont.NoFontRef", family: family))?,
308            font_blob_loader.ok_or_else(
309                || error_once!("WebFontToTypstFont.NoFontBlobLoader", family: family),
310            )?,
311            font_info,
312        ))
313    }
314}
315
316#[derive(Clone, Debug)]
317pub struct WebFont {
318    pub info: FontInfo,
319    pub context: JsValue,
320    pub blob: js_sys::Function,
321    pub index: u32,
322}
323
324impl WebFont {
325    pub fn load(&self) -> Option<ArrayBuffer> {
326        self.blob
327            .call1(&self.context, &self.index.into())
328            .unwrap()
329            .dyn_into::<ArrayBuffer>()
330            .ok()
331    }
332}
333
334/// Safety: `WebFont` is only used in the browser environment, and we
335/// cannot share data between workers.
336unsafe impl Send for WebFont {}
337
338#[derive(Debug)]
339pub struct WebFontLoader {
340    font: WebFont,
341    index: u32,
342}
343
344impl WebFontLoader {
345    pub fn new(font: WebFont, index: u32) -> Self {
346        Self { font, index }
347    }
348}
349
350impl FontLoader for WebFontLoader {
351    fn load(&mut self) -> Option<Font> {
352        let font = &self.font;
353        web_sys::console::log_3(
354            &"dyn init".into(),
355            &font.context,
356            &format!("{:?}", font.info).into(),
357        );
358        // let blob = pollster::block_on(JsFuture::from(blob.array_buffer())).unwrap();
359        let blob = font.load()?;
360        let blob = Bytes::new(js_sys::Uint8Array::new(&blob).to_vec());
361
362        Font::new(blob, self.index)
363    }
364}
365
366/// Searches for fonts.
367pub struct BrowserFontSearcher {
368    pub fonts: Vec<(FontInfo, FontSlot)>,
369}
370
371impl BrowserFontSearcher {
372    /// Create a new, empty browser searcher.
373    pub fn new() -> Self {
374        Self { fonts: vec![] }
375    }
376
377    /// Create a new browser searcher with fonts in a FontResolverImpl.
378    pub fn from_resolver(resolver: FontResolverImpl) -> Self {
379        let fonts = resolver
380            .slots
381            .into_iter()
382            .enumerate()
383            .map(|(idx, slot)| {
384                (
385                    resolver
386                        .book
387                        .info(idx)
388                        .expect("font should be in font book")
389                        .clone(),
390                    slot,
391                )
392            })
393            .collect();
394
395        Self { fonts }
396    }
397
398    /// Create a new browser searcher with fonts cloned from a FontResolverImpl.
399    /// Since FontSlot only holds QueryRef to font data, cloning is cheap.
400    pub fn new_with_resolver(resolver: &FontResolverImpl) -> Self {
401        let fonts = resolver
402            .slots
403            .iter()
404            .enumerate()
405            .map(|(idx, slot)| {
406                (
407                    resolver
408                        .book
409                        .info(idx)
410                        .expect("font should be in font book")
411                        .clone(),
412                    slot.clone(),
413                )
414            })
415            .collect();
416
417        Self { fonts }
418    }
419
420    /// Build a FontResolverImpl.
421    pub fn build(self) -> FontResolverImpl {
422        let (info, slots): (Vec<FontInfo>, Vec<FontSlot>) = self.fonts.into_iter().unzip();
423
424        let book = FontBook::from_infos(info);
425
426        FontResolverImpl::new(vec![], book, slots)
427    }
428}
429
430impl BrowserFontSearcher {
431    /// Add fonts that are embedded in the binary.
432    #[cfg(feature = "fonts")]
433    pub fn add_embedded(&mut self) {
434        for font_data in typst_assets::fonts() {
435            let buffer = Bytes::new(font_data);
436
437            self.fonts.extend(
438                Font::iter(buffer)
439                    .map(|font| (font.info().clone(), FontSlot::new_loaded(Some(font)))),
440            );
441        }
442    }
443
444    pub async fn add_web_fonts(&mut self, fonts: js_sys::Array) -> Result<()> {
445        let font_builder = FontBuilder {};
446
447        for v in fonts.iter() {
448            let (font_ref, font_blob_loader, font_info) = font_builder.font_web_to_typst(&v)?;
449
450            for (i, info) in font_info.into_iter().enumerate() {
451                let index = self.fonts.len();
452                self.fonts.push((
453                    info.clone(),
454                    FontSlot::new(WebFontLoader {
455                        font: WebFont {
456                            info,
457                            context: font_ref.clone(),
458                            blob: font_blob_loader.clone(),
459                            index: index as u32,
460                        },
461                        index: i as u32,
462                    }),
463                ))
464            }
465        }
466
467        Ok(())
468    }
469
470    pub fn add_font_data(&mut self, buffer: Bytes) {
471        for (i, info) in FontInfo::iter(buffer.as_slice()).enumerate() {
472            let buffer = buffer.clone();
473            self.fonts.push((
474                info,
475                FontSlot::new(BufferFontLoader {
476                    buffer: Some(buffer),
477                    index: i as u32,
478                }),
479            ))
480        }
481    }
482
483    pub fn with_fonts_mut(&mut self, func: impl FnOnce(&mut Vec<(FontInfo, FontSlot)>)) {
484        func(&mut self.fonts);
485    }
486}
487
488impl Default for BrowserFontSearcher {
489    fn default() -> Self {
490        Self::new()
491    }
492}