1use 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
20pub(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 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 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 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 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#[derive(Clone, Debug)]
325pub struct WebFont {
326 pub info: FontInfo,
328 pub context: JsValue,
330 pub blob: js_sys::Function,
332 pub index: u32,
334}
335
336impl WebFont {
337 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
347unsafe impl Send for WebFont {}
350
351#[derive(Debug)]
353pub struct WebFontLoader {
354 font: WebFont,
356 index: u32,
358}
359
360impl WebFontLoader {
361 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 = font.load()?;
377 let blob = Bytes::new(js_sys::Uint8Array::new(&blob).to_vec());
378
379 Font::new(blob, self.index)
380 }
381}
382
383pub struct BrowserFontSearcher {
385 base: MemoryFontSearcher,
387}
388
389impl BrowserFontSearcher {
390 pub fn new() -> Self {
392 Self {
393 base: MemoryFontSearcher::default(),
394 }
395 }
396
397 pub fn from_resolver(resolver: FontResolverImpl) -> Self {
399 let base = MemoryFontSearcher::from_resolver(resolver);
400 Self { base }
401 }
402
403 pub fn build(self) -> FontResolverImpl {
405 self.base.build()
406 }
407}
408
409impl BrowserFontSearcher {
410 pub fn resolve_opts(&mut self, opts: CompileFontOpts) -> Result<()> {
412 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 #[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 pub fn add_memory_fonts(&mut self, data: impl ParallelIterator<Item = Bytes>) {
439 self.base.add_memory_fonts(data);
440 }
441
442 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 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 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}