Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
>
> Zig 是一种通用的编程语言和工具链,用于维护健壮、最优和可重用的软件

![Cover Image](./course/public/cover_image.png "Cover Image")
<p align="center">
<img src="./course/.vitepress/epub/cover.png" alt="Zig 语言圣经" width="320" />
</p>

**Zig 语言圣经** 是一份开源的 Zig 语言综合教程,旨在为中文 Zig 爱好者提供一份高质量的学习资源,内容涵盖从基础语法到高级特性的方方面面
**Zig 语言圣经**(The Zig Programming Bible)是一份开源的 Zig 语言综合中文教程,内容涵盖从基础语法到 `comptime`、异步、内存管理等高级特性,旨在为中文 Zig 爱好者提供一份高质量、系统化的学习资源。除在线阅读外,还提供 PDF 与 EPUB 电子书,方便离线学习

## 📖 在线阅读

Expand Down
Binary file added assets/fonts/zigcourse-cjk-bold.ttf
Binary file not shown.
Binary file modified assets/fonts/zigcourse-cjk.ttf
Binary file not shown.
Binary file added assets/fonts/zigcourse-sans-bold.ttf
Binary file not shown.
17 changes: 16 additions & 1 deletion scripts/pdf/build-fonts.ts
Original file line number Diff line number Diff line change
@@ -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。
Expand Down Expand Up @@ -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 + 侧边栏标题 + 渲染器内置中文标题 + 代码片段
Expand Down
24 changes: 15 additions & 9 deletions scripts/pdf/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,19 @@ async function main(): Promise<void> {
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<string> =>
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[]);
// 仅对页面节点应用排除;分组节点保留(其下无页面会被自动跳过)。
Expand All @@ -68,6 +72,8 @@ async function main(): Promise<void> {
fontCjk,
fontSans,
fontMono,
fontCjkBold,
fontSansBold,
courseDir: COURSE,
});

Expand Down
36 changes: 23 additions & 13 deletions scripts/pdf/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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 可解析):
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
Loading