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 =
111        cx.builder(font_ref).size(gf.size).hint(false).normalized_coords(&gf.coords).build();
112    let mut render =
113        Render::new(&[Source::ColorOutline(0), Source::ColorBitmap(StrikeWith::BestFit), Source::Outline]);
114    render.format(Format::Alpha);
115    if let Some(deg) = gf.skew {
116        render.transform(Some(Transform::skew(Angle::from_degrees(deg), Angle::ZERO)));
117    }
118    if gf.embolden {
119        render.embolden(gf.size * 0.02);
120    }
121    let img = render.render(&mut scaler, glyph)?;
122    let (width, height) = (img.placement.width, img.placement.height);
123    let (color, data) = match img.content {
124        Content::Color => (true, img.data),
125        Content::Mask => (false, img.data),
126        // Format::Alpha 不会产出子像素蒙版;防御性平均成普通蒙版。
127        Content::SubpixelMask => {
128            (false, img.data.chunks_exact(3).map(|c| ((c[0] as u16 + c[1] as u16 + c[2] as u16) / 3) as u8).collect())
129        }
130    };
131    Some(GlyphImage { left: img.placement.left, top: img.placement.top, width, height, color, data })
132}
133
134impl FontHandle {
135    /// 起一个字体栈构建器(默认含内置兜底)。
136    pub fn builder() -> FontStackBuilder {
137        FontStackBuilder::new()
138    }
139
140    /// 全局懒加载默认句柄(内置兜底 + 系统字体),`RenderOptions::default()` 用它。
141    pub fn shared_default() -> FontHandle {
142        static DEFAULT: OnceLock<FontHandle> = OnceLock::new();
143        DEFAULT
144            .get_or_init(|| {
145                FontHandle::builder().bundled().system().build().unwrap_or_else(|_| {
146                    FontHandle::from_collection(Collection::new(CollectionOptions {
147                        shared: false,
148                        system_fonts: true,
149                    }))
150                })
151            })
152            .clone()
153    }
154
155    fn from_collection(collection: Collection) -> FontHandle {
156        let fonts = FontContext { collection, source_cache: Default::default() };
157        FontHandle(Arc::new(Inner {
158            fonts: Mutex::new(fonts),
159            layouts: Mutex::new(LayoutContext::new()),
160            raster: Mutex::new(RasterState { scale: ScaleContext::new(), cache: HashMap::new() }),
161        }))
162    }
163
164    /// 借出整形所需的一对上下文(parley 的 `ranged_builder` 同时要二者)。锁序固定:
165    /// 先 fonts 后 layouts。
166    ///
167    /// 锁中毒(某次渲染在持锁时 panic)后仍照常借出内层数据——上下文没有跨调用不变量,
168    /// 一次坏输入不应永久毒死整条渲染链(长驻 bot 致命)。
169    pub(crate) fn with_layout<R>(
170        &self,
171        f: impl FnOnce(&mut FontContext, &mut LayoutContext<Ink>) -> R,
172    ) -> R {
173        let mut fonts = self.0.fonts.lock().unwrap_or_else(|e| e.into_inner());
174        let mut layouts = self.0.layouts.lock().unwrap_or_else(|e| e.into_inner());
175        f(&mut fonts, &mut layouts)
176    }
177
178    /// 借出字形栅格器(取字形位图)。同样容忍中毒锁(理由见 [`Self::with_layout`])。
179    pub(crate) fn with_raster<R>(&self, f: impl FnOnce(&mut GlyphRaster) -> R) -> R {
180        let mut raster = self.0.raster.lock().unwrap_or_else(|e| e.into_inner());
181        f(&mut GlyphRaster(&mut raster))
182    }
183}
184
185/// 字体栈构建器:`builder().bundled().data(BYTES).dir("fonts").system().build()`。
186pub struct FontStackBuilder {
187    bundled: bool,
188    datas: Vec<Vec<u8>>,
189    dirs: Vec<PathBuf>,
190    system: bool,
191}
192
193impl FontStackBuilder {
194    fn new() -> Self {
195        Self { bundled: true, datas: Vec::new(), dirs: Vec::new(), system: false }
196    }
197
198    /// 加入内置兜底字体(默认开)。
199    pub fn bundled(mut self) -> Self {
200        self.bundled = true;
201        self
202    }
203
204    /// 不加入内置兜底字体。
205    pub fn no_bundled(mut self) -> Self {
206        self.bundled = false;
207        self
208    }
209
210    /// 加入一份字体数据(可多次)。接受裸字体字节,也接受 zstd 压缩字节(按魔数识别,
211    /// 构建时解压)——使用方可以像内置字体一样 `include_bytes!` 压缩资产再喂进来。
212    pub fn data(mut self, bytes: impl Into<Vec<u8>>) -> Self {
213        self.datas.push(bytes.into());
214        self
215    }
216
217    /// 加入一个字体目录(可多次)。
218    pub fn dir(mut self, p: impl Into<PathBuf>) -> Self {
219        self.dirs.push(p.into());
220        self
221    }
222
223    /// 加入系统字体。
224    pub fn system(mut self) -> Self {
225        self.system = true;
226        self
227    }
228
229    /// 构建字体句柄。字体栈为空则报 [`Error::FontLoad`]。
230    pub fn build(self) -> Result<FontHandle> {
231        let mut collection =
232            Collection::new(CollectionOptions { shared: false, system_fonts: self.system });
233        let mut registered = false;
234        let mut register = |collection: &mut Collection, raw: Vec<u8>| {
235            registered |= !collection.register_fonts(Blob::from(raw), None).is_empty();
236        };
237        if self.bundled {
238            for z in BUNDLED {
239                register(&mut collection, unzstd(z)?);
240            }
241        }
242        for d in self.datas {
243            if d.starts_with(&ZSTD_MAGIC) {
244                register(&mut collection, unzstd(&d)?);
245            } else {
246                register(&mut collection, d);
247            }
248        }
249        // 目录递归遍历(与 fontdb 的 load_fonts_dir 口径一致);坏文件 / 不可读项静默跳过。
250        let mut stack: Vec<PathBuf> = self.dirs.clone();
251        while let Some(dir) = stack.pop() {
252            let Ok(entries) = std::fs::read_dir(&dir) else { continue };
253            for e in entries.flatten() {
254                let p = e.path();
255                if p.is_dir() {
256                    stack.push(p);
257                    continue;
258                }
259                let is_font = p
260                    .extension()
261                    .and_then(|x| x.to_str())
262                    .is_some_and(|x| matches!(x.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc"));
263                if is_font {
264                    if let Ok(raw) = std::fs::read(&p) {
265                        register(&mut collection, raw);
266                    }
267                }
268            }
269        }
270        if !registered && !self.system {
271            return Err(Error::FontLoad("字体栈为空(未启用任何字体来源)".into()));
272        }
273        Ok(FontHandle::from_collection(collection))
274    }
275}
276
277/// 解压一只 zstd 压缩的内置字体。
278fn unzstd(data: &[u8]) -> Result<Vec<u8>> {
279    use std::io::Read;
280    let mut dec = ruzstd::decoding::StreamingDecoder::new(data)
281        .map_err(|e| Error::FontLoad(format!("内置字体解压失败:{e}")))?;
282    let mut out = Vec::new();
283    dec.read_to_end(&mut out).map_err(|e| Error::FontLoad(format!("内置字体解压失败:{e}")))?;
284    Ok(out)
285}