diff --git a/README.md b/README.md index 4a12fe5c..04b9a2fb 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,11 @@ > > Zig 是一种通用的编程语言和工具链,用于维护健壮、最优和可重用的软件 -![Cover Image](./course/public/cover_image.png "Cover Image") +

+ 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; }