本文已加入专栏文章目录,归入「进阶使用」文章系列。
本文可以看作对这个发生于 2019 年 7 月中旬的 TeX-SX 上自问自答的展开说明。那个回答中避免了 python 的使用,而是利用 zref
宏包把位置信息以文本形式在 pdf 中呈现,好处是不用引入 python,坏处是如果写成文章,需要额外介绍 zref
的使用。
问题的引入
fancyvrb
宏包提供了高度可配置的抄录环境,功能大致上和 listings
相当。
有些配置项提供了「跳出抄录环境,回到一般 latex」的功能,例如 commandchars
。它接受一串三个符号组成的值,分别代表命令开始、左侧分组、右侧分组(实际可以归结到 catcode,此处略过)。
直接看 fancyvrb
文档 Sec. 4.1.12 的例子
![8ec2a5ce409c6b5b0179927ef118418a.png](https://img-blog.csdnimg.cn/img_convert/8ec2a5ce409c6b5b0179927ef118418a.png)
文档截图中的第二个例子展示了一种使用方式,利用 commandchars
为抄录环境的某一行增加标签(label
),然后在正文中引用(ref
)它来获得行号。特别地,hyperref
包还会自动为行号添加超链接,点击行号就能跳转到抄录环境中的对应行。
上一段的最后一个分句,只是描述了我们期望的行为,实际的编译和测试结果不是这样的,点击引用(ref
)得到的行号后,无法跳转到对应行。
工具和示例准备
除了靠手去点超链接,然后根据阅读器跳转的位置来判断和分析,还可以借助工具直接读取 PDF 文件里的超链接跳转位置。例如,使用 Python 的 PyPDF2 库,
from PyPDF2 import PdfFileReader
fname = '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,点击那六个超链接,可以发现它们都跳转到同一位置。
![147b429339315b78eae16e501fda8e9e.png](https://img-blog.csdnimg.cn/img_convert/147b429339315b78eae16e501fda8e9e.png)
执行 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
,文中介绍的尝试,并未经过充分测试。