#[allow(unused_imports)]
use std::{
env::var,
error::Error,
fs::{self, File, read_to_string},
io::{self, Read, Write},
path::{Path, PathBuf},
};
use std::{
fs::{create_dir_all, read_dir},
io::copy,
};
use serde::Deserialize;
use toml::from_str;
use ureq::get;
#[derive(Deserialize)]
struct ProjectMetadata {
package: Package,
dependencies: Dependencies,
}
#[derive(Deserialize)]
struct Package {
#[serde(rename = "version")]
typwriter_version: String,
}
#[derive(Deserialize)]
struct Dependencies {
typst: Typst,
}
#[derive(Deserialize)]
pub struct Typst {
#[serde(rename = "version")]
pub typst_version: String,
}
#[allow(dead_code)]
fn cache_dir() -> PathBuf {
var("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| dirs::cache_dir().unwrap_or_else(|| PathBuf::from(".cache")))
.join("typwriter")
.join("fonts")
}
#[allow(dead_code)]
fn download(url: &str) -> Result<Vec<u8>, Box<dyn Error>> {
println!("cargo::warning=Downloading {url}");
let bytes = get(url)
.call()?
.body_mut()
.with_config()
.limit(500 * 1024 * 1024) .read_to_vec()?;
Ok(bytes)
}
#[allow(dead_code)]
fn extract_tar_gz(data: &[u8], dest: &Path) -> Result<(), Box<dyn Error>> {
use flate2::read::GzDecoder;
use tar::Archive;
create_dir_all(dest)?;
let decoder = GzDecoder::new(data);
let mut archive = Archive::new(decoder);
archive.unpack(dest)?;
Ok(())
}
#[allow(dead_code)]
fn extract_zip(data: &[u8], dest: &Path) -> Result<(), Box<dyn Error>> {
use std::io::Cursor;
use zip::ZipArchive;
create_dir_all(dest)?;
let reader = Cursor::new(data);
let mut archive = ZipArchive::new(reader)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let name = file.name().to_string();
if file.is_dir() {
continue;
}
if let Some(filename) = Path::new(&name).file_name() {
let filename_str = filename.to_string_lossy();
if filename_str.ends_with(".ttf")
|| filename_str.ends_with(".otf")
|| filename_str.ends_with(".otc")
{
let outpath = dest.join(filename);
let mut outfile = File::create(&outpath)?;
copy(&mut file, &mut outfile)?;
}
}
}
Ok(())
}
#[allow(dead_code)]
fn download_font(
name: &str,
url: &str,
archive_type: ArchiveType,
) -> Result<PathBuf, Box<dyn Error>> {
let cache = cache_dir().join(name);
if cache.exists() && read_dir(&cache)?.next().is_some() {
println!("cargo::warning=Using cached fonts from {}", cache.display());
return Ok(cache);
}
let data = download(url)?;
match archive_type {
ArchiveType::TarGz => extract_tar_gz(&data, &cache)?,
ArchiveType::Zip => extract_zip(&data, &cache)?,
}
println!("cargo::warning=Cached fonts to {}", cache.display());
Ok(cache)
}
#[allow(dead_code)]
enum ArchiveType {
TarGz,
Zip,
}
#[allow(dead_code)]
fn generate_font_includes(
out_dir: &Path,
feature_name: &str,
font_dir: &Path,
files: &[&str],
) -> Result<(), Box<dyn Error>> {
let include_file = out_dir.join(format!("embed_{feature_name}.rs"));
let mut f = File::create(&include_file)?;
writeln!(f, "{{")?;
for file in files {
let font_path = font_dir.join(file);
let path_str = font_path.display();
writeln!(f, " process(include_bytes!(\"{path_str}\"));")?;
}
writeln!(f, "}}")?;
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let out_dir = PathBuf::from(var("OUT_DIR")?);
let mut f = File::create(out_dir.join("version.rs"))?;
let ProjectMetadata {
package: Package { typwriter_version },
dependencies: Dependencies { typst: Typst { typst_version } },
} = from_str(&read_to_string("Cargo.toml")?)?;
write!(
f,
r#"/// Returns the version of the library.
///
/// # Example
///
/// ```rust
/// println!("Typwriter version: {{}}", typwriter::version());
/// ```
pub fn version() -> &'static str {{ "{typwriter_version}" }}
/// Returns the Typst version the library was compiled with.
///
/// # Example
///
/// ```rust
/// println!("Typst version: {{}}", typwriter::typst_version());
/// ```
pub fn typst_version() -> &'static str {{ "{typst_version}" }}
"#,
)?;
#[cfg(feature = "embed_warpnine_mono")]
{
let font_dir = download_font(
"WarpnineFonts",
"https://github.com/0x6b/warpnine-fonts/releases/download/v2026-01-11.1/warpnine-fonts-2026-01-11.1.zip",
ArchiveType::Zip,
)?;
generate_font_includes(
&out_dir,
"warpnine_mono",
&font_dir,
&[
"WarpnineMono-Black.ttf",
"WarpnineMono-BlackItalic.ttf",
"WarpnineMono-Bold.ttf",
"WarpnineMono-BoldItalic.ttf",
"WarpnineMono-ExtraBlack.ttf",
"WarpnineMono-ExtraBlackItalic.ttf",
"WarpnineMono-ExtraBold.ttf",
"WarpnineMono-ExtraBoldItalic.ttf",
"WarpnineMono-Italic.ttf",
"WarpnineMono-Light.ttf",
"WarpnineMono-LightItalic.ttf",
"WarpnineMono-Medium.ttf",
"WarpnineMono-MediumItalic.ttf",
"WarpnineMono-Regular.ttf",
"WarpnineMono-SemiBold.ttf",
"WarpnineMono-SemiBoldItalic.ttf",
],
)?;
}
#[cfg(feature = "embed_warpnine_sans")]
{
let font_dir = download_font(
"WarpnineFonts",
"https://github.com/0x6b/warpnine-fonts/releases/download/v2026-01-11.1/warpnine-fonts-2026-01-11.1.zip",
ArchiveType::Zip,
)?;
generate_font_includes(
&out_dir,
"warpnine_sans",
&font_dir,
&[
"WarpnineSans-Black.ttf",
"WarpnineSans-BlackItalic.ttf",
"WarpnineSans-Bold.ttf",
"WarpnineSans-BoldItalic.ttf",
"WarpnineSans-ExtraBold.ttf",
"WarpnineSans-ExtraBoldItalic.ttf",
"WarpnineSans-Italic.ttf",
"WarpnineSans-Light.ttf",
"WarpnineSans-LightItalic.ttf",
"WarpnineSans-Medium.ttf",
"WarpnineSans-MediumItalic.ttf",
"WarpnineSans-Regular.ttf",
"WarpnineSans-SemiBold.ttf",
"WarpnineSans-SemiBoldItalic.ttf",
"WarpnineSansCondensed-Black.ttf",
"WarpnineSansCondensed-BlackItalic.ttf",
"WarpnineSansCondensed-Bold.ttf",
"WarpnineSansCondensed-BoldItalic.ttf",
"WarpnineSansCondensed-ExtraBold.ttf",
"WarpnineSansCondensed-ExtraBoldItalic.ttf",
"WarpnineSansCondensed-Italic.ttf",
"WarpnineSansCondensed-Light.ttf",
"WarpnineSansCondensed-LightItalic.ttf",
"WarpnineSansCondensed-Medium.ttf",
"WarpnineSansCondensed-MediumItalic.ttf",
"WarpnineSansCondensed-Regular.ttf",
"WarpnineSansCondensed-SemiBold.ttf",
"WarpnineSansCondensed-SemiBoldItalic.ttf",
],
)?;
}
#[cfg(feature = "embed_noto_sans_jp")]
{
let font_dir = download_font(
"NotoSansJP",
"https://github.com/notofonts/noto-cjk/releases/download/Sans2.004/16_NotoSansJP.zip",
ArchiveType::Zip,
)?;
generate_font_includes(
&out_dir,
"noto_sans_jp",
&font_dir,
&[
"NotoSansJP-Black.otf",
"NotoSansJP-Bold.otf",
"NotoSansJP-DemiLight.otf",
"NotoSansJP-Light.otf",
"NotoSansJP-Medium.otf",
"NotoSansJP-Regular.otf",
"NotoSansJP-Thin.otf",
],
)?;
}
#[cfg(feature = "embed_noto_serif_jp")]
{
let font_dir = download_font(
"NotoSerifJP",
"https://github.com/notofonts/noto-cjk/releases/download/Serif2.003/12_NotoSerifJP.zip",
ArchiveType::Zip,
)?;
generate_font_includes(
&out_dir,
"noto_serif_jp",
&font_dir,
&[
"NotoSerifJP-Black.otf",
"NotoSerifJP-Bold.otf",
"NotoSerifJP-ExtraLight.otf",
"NotoSerifJP-Light.otf",
"NotoSerifJP-Medium.otf",
"NotoSerifJP-Regular.otf",
"NotoSerifJP-SemiBold.otf",
],
)?;
}
#[cfg(feature = "embed_recursive")]
{
let font_dir = download_font(
"Recursive",
"https://github.com/arrowtype/recursive/releases/download/v1.085/ArrowType-Recursive-1.085.zip",
ArchiveType::Zip,
)?;
generate_font_includes(&out_dir, "recursive", &font_dir, &["recursive-static-OTFs.otc"])?;
}
Ok(())
}