Skip to main content

nagisa_render/
theme.rs

1//! 主题与渲染配置。`RenderOptions` 是 `render_*` 的入参;`Theme` 是配色 / 字族 / 字号等
2//! 视觉口径,带亮 / 暗预设。所有尺寸是**逻辑值**,layout 前统一乘 `scale` 换设备像素。
3
4use std::collections::HashMap;
5
6use crate::build::ParaBuilder;
7use crate::font::FontHandle;
8use crate::model::{Align, Color, Inline, TextStyle};
9
10/// 四边内边距(逻辑像素)。
11#[derive(Clone, Copy, Debug)]
12pub struct Insets {
13    /// 上。
14    pub top: f32,
15    /// 右。
16    pub right: f32,
17    /// 下。
18    pub bottom: f32,
19    /// 左。
20    pub left: f32,
21}
22
23impl Insets {
24    /// 四边相等。
25    pub const fn all(v: f32) -> Self {
26        Self { top: v, right: v, bottom: v, left: v }
27    }
28    /// `v` = 上下,`h` = 左右。
29    pub const fn symmetric(v: f32, h: f32) -> Self {
30        Self { top: v, right: h, bottom: v, left: h }
31    }
32}
33
34/// 代码块语法上色盘。词类口径见 `highlight` 模块;未覆盖区间用 [`Theme::code_text`]。
35#[derive(Clone, Copy, Debug)]
36pub struct CodePalette {
37    /// 关键字色。
38    pub keyword: Color,
39    /// 数字 / 常量字面量色。
40    pub literal: Color,
41    /// 字符串色。
42    pub string: Color,
43    /// 注释色。
44    pub comment: Color,
45}
46
47/// 视觉主题:配色 + 字族 + 字号体系。预设见 [`Theme::light`] / [`Theme::dark`]。
48#[derive(Clone, Debug)]
49pub struct Theme {
50    /// 画布背景色。
51    pub background: Color,
52    /// 正文文字色。
53    pub text: Color,
54    /// 引用条 / 序号 / 链接等强调色。
55    pub accent: Color,
56    /// 图注 / 次要文字。
57    pub muted: Color,
58    /// 代码块 / 行内代码底色。
59    pub code_bg: Color,
60    /// 代码文字色。
61    pub code_text: Color,
62    /// 代码块语法上色盘(关键字 / 字面量 / 字符串 / 注释)。
63    pub code_palette: CodePalette,
64    /// `==高亮==` 的默认底色。
65    pub highlight: Color,
66    /// 表格 / 网格的边框线色(比 `muted` 更淡)。
67    pub border: Color,
68    /// 无衬线字族名。
69    pub font_sans: String,
70    /// 衬线字族名。
71    pub font_serif: String,
72    /// 等宽字族名。
73    pub font_mono: String,
74    /// 楷体字族名。
75    pub font_kai: String,
76    /// 彩色 emoji 字族名(emoji 表现序列统一切到它,黑体的单色字面不抢跑;
77    /// 不随包内置,默认指系统 / 自备的 Noto Color Emoji,缺则回退)。
78    pub font_emoji: String,
79    /// 基准字号(逻辑像素)。
80    pub base_size: f32,
81    /// 行高倍率。
82    pub line_height: f32,
83    /// h1..h6 相对基准字号的倍率。
84    pub heading_scale: [f32; 6],
85}
86
87impl Theme {
88    /// 亮色预设。
89    pub fn light() -> Self {
90        Self {
91            background: Color::rgb(0xff, 0xff, 0xff),
92            text: Color::rgb(0x1f, 0x23, 0x28),
93            accent: Color::rgb(0x25, 0x63, 0xeb),
94            muted: Color::rgb(0x6e, 0x77, 0x81),
95            code_bg: Color::rgb(0xf3, 0xf4, 0xf6),
96            code_text: Color::rgb(0x1f, 0x23, 0x28),
97            code_palette: CodePalette {
98                keyword: Color::rgb(0xcf, 0x22, 0x2e),
99                literal: Color::rgb(0x05, 0x50, 0xae),
100                string: Color::rgb(0x0a, 0x30, 0x69),
101                comment: Color::rgb(0x6e, 0x77, 0x81),
102            },
103            highlight: Color::rgb(0xff, 0xf1, 0xa8),
104            border: Color::rgb(0xe5, 0xe7, 0xeb),
105            ..Self::common()
106        }
107    }
108
109    /// 暗色预设。
110    pub fn dark() -> Self {
111        Self {
112            background: Color::rgb(0x0d, 0x11, 0x17),
113            text: Color::rgb(0xe6, 0xed, 0xf3),
114            accent: Color::rgb(0x58, 0xa6, 0xff),
115            muted: Color::rgb(0x8b, 0x94, 0x9e),
116            code_bg: Color::rgb(0x16, 0x1b, 0x22),
117            code_text: Color::rgb(0xe6, 0xed, 0xf3),
118            code_palette: CodePalette {
119                keyword: Color::rgb(0xff, 0x7b, 0x72),
120                literal: Color::rgb(0x79, 0xc0, 0xff),
121                string: Color::rgb(0xa5, 0xd6, 0xff),
122                comment: Color::rgb(0x8b, 0x94, 0x9e),
123            },
124            highlight: Color::rgb(0x57, 0x4a, 0x1a),
125            border: Color::rgb(0x30, 0x36, 0x3d),
126            ..Self::common()
127        }
128    }
129
130    /// 亮 / 暗共享的非配色部分(字族 / 字号 / 行高 / 标题阶梯)。黑体 / 等宽随包内置;
131    /// 衬线 / 楷体不内置,字族名是给使用方注入字体对的口径,缺则回退黑体。
132    fn common() -> Self {
133        Self {
134            background: Color::rgb(0, 0, 0),
135            text: Color::rgb(0, 0, 0),
136            accent: Color::rgb(0, 0, 0),
137            muted: Color::rgb(0, 0, 0),
138            code_bg: Color::rgb(0, 0, 0),
139            code_text: Color::rgb(0, 0, 0),
140            code_palette: CodePalette {
141                keyword: Color::rgb(0, 0, 0),
142                literal: Color::rgb(0, 0, 0),
143                string: Color::rgb(0, 0, 0),
144                comment: Color::rgb(0, 0, 0),
145            },
146            highlight: Color::rgb(0, 0, 0),
147            border: Color::rgb(0, 0, 0),
148            font_sans: "Noto Sans SC".to_string(), // 内置
149            font_serif: "Noto Serif SC".to_string(), // 不内置:使用方注入思源宋体即生效,缺则回退黑体
150            font_mono: "JetBrains Mono".to_string(), // 内置(CJK 在等宽语境回退 Noto)
151            font_kai: "LXGW WenKai GB".to_string(),  // 不内置:使用方注入霞鹜文楷即生效,缺则回退黑体
152            font_emoji: "Noto Color Emoji".to_string(), // 不内置:系统或自备,缺则回退
153            base_size: 30.0,
154            line_height: 1.5,
155            heading_scale: [2.0, 1.6, 1.35, 1.15, 1.0, 0.9],
156        }
157    }
158}
159
160impl Default for Theme {
161    fn default() -> Self {
162        Self::light()
163    }
164}
165
166/// 输出图片格式。文字图首选 `Webp`(最小 + 快);`Png` 通用兜底;`PngFast` 要 PNG 又要快。
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum OutputFormat {
169    /// PNG(无损,平衡压缩,默认——通用兼容)。
170    Png,
171    /// PNG(无损,快压缩:约 8 倍快、体积大 ~40%)。必须出 PNG 又要快时用。
172    PngFast,
173    /// WebP(无损;通常体积最小、速度也好)。文字图首选;画布单边 > 16383px(WebP 上限)时编码报错。
174    Webp,
175    /// WebP 优先,画布单边 > 16383px 时自动落 PNG。要 WebP 的体积、又不想为超长图单独处理报错时用
176    /// ——超限会**改格式**,显式选了才发生。
177    WebpOrPng,
178}
179
180/// 渲染入参。链式覆写;`default()` = 960 逻辑宽、亮色、scale 2、PNG、默认字体句柄。
181#[derive(Clone)]
182pub struct RenderOptions {
183    /// 逻辑内容宽(含左右内边距),默认 960。
184    pub width: f32,
185    /// 页边距(逻辑像素)。
186    pub padding: Insets,
187    /// 超采样系数(输出 = 逻辑尺寸 × scale),默认 2.0。越大越清晰也越慢 / 越大。
188    pub scale: f32,
189    /// 视觉主题。
190    pub theme: Theme,
191    /// 字体栈句柄。
192    pub fonts: FontHandle,
193    /// 输出格式,默认 PNG。
194    pub format: OutputFormat,
195    /// 标记文本里 `@名字` 图片 → 字节。
196    pub images: HashMap<String, Vec<u8>>,
197    /// 页眉条(可选):一行小字排在内容上方,与文档无关的固定标识(品牌 / 出处)。
198    pub header: Option<PageChrome>,
199    /// 页脚条(可选):一行小字排在内容下方,常放项目水印(如「abot · github.com/…」)。
200    pub footer: Option<PageChrome>,
201}
202
203/// 页眉 / 页脚条:一行小字(可富文本)+ 可选的与内容之间的细分割线。参与布局高度
204/// ([`measure_document`](crate::measure_document) 自然包含),不归文档内容管——同一品牌
205/// 标识配在 `RenderOptions` 上,所有出图统一带。
206#[derive(Clone, Debug)]
207pub struct PageChrome {
208    /// 行内内容(纯文字经 [`new`](Self::new),富文本经 [`rich`](Self::rich))。
209    pub inlines: Vec<Inline>,
210    /// 行尾内容(可选):与 `inlines` 同一行,右对齐——「左 logo 右署名」的分栏形态。
211    /// 设了它,`align` 只管 `inlines`(通常配左对齐)。
212    pub trailing: Option<Vec<Inline>>,
213    /// 水平对齐(`with_header` 默认左、`with_footer` 默认居中)。
214    pub align: Align,
215    /// 缺省文字色:未显式上色的 span 用它;`None` = 主题次要色(muted)。
216    pub color: Option<Color>,
217    /// 相对基准字号的倍率(默认 0.72)。
218    pub size: f32,
219    /// 与内容之间画一条细线(默认开;设了 `band` 自动不画)。
220    pub rule: bool,
221    /// 满幅色带(可选,仅页脚生效):整条画布宽的底色带贴住画布底,文字坐在带内——
222    /// 分享卡式的「底栏」。设色深时记得给 span 配亮色文字。
223    pub band: Option<Color>,
224}
225
226impl PageChrome {
227    /// 默认形态:次要色小字、带细线、左对齐。
228    pub fn new(text: impl Into<String>) -> Self {
229        Self::from_inlines(vec![Inline::Text { text: text.into(), style: TextStyle::default() }])
230    }
231
232    /// 富文本形态:闭包拼行内(粗细 / 色 / 字号倍率皆可,如品牌名加重、连接词浅色)。
233    /// `p.styled("abot", |s| { s.weight(600); })` 这类未显式上色的 span 仍按缺省色染。
234    pub fn rich<R>(f: impl FnOnce(&mut ParaBuilder) -> R) -> Self {
235        let mut pb = ParaBuilder::new();
236        let _ = f(&mut pb);
237        Self::from_inlines(pb.into_inlines())
238    }
239
240    fn from_inlines(inlines: Vec<Inline>) -> Self {
241        Self {
242            inlines,
243            trailing: None,
244            align: Align::Left,
245            color: None,
246            size: 0.72,
247            rule: true,
248            band: None,
249        }
250    }
251
252    /// 设行尾内容(右对齐,与主内容同一行):「左 logo 右署名」分栏。
253    pub fn trailing<R>(mut self, f: impl FnOnce(&mut ParaBuilder) -> R) -> Self {
254        let mut pb = ParaBuilder::new();
255        let _ = f(&mut pb);
256        self.trailing = Some(pb.into_inlines());
257        self
258    }
259
260    /// 设满幅色带(十六进制;非法忽略)。仅页脚生效,自动不再画细线。
261    pub fn band(mut self, hex: &str) -> Self {
262        if let Some(c) = Color::hex(hex) {
263            self.band = Some(c);
264        }
265        self
266    }
267    /// 设对齐。
268    pub fn align(mut self, a: Align) -> Self {
269        self.align = a;
270        self
271    }
272    /// 设文字色(十六进制;非法忽略)。
273    pub fn color(mut self, hex: &str) -> Self {
274        if let Some(c) = Color::hex(hex) {
275            self.color = Some(c);
276        }
277        self
278    }
279    /// 设字号倍率(非法忽略)。
280    pub fn size(mut self, mult: f32) -> Self {
281        if mult.is_finite() && mult > 0.0 {
282            self.size = mult;
283        }
284        self
285    }
286    /// 不画细线。
287    pub fn no_rule(mut self) -> Self {
288        self.rule = false;
289        self
290    }
291}
292
293impl Default for RenderOptions {
294    fn default() -> Self {
295        Self {
296            width: 960.0,
297            padding: Insets::symmetric(32.0, 40.0),
298            scale: 2.0,
299            theme: Theme::light(),
300            fonts: FontHandle::shared_default(),
301            format: OutputFormat::Png,
302            images: HashMap::new(),
303            header: None,
304            footer: None,
305        }
306    }
307}
308
309impl RenderOptions {
310    /// 设逻辑内容宽。
311    pub fn with_width(mut self, w: f32) -> Self {
312        self.width = w;
313        self
314    }
315    /// 设页边距(逻辑像素)。
316    pub fn with_padding(mut self, p: Insets) -> Self {
317        self.padding = p;
318        self
319    }
320    /// 设主题。
321    pub fn with_theme(mut self, t: Theme) -> Self {
322        self.theme = t;
323        self
324    }
325    /// 设字体句柄。
326    pub fn with_fonts(mut self, f: FontHandle) -> Self {
327        self.fonts = f;
328        self
329    }
330    /// 设超采样系数(清晰度档位,见 `fast`/`sharp`/`ultra` 预设)。
331    pub fn with_scale(mut self, s: f32) -> Self {
332        self.scale = s.clamp(0.25, 8.0);
333        self
334    }
335    /// 清晰度预设:快(scale 1)——最省、体积小,清晰度一般。
336    pub fn fast(self) -> Self {
337        self.with_scale(1.0)
338    }
339    /// 清晰度预设:标准(scale 1.5)。
340    pub fn standard(self) -> Self {
341        self.with_scale(1.5)
342    }
343    /// 清晰度预设:清晰(scale 2,默认)。
344    pub fn sharp(self) -> Self {
345        self.with_scale(2.0)
346    }
347    /// 清晰度预设:超清(scale 3)——最清晰也最慢 / 最大。
348    pub fn ultra(self) -> Self {
349        self.with_scale(3.0)
350    }
351    /// 设页眉(默认形态:左对齐次要色小字 + 细线);要微调用 [`with_header_chrome`](Self::with_header_chrome)。
352    pub fn with_header(self, text: impl Into<String>) -> Self {
353        self.with_header_chrome(PageChrome::new(text))
354    }
355    /// 设页眉(完整形态)。
356    pub fn with_header_chrome(mut self, c: PageChrome) -> Self {
357        self.header = Some(c);
358        self
359    }
360    /// 设页脚(默认形态:居中次要色小字 + 细线,适合项目水印);微调用 [`with_footer_chrome`](Self::with_footer_chrome)。
361    pub fn with_footer(self, text: impl Into<String>) -> Self {
362        self.with_footer_chrome(PageChrome::new(text).align(Align::Center))
363    }
364    /// 设页脚(完整形态)。
365    pub fn with_footer_chrome(mut self, c: PageChrome) -> Self {
366        self.footer = Some(c);
367        self
368    }
369    /// 设输出格式。
370    pub fn with_format(mut self, f: OutputFormat) -> Self {
371        self.format = f;
372        self
373    }
374    /// 输出 PNG(无损,平衡压缩)。
375    pub fn png(self) -> Self {
376        self.with_format(OutputFormat::Png)
377    }
378    /// 输出 PNG(无损,快压缩——更快但更大)。
379    pub fn png_fast(self) -> Self {
380        self.with_format(OutputFormat::PngFast)
381    }
382    /// 输出 WebP(无损,文字图首选)。画布单边 > 16383px 时编码报错。
383    pub fn webp(self) -> Self {
384        self.with_format(OutputFormat::Webp)
385    }
386    /// 输出 WebP,但画布单边 > 16383px(WebP 上限)时自动落 PNG。
387    pub fn webp_or_png(self) -> Self {
388        self.with_format(OutputFormat::WebpOrPng)
389    }
390}