tinymist_world/font/web/
mod.rs

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