pTeX 系列

TeX Live 2024 的 bin 目录里并列着六个命令:ptexuptexeptexeuptexplatexuplatex。可是用 ls -l 一看,六个全都是指向同一个可执行文件 euptex 的符号链接。为日语而生的这一系列 LaTeX 引擎——pTeX、upTeX、e-pTeX、e-upTeX——用了四分之一个世纪汇成一条实现。本页顺着这条汇流走,一路读实际的横幅信息、日志和实测数值。竖排的规矩与避头尾的细节留给别的页面,这里负责的是引擎本身

六个命令,一个可执行文件

根本不存在名为 platex 的可执行文件。 在 TeX Live 2024 中,platex 是指向 euptex 的符号链接,ptexuptexeptexuplatex 也一样。那么区别在哪里——在于各自加载的格式。TeX 系列的程序启动时会看自己被调用时的名字,去加载同名的格式文件(.fmt)。这张对应表就是 fmtutil.cnf,其中并排写着 platex euptex language.dat *platex.iniuplatex euptex language.dat *uplatex.ini 两行。第二列是引擎,第四列是烧制该格式所用的 ini 文件。换句话说,pLaTeX 与 upLaTeX 是同一个引擎 e-upTeX 之上的两种不同格式

terminal
$ ls -l /usr/local/texlive/2024/bin/universal-darwin/ | grep -E "ptex|platex"
lrwxr-xr-x  platex   -> euptex
lrwxr-xr-x  ptex     -> euptex
lrwxr-xr-x  uplatex  -> euptex
lrwxr-xr-x  uptex    -> euptex
lrwxr-xr-x  eptex    -> euptex
-rwxr-xr-x  euptex          # the only real binary

$ grep -E "^(platex|uplatex|ptex|uptex|eptex) " texmf-dist/web2c/fmtutil.cnf
platex  euptex language.dat *platex.ini
eptex   euptex language.def *eptex.ini
ptex    euptex -            ptex.ini
uplatex euptex language.dat *uplatex.ini
uptex   euptex -            uptex.ini

把六个都跑一遍,只留第一行,这个结构就一目了然。横幅的主体每次都一样——This is e-upTeX, Version 3.141592653-p4.1.1-u1.30-230214-2.6 (TeX Live 2024)——变的只有括号:platex 一侧打印 (utf8.euc)uplatex 一侧打印 (utf8.uptex)。这个括号标明的是内部的日文编码,也是区分 pTeX 谱系与 upTeX 谱系的唯一外部线索。常有人问「明明执行的是 platex,怎么显示 e-upTeX」——并没有出错,这正是正常的样子

terminal
$ for c in ptex uptex eptex euptex platex uplatex; do
>   printf "%s: " $c; echo "\\end" | $c | head -1
> done
ptex:    This is e-upTeX, Version 3.141592653-p4.1.1-u1.30-230214-2.6 (utf8.euc)   (TeX Live 2024) (preloaded format=ptex)
uptex:   This is e-upTeX, Version 3.141592653-p4.1.1-u1.30-230214-2.6 (utf8.uptex) (TeX Live 2024) (preloaded format=uptex)
eptex:   This is e-upTeX, Version 3.141592653-p4.1.1-u1.30-230214-2.6 (utf8.euc)   (TeX Live 2024) (preloaded format=eptex)
platex:  This is e-upTeX, Version 3.141592653-p4.1.1-u1.30-230214-2.6 (utf8.euc)   (TeX Live 2024) (preloaded format=platex)
uplatex: This is e-upTeX, Version 3.141592653-p4.1.1-u1.30-230214-2.6 (utf8.uptex) (TeX Live 2024) (preloaded format=uplatex)

那一长串数字并不是一个版本号,而是把四条各自独立的谱系拼接在一起。开头的 3.141592653 是原始 TeX,p4.1.1 是 pTeX,u1.30 是 upTeX,230214 是 e-pTeX(日期形式),末尾的 2.6 是 e-TeX。这不是猜测——引擎把每一项都做成了原语,可以在文档里用 \the\ptexversion 之类直接读出来。会读这一行之后,e-pTeX 是什么 这个问题就有了具体答案:它是把 e-TeX 并入 pTeX 的产物,而此刻运行的 e-upTeX 又在其上叠了 upTeX,一共四层。

latex
% read the version primitives back out of the running engine
\typeout{PTEX=\the\ptexversion.\the\ptexminorversion\ptexrevision}
\typeout{UPTEX=\the\uptexversion\uptexrevision}
\typeout{EPTEX=\the\epTeXversion}
\typeout{ETEX=\the\eTeXversion\eTeXrevision}
%  PTEX=4.1.1   UPTEX=1.30   EPTEX=230214   ETEX=2.6
横幅片段来自哪里如何读回
3.141592653高德纳的原始 TeX只出现在横幅上,没有对应原语
p4.1.1pTeX——源自 ASCII 的日文扩展\ptexversion\ptexminorversion\ptexrevision
u1.30upTeX——Unicode 化的改造\uptexversion\uptexrevision
230214e-pTeX——用日期当版本号\epTeXversion
2.6e-TeX 扩展\eTeXversion\eTeXrevision

pTeX 往引擎里埋进了什么——排版方向与 JFM

pTeX 之所以必须是对引擎的改造而不是一套宏,有两个原因:排版方向\tate\yoko)和 JFM(和文字体度量)。开发者是ASCII 公司——日本那家以电脑与游戏杂志闻名的出版社,名字里的 p 取自 publishing,目标正是做出能用于商业出版的日文 TeX。排版方向听上去只是文字朝哪边排,但在 TeX 内部,这是一桩盒子尺寸的含义整个对调的事件。实测一下,数字会直说。

把同样的「あいう」放进 \hbox,比较横排与竖排下的尺寸。横排时高 8.46753pt、深 1.15465pt——正是坐在基线上的那种不对称形状,和西文一样。竖排时高与深都变成 4.8111pt,也就是全角宽 9.6222pt 的恰好一半。竖排中字符根本不落在基线上,而是跨在栏的中心线上。能在引擎内部切换这套尺寸解释,正是 pTeX 的核心,也是宏包学不来的部分。

latex
\documentclass{utarticle}   % ut... = upLaTeX, vertical writing
\begin{document}
\setbox0=\hbox{あいう}
\typeout{TATE w=\the\wd0\space h=\the\ht0\space d=\the\dp0}
\yoko
\setbox0=\hbox{あいう}
\typeout{YOKO w=\the\wd0\space h=\the\ht0\space d=\the\dp0}
\end{document}
% TATE w=28.86658pt h=4.8111pt  d=4.8111pt
% YOKO w=28.86658pt h=8.46753pt d=1.15465pt

空隙住在哪里——\kanjiskip\xkanjiskip

和文的字间距不属于字形宽度,而是作为胶(可伸缩的空白)加进去的。pTeX 系用两个寄存器保存它:和文与和文之间是 \kanjiskip,和文与西文相接处是 \xkanjiskip。在 ujarticle(正文 10pt)下实测,\kanjiskip0.0pt plus 0.4pt minus 0.5pt——自然宽度为零但可伸缩,于是成了断行时可调剂的余量。而 \xkanjiskip2.40555pt plus 1.0pt minus 1.0pt,这个看着零碎的 2.40555,正是全角宽 9.6222pt 除以四,也就是四分之一个字宽。「日文与西文单词之间留四分之一空」这条排版惯例,就这样直接摆在寄存器的默认值里。

latex
\documentclass{ujarticle}
\begin{document}
\typeout{KANJISKIP=\the\kanjiskip}
\typeout{XKANJISKIP=\the\xkanjiskip}
\setbox0=\hbox{}\typeout{EM=\the\wd0}
\setbox0=\hbox{あAい}\typeout{WITH=\the\wd0}
{\xkanjiskip=0pt \setbox0=\hbox{あAい}\typeout{WITHOUT=\the\wd0}}
\end{document}
% KANJISKIP=0.0pt plus 0.4pt minus 0.5pt
% XKANJISKIP=2.40555pt plus 1.0pt minus 1.0pt
% EM=9.6222pt   WITH=31.5555pt   WITHOUT=26.7444pt
% 31.5555 - 26.7444 = 4.8111 = 2 x 2.40555
寄存器TeX Live 2024 实测值决定什么
\kanjiskip0.0pt plus 0.4pt minus 0.5pt和文之间的空隙;自然宽度为零,只留伸缩
\xkanjiskip2.40555pt plus 1.0pt minus 1.0pt和文与西文相接处的空隙;正好是 9.6222pt 全角的四分之一
\prebreakpenalty对顿号为 10000不得出现在行首的字符;10000 表示绝不断开
\postbreakpenalty对左直角引号为 10000不得出现在行尾的字符;避头尾以罚值表达
\jcharwidowpenalty500抑制段落末行只剩一个和文字符

这里值得记住的是:避头尾并非编码为「规则」,而是编码为一个数值。TeX 断行的做法是把各候选断点的代价相加取最小,所以只要给顿号一个 10000 的上限罚值,「绝不在此处起行」就成了绝对的。反过来,把数值调小,禁则就松了。而 JFM 是一种度量,它除了记录每个和文字形的宽、高、深,还记录哪两类字符之间该留多少空,标点与括号周围的收紧正出自这里。个别约物的调法、以及 \inhibitglue 用在哪里,则归日文排版的页面管。

pTeX 与 upTeX 的区别——一个字就能分出来

差别归根到底只在内部使用的字符编码这一点。pTeX 内部用 EUC-JP 或 Shift_JIS,能处理的汉字实际上被绑在 JIS 的范围内。upTeX 内部是 Unicode,没有这道栅栏。抽象地说不好懂,用一个字试最快——所谓「梯子高」(U+9AD9)。喂给 platex,运行会停在 ! LaTeX Error: Unicode character ^^e9^^ab^^99 (U+9AD9) not set up for use with LaTeX.。把同一个字交给 uplatex(用 \documentclass[uplatex]{jsarticle}),则一路跑到 Output written on ... .dvi。满是人名地名的文档在 platex 下突然报错,原因几乎总是这个。

terminal
$ platex kanji.tex          # \documentclass{jsarticle} + 髙
! LaTeX Error: Unicode character ^^e9^^ab^^99 (U+9AD9)
               not set up for use with LaTeX.

$ uplatex kanji2.tex        # \documentclass[uplatex]{jsarticle} + 髙
Output written on kanji2.dvi (1 page, 304 bytes).

upTeX 自 2007 年起由 田中琢尔(Takuji Tanaka) 开发。TeX Live 附带的 uplcore.ltx 文件头至今仍留着 Copyright (c) 2016 Takuji Tanaka 这一行,为这条支脉的出处作证。upTeX 设计中最见效的是 \kcatcode 机制:可以按字符区段切换「按和文处理/按西文处理」。因此 upTeX 不仅能排日文,还能在同一份文档里同时排简体中文、繁体中文与韩文谚文——TeX Live 的 tlmgr info uptex 说明中就明确写着能同时处理这四种文字。这是 pTeX 时代够不到的领域。

e-pTeX 从哪里来——一份大学课程作业报告

e-pTeX 的起点,是 北川弘典 为大学实习课「计算数学 II」提交的一份作业记录。听着难以置信,但那份文档至今仍随 TeX Live 一起发行,文件名 eptex_resume.pdf——封面写着「计算数学 II 作业记录」,组名是 epsilon。同一份报告还记下:角藤亮在 2007 年 12 月 30 日做过把 pTeX 与 e-TeX 合起来的实验,并称之为 peTeX(作者顺带写道,p 与 e 位置反过来这点挺有意思)。TeX Live 的 ptex-manual 包在 README 里声明,这类旧资料收录进来「仅出于历史原因」。如今每天编译着成千上万份日文文档的引擎,其创始文件竟是一份学生作业。

那么 e-TeX 扩展究竟加了什么?\numexpr 之类的整数运算、防止宏在不该展开处展开的 \protected、大幅增加的寄存器数量,以及双向排版的基础设施。都不起眼——但如今大量 LaTeX 宏包正是以它们存在为前提写成的。于是日文生态里越来越多东西离了 e-TeX 就跑不动,e-pTeX / e-upTeX 便成了默认。没有 \protected,放进 \section 参数里的命令就会展开进目录并把它弄坏;这是每天都在发生的事,不是理论上的顾虑。这场汇流的终点,正是开头那幅景象:六个命令,一个 euptex

现在由谁在维护

日本語 TeX 開発コミュニティ(Japanese TeX Development Community,通称 texjporg)。从企业交到社群手上的那一刻,就刻在文件的版权声明里。TeX Live 2024 所含 plcore.ltx(pLaTeX 的核心)开头两行是 Copyright (c) 2010 ASCII MEDIA WORKSCopyright (c) 2016-2020 Japanese TeX Development Community,其下接着 This file is part of the pLaTeX2e system (community edition).。upLaTeX 那边的 uplcore.ltx 在同一位置并列了三方:ASCII MEDIA WORKS、Takuji Tanaka、Japanese TeX Development Community。日文 TeX 从出版社到个人再到共同体的三十年,就折叠在这三行注释里。

在这条谱系之外还有 pTeX-ng(命令名为 ApTeX,Clerk Ma 的重新实现)。它备齐 e-TeX、pTeX、upTeX 的原语以保持与 e-upTeX 兼容,同时把内部扩展到整个 Unicode,并内嵌把 dvipdfmx 库化而成的 libdpx,从而不经 DVI 直接输出 PDF。不过老实说:它并不在 TeX Live 2024 里。 tlmgr info ptex-ng 会回答 cannot find package ptex-ng。(存在的痕迹倒是有:ctex 宏包附带了名为 ctex-engine-aptex.def 的引擎定义文件。)实务上的默认仍是 e-upTeX,pTeX-ng 属于另行安装来试的对象。

引擎内部字符编码e-TeX在 TeX Live 2024 中的实体
pTeXEUC-JP / Shift_JIS没有独立可执行文件;ptex 是指向 euptex 的符号链接
upTeXUnicode(UTF-8)同上;uptex 也是指向 euptex 的符号链接
e-pTeXEUC-JP / Shift_JISeptex 格式;横幅显示 (utf8.euc)
e-upTeXUnicode(UTF-8)唯一真正的二进制;platexuplatex 都跑在它上面
pTeX-ng / ApTeXUnicode(至 0x10FFFF)未随发行版提供;tlmgr info ptex-ng 报告找不到

实务——从 uplatexdvipdfmx 到 PDF

pTeX 系不会直接吐出 PDF。它的输出永远是 DVI,再由 dvipdfmx 转成 PDF。这是与直接写出 PDF 的 pdfTeX、XeTeX、LuaTeX 的决定性区别;不知道这点,就会卡在「跑了 uplatex 却没有 PDF」上。标准流程是 uplatex 跑两遍(让交叉引用和目录稳定),再跑一遍 dvipdfmx。另外,在 TeX Live 中 dvipdfmx 本身也是指向 xdvipdfmx 的符号链接,用 ls -l 一看名字变了不必吃惊,这同样正常。

latex
\documentclass{ujarticle}   % uj... = upLaTeX, horizontal;  ut... = vertical
\begin{document}
こんにちは、\LaTeX。日本語が美しく組めます。
\[ E = mc^2 \]
\end{document}
terminal
$ uplatex document.tex      # pass 1 -> document.dvi + document.aux
$ uplatex document.tex      # pass 2 -> cross-references settle
$ dvipdfmx document.dvi     # -> document.pdf
  • 没有 PDF 时,先看 .dvi 在不在。在,问题就出在 dvipdfmx 一侧;不在,就出在 uplatex 一侧。这一步就能把排查范围砍掉一半。
  • 出现乱码,依次检查源文件是否存为 UTF-8、文档类是否面向 upLaTeX(ujarticleutarticle,或 \documentclass[uplatex]{jsarticle})。引擎一侧的开关是 -kanji=STRING(输入与输出,euc|jis|sjis|utf8|uptex)和 -kanji-internal=STRING(内部编码,euc|sjis|uptex)。
  • 提问或报缺陷时,务必附上三样:命令名(platex 还是 uplatex)、日志第一行的横幅、源文件的保存编码。光凭横幅里是 (utf8.euc) 还是 (utf8.uptex),问题就定了一半。
  • 要新开一份文档,若无硬性模板,就用 UTF-8 源文件加 uplatexdvipdfmx,文档类取 jlreq 或学校指定的那一个。既有模板若以 platex 为前提,就照它的编译步骤走,不要为赶时髦而改写。
  • 想自由挑字体,别在 pTeX 一路死磕,可以考虑 LuaLaTeX(LuaTeX-ja)或 XeLaTeX(xeCJK):能直接按名字指定系统 OpenType 字体,而且一步就出 PDF。

归结起来,判断可以这样折起来:要以成熟与速度稳妥地排日文论文,就走 e-upTeX(uplatex)→ dvipdfmx需要系统字体或 Lua 层扩展,就用 LuaLaTeX手上已有 XeTeX 资产,就用 XeLaTeX 加 xeCJK。另外别把 pTeX 系的「老」当成缺点。正是 1980 年代那个把竖排与 JFM 放进引擎内部的决定,才让日文商业出版能在 TeX 上成立。今天你敲下 uplatex 时,启动的是那个决定的第四十年。