本文已加入专栏文章目录,归入「进阶使用」文章系列。
本文可以看作对这个发生于 2019 年 7 月中旬的 TeX-SX 上自问自答的展开说明。那个回答中避免了 python 的使用,而是利用 zref
宏包把位置信息以文本形式在 pdf 中呈现,好处是不用引入 python,坏处是如果写成文章,需要额外介绍 zref
的使用。
问题的引入
fancyvrb
宏包提供了高度可配置的抄录环境,功能大致上和 listings
相当。
有些配置项提供了「跳出抄录环境,回到一般 latex」的功能,例如 commandchars
。它接受一串三个符号组成的值,分别代表命令开始、左侧分组、右侧分组(实际可以归结到 catcode,此处略过)。
直接看 fancyvrb
文档 Sec. 4.1.12 的例子
文档截图中的第二个例子展示了一种使用方式,利用 commandchars
为抄录环境的某一行增加标签(label
),然后在正文中引用(ref
)它来获得行号。特别地,hyperref
包还会自动为行号添加超链接,点击行号就能跳转到抄录环境中的对应行。
上一段的最后一个分句,只是描述了我们期望的行为,实际的编译和测试结果不是这样的,点击引用(ref
)得到的行号后,无法跳转到对应行。
工具和示例准备
除了靠手去点超链接,然后根据阅读器跳转的位置来判断和分析,还可以借助工具直接读取 PDF 文件里的超链接跳转位置。例如,使用 Python 的 PyPDF2 库,
from PyPDF2 import PdfFileReaderfname = 'xxx.pdf'
pdf = PdfFileReader(fname)
named_dests = pdf.namedDestinations.items()print('Coordinates of named destinations')
for k, v in named_dests:print(k, [v['/Left'], v['/Top']])top = None
print('nVertical distances between labels of line numbers')
for k, v in named_dests:if 'FancyVerbLine' in k or 'lstnumber' in k:curr = v['/Top']if top is not None:print(k, float(top - curr) / 72 * 72.27, 'pt')top = curr
有关 PDF 格式的补充说明:
- 「超链接跳转位置」在 PDF 格式中称为 named destination
- 每个 named destination 拥有一个全文档唯一的名称
- 它的内容,在本文中我们关心的是横纵坐标信息,有时也关心它的目标页面
- 它的使用,是成为某个 annotation(例如
hyperref
自动添加的)的跳转目标
有关上述 python 脚本的说明:
- 第一组
print
,输出文档内所有 named destinations 的名称和坐标 - 第二组
print
,仅输出与fancyvrb
(和listings
,用于对照) 有关的相邻 named destinations 的纵坐标差值
同时,使用以下 latex 示例文档
(注意,示例中的 newpagenull
是特意添加的,为的是保证 pdf 阅读器有跳转,也就是把第一页往上翻,的空间)
documentclass{article}
usepackage{fancyvrb}
usepackage{hyperref}% <possible config appears here>begin{document}
begin{Verbatim}[numbers=left, commandchars={}]
firstlabel{vrb:1}
secondlabel{vrb:2}
thirdlabel{vrb:3}
forthlabel{vrb:4}
fifthlabel{vrb:5}
sixthlabel{vrb:6}
aend{Verbatim}ref{vrb:1}, ref{vrb:2}, ref{vrb:3}, ref{vrb:4}, ref{vrb:5}, and ref{vrb:6}
newpagenull
end{document}
最后,需要留意示例文档的编译方式
如果使用 xelatex
,因为默认情况下 xdvipdfmx
会去掉未使用的 named destinations,并简化所有 named destinations 的名称,所以需要通过选项让 xdvipdfmx
不对 named destinations 自动优化。
xelatex -no-pdf xxx
xelatex -no-pdf xxx
xdvipdfmx -C 0x0010 xxx
如果使用 pdflatex
或 lualatex
,直接使用即可。
不同引擎生成的 pdf 中,named destination 的信息有微小差异。本文默认使用 xelatex
。
初步尝试
编译 latex 示例文档生成 pdf,点击那六个超链接,可以发现它们都跳转到同一位置。
执行 python 脚本读取这个 pdf 里的信息,会获得如下输出
Coordinates of named destinations
Doc-Start [133.77, 667.2]
page.1 [132.77, 705.06]
page.2 [132.77, 705.06]Vertical distances between labels of line numbers
似乎六个 label
根本没有生成六个不同的跳转目标,连一个也没有生成。如果直接使用 xelatex xxx.tex
,生成的 pdf 里就只有一条记录
Coordinates of named destinations
0 [133.77, 667.2]
如果继续使用 PyPDF2 的功能去看第一页的所有 annotations 的跳转目标(此处略去代码),就可以完全确定:六个 label
完全没有生成新跳转目标,六个 ref
都跳转去了当前页的开始处(具体位置是 page.1
跳转目标标记的、页面版心的左上角)。
以上是从 pdf 一侧进行的分析和探索。如果从 latex 一侧进行,从相关宏包的源码入手,则能了解到以下事实:
- 在
fancyvrb
内部负责递增行号的宏FV@refstepcounter
的定义中,重写了一遍 latex2e 中refstepcounter
的原始定义,刻意避免了直接使用refstepcounter
hyperref
重定义后的refstepcounter
会在展开时插入新的跳转目的地, 并把该目的地储存在@currentHref
中以供label
在内部引用(这则「事实」的展开介绍,可能需要额外的一篇或多篇文章,此处略过)
这样,因为fancyvrb
在递增行号时没有使用 refstepcounter
,所以对应于新行号的跳转位置无法生成,@currentHref
得不到更新,label
关联的就变成了上一次更新过的 @currentHref
信息,也即 hyperref
在每一页开头默认插入的跳转目标。
第一步尝试很简单,让 FV@refstepcounter
成为 refstepcounter
letFV@refstepcounterrefstepcounter
继续尝试
修改保存、编译 tex 文件、执行 python,会发现问题没有完全解决。
Coordinates of named destinations
Doc-Start [133.77, 667.2]
FancyVerbLine.1 [133.77, 667.2]
FancyVerbLine.2 [133.77, 657.18]
FancyVerbLine.3 [133.77, 657.18]
FancyVerbLine.4 [133.77, 645.22]
FancyVerbLine.5 [133.77, 633.22]
FancyVerbLine.6 [133.77, 621.31]
page.1 [132.77, 705.06]
page.2 [132.77, 705.06]Vertical distances between labels of line numbers
FancyVerbLine.2 10.057574999999998 pt
FancyVerbLine.3 0.0 pt
FancyVerbLine.4 12.004850000000001 pt
FancyVerbLine.5 12.044999999999998 pt
FancyVerbLine.6 11.954662499999998 pt
从 python 脚本的输出可以看出,虽然现在每个 label
都对应了不同的跳转目标,但是目标之间的纵坐标差异并不一致。
- 预期输出是,每两个相邻目标,在纵坐标上都相差 12pt(对应 latex 中
baselineskip
储存的值,也即行距) - 实际得到的是,
- line 2 和 line 1 只差了 10pt(与字号有关,与行距无关,例如用
fontsize{10}{50}selectfont
修改行距后仍然是 10pt), - line 3 和 line 2 差 0pt,
- 后面的正常。
- line 2 和 line 1 只差了 10pt(与字号有关,与行距无关,例如用
推断,FV@refstepcounter
展开的位置有问题。
根据对类似示例代码的手动展开(见项目 muzimuzhi/latex-expansion 中以 fancyvrb 打头的文件),判断纵坐标差异应该源于 fancyvrb
对抄录环境前三行的特殊处理(可能是为了控制在环境中间换页的条件)具体涉及命令 FV@ListProcessLine@(i|ii|iii|iv)
。这几个宏的具体作用,限于时间和水平笔者还没能了解清楚。
笔者采取了一个讨巧(但可能带来其他未知问题)的解决方案:把 FV@refstepcounter
(具体是调用它的 FV@StepLineNo
宏 )的展开位置延迟到抄录行文本刚要输出之前,以保证通过 refstepcounter
递增行号并插入新跳转目标时,所处高度和抄录文本行一致。
这样,要做的修改就很简单:把 FV@StepLineNo
从原来的位置删掉,再在一个新的位置插入。
usepackage{etoolbox}% move FV@StepLineNo into FV@ListProcessLine
patchcmdFV@@PreProcessLine{FV@StepLineNo}{}{}{fail}patchcmdFV@ListProcessLine{kernleftmargin}{FV@StepLineNokernleftmargin}{}{fail}
从 pdf 阅读器里的点击跳转效果,和 python 脚本的输出看,问题似乎修好了。
其他
- 包含修改代码的 tex 文档,见项目 muzimuzhi/latex-examples 中的文件 fancyvrb-improvements.tex。文件中还包含修改行号引用风格的代码,会在后续文章里介绍。
- 最困难的部分可能是定位问题和知道可以把
FV@StepLineNo
挪到哪,笔者主要是通过手动展开来探索的。 fancyvrb
被其他一些宏包依赖,依赖关系比较深的是tcolorbox -> minted -> fvextra -> fancyvrb
,文中介绍的尝试,并未经过充分测试。