Skip to content

Commit 9a695ab

Browse files
committed
Add bold font variants for PDF rendering
1 parent 3b13f87 commit 9a695ab

6 files changed

Lines changed: 47 additions & 14 deletions

File tree

657 KB
Binary file not shown.

assets/fonts/zigcourse-cjk.ttf

4.46 KB
Binary file not shown.
90.4 KB
Binary file not shown.

scripts/pdf/build-fonts.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// scripts/pdf/build-fonts.ts
2-
// 生成内嵌 PDF 用的子集字体:assets/fonts/zigcourse-{cjk,sans,mono}.ttf
2+
// 生成内嵌 PDF 用的子集字体:assets/fonts/zigcourse-{cjk,sans,mono,cjk-bold,sans-bold}.ttf
33
//
44
// 纯 Bun/JS:用 subset-font(harfbuzz) 从 Google Fonts 的 glyf 型「可变字体」
55
// 做「子集 + 钉轴」,输出只含课程用到字形的静态 glyf TrueType。
@@ -50,6 +50,21 @@ const FONTS = [
5050
url: `${GF}/jetbrainsmono/JetBrainsMono%5Bwght%5D.ttf`,
5151
axes: { wght: 400 },
5252
},
53+
// 粗体子集(wght:700):用于 **加粗** 富文本,采用真粗体字形而非描边伪粗体,
54+
// 从根本上避免“描边外扩吃掉中文字间距/行距导致字符重叠/挤压”的问题,与 EPUB 端一致。
55+
// 复用同一份可变字体源文件(串行循环里 fetchFont 命中缓存,不会重复下载),只是把 wght 轴钉到 700。
56+
{
57+
name: "cjk-bold",
58+
file: "NotoSerifSC.ttf",
59+
url: `${GF}/notoserifsc/NotoSerifSC%5Bwght%5D.ttf`,
60+
axes: { wght: 700 },
61+
},
62+
{
63+
name: "sans-bold",
64+
file: "Inter.ttf",
65+
url: `${GF}/inter/Inter%5Bopsz,wght%5D.ttf`,
66+
axes: { wght: 700, opsz: 14 },
67+
},
5368
] as const;
5469

5570
// 收集课程会渲染到的所有字符:正文 md + 侧边栏标题 + 渲染器内置中文标题 + 代码片段

scripts/pdf/main.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ async function main(): Promise<void> {
5252
const fontMono = (
5353
await readFile(path.join(ROOT, "assets/fonts/zigcourse-mono.ttf"))
5454
).toString("base64");
55+
const fontCjkBold = (
56+
await readFile(path.join(ROOT, "assets/fonts/zigcourse-cjk-bold.ttf"))
57+
).toString("base64");
58+
const fontSansBold = (
59+
await readFile(path.join(ROOT, "assets/fonts/zigcourse-sans-bold.ttf"))
60+
).toString("base64");
5561

5662
let nodes: FlatNode[] = flattenSidebar(sidebar as DefaultTheme.SidebarItem[]);
5763
// 仅对页面节点应用排除;分组节点保留(其下无页面会被自动跳过)。
@@ -68,6 +74,8 @@ async function main(): Promise<void> {
6874
fontCjk,
6975
fontSans,
7076
fontMono,
77+
fontCjkBold,
78+
fontSansBold,
7179
courseDir: COURSE,
7280
});
7381

scripts/pdf/renderer.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export interface RendererOptions {
6565
fontCjk: string; // base64 — 思源宋体(中文正文)
6666
fontSans: string; // base64 — Inter(正文英文/数字,无衬线比例字体)
6767
fontMono: string; // base64 — JetBrains Mono(代码/行内代码,等宽)
68+
fontCjkBold: string; // base64 — 思源宋体 700(中文加粗)
69+
fontSansBold: string; // base64 — Inter 700(英文/数字加粗)
6870
courseDir: string;
6971
}
7072

@@ -85,7 +87,14 @@ export class PdfRenderer {
8587
/** 单元格默认字色(标题渲染时临时覆盖)。 */
8688
private _cellDefaultColor: [number, number, number] | null = null;
8789

88-
constructor({ fontCjk, fontSans, fontMono, courseDir }: RendererOptions) {
90+
constructor({
91+
fontCjk,
92+
fontSans,
93+
fontMono,
94+
fontCjkBold,
95+
fontSansBold,
96+
courseDir,
97+
}: RendererOptions) {
8998
this.courseDir = courseDir;
9099
this.doc = new jsPDF({ unit: "mm", format: "a4" });
91100
// 三字体(均为 glyf TrueType,jsPDF 可解析):
@@ -98,6 +107,13 @@ export class PdfRenderer {
98107
this.doc.addFont("Sans.ttf", "Sans", "normal");
99108
this.doc.addFileToVFS("Mono.ttf", fontMono);
100109
this.doc.addFont("Mono.ttf", "Mono", "normal");
110+
// 真粗体字型(wght:700):注册为同名字体族的 "bold" 风格,setFont(name, "bold") 即可切换。
111+
// 关键:用真粗体字形后 getTextWidth 返回粗体自身的 advance 宽度,排版按真实宽度推进,
112+
// 从根本上消除描边伪粗体导致的中文字符重叠/行距挤压(与 EPUB 端真粗体方案一致)。
113+
this.doc.addFileToVFS("CJK-Bold.ttf", fontCjkBold);
114+
this.doc.addFont("CJK-Bold.ttf", "CJK", "bold");
115+
this.doc.addFileToVFS("Sans-Bold.ttf", fontSansBold);
116+
this.doc.addFont("Sans-Bold.ttf", "Sans", "bold");
101117
this.doc.setFont("CJK", "normal");
102118

103119
this.y = MARGIN.top;
@@ -323,7 +339,10 @@ export class PdfRenderer {
323339
// CJK 走 CJK 字体;正文英文走无衬线 Sans;行内代码走等宽 Mono
324340
const isCjkPiece = this.isCjk(piece[0] || "");
325341
const fn = isCode ? "Mono" : isCjkPiece ? cjkFont : "Sans";
326-
this.doc.setFont(fn, "normal");
342+
// 加粗用真粗体字型(仅 CJK/Sans 有 bold 变体;Mono 行内代码保持 normal)。
343+
// 用 bold 字体测宽,getTextWidth 返回粗体真实 advance,排版按真实宽度推进。
344+
const style = bold && fn !== "Mono" ? "bold" : "normal";
345+
this.doc.setFont(fn, style);
327346
const w = this.doc.getTextWidth(piece);
328347
if (x + w > startX + maxW && piece !== " ") {
329348
x = startX;
@@ -349,17 +368,8 @@ export class PdfRenderer {
349368
}
350369
this.doc.setTextColor(20, 90, 200);
351370
}
352-
if (!this._dry) {
353-
if (bold) {
354-
// 伪粗体:用填充 + 描边模式加粗笔画(无需额外 bold 字体)
355-
const dc = link ? [20, 90, 200] : [30, 30, 30];
356-
this.doc.setDrawColor(dc[0], dc[1], dc[2]);
357-
this.doc.setLineWidth(0.25);
358-
this.doc.text(piece, x, curY, { renderingMode: "fillThenStroke" });
359-
} else {
360-
this.doc.text(piece, x, curY);
361-
}
362-
}
371+
// 用真粗体字形绘制(style 已按 bold 设好),不再使用描边伪粗体。
372+
if (!this._dry) this.doc.text(piece, x, curY);
363373
if (link && !this._dry) this.doc.setTextColor(30, 30, 30);
364374
x += w;
365375
}

0 commit comments

Comments
 (0)