diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml index c8a9aefc7..28468ae15 100644 --- a/compiler/Cargo.toml +++ b/compiler/Cargo.toml @@ -20,6 +20,7 @@ codespan-reporting = "0.11" parking_lot = "0.12.1" serde_json = "1.0.96" +serde-wasm-bindgen = { version = "^0.5", optional = true } hex = "0.4.3" sha2 = "0.10.6" @@ -50,6 +51,6 @@ serde_json = "1.0.95" system = ["dep:memmap2", "dep:dirs", "dep:walkdir", "dep:same-file"] __web = ["dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:js-sys", "dep:web-sys", "dep:pollster"] web-render = ["__web"] -browser-compile = ["__web", "web-render"] +browser-compile = ["__web", "web-render", "dep:serde-wasm-bindgen"] web = ["__web", "web-render", "browser-compile"] default = ["system"] diff --git a/compiler/src/font/web/mod.rs b/compiler/src/font/web/mod.rs index 0ebc1bdf9..949b5bcb5 100644 --- a/compiler/src/font/web/mod.rs +++ b/compiler/src/font/web/mod.rs @@ -1,88 +1,379 @@ use std::sync::{Arc, RwLock}; -use js_sys::Promise; +use js_sys::ArrayBuffer; use typst::{ - font::{Font, FontBook, FontInfo}, + font::{Font, FontBook, FontFlags, FontInfo, FontStretch, FontStyle, FontVariant, FontWeight}, util::Buffer, }; use typst_ts_core::{ + cache::FontInfoCache, font::{BufferFontLoader, FontProfile, FontResolverImpl, PartialFontBook}, - FontLoader, FontSlot, ReadAllOnce, + FontLoader, FontSlot, }; use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::JsFuture; -use web_sys::Blob; + +use crate::font::info::typst_typographic_family; + +/// Destructures a JS `[key, value]` pair into a tuple of [`Deserializer`]s. +pub(crate) fn convert_pair(pair: JsValue) -> (JsValue, JsValue) { + let pair = pair.unchecked_into::(); + (pair.get(0), pair.get(1)) +} +struct FontBuilder {} + +fn font_family_web_to_typst(family: &str, full_name: &str) -> Result { + let mut family = family; + if family.starts_with("Noto") + || family.starts_with("NewCM") + || family.starts_with("NewComputerModern") + { + family = full_name; + } + + if family.is_empty() { + return Err(JsValue::from_str( + "empty family (cannot infer from font.family and font.fullName)", + )); + } + + Ok(typst_typographic_family(family).to_string()) +} + +struct WebFontInfo { + family: String, + full_name: String, + postscript_name: String, + style: String, +} + +fn infer_info_from_web_font( + WebFontInfo { + family, + full_name, + postscript_name, + style, + }: WebFontInfo, +) -> Result { + let family = font_family_web_to_typst(&family, &full_name)?; + + let mut full = full_name; + full.make_ascii_lowercase(); + + let mut postscript = postscript_name; + postscript.make_ascii_lowercase(); + + let mut style = style; + style.make_ascii_lowercase(); + + let search_scopes = [style.as_str(), postscript.as_str(), full.as_str()]; + + let variant = { + // Some fonts miss the relevant bits for italic or oblique, so + // we also try to infer that from the full name. + let italic = full.contains("italic"); + let oblique = full.contains("oblique") || full.contains("slanted"); + + let style = match (italic, oblique) { + (false, false) => FontStyle::Normal, + (true, _) => FontStyle::Italic, + (_, true) => FontStyle::Oblique, + }; + + let weight = { + let mut weight = None; + let mut secondary_weight = None; + 'searchLoop: for &search_style in &[ + "thin", + "extralight", + "extra light", + "extra-light", + "light", + "regular", + "medium", + "semibold", + "semi bold", + "semi-bold", + "bold", + "extrabold", + "extra bold", + "extra-bold", + "black", + ] { + for (idx, &search_scope) in search_scopes.iter().enumerate() { + if search_scope.contains(search_style) { + let guess_weight = match search_style { + "thin" => Some(FontWeight::THIN), + "extralight" => Some(FontWeight::EXTRALIGHT), + "extra light" => Some(FontWeight::EXTRALIGHT), + "extra-light" => Some(FontWeight::EXTRALIGHT), + "light" => Some(FontWeight::LIGHT), + "regular" => Some(FontWeight::REGULAR), + "medium" => Some(FontWeight::MEDIUM), + "semibold" => Some(FontWeight::SEMIBOLD), + "semi bold" => Some(FontWeight::SEMIBOLD), + "semi-bold" => Some(FontWeight::SEMIBOLD), + "bold" => Some(FontWeight::BOLD), + "extrabold" => Some(FontWeight::EXTRABOLD), + "extra bold" => Some(FontWeight::EXTRABOLD), + "extra-bold" => Some(FontWeight::EXTRABOLD), + "black" => Some(FontWeight::BLACK), + _ => unreachable!(), + }; + + if let Some(guess_weight) = guess_weight { + if idx == 0 { + weight = Some(guess_weight); + break 'searchLoop; + } else { + secondary_weight = Some(guess_weight); + } + } + } + } + } + + weight.unwrap_or(secondary_weight.unwrap_or(FontWeight::REGULAR)) + }; + + let stretch = { + let mut stretch = None; + 'searchLoop: for &search_style in &[ + "ultracondensed", + "ultra_condensed", + "ultra-condensed", + "extracondensed", + "extra_condensed", + "extra-condensed", + "condensed", + "semicondensed", + "semi_condensed", + "semi-condensed", + "normal", + "semiexpanded", + "semi_expanded", + "semi-expanded", + "expanded", + "extraexpanded", + "extra_expanded", + "extra-expanded", + "ultraexpanded", + "ultra_expanded", + "ultra-expanded", + ] { + for (idx, &search_scope) in search_scopes.iter().enumerate() { + if search_scope.contains(search_style) { + let guess_stretch = match search_style { + "ultracondensed" => Some(FontStretch::ULTRA_CONDENSED), + "ultra_condensed" => Some(FontStretch::ULTRA_CONDENSED), + "ultra-condensed" => Some(FontStretch::ULTRA_CONDENSED), + "extracondensed" => Some(FontStretch::EXTRA_CONDENSED), + "extra_condensed" => Some(FontStretch::EXTRA_CONDENSED), + "extra-condensed" => Some(FontStretch::EXTRA_CONDENSED), + "condensed" => Some(FontStretch::CONDENSED), + "semicondensed" => Some(FontStretch::SEMI_CONDENSED), + "semi_condensed" => Some(FontStretch::SEMI_CONDENSED), + "semi-condensed" => Some(FontStretch::SEMI_CONDENSED), + "normal" => Some(FontStretch::NORMAL), + "semiexpanded" => Some(FontStretch::SEMI_EXPANDED), + "semi_expanded" => Some(FontStretch::SEMI_EXPANDED), + "semi-expanded" => Some(FontStretch::SEMI_EXPANDED), + "expanded" => Some(FontStretch::EXPANDED), + "extraexpanded" => Some(FontStretch::EXTRA_EXPANDED), + "extra_expanded" => Some(FontStretch::EXTRA_EXPANDED), + "extra-expanded" => Some(FontStretch::EXTRA_EXPANDED), + "ultraexpanded" => Some(FontStretch::ULTRA_EXPANDED), + "ultra_expanded" => Some(FontStretch::ULTRA_EXPANDED), + "ultra-expanded" => Some(FontStretch::ULTRA_EXPANDED), + _ => None, + }; + + if let Some(guess_stretch) = guess_stretch { + if idx == 0 { + stretch = Some(guess_stretch); + break 'searchLoop; + } + } + } + } + } + + stretch.unwrap_or(FontStretch::NORMAL) + }; + + FontVariant { + style, + weight, + stretch, + } + }; + + let flags = { + // guess mono and serif + let mut flags = FontFlags::empty(); + + for search_scope in search_scopes { + if search_scope.contains("mono") { + flags |= FontFlags::MONOSPACE; + } else if search_scope.contains("serif") { + flags |= FontFlags::SERIF; + } + } + + flags + }; + let coverage = serde_json::from_str("[0, 4294967295]").unwrap(); + + Ok(FontInfo { + family, + variant, + flags, + coverage, + }) +} + +impl FontBuilder { + // fn to_f64(&self, field: &str, val: &JsValue) -> Result { + // Ok(val + // .as_f64() + // .ok_or_else(|| JsValue::from_str(&format!("expected f64 for {}, got {:?}", field, val))) + // .unwrap()) + // } + + fn to_string(&self, field: &str, val: &JsValue) -> Result { + Ok(val + .as_string() + .ok_or_else(|| { + JsValue::from_str(&format!("expected string for {}, got {:?}", field, val)) + }) + .unwrap()) + } + + fn font_web_to_typst( + &self, + val: &JsValue, + ) -> Result<(JsValue, js_sys::Function, Vec), JsValue> { + let mut postscript_name = String::new(); + let mut family = String::new(); + let mut full_name = String::new(); + let mut style = String::new(); + let mut font_ref = None; + let mut font_blob_loader = None; + let mut font_cache: Option = None; + + for (k, v) in js_sys::Object::entries(val.dyn_ref().ok_or_else(|| { + JsValue::from_str(&format!( + "expected object for iterating font, got {:?}", + val + )) + })?) + .iter() + .map(convert_pair) + { + let k = self.to_string("web_font.key", &k)?; + match k.as_str() { + "postscriptName" => { + postscript_name = self.to_string("web_font.postscriptName", &v)?; + } + "family" => { + family = self.to_string("web_font.family", &v)?; + } + "fullName" => { + full_name = self.to_string("web_font.fullName", &v)?; + } + "style" => { + style = self.to_string("web_font.style", &v)?; + } + "ref" => { + font_ref = Some(v); + } + "info" => { + // a previous calculated font info + font_cache = serde_wasm_bindgen::from_value(v).ok(); + } + "blob" => { + font_blob_loader = Some(v.dyn_into()?); + } + _ => panic!("unknown key for {}: {}", "web_font", k), + } + } + + let font_info = match font_cache { + Some(font_cache) => Some( + // todo cache invalidatio: font_cache.conditions.iter() + font_cache.info, + ), + None => None, + }; + + let font_info: Vec = match font_info { + Some(font_info) => font_info, + None => { + vec![infer_info_from_web_font(WebFontInfo { + family: family.clone(), + full_name, + postscript_name, + style, + })?] + } + }; + + Ok(( + font_ref.ok_or_else(|| { + JsValue::from_str(&format!("Could not find font reference for {}", family,)) + })?, + font_blob_loader.ok_or_else(|| { + JsValue::from_str(&format!("Could not find font blob loader for {}", family,)) + })?, + font_info, + )) + } +} #[derive(Clone, Debug)] pub struct WebFont { - pub family: String, - pub style: String, - pub full_name: String, - pub postscript_name: String, + pub info: FontInfo, pub context: JsValue, pub blob: js_sys::Function, + pub index: u32, } impl WebFont { - pub async fn load(&self) -> Blob { - JsFuture::from( - self.blob - .call0(&self.context) - .unwrap() - .dyn_into::() - .unwrap(), - ) - .await - .unwrap() - .into() + pub fn load(&self) -> Option { + self.blob + .call1(&self.context, &self.index.into()) + .unwrap() + .dyn_into::() + .ok() } } pub struct WebFontLoader { - font: Option, + font: WebFont, index: u32, } impl WebFontLoader { pub fn new(font: WebFont, index: u32) -> Self { - Self { - font: Some(font), - index, - } + Self { font, index } } } impl FontLoader for WebFontLoader { fn load(&mut self) -> Option { - let blob = pollster::block_on(self.font.take().unwrap().load()); - let blob = pollster::block_on(JsFuture::from(blob.array_buffer())).unwrap(); + let font = &self.font; + web_sys::console::log_3( + &"dyn init".into(), + &font.context, + &format!("{:?}", font.info).into(), + ); + // let blob = pollster::block_on(JsFuture::from(blob.array_buffer())).unwrap(); + let blob = font.load()?; let blob = Buffer::from(js_sys::Uint8Array::new(&blob).to_vec()); Font::new(blob, self.index) } } -pub struct WebFontBlob { - font: Option, -} - -impl WebFontBlob { - pub fn new(font: WebFont) -> Self { - Self { font: Some(font) } - } -} - -impl ReadAllOnce for WebFontBlob { - fn read_all(mut self, buf: &mut Vec) -> std::io::Result { - let blob = pollster::block_on(self.font.take().unwrap().load()); - let blob = pollster::block_on(JsFuture::from(blob.array_buffer())).unwrap(); - let mut blob = js_sys::Uint8Array::new(&blob).to_vec(); - let blob_len = blob.len(); - buf.append(&mut blob); - Ok(blob_len) - } -} - /// Searches for fonts. pub struct BrowserFontSearcher { pub book: FontBook, @@ -106,24 +397,29 @@ impl BrowserFontSearcher { } } - pub async fn add_web_fonts(&mut self, _fonts: js_sys::Array) { - // family: String, - // style: String, - // full_name: String, - // postscript_name: String, - // context: JsValue, - // blob: js_sys::Function, - // let blob = font.load().await; - // let blob = JsFuture::from(blob.array_buffer()).await.unwrap(); - // let buffer = Buffer::from(js_sys::Uint8Array::new(&blob).to_vec()); - - // for f in fonts.iter() { - // // todo: load lazily - // self.fonts.push(FontSlot::new(Box::new(WebFontLoader { - // font: Some(f.dyn_into::().unwrap()), - // index: 0 as u32, - // }))) - // } + pub async fn add_web_fonts(&mut self, fonts: js_sys::Array) -> Result<(), JsValue> { + let font_builder = FontBuilder {}; + + for v in fonts.iter() { + let (font_ref, font_blob_loader, font_info) = font_builder.font_web_to_typst(&v)?; + + for (i, info) in font_info.into_iter().enumerate() { + self.book.push(info.clone()); + + let index = self.fonts.len(); + self.fonts.push(FontSlot::new(Box::new(WebFontLoader { + font: WebFont { + info, + context: font_ref.clone(), + blob: font_blob_loader.clone(), + index: index as u32, + }, + index: i as u32, + }))) + } + } + + Ok(()) } pub fn add_font_data(&mut self, buffer: Buffer) { diff --git a/packages/compiler/src/builder.rs b/packages/compiler/src/builder.rs index cd3fd19af..f02709a78 100644 --- a/packages/compiler/src/builder.rs +++ b/packages/compiler/src/builder.rs @@ -5,7 +5,6 @@ use typst_ts_compiler::font::web::BrowserFontSearcher; use js_sys::Uint8Array; use typst::util::Buffer; use wasm_bindgen::prelude::*; -use web_sys::console; #[wasm_bindgen] pub struct TypstCompilerBuilder { @@ -34,12 +33,7 @@ impl TypstCompilerBuilder { // 100 KB pub async fn add_web_fonts(&mut self, fonts: js_sys::Array) -> Result<(), JsValue> { - let v: JsValue = format!("web font loading: {:?}", fonts).into(); - console::info_1(&v); - - self.searcher.add_web_fonts(fonts).await; - - Ok(()) + self.searcher.add_web_fonts(fonts).await } // 24 MB diff --git a/packages/renderer/src/builder.rs b/packages/renderer/src/builder.rs index fb246feb8..417e066af 100644 --- a/packages/renderer/src/builder.rs +++ b/packages/renderer/src/builder.rs @@ -5,7 +5,6 @@ use typst_ts_compiler::font::web::BrowserFontSearcher; use js_sys::Uint8Array; use typst::util::Buffer; use wasm_bindgen::prelude::*; -use web_sys::console; #[wasm_bindgen] pub struct TypstRendererBuilder { @@ -33,12 +32,7 @@ impl TypstRendererBuilder { // 100 KB pub async fn add_web_fonts(&mut self, font: js_sys::Array) -> Result<(), JsValue> { - let v: JsValue = format!("web font loading: {:?}", font).into(); - console::info_1(&v); - - self.searcher.add_web_fonts(font).await; - - Ok(()) + self.searcher.add_web_fonts(font).await } // 24 MB