ElegantBook icon indicating copy to clipboard operation
ElegantBook copied to clipboard

PDF的bookmark和toc实现page定位的方法

Open WangDongYao opened this issue 2 years ago • 0 comments

  首先解释一下这个标题,PDF的bookmark是指在PDF软件中可以点击的,下面称bookmark为书签,而toc是Table of Contents的缩写,是一本书的目录,下面称toc为目录,一般默认生成PDF的书签和目录是紧挨着的,我称这种为精准定位,而有时候我们并不想要精准定位,只想定位到某个page,我称这种为页面定位,下面介绍一下如何实现页面定位。

  先介绍一下需要用到包:

  1. hyperref:可以实现点击目录中章节进行跳转,当然功能不止于此,可以搞定交叉引用;   2. bookmark:用来添加书签,支持页面定位;   3. geometry:用来设置页面margin;   4. atbegshi:用来设置全局页面计数器。

  因为目录的页面定位实现起来相对麻烦一些,如果只需要书签页面定位,会简单很多,下面分书签页面定位和全部页面定位两种来介绍:

  1. 书签页面定位:先说实现思想,一本书的标准页码应该是封面版权等部分使用大写英文字母A, B, C或者C1, C2, C3,前言目录等部分使用小写罗马数字i, ii, iii(我喜欢大写罗马数字,后面会介绍如何修改),正文部分使用阿拉伯数字1, 2, 3,正文后面的附录参考文献部分一般来说依旧延续阿拉伯数字不变,如果不按标准直接从阿拉伯数字开始没有问题,如果按照标准就有个问题,前面的那些页码是逻辑页码,就会产生和真实的物理页面不一致的情况,就是说把C1, C2, C3当成数字来看其实是1, 2, 3,正文的阿拉伯数字也是1, 2, 3,这样就会产生重复都从1开始,后面正文部分的页码就是错的,想解决这个问题,需要一个全局的页面计数器,每当产生一个页面就记录下来,这样引用这个全局的绝对页码就不会产生问题了,把问题解释清楚之后来看具体实现:

  • 先定义一个全局的页面计数器
\RequirePackage{atbegshi}
\begingroup
  \let\@addtoreset\ltx@gobbletwo
  \newcounter{abspage}%
\endgroup
\setcounter{abspage}{1}%
\AtBeginShipout{%
  \stepcounter{abspage}%
}%
  • 然后使用bookmark包进行设置,一般情况下只对chapter级别或part级别进行页面定位,下面对chapter举例,part同理可以自己修改,需要重写下chapter方法,或者仿照chapter自己重新定义一个mchapter:
\usepackage{bookmark}
\RequirePackage{hyperref}
\hypersetup{
  pageanchor=true,
  breaklinks,
  unicode,
  linktoc=all,
  bookmarksnumbered=true,
  bookmarksopen=true,
  pdfkeywords={MathBook},
  colorlinks,
  linkcolor=winered,
  citecolor=winered,
  urlcolor=winered,
  plainpages=false,
  pdfstartview=Fit,
  pdfpagelayout=OneColumn,
  pdfborder={0 0 0},
  linktocpage
}

\newcommand\mchapter{\if@openright\cleardoublepage\else\clearpage\fi
\thispagestyle{plain}%
\global\@topnum\z@
\@afterindentfalse
\secdef\@mchapter\@smchapter}

\def\@mchapter[#1]#2{\ifnum \c@secnumdepth >\m@ne
\if@mainmatter
\refstepcounter{chapter}%
\typeout{\@chapapp\space\thechapter.}%
% 修复了书签中第1章只显示1的问题,现在使其正确的显示为第1章
\addtocontents{toc}{\protect\contentsline{chapter}{\xchaptertitle\ #1}{\arabic{page}}{page.\arabic{page}}}%
\bookmark[level=chapter,page=\arabic{abspage}]{\xchaptertitle\ #1}
\else
\addcontentsline{toc}{chapter}{\xchaptertitle\ #1}%
\fi
\else
\addcontentsline{toc}{chapter}{\xchaptertitle\ #1}%
\fi
\chaptermark{#1}%
\addtocontents{lof}{\protect\addvspace{10\p@}}%
\addtocontents{lot}{\protect\addvspace{10\p@}}%
\if@twocolumn
\@topnewpage[\@makechapterhead{#2}]%
\else
\@makechapterhead{#2}%
\@afterheading
\fi}

% 修改未编号的chapter*也写入书签和目录,如果不需要可以不写
\def\@smchapter#1#2{
\Hy@MakeCurrentHrefAuto{\Hy@chapapp*}
\Hy@raisedlink{
\hyper@anchorstart{\@currentHref}\hyper@anchorend
}
\typeout{\@chapapp\space\thechapter.}%
\addtocontents{toc}{\protect\contentsline{chapter}{#2}{\Roman{page}}{page.\Roman{page}}}%
\bookmark[level=chapter,page=\arabic{abspage}]{#2}
\markboth{#1}{}
\addtocontents{lof}{\protect\addvspace{10\p@}}%
\addtocontents{lot}{\protect\addvspace{10\p@}}%
\if@twocolumn
\@topnewpage[\@makeschapterhead{#1}]%
\else
\@makeschapterhead{#1}%
\@afterheading
\fi}

  2. 全局页面定位:bookmark包只提供了书签页码写入,所以需要借助hyperref提供的pageanchor,先介绍一个概念anchor,中文翻译成锚点之类的,就是用来定位的,而pageanchor顾名思义就是页面锚点,好像和页面定位很像啊,试试能不能实现呢?很可惜设置之后总是差了一点点距离,和书签那种页面定位不一样,这个也是我花了很长时间才研究明白的,究竟如何把差那一点点距离给修复呢?省略中间的艰辛过程,终于找到了实现方法,一切都是由于页面margin导致的,如果把页面margin设置为0,确实可以实现和书签一样的页面定位效果,但文档的样式也被破坏了,该如何对页面进行hook,使锚点位置不含margin又不影响文档样式,经过各种失败的尝试后,终于在hyperref的源码中找到了pageanchor的实现方法,只需要对最关键的那步进行修改就行了,下面是代码实现:

\RequirePackage{geometry}

% 重新定义pageanchor的页面样式,使其margin为0.
\def\Hy@EveryPageAnchor{%
  \Hy@DistillerDestFix
  \ifHy@pageanchor
    \ifHy@hypertexnames
      \ifHy@plainpages
        \def\Hy@TempPageAnchor{\hyper@@anchor{page.\the\c@page}}%
        \Hy@PageAnchorSlidesPlain
      \else
        \begingroup
          \let\@number\@firstofone
          \Hy@unicodefalse
          \Hy@PageAnchorSlide
          \pdfstringdef\@the@H@page{\thepage}%
        \endgroup
        \EdefUnescapeString\@the@H@page{\@the@H@page}%
        \def\Hy@TempPageAnchor{\hyper@@anchor{page.\@the@H@page}}%
      \fi
    \else
      \Hy@GlobalStepCount\Hy@pagecounter
      \def\Hy@TempPageAnchor{%
        \hyper@@anchor{page.\the\Hy@pagecounter}%
      }%
    \fi
    %在这里进行修改,在其设置box前先把margin设置为0
    \newgeometry{margin=0in}
    \vbox to 0pt{%
      \kern\voffset
      \kern\topmargin
      \kern-1bp\relax
      \hbox to 0pt{%
        \kern\hoffset
        \kern\ifodd\value{page}%
               \oddsidemargin
             \else
               \evensidemargin
             \fi
        \kern-1bp\relax
        \Hy@TempPageAnchor\relax
        \hss
      }%
      \vss
    }%
    % 设置完之后在把margin恢复,这样不会影响到其它位置的样式
    \restoregeometry
  \fi
}

补充一下封面页书签的设置:

\renewcommand{\thepage}{C\arabic{page}} % 封面部分页码以C(Cover)开头
\pagenumbering{Roman} % 也可选择设置为大写字母
% 上面两种选择一种
\bookmark[level=chapter,page=\arabic{abspage}]{封面}
% 如果想把封面加到目录中,可以类似这样操作:
\addtocontents{toc}{\protect\contentsline{chapter}{封面}{\Roman{page}}{page.\Roman{page}}}%

  附赠一个如何设置前言目录页为大写罗马数字的方法:

\renewcommand\frontmatter{
  \if@openright
    \cleardoublepage
  \else
    \clearpage
  \fi
  \@mainmatterfalse
  \pagenumbering{Roman}
 }

  写在最后,对于书签和目录的定制需求可能很少有人在意,默认生成的能用就行了,但我可能有点强迫症,希望书籍的书签和目录按照我自己的需求设置,因此花了一些时间把这个需求搞定了,希望这篇文章能帮到更多有此需求的朋友。

WangDongYao avatar May 27 '22 04:05 WangDongYao