本文已加入专栏文章目录,归入「进阶使用」文章系列。
插入 PDF 的两种效果
用 latex 生成 pdf 时,插图功能(通常通过 graphicx
宏包的 includegraphics
命令)除了支持 eps、jpg 等常见图片格式,还支持 pdf 格式。
插入 pdf 图片,有两种效果
- 像插图那样,一页 pdf 就像是一个 eps 或 jpg 图片,插一张图就像是输入了一个大号的字符。这是
includegraphics
提供的功能。 - 像合并 pdf 文件那样,相当于先用 latex 生成 main.pdf,然后把另一个 pdf 文件(的一部分)和 main.pdf 合并。这是
pdfpages
宏包提供的功能。
两者的差异是,在最终输出的 pdf 里,
graphicx
插入的 pdf 页面,角色是某页里的一个大一点的符号,这一页里还能包含其他内容;pdfpages
插入的 pdf 页面,角色是一个整页。
本文提供一种尝试,使得在用第一种效果插入 pdf 时,能一次插入所有页面。
指定页面的选项
pdfpages
提供的 pages
选项, 允许用户指定要插入
- 哪一页
pages=2
、 - 哪几页
pages=1,3,5
、 - 哪个页面范围
pages=1-3,4-
includegraphics[<options>]{<image file>}
也有 page
选项,从选项命名上可以看出,它只接受一个数字,即每次只插入一页。
获取 PDF 文件的总页码
插图终究需要引擎的底层支持,而 xetex 和 pdftex 引擎,
- 都只支持一次插入一页 pdf,这意味着需要在 latex 层面做「输入所有页面」的循环
- 也都提供了获取 pdf 文件总页数的 primitive
TeX-SX 上的问答 Get number of pages of external PDF 提供了在 pdftex、xetex 和 luatex 三种引擎下的页码获取方法。我们参考它,做一个只支持 pdftex 和 xetex 的
usepackage{iftex}
% Store number of pages of pdf file #2 in command #1.
% Usage: @getPageNumOfPdf{@pagecount}{image.pdf}
% Ref: https://tex.stackexchange.com/q/198091
def@getPageNumOfPdf#1#2{%
ifPDFTeX
pdfximage{#2}%
xdef#1{thepdflastximagepages}%
elseifXeTeX
% trailing space in `"#2" ` is necessary
xdef#1{theXeTeXpdfpagecount"#2" }%
else
PackageError{TEST}{Support engines pdftex and xetex only.}{}%
fi
}
makeatother
获取 PDF 文件的完整文件名
通过 primitive 获取 PDF 的总页数时,有一个限制:必须提供完整的 PDF 文件名。这里对「完整」的要求,包含两个部分
- 文件的拓展名必须完整。
pdf-file.pdf
,正确;pdf-file
,错误。
- 文件的路径必须完整。假设我们设置了
graphicspath{{figures/}{images/}}
,那么获取一张只存储于figures
子文件夹里的 PDF 的页码时,使用figures/pdf-file.pdf
,正确;images/pdf-file.pdf
,错误 ;pdf-file.pdf
,错误。
插图 primitive 对完整文件名的要求一直存在,而我们感知不到,是因为 includegraphics
内部帮我们做了这样的事:补全文件拓展名、补全文件相对路径。
这意味着,在 includegraphics
展开到某个中间状态时,我们能获取 PDF 文件的完整文件名。这个状态,第一次出现于宏 Ginclude@graphics
的内部。我们在这个位置做 patch,把完整文件名记录下来。
% detokenized ".pdf", all four characters have category code 12 (other).
edefGin@ext@pdf{detokenize{.pdf}}
ifxGin@extGin@ext@pdf
xdefGin@full{Gin@baseGin@ext}
fi
一些说明
- 真正用于记录文件名的代码是
xdefGin@full{Gin@baseGin@ext}
- 因为 xetex 下获取 pdf 页数的 primitive
XeTeXpdfpagecount
只接受 PDF 文件名,出于通用性的考虑,额外判断「图片拓展名是否为 PDF」 - 图片的拓展名(例如
.pdf
、.jpg
)储存在Gin@ext
里,- 如果直接定义
defGin@ext@pdf{.pdf}
,那么ifx
总是 false 的。 - 这是因为
Gin@ext
内部每个符号的 category code 都是 12(other) ,而在defGin@ext@pdf{.pdf}
里,.
是 12,pdf
三个符号是 11(letter)。 - 所以这里使用了
etoolbox
宏包提供的、忽略 category code 的ifstrequal
来比较字符。
(已调整为在定义中也使用detokenize
,这是ifstrequal
内部和Ginclude@graphics
里的做法)
- 如果直接定义
- 完整的 patch,是对
Ginclude@graphics
的重定义(见下方)。- 因为
Ginclude@graphics
的原始定义中包含修改 category code 的 primitivedetokenize
,所以无法使用etoolbox/xpatch
等宏包提供的patchcmd/xpatchcmd
(这种 patch 方式,要写的代码行数较少) - 这样,我们在重定义时,就不得不把原始定义抄一遍
- 因为
% detokenized ".pdf", all four characters have category code 12 (other).
edefGin@ext@pdf{detokenize{.pdf}}
defpatch@Ginclude@graphics{%
defGinclude@graphics##1{%
ifxdetokenize@undefinedelse
edefGin@extensions{detokenizeexpandafter{Gin@extensions}}%
fi
begingroup
letinput@pathGinput@path
set@curr@file{##1}%
edefuq@curr@file{expandafterunquote@nameexpandafter{@curr@file}}%
expandafterfilename@parseexpandafter{uq@curr@file}%
edeffilename@area{expandafterquote@nameexpandafter{filename@area}}%
edeffilename@base{expandafterquote@nameexpandafter{filename@base}}%
ifxfilename@extrelax
@forGin@temp:=Gin@extensionsdo{%
ifxGin@extrelax
Gin@getbaseGin@temp
fi}%
else
Gin@getbase{Gin@sepdefaultfilename@ext}%
ifxGin@extrelax
@warning{File `##1' not found}%
defGin@base{filename@areafilename@base}%
edefGin@ext{Gin@sepdefaultfilename@ext}%
fi
fi
%% PATCH BEGIN
% - Base and ext part (e.g., ".eps") of an image are not known until now.
% - Store full file name (dir + basename + ext) in Gin@full if extension
% equals to the detokenized ".pdf".
ifxGin@extGin@ext@pdf
xdefGin@full{Gin@baseGin@ext}
fi
%% PATCH END
ifxGin@extrelax
@latex@error{File `##1' not found}%
{I could not locate the file with any of these extensions:^^J%
Gin@extensions^^J@ehc}%
else
@ifundefined{Gin@rule@Gin@ext}%
{ifxGin@rule@*@undefined
@latex@error{Unknown graphics extension: Gin@ext}@ehc
else
expandafterGin@setfileGin@rule@*{Gin@baseGin@ext}%
fi}%
{expandafterexpandafterexpandafterGin@setfile
csname Gin@rule@Gin@extendcsname{Gin@baseGin@ext}}%
fi
endgroup}%
}
写个循环,输出所有页面
主体是一个循环,例如用 tikz
的子包 pgffor
提供的循环功能
foreach i in {1, ..., @pagecount} {
includegraphics[<other options>, page=i]{Gin@full}
}
大致思路是,
- 执行一次 patch 后的
includegraphics
,记录完整文件名 - 执行一次
@getPageNumOfPdf
,记录总页码 - 对页码循环,输出所有页面
% Typeset an image like includegraphics[<options>]{<image>}. If it is
% a PDF file containing multiple pages, typeset all of them.
% #1 = <options> passed to includegraphics
% #2 = image file name
newcommand{includeAllPages}[2][]{%
%% init
defGin@count{1}%
letGin@full@empty
%% get page number
savebox@tempboxa{% pre-expand the typesetting of first page
begingroup
patch@Ginclude@graphics
includegraphics[#1]{#2}%
endgroup
}%
ifxGin@full@empty
else
@getPageNumOfPdf{Gin@count}{Gin@full}%
fi
usebox{@tempboxa}% typeset the first page
ifnumGin@count>1% typeset the rest pages if there exist.
foreach i in {2, ..., Gin@count} {%
allowbreakincludegraphics[#1, page=i]{Gin@full}%
}%
fi
}
一些说明
- 用分组限制 patch 的范围
- 先把第一页的结果存储在盒子里的做法并非必须,但这样能让后面的循环变量从 1 开始,在进行某些扩展时可能带来少许便利。
allowbreak
是为了允许在图片之间换行,并非必须。
其他
可能的功能扩展
- 一种需求是,希望在两张图之间插入分隔符(例如换行换段),形成
<page 1> <sep> <page 2> <sep> ... <page n>
的输出。
上面的例子中,includeAllPages
定义里的allowbreak
就是一种分隔符。 - 更一般的,是允许用户自定义宏
pageFilter{<current page num>}{<min num>}{<max num>}{<code>}
- 另一方面的扩展,可能是提供类似
pgfpages
的pages
选项,即允许接受一个页面范围列表page_range_list
,列表接受的语法为
page_range_list ::= page_range ("," page_range)*
page_range ::= int | [int] "-" [int]
关于 patch 的位置
- patch 的位置不唯一,本文使用的是最早的位置
- patch
Ginclude@graphics
好处是,代码与引擎无关 - 一个和引擎有关的 patch 位置是宏
Ginclude@pdf
(这是最晚的 patch 位置,一种实现见下面提到的graphicx-output-every-page.tex
文件)。- 会展开这个宏,说明拓展名已经确定是
.pdf
,patch 时只需记录完整路径。 - 但因为引擎相关,所以需要 patch 每个引擎下的
Ginclude@pdf
。
- 会展开这个宏,说明拓展名已经确定是
完整实现,见项目muzimuzhi/latex-examples 中的文件
graphicx-output-every-page.*
,包含完整实现;figures/demo-multipage-pdf.*
,用于生成多页的 pdf 小文件。