Skip to main content

nagisa_render/
font.rs

1//! 字体栈 —— 把内置兜底 / 自定义目录 / 系统字体注册进一个 fontique `Collection`(经 parley
2//! `FontContext` 暴露),配一个 parley `LayoutContext`(整形)与 swash `ScaleContext` + 字形
3//! 位图缓存(栅格化)。构建一次很贵,故用 `FontHandle`(`Arc` 共享)复用。
4//!
5//! 内置一套兜底:Noto Sans SC + JetBrains Mono 正斜两份(等宽拉丁;CJK 在等宽语境靠回退
6//! 到 Noto)——都是可变字体,含全字重命名实例(粗 / 细体即由此而来)。保证「开箱出中文 +
7//! 真字重」。衬线 / 楷体两个角色**不带字体**(crates.io 包体上限装不下),字族名默认指
8//! Noto Serif SC(思源宋体)与 LXGW WenKai GB(霞鹜文楷),由使用方经 [`FontStackBuilder::data`] /
9//! [`FontStackBuilder::dir`] / 系统字体提供;缺字体时这两个角色回退黑体。
10//! CJK 没有斜体字面(思源系列不发行斜体),`italic` 时由 fontique 合成仿斜(错切),
11//! 栅格时按 [`Synthesis`](parley::fontique::Synthesis) 的角度施加;等宽拉丁有真斜体字面,优先命中。
12//!
13//! 内置字体以 zstd 压缩内嵌(`include_bytes!` 的是 `.ttf.zst`),构建字体栈时解压——
14//! `shared_default` 懒加载,解压只在首次渲染前发生一次。[`FontStackBuilder::data`] 同样
15//! 接受 zstd 压缩字节(按魔数识别),使用方可以用同一招内嵌自己的字体。
16
17use std::collections::HashMap;
18use std::path::PathBuf;
19use std::sync::{Arc, Mutex, OnceLock};
20
21use parley::fontique::{Blob, Collection, CollectionOptions};
22use parley::{FontContext, LayoutContext};
23use swash::scale::image::Content;
24use swash::scale::{Render, ScaleContext, Source, StrikeWith};
25use swash::zeno::{Angle, Format, Transform};
26
27use crate::error::{Error, Result};
28use crate::layout::{GlyphFont, Ink};
29
30/// 内置兜底字体数据(zstd 压缩)。
31const BUNDLED: &[&[u8]] = &[
32    include_bytes!("../assets/fonts/NotoSansSC.ttf.zst"),
33    include_bytes!("../assets/fonts/JetBrainsMono.ttf.zst"),
34    include_bytes!("../assets/fonts/JetBrainsMono-Italic.ttf.zst"),
35];
36
37/// zstd 帧魔数(RFC 8878),`data()` 靠它识别压缩字节。
38const ZSTD_MAGIC: [u8; 4] = [0x28, 0xb5, 0x2f, 0xfd];
39
40/// 可克隆的共享字体句柄:内部持有注册好字体的 parley `FontContext`、复用的 `LayoutContext`
41/// 与字形栅格(`ScaleContext` + 位图缓存),各用 `Mutex` 包(整形 / 栅格都要 `&mut`)。
42/// 克隆只增引用计数。
43#[derive(Clone)]
44pub struct FontHandle(Arc<Inner>);
45
46struct Inner {
47    fonts: Mutex<FontContext>,
48    layouts: Mutex<LayoutContext<Ink>>,
49    raster: Mutex<RasterState>,
50}
51
52/// 栅格态:swash 缩放上下文 + 已栅格字形位图缓存(键含字体 / 字号 / 变量轴 / 合成参数)。
53struct RasterState {
54    scale: ScaleContext,
55    cache: HashMap<GlyphKey, Option<GlyphImage>>,
56}
57
58/// 字形位图缓存键。`coords` 是可变字体归一化轴值(字重由此体现),`skew` / `embolden`
59/// 是 fontique 给的合成参数(仿斜 / 假粗),都影响像素,都进键。
60#[derive(Clone, PartialEq, Eq, Hash)]
61struct GlyphKey {
62    blob: u64,
63    index: u32,
64    glyph: u16,
65    size: u32,
66    coords: Vec<i16>,
67    skew: u32,
68    embolden: bool,
69}
70
71/// 栅格化后的字形位图(`left`/`top` 为相对笔位的摆放偏移,swash 口径)。
72pub(crate) struct GlyphImage {
73    pub left: i32,
74    pub top: i32,
75    pub width: u32,
76    pub height: u32,
77    /// true = RGBA 彩色位图(emoji),false = 单通道覆盖率蒙版。
78    pub color: bool,
79    pub data: Vec<u8>,
80}
81
82/// 借给 `paint` 的字形栅格器:按需栅格并缓存。
83pub(crate) struct GlyphRaster<'a>(&'a mut RasterState);
84
85impl GlyphRaster<'_> {
86    /// 取一个字形的位图(无字形 / 栅格失败返回 `None`,结果含失败也缓存)。
87    pub(crate) fn image(&mut self, gf: &GlyphFont, glyph: u16) -> Option<&GlyphImage> {
88        let key = GlyphKey {
89            blob: gf.font.data.id(),
90            index: gf.font.index,
91            glyph,
92            size: gf.size.to_bits(),
93            coords: gf.coords.clone(),
94            skew: gf.skew.map_or(0, f32::to_bits),
95            embolden: gf.embolden,
96        };
97        if !self.0.cache.contains_key(&key) {
98            let img = raster(&mut self.0.scale, gf, glyph);
99            self.0.cache.insert(key.clone(), img);
100        }
101        self.0.cache.get(&key).and_then(|o| o.as_ref())
102    }
103}
104
105/// 栅格化一个字形:彩色轮廓 / 彩色位图(emoji)优先,退普通轮廓;不开 hinting(本引擎
106/// 默认 2× 超采样,hinting 无益,且 swash 0.2 的 hint 实例缓存不按字号失效,开了会在
107/// 共享句柄连续渲染 / 同文档多字号时按「上一次的字号」栅格化——别打开)。
108fn raster(cx: &mut ScaleContext, gf: &GlyphFont, glyph: u16) -> Option<GlyphImage> {
109    let font_ref = swash::FontRef::from_index(gf.font.data.as_ref(), gf.font.index as usize)?;
110    let mut scaler = cx.builder(font_ref).size(gf.size).hint(false).normalized_coords(&gf.coords).build();
111    let mut render = Render::new(&[Source::ColorOutline(0), Source::ColorBitmap(StrikeWith::BestFit), Source::Outline]);
112    render.format(Format::Alpha);
113    if let Some(deg) = gf.skew {
114        render.transform(Some(Transform::skew(Angle::from_degrees(deg), Angle::ZERO)));
115    }
116    if gf.embolden {
117        render.embolden(gf.size * 0.02);
118    }
119    let img = render.render(&mut scaler, glyph)?;
120    let (width, height) = (img.placement.width, img.placement.height);
121    let (color, data) = match img.content {
122        Content::Color => (true, img.data),
123        Content::Mask => (false, img.data),
124        // Format::Alpha 不会产出子像素蒙版;防御性平均成普通蒙版。
125        Content::SubpixelMask => {
126            (false, img.data.chunks_exact(3).map(|c| ((c[0] as u16 + c[1] as u16 + c[2] as u16) / 3) as u8).collect())
127        }
128    };
129    Some(GlyphImage { left: img.placement.left, top: img.placement.top, width, height, color, data })
130}
131
132impl FontHandle {
133    /// 起一个字体栈构建器(默认含内置兜底)。
134    pub fn builder() -> FontStackBuilder {
135        FontStackBuilder::new()
136    }
137
138    /// 全局懒加载默认句柄(内置兜底 + 系统字体),`RenderOptions::default()` 用它。
139    pub fn shared_default() -> FontHandle {
140        static DEFAULT: OnceLock<FontHandle> = OnceLock::new();
141        DEFAULT
142            .get_or_init(|| {
143                FontHandle::builder().bundled().system().build().unwrap_or_else(|_| {
144                    FontHandle::from_collection(Collection::new(CollectionOptions {
145                        shared: false,
146                        system_fonts: true,
147                    }))
148                })
149            })
150            .clone()
151    }
152
153    fn from_collection(collection: Collection) -> FontHandle {
154        let fonts = FontContext { collection, source_cache: Default::default() };
155        FontHandle(Arc::new(Inner {
156            fonts: Mutex::new(fonts),
157            layouts: Mutex::new(LayoutContext::new()),
158            raster: Mutex::new(RasterState { scale: ScaleContext::new(), cache: HashMap::new() }),
159        }))
160    }
161
162    /// 借出整形所需的一对上下文(parley 的 `ranged_builder` 同时要二者)。锁序固定:
163    /// 先 fonts 后 layouts。
164    ///
165    /// 锁中毒(某次渲染在持锁时 panic)后仍照常借出内层数据——上下文没有跨调用不变量,
166    /// 一次坏输入不应永久毒死整条渲染链(长驻 bot 致命)。
167    pub(crate) fn with_layout<R>(&self, f: impl FnOnce(&mut FontContext, &mut LayoutContext<Ink>) -> R) -> R {
168        let mut fonts = self.0.fonts.lock().unwrap_or_else(|e| e.into_inner());
169        let mut layouts = self.0.layouts.lock().unwrap_or_else(|e| e.into_inner());
170        f(&mut fonts, &mut layouts)
171    }
172
173    /// 借出字形栅格器(取字形位图)。同样容忍中毒锁(理由见 [`Self::with_layout`])。
174    pub(crate) fn with_raster<R>(&self, f: impl FnOnce(&mut GlyphRaster) -> R) -> R {
175        let mut raster = self.0.raster.lock().unwrap_or_else(|e| e.into_inner());
176        f(&mut GlyphRaster(&mut raster))
177    }
178}
179
180/// 字体栈构建器:`builder().bundled().data(BYTES).dir("fonts").system().build()`。
181pub struct FontStackBuilder {
182    bundled: bool,
183    datas: Vec<Vec<u8>>,
184    dirs: Vec<PathBuf>,
185    system: bool,
186}
187
188impl FontStackBuilder {
189    fn new() -> Self {
190        Self { bundled: true, datas: Vec::new(), dirs: Vec::new(), system: false }
191    }
192
193    /// 加入内置兜底字体(默认开)。
194    pub fn bundled(mut self) -> Self {
195        self.bundled = true;
196        self
197    }
198
199    /// 不加入内置兜底字体。
200    pub fn no_bundled(mut self) -> Self {
201        self.bundled = false;
202        self
203    }
204
205    /// 加入一份字体数据(可多次)。接受裸字体字节,也接受 zstd 压缩字节(按魔数识别,
206    /// 构建时解压)——使用方可以像内置字体一样 `include_bytes!` 压缩资产再喂进来。
207    pub fn data(mut self, bytes: impl Into<Vec<u8>>) -> Self {
208        self.datas.push(bytes.into());
209        self
210    }
211
212    /// 加入一个字体目录(可多次)。
213    pub fn dir(mut self, p: impl Into<PathBuf>) -> Self {
214        self.dirs.push(p.into());
215        self
216    }
217
218    /// 加入系统字体。
219    pub fn system(mut self) -> Self {
220        self.system = true;
221        self
222    }
223
224    /// 构建字体句柄。字体栈为空则报 [`Error::FontLoad`]。
225    pub fn build(self) -> Result<FontHandle> {
226        let mut collection = Collection::new(CollectionOptions { shared: false, system_fonts: self.system });
227        let mut registered = false;
228        let mut register = |collection: &mut Collection, raw: Vec<u8>| {
229            registered |= !collection.register_fonts(Blob::from(raw), None).is_empty();
230        };
231        if self.bundled {
232            for z in BUNDLED {
233                register(&mut collection, unzstd(z)?);
234            }
235        }
236        for d in self.datas {
237            if d.starts_with(&ZSTD_MAGIC) {
238                register(&mut collection, unzstd(&d)?);
239            } else {
240                register(&mut collection, d);
241            }
242        }
243        // 目录递归遍历(与 fontdb 的 load_fonts_dir 口径一致);坏文件 / 不可读项静默跳过。
244        let mut stack: Vec<PathBuf> = self.dirs.clone();
245        while let Some(dir) = stack.pop() {
246            let Ok(entries) = std::fs::read_dir(&dir) else { continue };
247            for e in entries.flatten() {
248                let p = e.path();
249                if p.is_dir() {
250                    stack.push(p);
251                    continue;
252                }
253                let is_font = p
254                    .extension()
255                    .and_then(|x| x.to_str())
256                    .is_some_and(|x| matches!(x.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc"));
257                if is_font {
258                    if let Ok(raw) = std::fs::read(&p) {
259                        register(&mut collection, raw);
260                    }
261                }
262            }
263        }
264        if !registered && !self.system {
265            return Err(Error::FontLoad("字体栈为空(未启用任何字体来源)".into()));
266        }
267        Ok(FontHandle::from_collection(collection))
268    }
269}
270
271/// 解压一只 zstd 压缩的内置字体。
272fn unzstd(data: &[u8]) -> Result<Vec<u8>> {
273    use std::io::Read;
274    let mut dec =
275        ruzstd::decoding::StreamingDecoder::new(data).map_err(|e| Error::FontLoad(format!("内置字体解压失败:{e}")))?;
276    let mut out = Vec::new();
277    dec.read_to_end(&mut out).map_err(|e| Error::FontLoad(format!("内置字体解压失败:{e}")))?;
278    Ok(out)
279}