Skip to main content

nagisa_render/
font.rs

1//! 字体栈 —— 把内置兜底 / 自定义目录 / 系统字体合进一个 cosmic-text `FontSystem`,并缓存
2//! `SwashCache`(字形栅格化)。构建一次很贵,故用 `FontHandle`(`Arc` 共享)复用。
3//!
4//! 内置一套兜底:Noto Sans SC + JetBrains Mono 正斜两份(等宽拉丁;CJK 在等宽语境靠回退
5//! 到 Noto)——都是可变字体,含全字重命名实例(粗 / 细体即由此而来)。保证「开箱出中文 +
6//! 真字重」。衬线 / 楷体两个角色**不带字体**(crates.io 包体上限装不下),字族名默认指
7//! Noto Serif SC(思源宋体)与 LXGW WenKai GB(霞鹜文楷),由使用方经 [`FontStackBuilder::data`] /
8//! [`FontStackBuilder::dir`] / 系统字体提供;缺字体时这两个角色回退黑体。
9//! CJK 没有斜体字面(思源系列不发行斜体),`italic` 时由 cosmic-text 仿斜(错切)合成;
10//! 等宽拉丁有真斜体字面,优先命中。
11//!
12//! 内置字体以 zstd 压缩内嵌(`include_bytes!` 的是 `.ttf.zst`),构建字体栈时解压——
13//! `shared_default` 懒加载,解压只在首次渲染前发生一次。[`FontStackBuilder::data`] 同样
14//! 接受 zstd 压缩字节(按魔数识别),使用方可以用同一招内嵌自己的字体。
15
16use std::path::PathBuf;
17use std::sync::{Arc, Mutex, OnceLock};
18
19use cosmic_text::{fontdb, FontSystem, SwashCache};
20
21use crate::error::{Error, Result};
22
23/// 内置兜底字体数据(zstd 压缩)。
24const BUNDLED: &[&[u8]] = &[
25    include_bytes!("../assets/fonts/NotoSansSC.ttf.zst"),
26    include_bytes!("../assets/fonts/JetBrainsMono.ttf.zst"),
27    include_bytes!("../assets/fonts/JetBrainsMono-Italic.ttf.zst"),
28];
29
30/// zstd 帧魔数(RFC 8878),`data()` 靠它识别压缩字节。
31const ZSTD_MAGIC: [u8; 4] = [0x28, 0xb5, 0x2f, 0xfd];
32
33/// 可克隆的共享字体句柄:内部持有加好字体的 `FontSystem` 与一个 `SwashCache`,各用 `Mutex`
34/// 包(cosmic-text 整形 / 取字形位图都要 `&mut`)。克隆只增引用计数。
35#[derive(Clone)]
36pub struct FontHandle(Arc<Inner>);
37
38struct Inner {
39    system: Mutex<FontSystem>,
40    cache: Mutex<SwashCache>,
41}
42
43impl FontHandle {
44    /// 起一个字体栈构建器(默认含内置兜底)。
45    pub fn builder() -> FontStackBuilder {
46        FontStackBuilder::new()
47    }
48
49    /// 全局懒加载默认句柄(内置兜底 + 系统字体),`RenderOptions::default()` 用它。
50    pub fn shared_default() -> FontHandle {
51        static DEFAULT: OnceLock<FontHandle> = OnceLock::new();
52        DEFAULT
53            .get_or_init(|| {
54                FontHandle::builder()
55                    .bundled()
56                    .system()
57                    .build()
58                    .unwrap_or_else(|_| FontHandle::from_db(fontdb::Database::new()))
59            })
60            .clone()
61    }
62
63    fn from_db(db: fontdb::Database) -> FontHandle {
64        let system = FontSystem::new_with_locale_and_db("zh-CN".to_string(), db);
65        FontHandle(Arc::new(Inner {
66            system: Mutex::new(system),
67            cache: Mutex::new(SwashCache::new()),
68        }))
69    }
70
71    /// 借出 `FontSystem`(整形需 `&mut`)。
72    ///
73    /// 锁中毒(某次渲染在持锁时 panic)后仍照常借出内层数据——`FontSystem` 没有跨调用不变量,
74    /// 一次坏输入不应永久毒死整条渲染链(长驻 bot 致命)。
75    pub(crate) fn with_system<R>(&self, f: impl FnOnce(&mut FontSystem) -> R) -> R {
76        let mut sys = self.0.system.lock().unwrap_or_else(|e| e.into_inner());
77        f(&mut sys)
78    }
79
80    /// 借出 `SwashCache` + `FontSystem`(取字形位图需二者)。锁序固定:先 cache 后 system。
81    /// 同样容忍中毒锁(理由见 [`Self::with_system`])。
82    pub(crate) fn with_cache<R>(&self, f: impl FnOnce(&mut SwashCache, &mut FontSystem) -> R) -> R {
83        let mut cache = self.0.cache.lock().unwrap_or_else(|e| e.into_inner());
84        let mut sys = self.0.system.lock().unwrap_or_else(|e| e.into_inner());
85        f(&mut cache, &mut sys)
86    }
87}
88
89/// 字体栈构建器:`builder().bundled().data(BYTES).dir("fonts").system().build()`。
90pub struct FontStackBuilder {
91    bundled: bool,
92    datas: Vec<Vec<u8>>,
93    dirs: Vec<PathBuf>,
94    system: bool,
95}
96
97impl FontStackBuilder {
98    fn new() -> Self {
99        Self { bundled: true, datas: Vec::new(), dirs: Vec::new(), system: false }
100    }
101
102    /// 加入内置兜底字体(默认开)。
103    pub fn bundled(mut self) -> Self {
104        self.bundled = true;
105        self
106    }
107
108    /// 不加入内置兜底字体。
109    pub fn no_bundled(mut self) -> Self {
110        self.bundled = false;
111        self
112    }
113
114    /// 加入一份字体数据(可多次)。接受裸字体字节,也接受 zstd 压缩字节(按魔数识别,
115    /// 构建时解压)——使用方可以像内置字体一样 `include_bytes!` 压缩资产再喂进来。
116    pub fn data(mut self, bytes: impl Into<Vec<u8>>) -> Self {
117        self.datas.push(bytes.into());
118        self
119    }
120
121    /// 加入一个字体目录(可多次)。
122    pub fn dir(mut self, p: impl Into<PathBuf>) -> Self {
123        self.dirs.push(p.into());
124        self
125    }
126
127    /// 加入系统字体。
128    pub fn system(mut self) -> Self {
129        self.system = true;
130        self
131    }
132
133    /// 构建字体句柄。字体栈为空则报 [`Error::FontLoad`]。
134    pub fn build(self) -> Result<FontHandle> {
135        let mut db = fontdb::Database::new();
136        if self.bundled {
137            for z in BUNDLED {
138                db.load_font_data(unzstd(z)?);
139            }
140        }
141        for d in self.datas {
142            if d.starts_with(&ZSTD_MAGIC) {
143                db.load_font_data(unzstd(&d)?);
144            } else {
145                db.load_font_data(d);
146            }
147        }
148        for d in &self.dirs {
149            db.load_fonts_dir(d);
150        }
151        if self.system {
152            db.load_system_fonts();
153        }
154        if db.is_empty() {
155            return Err(Error::FontLoad("字体栈为空(未启用任何字体来源)".into()));
156        }
157        Ok(FontHandle::from_db(db))
158    }
159}
160
161/// 解压一只 zstd 压缩的内置字体。
162fn unzstd(data: &[u8]) -> Result<Vec<u8>> {
163    use std::io::Read;
164    let mut dec = ruzstd::decoding::StreamingDecoder::new(data)
165        .map_err(|e| Error::FontLoad(format!("内置字体解压失败:{e}")))?;
166    let mut out = Vec::new();
167    dec.read_to_end(&mut out).map_err(|e| Error::FontLoad(format!("内置字体解压失败:{e}")))?;
168    Ok(out)
169}
170