试卷字体统一为思源宋体 #20

用户要求:「试卷的字体,需要改一下,全部都用思源宋体就可以了。」

pymupdf 提取两份成品 PDF 定位到四类需要统一的字体:

  试卷题型示例(6 页)            参考答案与评分标准示例(3 页)
  OUCSerif-Regular 203 中文        OUCSerif-Regular 333 中文
  TeXGyreTermes-Regular 133 西文   TeXGyreTermes-Regular 197 西文
  TeXGyreTermes-Bold 85 题号分值   OUCSans-Regular 37 黑体小标题
  OUCSerif-Bold 81 中文加粗         CMR10/CMMI10/CMSY10 49 数学公式
  OUCKai-Regular 25 卷头与成绩表    TeXGyreTermes-Bold 21 数字

一、西文与数字:Termes → 思源宋体(oucexam.sty / ouckey.sty)

先在包里用 \newfontfamily 登记 \OUCLatinSerif / \OUCLatinSerifBold
(四个档位全给,斜体指回 Regular —— 思源宋体没有真斜体),再把
\tnr / \tnrb / \sunfont 都 \let 过去。

⚠️ 只做这一步**不够**,这点踩了一轮:模板里大量文字根本没走 \tnr,
而是走 \setmainfont(默认西文族)——\textbf{learning outcome} 的裸西文、
页脚的页码与总页数、任何 \selectfont 之后的裸 ASCII。
第一轮只 \let 时,PDF 里仍残留 22 个 TeXGyreTermes span:
页脚 6 个页码 + 6 个总页数 + 第 4 页的 A./B./C./D. 与 learning outcome 等。
⇒ 必须连 \setmainfont / \setsansfont 一起指到 OUCSerif。

二、卷头与成绩表题号:霞鹜文楷 → 思源宋体(oucexam.sty)

\kh 与 \scoretable 用 \kaiti。包里 \let\kaiti\songti 一次性收口,
不动 ../fonts.tex 的 \def\kaiti —— 别的模板还要用楷体。

三、答案卷黑体小标题:思源黑体 → 思源宋体(ouckey.sty)

\heiti 共 37 处(卷头大标题与各节小标题)。同样 \let\heiti\songti。

四、数学公式:保持不动

$x$ / $\dfrac{1}{45}\sum f_i x_i$ 等 49 处仍走 Computer Modern。
思源宋体没有 MATH 表(实测只有 GSUB/GPOS),不能作数学字体;
强行塞进正文字体会破坏斜体变量、正体数字、运算符间距。用户已确认。

五、装订线虚线:连字符排字 → 用线画(oucexam.sty)

原卷那 6 段竖虚线是 18bp Times 连字符排出来的。换成思源宋体后连字符
每个宽 0.346 em(Termes 0.333),每串会长 4~7bp,破坏"复刻原卷长度"。
⇒ 改为几何画的短横线段,长度锁死。实测原卷(720dpi 逐笔画测量):
  单个连字符笔画 长 4.40bp、粗 1.10bp、x 中心 51.60bp
  相邻笔画步距 6.00bp
新增 \ouc@dashseg{起点y}{笔画数},六段坐标与笔画数逐段照抄原卷
(含原卷被白底遮断成 13+3 两截的那一段)。
⚠️ \newcommand 必须定义在 \ouc@dobindstrip 外面,否则每次 shipout
重新定义,第二页起报 already defined。
⚠️ y 坐标不能写 \dimexpr#1bp+\i*6bp —— TikZ 坐标解析器不认小数 +
\dimexpr 组合,报 Illegal unit of measure。要先 \pgfmathsetmacro。

六、直引号归入 CJK(ouckey.sty)

答案卷正文用 ASCII 直引号 "" 表示中文引号(32 处)。U+0022 在
Basic Latin,xeCJK 判为西文 ⇒ 引号走 \setmainfont,与两侧汉字不同字体,
并挤压出 Overfull(第 88 行 +8.07pt)。
\xeCJKDeclareCharClass{CJK}{"0022} 归位后解决。
⚠️ 不在 fonts.tex 里改:那里全库共用,会让正文真正的英文引号(代码
   片段 "hello world")也变全角。

七、Overfull 的修法:改源码断行,不调参数(ouckey.sty 留有完整记录)

换字体后 \keyitem{3}{4} 那段 Overfull 2.61pt,实际压出版心
(最右 527.01bp,版心止于 524.41)。试过并确认**全部无效**的五种:
  · \emergencystretch 1.5em —— 本行没有弹性胶可拉,无从施展
  · \tolerance 3000 —— 不是不满意断点,是没有更早的合法断点
  · PunctStyle=kaiming —— 能消 Overfull,但标点悬挂探出版心 6.5bp
  · PunctStyle=quanjiao —— 同样探出 8bp
    (xeCJK 这几个 PunctStyle 做的是"标点悬挂"不是"标点压缩",
      指望它消 Overfull 必翻车)
  · \rightskip 0pt plus 6bp(含 \AtBeginDocument 版)—— 6bp 远小于
    需要的 150bp,TeX 仍选"撑破"这个解
正解:第 124 行句号「。」后加 \\。分项给分的答案本来就该一句一行,
读者靠句号扫分点 —— 既解决排版,又让内容结构更清楚。

验证(两套环境各跑,逐项同数)

  WSL / TeX Live 2025    试卷 rc=0 err=0 over=0 6页
                         答案卷 rc=0 err=0 over=0 3页
  Windows / TeX Live 2022 试卷 rc=0 err=0 over=0 6页
                         答案卷 rc=0 err=0 over=0 3页

pymupdf 提取字体分布:

  试卷题型示例      OUCSerif-Regular 291 + OUCSerif-Bold 70
                    → 非宋体文字字体:无
  参考答案示例      OUCSerif-Regular 235 + OUCSerif-Bold 53
                    + CMR10/CMMI10/CMSY10/CMMI7/CMEX10(数学,保留)
                    → 非宋体文字字体:无

几何核对(逐 span 比对,均与基线一致或更优):
  · 成绩表 17 栏逐字符坐标与基线**完全相同**(硬编码坐标,不受字体影响)
  · 装订线 96 笔全部绘出:x 49.40~53.60(宽 4.40bp)、步距 6.00bp,
    六段位置与原卷实测一致
  · 三页最右墨迹均为 524.41bp,与版心严丝合缝,无越界
  · 试卷 6 页最右最大 523.33bp(第 4 页 \mtpair 的 44% 栏宽设计,
    基线同位置同样数值,非本次引入)

涉及文件:
  试卷/oucexam.sty                 默认族、西文字族、\kaiti、装订线虚线
  试卷/ouckey.sty                  默认族、西文字族、\heiti、直引号、踩坑记录
  试卷/参考答案与评分标准示例.tex  第 124 行句号后加断行
This commit is contained in:
2026-10-29 21:29:15 +08:00
parent 5d59aa1853
commit 93b03c8a78
3 changed files with 304 additions and 35 deletions
+132 -3
View File
@@ -59,20 +59,143 @@
\pagestyle{empty}
%----------------------------------------------------------------------
% 字体(与 oucexam.sty 保持一致,全部取自公共字体包 ../fonts.tex)
% 字体(#20:与 oucexam.sty 同步,全部统一为思源宋体)
% 全部取自公共字体包 ../fonts.tex,再在包内重定向。
%
% ⚠️ 必须和 oucexam.sty 做**完全相同的处理**,否则两份文件观感不一致:
% oucexam 是试卷、ouckey 是答案卷,学校通常一起印发。
%
% ⚠️⚠️ 只重定向 \tnr / \tnrb 不够 —— 模板里大量文字没走它们:
% · \textbf{...} 的裸西文 → 走 \setmainfont
% · 「(4 分)」里的数字 → 走 \setmainfont
% · \thepage / \pageref 页码 → 走 \setmainfont
% ⇒ 必须连 \setmainfont 一起改,否则 PDF 里会残留 Termes span。
% 实测第一轮只做 \let 时,答案卷残留 197 个 Termes span。
%
% ⚠️ 数学公式($x$ / $\dfrac{}{}$ / $\sum$)**不在统一范围内**,
% 仍走 Computer Modern。原因见 oucexam.sty 的说明:思源宋体没有
% MATH 表,不能作数学字体。用户已确认保持不动。
%----------------------------------------------------------------------
\input{../fonts.tex}
% \sunfont 原指 SimSun,现统一指 Termes(见 oucexam.sty 的说明)
\let\sunfont\tnr
%----------------------------------------------------------------------
% 西文字族:思源宋体(与 oucexam.sty 同一套定义,复制而非 \input,
% 因为两个 .sty 是并列关系,谁先加载不确定,不能互相依赖)
%----------------------------------------------------------------------
\newfontfamily\OUCLatinSerif{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-Regular,
BoldItalicFont = *-Bold,
]
\newfontfamily\OUCLatinSerifBold{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Bold,
BoldFont = *-Bold,
ItalicFont = *-Bold,
BoldItalicFont = *-Bold,
]
% 默认西文字体 → 思源宋体(这一步不能省,见上面的说明)
\setmainfont{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-Regular,
BoldItalicFont = *-Bold,
]
\setsansfont{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-Regular,
BoldItalicFont = *-Bold,
]
% \sunfont 原指 SimSun,现统一指思源宋体
\let\sunfont\OUCLatinSerif
\let\tnr\OUCLatinSerif
\let\tnrb\OUCLatinSerifBold
% 答案卷里的黑体小标题(\heiti,共 37 处)一并换成思源宋体,#20 要求统一。
% ⚠️ 用 \let 而非改 \def\heiti —— 后者会破坏 ../fonts.tex,影响别的模板。
% ⚠️ 若将来要恢复黑体标题,注释掉下面这行即可。
\let\heiti\songti
% 局部字体切换必须显式收尾,否则 xeCJK 字体会"溢出"到后一段
\newcommand{\TNR}[1]{{\tnr #1\normalfont}}
\newcommand{\TNRB}[1]{{\tnrb #1\normalfont}}
\newcommand{\SUN}[1]{{\sunfont #1\normalfont}}
% 中西文间距 2.70bp(原卷实测值,不随字号缩放)。
\xeCJKsetup{CJKecglue={\hskip 2.70bp\relax},PunctStyle=plain}
%----------------------------------------------------------------------
% 直引号 " (U+0022) 归入 CJK 字符类(#20 补充修正)
%
% ⚠️ 问题:本文件正文用 ASCII 直引号 "" 表示中文引号(共 32 处),
% 而 xeCJK 按 Unicode 区段分类 —— U+0022 在 Basic Latin,被判成**西文**,
% 于是引号走 \setmainfont(原为 Termes),与两侧汉字不同字体、不同宽度。
% 换字体后表现为一行里出现 TeXGyreTermes-Regular 的 ",且因为西文引号
% 比中文引号窄、字距算法不同,挤压出 Overfull(实测第 88 行 +8.07pt)。
%
% ⚠️ 为什么不在 fonts.tex 里改:那里是**全库共用**的,把 U+0022 全局归 CJK
% 会让正文里真正的英文引号(如 code 片段 "hello world")也变成全角,
% 破坏那些地方。这是试卷的局部需求 —— 试卷正文基本是中文,直引号就是
% 中文引号的意思。
% ⚠️ 必须在 \input{../fonts.tex} **之后**声明,否则被随后加载的默认分类覆盖。
%----------------------------------------------------------------------
\xeCJKDeclareCharClass{CJK}{"0022} % " 直引号(当中文引号用)
%----------------------------------------------------------------------
% 【#20 踩坑记录】换字体引起的 Overfull 怎么修(结论:改源码断行,别调参数)
%
% 现象:字体统一为思源宋体后,参考答案与评分标准示例.tex 第 123-125 行
% (\keyitem{3}{4})出现 Overfull 2.61pt,渲染后压出版心右边界
% (最右墨迹 527.01bp,版心止于 524.41bp)。
% 基线同一行本来就有 0.21pt 轻微 Overfull,换字体后西文变宽
% (数字 +6%、小写字母 +13~20%),把 0.21 放大到 2.61。
%
% 根因:该行末尾是「…正迁移与负迁移(1 分),」。
% · 「,」按中文标点禁则不能出现在行首 ⇒ 不许在「)」与「,」之间断
% · \pt 定义里有 `~`(\nobreakspace)⇒ 「(1 分)」内部也不可断
% ⇒ 「(1 分),」约 16bp,是不可断尾部,必须整体塞进本行。
% 而本行没有弹性胶可拉(汉字定宽、唯一的西文空格已满),
% 往前找更早的合法断点(句号「。」之后)的话,第一行只填 65%,
% 欠宽 150bp 无处拉伸 ⇒ badness 爆掉 ⇒ TeX 宁可撑破 2.61bp。
%
% ⚠️⚠️ 试过且**全部无效**的方案(别再花时间重复,每种都实测过):
% ① \emergencystretch 1.5em —— 无效。只追加"可拉伸量",
% 本行没有胶可拉,给多少额度都无从施展。
% ② \tolerance 3000 —— 无效。不是不满意断点,是没有更早的**合法**断点。
% ③ PunctStyle=kaiming —— 能消 Overfull,但句末标点悬挂探出版心
% 6.5bp(第 3 页右缘 530.93 > 524.41),比不修更难看。
% ④ PunctStyle=quanjiao —— 同样探出 8bp(532.5),一样不可接受。
% 根子在于 xeCJK 的这几个 PunctStyle 做的是"行末标点**悬挂**",
% 不是"标点**压缩**"—— 中文排版里这是两回事,指望它消 Overfull 必翻车。
% ⑤ \rightskip 0pt plus 6bp(含 \AtBeginDocument 版)—— 无效。
% 6bp 远小于需要的 150bp,TeX 仍选"撑破"这个解。
% 注意:preamble 里直接 \setlength 还会被 ctex/xeCJK 初始化重置,
% 必须 \AtBeginDocument 才可能生效 —— 这类"设了没反应"的常见原因。
%
% ✅ 正解:**在源码的自然断点处手动断行**。
% 第 124 行末尾句号「。」之后本来就是语义上的句子边界,加 \\ 即可:
% \keyitem{3}{4}{%
% \textbf{学习迁移}:指一种学习对另一种学习的影响\pt{2}。\\
% 按方向分正迁移与负迁移\pt{1},其发生依赖于共同要素与概括水平\pt{1}。}
% 已实测:Overfull 归零,三页最右墨迹均为 524.41bp,与版心严丝合缝。
%
% 为什么这是对的做法而不是"绕过问题":
% 分项给分的答案本来就该一句一行,读者(阅卷人)靠句号扫分点。
% 手动断行既解决排版,又让内容结构更清楚 —— 不是妥协,是双赢。
% ⚠️ 若将来修改了这段文字导致又不齐,先看这里:多半还是在句号处补 \\。
%----------------------------------------------------------------------
\newcommand{\Fivhao}{\fontsize{10.5bp}{16bp}\selectfont}
\newcommand{\Sihao}{\fontsize{12bp}{18bp}\selectfont}
\newcommand{\Xiaowu}{\fontsize{9bp}{13bp}\selectfont}
@@ -161,6 +284,12 @@
% \pt{2} → 「(2 分)」——嵌在答案文字中间,标"这一点值 2 分"
% \ptline{2}{要点} → 要点靠左、分值靠右对齐(长要点更清爽)
%======================================================================
% 分值标注「(X 分)」。
% ⚠️ 里面用 `~`(\nobreakspace)是**有意为之** —— 不让「X」和「分」
% 分处两行,否则会看到「(1」在行末、「分)」在下一行,很难看。
% 代价是「(X 分)」成了不可断的整体,极端行宽下会撑破版心。
% ⇒ 这个代价接受,解决办法是在**源码的句子边界手动断行**,
% 不是在这里放开可断。详见上面「#20 踩坑记录」一节。
\newcommand{\pt}[1]{{\small(\TNR{#1}~分)}}
\newcommand{\ptline}[2]{%
\par\noindent #2\unskip\par\nobreak