CTeX-org / ctex-kit

Macro Packages and Scripts for Chinese TeX users
976 stars 124 forks source link

xeCJK: 关于重构的一些要点 #511

Open RuixiZhang42 opened 4 years ago

RuixiZhang42 commented 4 years ago

目的

开这条 issue 的目的是为了统一整合 xeCJK 未来重构需要注意的要点。个人能力有限,会有总结不到位之处,欢迎大家补充。

本 issue 的结构

这条评论里提出第一个要点,未来的要点在下面依次提出。每个要点关注某些具体方面,会提供 MWE、截取相关 log、指出目前的不足、提出「理想的效果」。

单个标点符号的处理

通过如下 MWE 可以看出 xeCJK 其实对待标点符号也是「加法模式」,但是「过于激进」。

\documentclass{article}
\usepackage{xeCJK}
\newcommand\sampletext{逗,句。号}
\begin{document}

\showboxbreadth=\maxdimen
\showboxdepth=\maxdimen

\CJKfontspec{SimSun.ttc}%
\sampletext

\CJKfontspec{SourceHanSerifSC-Regular.otf}[Language=Chinese Simplified]%
\sampletext

\CJKfontspec{SourceHanSerifSC-Regular.otf}[Language=Chinese Traditional]%
\sampletext

\showlists

\end{document}

log 里相关的部分如下:

...
.\TU/SimSun.ttc(0)/m/n/10 逗
.\penalty 10000
.\TU/SimSun.ttc(0)/m/n/10 ,
.\rule(0.0+0.0)x-7.22656
.\glue 7.22656 minus 6.01563
.\glue 0.0 plus 0.96002
.\TU/SimSun.ttc(0)/m/n/10 句
.\penalty 10000
.\TU/SimSun.ttc(0)/m/n/10 。
.\rule(0.0+0.0)x-6.44531
.\glue 6.44531 minus 5.07813
.\glue 0.0 plus 0.96002
.\TU/SimSun.ttc(0)/m/n/10 号
...
.\TU/SourceHanSerifSC-Regular.otf(0)/m/n/10 逗
.\penalty 10000
.\TU/SourceHanSerifSC-Regular.otf(0)/m/n/10 ,
.\rule(0.0+0.0)x-7.71
.\glue 7.71 minus 6.95001
.\glue 0.0 plus 0.96002
.\TU/SourceHanSerifSC-Regular.otf(0)/m/n/10 句
.\penalty 10000
.\TU/SourceHanSerifSC-Regular.otf(0)/m/n/10 。
.\rule(0.0+0.0)x-6.77
.\glue 6.77 minus 6.35
.\glue 0.0 plus 0.96002
.\TU/SourceHanSerifSC-Regular.otf(0)/m/n/10 号
...
.\TU/SourceHanSerifSC-Regular.otf(1)/m/n/10 逗
.\penalty 10000
.\TU/SourceHanSerifSC-Regular.otf(1)/m/n/10 ,
.\rule(0.0+0.0)x-7.71
.\glue 7.71 minus 6.95001
.\glue 0.0 plus 0.96002
.\TU/SourceHanSerifSC-Regular.otf(1)/m/n/10 句
.\penalty 10000
.\TU/SourceHanSerifSC-Regular.otf(1)/m/n/10 。
.\rule(0.0+0.0)x-6.77
.\glue 6.77 minus 6.35
.\glue 0.0 plus 0.96002
.\TU/SourceHanSerifSC-Regular.otf(1)/m/n/10 号
...

问题分析

以全角句号 U+3002 为例,目前的效果是

<字> <penalty> <句号> <rule> <glue> <CJKglue> <字>

通过 log 可见两大不足:

  1. \rule 的宽度随字体而变,例如中易宋体的句号后面紧跟 -64.453125% 宽的 rule,而思源宋体(简体)的句号后面紧跟 -67.7% 宽的 rule;
  2. 思源宋体切换成繁体之后,句号已经居中,但其后的 rule 仍然是 -67.7% 宽。

重构要点

我们从铅字排印入手,提出如下两条要点:

  1. 我们应该摒弃目前的「测量标点字面」的做法,对那些占据字面小于 50% 的标点,不应该「抹掉全部空白」,而应该「统一抹掉 50%」;
  2. 我们应该分离「避头尾禁则」与「字面位置」这两个属性。例如「全角句号」是 closing 标点,不可以出现在行首,这是「禁则」属性;与之独立的应该还有「偏左半边」、「偏右半边」、「居中一半」、「居中占满」这些「字面位置」属性,其中「偏左半边」、「偏右半边」可以直接对应到直排/竖排的「偏上半边」、「偏下半边」,而保持代码实现不变。

组合:closing + 偏左半边

例如

理想效果应该是

<字>
<nobreak CJKglue> % 支持疏排,标点、汉字一起被拉开
% <nobreak penalty> 此处不再需要 penalty 了,因为已经不是合法的断行点了
<这种标点>
<rule: -50%> % 抹掉二分空白
<glue: 50% minus 50%> % 这里是可断行的位置
<CJKglue>
<字>

组合:closing + 偏右半边

这种组合不可能。

组合:closing + 居中一半

例如

理想效果应该是

<字>
<nobreak CJKglue> % 支持疏排,标点、汉字一起被拉开
% <nobreak penalty> 此处不再需要 penalty 了,因为已经不是合法的断行点了
<nobreak glue: 25% minus 25%> % 这里不可断行
<rule: -25%> % 抹掉四分空白
<这种标点>
<rule: -25%> % 抹掉四分空白
<glue: 25% minus 25%> % 这里是可断行的位置
<CJKglue>
<字>

组合:closing + 居中占满

例如

理想效果应该是

<字>
<nobreak CJKglue> % 支持疏排,标点、汉字一起被拉开
% <nobreak penalty> 此处不再需要 penalty 了,因为已经不是合法的断行点了
<这种标点> % 没有空间可以挤压的标点
<CJKglue>
<字>

其余组合:opening + XXXX

如上分析,可以依次给出理想的效果。

RuixiZhang42 commented 4 years ago

单个标点符号的处理(示意图)

数了一下,好像只有4种组合(拓展还是重写?):

  1. closing + 偏左半边/上半边;
  2. closing + 居中一半;
  3. closing + 居中占满;
  4. opening + 偏右半边/下半边。

combo1

combo2

combo3

combo4

qinglee commented 4 years ago

嗯,当初模型没选好,做法激进了些。

另外,xeCJK 在标点后面加的 \vrule 的宽度是通过 \XeTeXglyphbounds 直接从字体中取得的。但 \XeTeXglyphbounds 似乎对 language 标识的支持有问题,导致顶楼的例子中,繁体中文居中的标点度量信息不准确。

简单的例子:

\font\zhs   = "[SourceHanSerifSC-Regular.otf]" at 10pt \relax
\font\zhti  = "[SourceHanSerifSC-Regular.otf];language=ZHT" at 10pt \relax
\font\zhtii = "[SourceHanSerifTC-Regular.otf]" at 10pt \relax

\def\SHOW{\showthe\XeTeXglyphbounds 3 \XeTeXcharglyph`,\relax}

\zhs   \SHOW
\zhti  \SHOW
\zhtii \SHOW

\bye

结果是

> 7.71pt.
<to be read again> 
                   \relax 
l.8 \zhs   \SHOW

? 
> 7.71pt.
<to be read again> 
                   \relax 
l.9 \zhti  \SHOW

? 
> 4.34pt.
<to be read again> 
                   \relax 
l.10 \zhtii \SHOW

\zhti\zhtii 的字形是一样的,但 \XeTeXglyphbounds\zhti 的度量结果不准确。

RuixiZhang42 commented 4 years ago

@qinglee 嗯,最早我是在「破折号宽度」那个 issue 里发现的,其实不只是 language 的问题,而是 GSUB 的问题。通过

\XeTeXglyphbounds <int> \XeTeXcharglyph`<char>

测量出来的值全都对应被替换之前的,如果给简体中文字体加上 vertical 的标签,那么度量逗号 的结果仍然是对应横排符号,而不是直排符号。

  1. 这就是为什么我提议「摒弃 \XeTeXglyphbounds」,GSUB 到 glyph 都变了,但度量仍然是替换之前的,不可靠(也不稳定),还不如统一「抹掉二分空」;
  2. 就算 \XeTeXglyphbounds 可以提取替换之后的度量,标点符号的挤压也得重构。例如逗号 ,现在的规则是「抹掉右边全部、再补上 glue」,替换成繁体逗号之后,不能直接「用正确的度量代入现有规则」,因为这样的结果是只挤压了右侧,繁体必须改成「抹掉左右两边、再分别补上 glue」。这就是我提到的第二点「分离属性、给各种组合设置不同的挤压方案」。
qinglee commented 4 years ago

目前居中标点是在 FullLeftFullRight 类当作特例处理的,实现起来比较省力,其实并不合理也低效。

RuixiZhang42 commented 4 years ago

@qinglee 我觉得新模型可以试一试,我加了一小节「兼容性讨论」:

backward-compatibility

RuixiZhang42 commented 4 years ago

代码得再整理整理,但是效果还不错的样子。

% 只载入 fontspec,不载入 xeCJK,从底层重建
\makeatletter
\ExplSyntaxOn

\tl_new:N  \l_@@_CJKglue_tl
\tl_set:Nn \l_@@_CJKglue_tl { 0em plus 0.25em }
\fp_new:N  \l_@@_punct_width_ratio_fp
\fp_set:Nn \l_@@_punct_width_ratio_fp { 1 } % 1=全角样式,0.5=半角样式

\XeTeXinterchartokenstate = \c_one_int

\newXeTeXintercharclass \g_@@_CJK_class
\newXeTeXintercharclass \g_@@_Closing_LeftHalf_class
\newXeTeXintercharclass \g_@@_Closing_MiddleHalf_class
\newXeTeXintercharclass \g_@@_Closing_Full_class
\newXeTeXintercharclass \g_@@_Opening_RightHalf_class
\newXeTeXintercharclass \g_@@_Opening_Full_class

% 省略 300 多行非常粗糙的实验代码……

对简体中文横排的默认设置如下

scsetup

试排《后汉书》

全角样式: scquanjiao

半角样式: scbanjiao

对繁体中文横排需要重新分配类型

全角样式: tcquanjiao

半角样式: tcbanjiao

tanukihee commented 4 years ago

繁中的感叹号是可以左右挤压的吗?在 clreq 的此条评论中

不可调整的标点包括:中国大陆 GB 式的半字连接号、间隔号、分隔号,因为这几个标点固定半个字宽;横排的港台式问号、感叹号和直排的冒号、分号、问号、感叹号(包括 GB 偏靠式和港台居中式),因为这几个标点固定一个字宽。

似乎认为繁体感叹号跟问号一样是不可挤压的。

RuixiZhang42 commented 4 years ago

@tanukihee

  1. 简中叹号 GB 里说「可以挤压」,日文叹号 JIS 里说「禁止挤压」,繁中我没查到港台的相关文献(欢迎补充)。
  2. 我现在把繁中横排的叹号设为「居中一半」,很大原因是个人审美:叹号这么苗条,感觉它的两端能被挤压好看一点。不可挤压的话,那么叹号在半角样式下仍然全宽,有点丑……
  3. 重构之后,各语言的默认 configurations 也不是定死的,还得设计 UI 允许用户 customize,比如下面这个「简中、横排、半角」的例子: yahei
  4. 底层的新模型还没写完呢,标点到标点的挤压规则还没开始仔细研究,这个必须是 long term effort,且等着……
tanukihee commented 4 years ago

标点到标点的挤压规则还没开始仔细研究

感觉这个可以直接参考 LuaTeX-ja 的默认标点挤压规则了,在 texmf-dist/tex/luatex/luatexja 下的 jfm-ujis.lua(横排)和 jfm-ujisv.lua(竖排)内,默认规则很详细,调整方法跟 InDesign 类似,也挺直观的。

tanukihee commented 4 years ago

JIS 对于中间标点的密排规定还有些特殊: 「(行末における)中点類の前は原則として四分アキとし,後ろはベタ組とする.((行末时)中间标点前原则上加四分空,后密排)」 效果如下图 image

不过应该可以先暂时不管这个

RuixiZhang42 commented 4 years ago

@tanukihee JIS 对中间标点的规定已经实现呀,按照下面这个算法

<closing + 居中一半>
<rule: -25%> % 抹掉四分空白
<glue: 25% minus 25%> % 这里是可断行的位置
<CJKglue>
<字>

如果 TeX 决定在 <glue: 25% minus 25%> 处断行,那么 <closing + 居中一半> 就会「密排」在行末(因为这个标点右边的「四分空」被 <rule: -25%> 抹掉了)。你可以回头看看繁中全角的那张图,行末的句号、逗号都是占据 3/4 的空间(前面四分空,加本身二分)。

另外,你展示的那张图里有个比较迷的问题:

我猜第4行是「为了网格对齐,牺牲行末对齐」,但是第5行又「为了行末对齐,牺牲网格对齐」,很不一致啊。按照目前「closing + 偏左半边/上半边」的算法,是处于行末的偏靠式标点一律占据半角空间(段末除外),这是符合 GB/T 15834—2011 的:

[GB/T 15834—2011] 5.1.10 标点符号排在一行末尾时,若为全角字符则应占半角字符的宽度(即半个字位置),以使视觉效果更美观。

tanukihee commented 4 years ago

右起第 4 行,行末句号占据全角空间;右起第 5 行,行末顿号占据半角空间。

JIS 中句号在末尾是不能调整的 「句点類の後ろは,行末に配置する場合を含めて必ず二分アキを確保する.この二分アキは,行の調整処理の詰める場合の対象にしてはならない.」 (句点类后的二分空必须保留,即使在在行末,也不能作为行调整的挤压对象)

但是在 DTP 中,句点后面的二分空也会去掉 image

这大概也能算是一种标准与实际的脱节?

回头看看繁中全角的那张图,行末的句号、逗号都是占据 3/4 的空间(前面四分空,加本身二分)。

繁中的标点挤压我并不太清楚,不过 MS Word 的行末标点确实是优先调右空白,再调左空白的……在大多数 DTP 软件中(比如 Adobe InDesign)中,中点前后的空白是要一起挤压,前后必须相同的,看来是我先入为主了😂

RuixiZhang42 commented 4 years ago

@tanukihee

中点前后的空白是要一起挤压,前后必须相同的

新算法的确是前后空白一起挤压。但是断行时发生的事情,是另外一回事。举例「繁中全角的句号」,出现在行中时大概是这样的:

% 繁中全角
... 行中句号 <可挤压四分> <二分居中句号> <可挤压四分> 后文继续 ...

而出现在行末时会变成这样:

% 繁中全角
... ... ... ... 行末句号 <可挤压四分> <二分居中句号>
下一行后文继续 ...

所谓「前后必须相同的」,其实是要求保留行末句号后面的 <可挤压四分>,跟行中句号一致:

% 繁中全角
... ... ... ... 行末句号 <可挤压四分> <二分居中句号> <可挤压四分>
下一行后文继续 ...

小结一下,对于这些 closing 标点(偏靠式、居中一半式)后面补上的可以挤压的空白,各文种要求如下:

RuixiZhang42 commented 4 years ago

新模型(仍在开发)现在支持「断行时保留标点后方的空白」,可以自定义这种属性的标点列表,以适应不同规范:

日文设置

jkeepblank 句号后方保留空白(可压缩): jkeepblank2 顿号占据二分空间,间隔号占据 3/4 空间: jkeepblank3

繁中设置

行末时,统一保留居中标点后方的空白(可压缩): tckeepblank 偏靠式标点则只占据二分空间: tckeepblank2

RuixiZhang42 commented 4 years ago

支持「真・plain」样式

仅按照「避头尾/禁则」排版,真正地「完全不做调整」,包括「行末不挤压」、「行首不挤压」、「相邻标点不挤压」。

plainstyle

支持「真・开明」样式

句末标点全角、其余一律半角:

kaimingstyle

请注意句号的影响可以跨越别的标点!!

RuixiZhang42 commented 4 years ago

最后需要解决的是「标点悬挂」的问题。根据 JIS X 4051,「允许被悬挂的标点」只包括句号类(句号、点号)和逗号类(逗号、顿号)。更进一步,所谓「允许标点悬挂」,是指「优先考虑挤压行中的空白,试图『推进』行末标点」,而「实在推进不了,再考虑悬挂出版心」。

可以悬挂的标点列表,悬挂的算法仅对「偏靠式」标点有效,「居中式」标点一律不可悬挂。

hangable

做出来大概是这种效果:

hanging

RuixiZhang42 commented 4 years ago

以下内容摘自《組文社的青葱歲月》(作者:許定銘,《明報月刊》文化附冊《明月》2015年三月號),截图来源:香港文化資料庫。遵守《中华人民共和国著作权法》和《著作權法》合理使用。

繁中印刷品里,标点符号的排版有点迷啊

几乎不考虑「避头尾」

sample1 《阡陌》,1963年。

推测两个原因:

  1. 居中标点体积庞大,不便调整;
  2. 栏宽太窄(行长只有9个字),不便调整。

但其实也有为了「避头尾」调过标点啊

sample2 《蒲公英》,1964年。

标点之间的挤压更迷

sample3 《蒲公英》,1964年。

RuixiZhang42 commented 3 years ago

字符类之间需要插入的代码基本稳定下来了,做了一个比较实用的拓展——支持「窄体」(等价地支持「宽体」)。

测试字体是「未来荧黑」,用了GlowSansSC-Wide-Regular.otf(120%宽体)和GlowSansSC-Compressed-Regular.otf(80%窄体)。

condensedCJK

开发仓库:https://github.com/RuixiZhang42/newxeCJK 尚不支持中文字体独立设置,没有代码说明/注释,也没有用户手册,只有一个大概的骨架。 目前的「用法」是:

\documentclass{article}
\usepackage{fontspec}
\input{newxeCJK}
\setmainfont{<中文字体家族>}
\begin{document}
<正文>
\end{document}
suiyun0234 commented 3 years ago

大神,现在写latex时,要求用microtype包,可是没法用xelatex编译。我看到你空间里给了个兼容的包,然后该怎么办呢?我是小白,不知道怎么重新替换掉已经安装的xelatex了,谢谢大神。

muzimuzhi commented 3 years ago

@suiyun0234 建议单独提问,可以发到 https://github.com/CTeX-org/forum