use std::time::Instant;
use log::info;
use typst::diag::FileResult;
use typst::foundations::{Bytes, Datetime};
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, LibraryExt, World};
use typst_kit::fonts::{FontSearcher, FontSlot, Fonts};
include!(concat!(env!("OUT_DIR"), "/embedded_fonts.rs"));
pub struct FontCache {
book: FontBook,
fonts: Vec<FontSlot>,
custom_fonts: Vec<Font>,
}
impl Default for FontCache {
fn default() -> Self {
Self::new()
}
}
impl FontCache {
pub fn new() -> Self {
let start = Instant::now();
let Fonts { mut book, fonts } = FontSearcher::new().include_system_fonts(true).search();
info!(
"world: font search completed in {:.1}ms",
start.elapsed().as_secs_f64() * 1000.0
);
let custom_fonts = Self::load_embedded_fonts(&mut book);
let has_cjk = book.contains_family("ipagothic")
|| book.contains_family("noto sans jp")
|| book.contains_family("noto sans cjk jp")
|| book.contains_family("noto serif cjk jp")
|| book.contains_family("ipamincho");
if !has_cjk {
eprintln!("warning: no CJK font found. Japanese text may not render correctly.");
eprintln!(" Install IPAGothic or Noto Sans CJK JP for proper rendering.");
}
Self {
book,
fonts,
custom_fonts,
}
}
fn load_embedded_fonts(book: &mut FontBook) -> Vec<Font> {
let start = Instant::now();
let mut custom = Vec::new();
let mut total_compressed = 0usize;
let mut total_decompressed = 0usize;
for data in embedded_font_data() {
total_compressed += data.len();
let decompressed =
zstd::decode_all(&data[..]).expect("failed to decompress embedded font");
total_decompressed += decompressed.len();
let buffer = Bytes::new(decompressed);
for font in Font::iter(buffer) {
book.push(font.info().clone());
custom.push(font);
}
}
info!(
"world: decompressed {} embedded fonts ({:.1} MB -> {:.1} MB) in {:.1}ms",
custom.len(),
total_compressed as f64 / (1024.0 * 1024.0),
total_decompressed as f64 / (1024.0 * 1024.0),
start.elapsed().as_secs_f64() * 1000.0,
);
custom
}
pub fn font(&self, index: usize) -> Option<Font> {
if index < self.fonts.len() {
self.fonts[index].get()
} else {
self.custom_fonts.get(index - self.fonts.len()).cloned()
}
}
}
pub struct MluxWorld<'f> {
library: LazyHash<Library>,
book: LazyHash<FontBook>,
font_cache: &'f FontCache,
main_id: FileId,
main_source: Source,
content_offset: usize,
data_files: crate::theme::DataFiles,
image_files: crate::image::LoadedImages,
}
impl<'f> MluxWorld<'f> {
pub fn new(
theme_text: &str,
data_files: crate::theme::DataFiles,
content_text: &str,
width: f64,
fonts: &'f FontCache,
image_files: crate::image::LoadedImages,
) -> Self {
let start = Instant::now();
let mitex_compat = include_str!("../../themes/mitex-compat.typ");
let prefix = format!("{theme_text}\n{mitex_compat}\n#set page(width: {width}pt)\n");
let content_offset = prefix.len();
let main_text = format!("{prefix}{content_text}\n");
let mut world = Self::from_source(&main_text, fonts);
world.data_files = data_files;
world.content_offset = content_offset;
world.image_files = image_files;
info!(
"world: new() completed in {:.1}ms",
start.elapsed().as_secs_f64() * 1000.0
);
world
}
pub fn new_raw(source: &str, fonts: &'f FontCache) -> Self {
Self::from_source(source, fonts)
}
pub fn main_source(&self) -> &Source {
&self.main_source
}
pub fn content_offset(&self) -> usize {
self.content_offset
}
fn from_source(main_text: &str, fonts: &'f FontCache) -> Self {
let vpath = VirtualPath::new("main.typ");
let main_id = FileId::new(None, vpath);
let main_source = Source::new(main_id, main_text.to_string());
Self {
library: LazyHash::new(Library::default()),
book: LazyHash::new(fonts.book.clone()),
font_cache: fonts,
main_id,
main_source,
content_offset: 0,
data_files: &[],
image_files: crate::image::LoadedImages::default(),
}
}
}
impl World for MluxWorld<'_> {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
fn main(&self) -> FileId {
self.main_id
}
fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.main_id {
Ok(self.main_source.clone())
} else {
Err(typst::diag::FileError::NotFound(
id.vpath().as_rootless_path().into(),
))
}
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
let path = id.vpath().as_rootless_path();
for &(name, data) in self.data_files {
if path == std::path::Path::new(name) {
return Ok(Bytes::new(data.to_vec()));
}
}
let path_str = path.to_str().unwrap_or("");
if let Some(bytes) = self.image_files.get(path_str) {
return Ok(bytes.clone());
}
if let Some(bytes) = restore_url_scheme(path_str)
.as_deref()
.and_then(|restored| self.image_files.get(restored))
{
return Ok(bytes.clone());
}
if id == self.main_id {
Ok(Bytes::from_string(self.main_source.clone()))
} else {
Err(typst::diag::FileError::NotFound(path.into()))
}
}
fn font(&self, index: usize) -> Option<Font> {
self.font_cache.font(index)
}
fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
None
}
}
fn restore_url_scheme(path: &str) -> Option<String> {
for scheme in &["https:/", "http:/"] {
if let Some(rest) = path.strip_prefix(scheme)
&& !rest.starts_with('/')
{
return Some(format!("{scheme}/{rest}"));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn restore_url_scheme_https() {
assert_eq!(
restore_url_scheme("https:/example.com/img.png"),
Some("https://example.com/img.png".to_string()),
);
}
#[test]
fn restore_url_scheme_http() {
assert_eq!(
restore_url_scheme("http:/example.com/img.png"),
Some("http://example.com/img.png".to_string()),
);
}
#[test]
fn restore_url_scheme_already_double_slash() {
assert_eq!(restore_url_scheme("https://example.com/img.png"), None);
}
#[test]
fn restore_url_scheme_local_path() {
assert_eq!(restore_url_scheme("images/photo.png"), None);
}
}