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        axes: Vec::new(),
232        coverage,
233    })
234}
235
236impl FontBuilder {
237    fn to_string(&self, field: &str, val: &JsValue) -> Result<String> {
238        Ok(val
239            .as_string()
240            .ok_or_else(|| JsValue::from_str(&format!("expected string for {field}, got {val:?}")))
241            .unwrap())
242    }
243
244    fn font_web_to_typst(
245        &self,
246        val: &JsValue,
247    ) -> Result<(JsValue, js_sys::Function, Vec<typst::text::FontInfo>)> {
248        let mut postscript_name = String::new();
249        let mut family = String::new();
250        let mut full_name = String::new();
251        let mut style = String::new();
252        let mut font_ref = None;
253        let mut font_blob_loader = None;
254        let mut font_cache: Option<FontInfoCache> = None;
255
256        for (k, v) in
257            js_sys::Object::entries(val.dyn_ref().ok_or_else(
258                || error_once!("WebFontToTypstFont.entries", val: format!("{:?}", val)),
259            )?)
260            .iter()
261            .map(convert_pair)
262        {
263            let k = self.to_string("web_font.key", &k)?;
264            match k.as_str() {
265                "postscriptName" => {
266                    postscript_name = self.to_string("web_font.postscriptName", &v)?;
267                }
268                "family" => {
269                    family = self.to_string("web_font.family", &v)?;
270                }
271                "fullName" => {
272                    full_name = self.to_string("web_font.fullName", &v)?;
273                }
274                "style" => {
275                    style = self.to_string("web_font.style", &v)?;
276                }
277                "ref" => {
278                    font_ref = Some(v);
279                }
280                "info" => {
281                    // a previous calculated font info
282                    font_cache = serde_wasm_bindgen::from_value(v).ok();
283                }
284                "blob" => {
285                    font_blob_loader = Some(v.clone().dyn_into().map_err(error_once_map!(
286                        "web_font.blob_builder",
287                        v: format!("{:?}", v)
288                    ))?);
289                }
290                _ => panic!("unknown key for {}: {}", "web_font", k),
291            }
292        }
293
294        let font_info = match font_cache {
295            Some(font_cache) => Some(
296                // todo cache invalidatio: font_cache.conditions.iter()
297                font_cache.info,
298            ),
299            None => None,
300        };
301
302        let font_info: Vec<FontInfo> = match font_info {
303            Some(font_info) => font_info,
304            None => {
305                vec![infer_info_from_web_font(WebFontInfo {
306                    family: family.clone(),
307                    full_name,
308                    postscript_name,
309                    style,
310                })?]
311            }
312        };
313
314        Ok((
315            font_ref.ok_or_else(|| error_once!("WebFontToTypstFont.NoFontRef", family: family))?,
316            font_blob_loader.ok_or_else(
317                || error_once!("WebFontToTypstFont.NoFontBlobLoader", family: family),
318            )?,
319            font_info,
320        ))
321    }
322}
323
324/// A web font.
325#[derive(Clone, Debug)]
326pub struct WebFont {
327    /// The font info.
328    pub info: FontInfo,
329    /// The context of the font.
330    pub context: JsValue,
331    /// The blob loader.
332    pub blob: js_sys::Function,
333    /// The index in a font file.
334    pub index: u32,
335}
336
337impl WebFont {
338    /// Loads the font from the blob.
339    pub fn load(&self) -> Option<ArrayBuffer> {
340        self.blob
341            .call1(&self.context, &self.index.into())
342            .unwrap()
343            .dyn_into::<ArrayBuffer>()
344            .ok()
345    }
346}
347
348/// Safety: `WebFont` is only used in the browser environment, and we
349/// cannot share data between workers.
350unsafe impl Send for WebFont {}
351
352/// A web font loader.
353#[derive(Debug)]
354pub struct WebFontLoader {
355    /// The font.
356    font: WebFont,
357    /// The index in a font file.
358    index: u32,
359}
360
361impl WebFontLoader {
362    /// Creates a new web font loader.
363    pub fn new(font: WebFont, index: u32) -> Self {
364        Self { font, index }
365    }
366}
367
368impl FontLoader for WebFontLoader {
369    fn load(&mut self) -> Option<Font> {
370        let font = &self.font;
371        web_sys::console::log_3(
372            &"dyn init".into(),
373            &font.context,
374            &format!("{:?}", font.info).into(),
375        );
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}