diff --git a/README.md b/README.md
index 4a12fe5c..04b9a2fb 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,11 @@
>
> Zig 是一种通用的编程语言和工具链,用于维护健壮、最优和可重用的软件
-
+
+
+
-**Zig 语言圣经** 是一份开源的 Zig 语言综合教程,旨在为中文 Zig 爱好者提供一份高质量的学习资源,内容涵盖从基础语法到高级特性的方方面面。
+**Zig 语言圣经**(The Zig Programming Bible)是一份开源的 Zig 语言综合中文教程,内容涵盖从基础语法到 `comptime`、异步、内存管理等高级特性,旨在为中文 Zig 爱好者提供一份高质量、系统化的学习资源。除在线阅读外,还提供 PDF 与 EPUB 电子书,方便离线学习。
## 📖 在线阅读
diff --git a/assets/fonts/zigcourse-cjk-bold.ttf b/assets/fonts/zigcourse-cjk-bold.ttf
new file mode 100644
index 00000000..d2698b65
Binary files /dev/null and b/assets/fonts/zigcourse-cjk-bold.ttf differ
diff --git a/assets/fonts/zigcourse-cjk.ttf b/assets/fonts/zigcourse-cjk.ttf
index 575c6e79..e5dc6eac 100644
Binary files a/assets/fonts/zigcourse-cjk.ttf and b/assets/fonts/zigcourse-cjk.ttf differ
diff --git a/assets/fonts/zigcourse-sans-bold.ttf b/assets/fonts/zigcourse-sans-bold.ttf
new file mode 100644
index 00000000..dfa6c15b
Binary files /dev/null and b/assets/fonts/zigcourse-sans-bold.ttf differ
diff --git a/scripts/pdf/build-fonts.ts b/scripts/pdf/build-fonts.ts
index 3df9c629..b1a8e100 100644
--- a/scripts/pdf/build-fonts.ts
+++ b/scripts/pdf/build-fonts.ts
@@ -1,5 +1,5 @@
// scripts/pdf/build-fonts.ts
-// 生成内嵌 PDF 用的子集字体:assets/fonts/zigcourse-{cjk,sans,mono}.ttf
+// 生成内嵌 PDF 用的子集字体:assets/fonts/zigcourse-{cjk,sans,mono,cjk-bold,sans-bold}.ttf
//
// 纯 Bun/JS:用 subset-font(harfbuzz) 从 Google Fonts 的 glyf 型「可变字体」
// 做「子集 + 钉轴」,输出只含课程用到字形的静态 glyf TrueType。
@@ -50,6 +50,21 @@ const FONTS = [
url: `${GF}/jetbrainsmono/JetBrainsMono%5Bwght%5D.ttf`,
axes: { wght: 400 },
},
+ // 粗体子集(wght:700):用于 **加粗** 富文本,采用真粗体字形而非描边伪粗体,
+ // 从根本上避免“描边外扩吃掉中文字间距/行距导致字符重叠/挤压”的问题,与 EPUB 端一致。
+ // 复用同一份可变字体源文件(串行循环里 fetchFont 命中缓存,不会重复下载),只是把 wght 轴钉到 700。
+ {
+ name: "cjk-bold",
+ file: "NotoSerifSC.ttf",
+ url: `${GF}/notoserifsc/NotoSerifSC%5Bwght%5D.ttf`,
+ axes: { wght: 700 },
+ },
+ {
+ name: "sans-bold",
+ file: "Inter.ttf",
+ url: `${GF}/inter/Inter%5Bopsz,wght%5D.ttf`,
+ axes: { wght: 700, opsz: 14 },
+ },
] as const;
// 收集课程会渲染到的所有字符:正文 md + 侧边栏标题 + 渲染器内置中文标题 + 代码片段
diff --git a/scripts/pdf/main.ts b/scripts/pdf/main.ts
index 96ed4533..774f3166 100644
--- a/scripts/pdf/main.ts
+++ b/scripts/pdf/main.ts
@@ -43,15 +43,19 @@ async function main(): Promise {
await mkdir(OUT_DIR, { recursive: true });
await initHighlighter();
- const fontCjk = (
- await readFile(path.join(ROOT, "assets/fonts/zigcourse-cjk.ttf"))
- ).toString("base64");
- const fontSans = (
- await readFile(path.join(ROOT, "assets/fonts/zigcourse-sans.ttf"))
- ).toString("base64");
- const fontMono = (
- await readFile(path.join(ROOT, "assets/fonts/zigcourse-mono.ttf"))
- ).toString("base64");
+ // 五份字体相互独立,并行读取并转 base64(顺手去掉 5 段重复的 readFile 样板)。
+ const readFontB64 = (name: string): Promise =>
+ readFile(path.join(ROOT, `assets/fonts/${name}.ttf`)).then((b) =>
+ b.toString("base64"),
+ );
+ const [fontCjk, fontSans, fontMono, fontCjkBold, fontSansBold] =
+ await Promise.all([
+ readFontB64("zigcourse-cjk"),
+ readFontB64("zigcourse-sans"),
+ readFontB64("zigcourse-mono"),
+ readFontB64("zigcourse-cjk-bold"),
+ readFontB64("zigcourse-sans-bold"),
+ ]);
let nodes: FlatNode[] = flattenSidebar(sidebar as DefaultTheme.SidebarItem[]);
// 仅对页面节点应用排除;分组节点保留(其下无页面会被自动跳过)。
@@ -68,6 +72,8 @@ async function main(): Promise {
fontCjk,
fontSans,
fontMono,
+ fontCjkBold,
+ fontSansBold,
courseDir: COURSE,
});
diff --git a/scripts/pdf/renderer.ts b/scripts/pdf/renderer.ts
index 7fafef24..5bb47138 100644
--- a/scripts/pdf/renderer.ts
+++ b/scripts/pdf/renderer.ts
@@ -65,6 +65,8 @@ export interface RendererOptions {
fontCjk: string; // base64 — 思源宋体(中文正文)
fontSans: string; // base64 — Inter(正文英文/数字,无衬线比例字体)
fontMono: string; // base64 — JetBrains Mono(代码/行内代码,等宽)
+ fontCjkBold: string; // base64 — 思源宋体 700(中文加粗)
+ fontSansBold: string; // base64 — Inter 700(英文/数字加粗)
courseDir: string;
}
@@ -85,7 +87,14 @@ export class PdfRenderer {
/** 单元格默认字色(标题渲染时临时覆盖)。 */
private _cellDefaultColor: [number, number, number] | null = null;
- constructor({ fontCjk, fontSans, fontMono, courseDir }: RendererOptions) {
+ constructor({
+ fontCjk,
+ fontSans,
+ fontMono,
+ fontCjkBold,
+ fontSansBold,
+ courseDir,
+ }: RendererOptions) {
this.courseDir = courseDir;
this.doc = new jsPDF({ unit: "mm", format: "a4" });
// 三字体(均为 glyf TrueType,jsPDF 可解析):
@@ -98,6 +107,13 @@ export class PdfRenderer {
this.doc.addFont("Sans.ttf", "Sans", "normal");
this.doc.addFileToVFS("Mono.ttf", fontMono);
this.doc.addFont("Mono.ttf", "Mono", "normal");
+ // 真粗体字型(wght:700):注册为同名字体族的 "bold" 风格,setFont(name, "bold") 即可切换。
+ // 关键:用真粗体字形后 getTextWidth 返回粗体自身的 advance 宽度,排版按真实宽度推进,
+ // 从根本上消除描边伪粗体导致的中文字符重叠/行距挤压(与 EPUB 端真粗体方案一致)。
+ this.doc.addFileToVFS("CJK-Bold.ttf", fontCjkBold);
+ this.doc.addFont("CJK-Bold.ttf", "CJK", "bold");
+ this.doc.addFileToVFS("Sans-Bold.ttf", fontSansBold);
+ this.doc.addFont("Sans-Bold.ttf", "Sans", "bold");
this.doc.setFont("CJK", "normal");
this.y = MARGIN.top;
@@ -323,7 +339,10 @@ export class PdfRenderer {
// CJK 走 CJK 字体;正文英文走无衬线 Sans;行内代码走等宽 Mono
const isCjkPiece = this.isCjk(piece[0] || "");
const fn = isCode ? "Mono" : isCjkPiece ? cjkFont : "Sans";
- this.doc.setFont(fn, "normal");
+ // 加粗用真粗体字型(仅 CJK/Sans 有 bold 变体;Mono 行内代码保持 normal)。
+ // 用 bold 字体测宽,getTextWidth 返回粗体真实 advance,排版按真实宽度推进。
+ const style = bold && fn !== "Mono" ? "bold" : "normal";
+ this.doc.setFont(fn, style);
const w = this.doc.getTextWidth(piece);
if (x + w > startX + maxW && piece !== " ") {
x = startX;
@@ -349,17 +368,8 @@ export class PdfRenderer {
}
this.doc.setTextColor(20, 90, 200);
}
- if (!this._dry) {
- if (bold) {
- // 伪粗体:用填充 + 描边模式加粗笔画(无需额外 bold 字体)
- const dc = link ? [20, 90, 200] : [30, 30, 30];
- this.doc.setDrawColor(dc[0], dc[1], dc[2]);
- this.doc.setLineWidth(0.25);
- this.doc.text(piece, x, curY, { renderingMode: "fillThenStroke" });
- } else {
- this.doc.text(piece, x, curY);
- }
- }
+ // 用真粗体字形绘制(style 已按 bold 设好),不再使用描边伪粗体。
+ if (!this._dry) this.doc.text(piece, x, curY);
if (link && !this._dry) this.doc.setTextColor(30, 30, 30);
x += w;
}