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
13pub(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 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 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 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 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
334unsafe 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 = font.load()?;
360 let blob = Bytes::new(js_sys::Uint8Array::new(&blob).to_vec());
361
362 Font::new(blob, self.index)
363 }
364}
365
366pub struct BrowserFontSearcher {
368 pub fonts: Vec<(FontInfo, FontSlot)>,
369}
370
371impl BrowserFontSearcher {
372 pub fn new() -> Self {
374 Self { fonts: vec![] }
375 }
376
377 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 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 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 #[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}