LaTeXで現在のページ番号を取得・出力する話

  • 6
    いいね
  • 0
    コメント

これは「TeX/LaTeX Advent Caleandar 2016」の1日目の記事です。
(2日目は doraTeX さん です。)

LaTeX文書を作っているときに、「現在のページ番号に基づいてナントカしたい」と思うことって、よくありますね。(あるでしょう!?)

  • 現在のページ番号を紙面に出力したい
  • 現在のページ番号に応じて処理を分けたい
    • 例えば「見開き左側(偶数ページ)では左寄せ、見開き右側(奇数ページ)では右寄せで出力したい」とか。

本記事では、LaTeXにおいて「現在のページ番号」を扱う方法を、ホンキで解説します。

:sushi: 例によって、TeX言語レベル(LaTeXの文法の範囲外)の話をする場合には:sushi:を付けることにします。

ヘッダ・フッタの中では

ページ番号を出力するのはヘッダ・フッタの中であることが多いでしょう。ヘッダ・フッタの中で「現在のページ番号」を扱うのは比較的容易です。

pageカウンタ

LaTeXには標準で“page”という名前のLaTeXカウンタがあります。ヘッダ・フッタの出力中は、このpageカウンタが現在のページ番号を保持します。つまり、LaTeXカウンタの仕様に従って、以下のような命令が用意されます。

  • \thepage: 現在のページ番号表記(“42”とか“xxvii”とか1)を出力します。
  • \value{page}: 現在のページ番号の数値2を表します3

fancyhdrパッケージを使う

それでは、pageカウンタを利用して実際にヘッダ・フッタにページ番号を出力してみましょう。とはいっても、標準の文書クラスでは既定で既にページ番号が出力される設定になっています4。そこで少し工夫をして、「ページ番号がxのときにx度だけ回転する」という、チョット素敵な書式で出力してみましょう。

current-page-1.png

LaTeXの範囲でフッタ内容を設定するにはfancyhdrパッケージを利用するのが一般的です。なのでここでもfancyhdrを使うことにします。

example1.tex
\documentclass[a4paper]{article}
\usepackage{graphicx}% 回転したい
\usepackage{fancyhdr}% フッタ設定したい
\pagestyle{fancy}
\fancyhf{} % 既定設定をリセット
\renewcommand{\headrulewidth}{0pt} % 罫線無し
% フッタ中央にページ番号表記を回転して出力
\fancyfoot[C]{\raisebox{0pt}[1ex][0pt]{%←高さ揃え
  % \value{page} は回転角度(度単位)
  \rotatebox[origin=c]{\value{page}}{\thepage}}}
\begin{document}
% 16ページ分のダミー文章
\newcommand*{\xPage}{Foo.\newpage Bar.\newpage}
\xPage\xPage\xPage\xPage\xPage\xPage\xPage\xPage
\end{document}

(出力の16ページ目のフッタ部)

current-page-2.png

チョット素敵ですね。

:sushi: ページスタイルを自力で定義する

マッチョTeXプログラマの方は、自分でTeX言語コードを書いてページスタイルを定義してもよいでしょう。この場合もpageカウンタの値を利用することができます。

以下の例では、「ページ番号の値がxのときにヘッダにx個のゆきだるま〈☃〉を均等に配置する」というページスタイル“snowman”を自分で作成して適用しています。

ps-snowman.sty
% 文字コードはUTF-8
% Unicode文字(☃とか)が直接入力可能な環境を仮定する
% 'snowman'ページスタイルを定義
\def\ps@snowman{%
  \let\@mkboth\@gobbletwo
  \def\@oddhead{\normalfont\hfil
    \count@\value{page}%
    \@whilenum{\count@>\z@}\do{%
      \advance\count@\m@ne
      ☃\hfil}}%
  \let\@evenhead\@oddhead
  \let\@oddfoot\@empty
  \let\@evenfoot\@oddfoot}
% ページスタイルを変更
\pagestyle{snowman}
example2.tex
% upLaTeX文書; UTF-8
\documentclass[uplatex,a4paper]{jsarticle}
\usepackage{ps-snowman}
\begin{document}
% 16ページ分のダミー文章
\newcommand*{\xPage}{なんとか。\newpage かんとか。\newpage}
\xPage\xPage\xPage\xPage\xPage\xPage\xPage\xPage
\end{document}

(出力の3ページ目の上部)

current-page-3.png

(出力の16ページ目の上部)

current-page-4.png

とっても素敵ですね。

本文領域の場合

pageカウンタ自体は大域的に定義されていてLaTeX文書の任意の箇所で参照できます。ということは、本文領域においてもヘッダ・フッタと同じ方法が使えそうにも思えますが、実際にはそれでは想定通りの動作になりません。

うまくいかない方法

何が問題なのかをみるために、試しに

見開き左側(偶数ページ)では左寄せ、見開き右側(奇数ページ)では右寄せで出力

を素朴に「pageカウンタの値をみる」という方法で実装してみましょう。条件分岐が必要になるので、ifthenパッケージを利用します。

example3.tex
% pLaTeX 文書
\documentclass[a5paper,papersize]{jsbook}
\usepackage{ifthen}% 条件分岐したい
\usepackage{bxjalipsum}% ダミーテキストを出力したい
%%<*> \textflushouter{<テキスト>}
% テキストを"小口側寄せ"(奇数ページは右寄せ, 偶数
% ページは左寄せ)で出力する.
\newcommand\textflushouter[1]{%
  % 単純にpageの値を見て分岐する
  \ifthenelse{\isodd{\value{page}}}{% 奇数ページなら
    % (確認のため参照したページ番号を追記する)
    \begin{flushright}\sffamily #1(\thepage\end{flushright}% 右寄せ
  }{%else                             偶数ページなら
    \begin{flushleft}\sffamily\thepage)#1\end{flushleft}% 左寄せ
  }}
\begin{document}
\textflushouter{ニャーン}
\textflushouter{ニャーン}
\jalipsum[1-4]{wagahai}% ダミーテキスト(我輩は猫である)
\textflushouter{ニャーン}
% ここで改ページが起こる
\textflushouter{ニャーン}
\textflushouter{ニャーン}
\textflushouter{ニャーン}
\jalipsum[5-6]{wagahai}% ダミーテキスト
\end{document}

この文書の組版結果の2ページ目の冒頭は以下のようになっています。

current-page-5.png

偶数ページなので \textflushouter の出力は左寄せになるべきですが、最初のものだけ右寄せになってしまいます。また、チェックのためにpageカウンタの値を実際に出力(\thepage)させていますが、後の2回は正しい値の「2」が出力されるのに対し、最初だけ「1」が出力されています。何だかLaTeXがバグっているようにも思えますが、実際にはこれは仕様上の制限に当たります。

うまくいかない理由

(La)TeXが複数ページに渡る長い文書を組版する場合、一連の文書データを本文領域の高さに合わせて“ページ分割をする処理”が行われます。本文領域のテキストに対する組版処理が行われるのはページ分割処理よりも前なので、この時点ではそもそもまだ「現在のページ番号」が確定していないわけです5。つまり、本文領域で「現在のページ番号」を取得するのは原理的に不可能ということになります。

従って、LaTeXの仕様としては、本文領域で直接pageカウンタを参照した場合、その値は不定になります。何らかの整数を返すのは確かであり、また多くの場合に「現在のページ番号」に近い値を返すでしょうが、必ずしもその番号と一致せず、またその値を確実に予測するのは(たとえTeX言語の十分な知識があっても)ほとんど不可能です。

LaTeXの相互参照の話

これまでの話から考えると、本文領域で正しいページ番号を参照すること自体が不可能のように思えるでしょう。ところが実は、LaTeXの標準の機能には「ページ番号を参照する」ことに関するものがあります。それは相互参照の機能です。

example4.tex
% upLaTeX文書; UTF-8
\documentclass[uplatex,a4paper]{jsarticle}
\begin{document}
\section{{\TeX}はアレ}\label{sec:TeX}
なんとかかんとか…。

なお、よく誤解されるが、{\TeX}と☃の間には何の関係もない。
詳しくは\ref{sec:snowman}節
(\pageref{sec:snowman}ページ)% ラベル sec:snowman のページ番号を参照している
を参照されたい。

\newpage
\section{☃はいいぞ}\label{sec:snowman}% 参照先ラベル
いいぞ。

なお、{\TeX}は……

\end{document}

ご存知の通り、相互参照を使った文書では2回コンパイルが必要になります。出力結果を見ると、1ページ目に sec:snowman のあるページ番号である「2」が正しく出力されます。

current-page-6.png

それではなぜ \pageref は本文領域において(“現在”どころか)「“未来”のページ番号」を参照できているのでしょうか? ……もうお判りですね。そう、2回コンパイルしているからです。1回目のコンパイルでページ分割処理が実施されて、sec:snowman ラベルのあるページ番号(2)が判明します。この際に補助ファイル example4.aux に当該の情報が書き出されます。2回目のコンパイルではこのファイルを利用することで、1ページ目に「未来のページ番号」を出力しているわけです。

(とりあえず)うまくいく方法

これを利用すると次のようなトリックが考えられます。本文領域の特定の箇所で「現在のページ番号」を参照したい場合は、まずそこに適当な名前のラベル(例えばhere:)を置きます。その上で、そのラベルのページ番号を参照(\pageref{here:})すればよいわけです。

ただし、ラベル(\label)を置くときには、それと紐づく“何らかのカウンタの値”が必要になります6。そこでこの用途のために使うカウンタ“here”を用意して、ラベルを置く直前にこのカウンタを更新(\refstepcounter)することにします。

example5.tex
% pLaTeX文書
\documentclass[a5paper,papersize]{jsbook}
\usepackage{bxjalipsum}
\newcounter{here}%(無意味な)"参照対象"のカウンタ
\begin{document}
\jalipsum[1-4]{wagahai}%吾輩は猫である

現在のページ番号は「%
\refstepcounter{here}\label{here:A}% ラベルを置いて
\pageref{here:A}% そのラベルのページ番号を出力する
」です。
\jalipsum{jugemu}%寿限無寿限無
現在のページ番号は「%
\refstepcounter{here}\label{here:B}% 別のラベルを置く
\pageref{here:B}%
」です。
\jalipsum{jugemu}%寿限無寿限無
現在のページ番号は「%
\refstepcounter{here}\label{here:C}% 別のラベルを置く
\pageref{here:C}%
」です。
\end{document}

(1ページ目の最後)

current-page-7.png

(2ページ目の冒頭)

current-page-8.png

正しいページ番号が出力されました。このように「本文領域において現在のページ番号を参照する」という問題に対して、相互参照は一つの解決策になります。

しかし現在の方法には問題もあります。一つの文書中で同じラベルを重複させることは当然できないので、この方法を利用する度に毎回ユーザが一意なラベル名(here:Ahere:B、……)を与えなければいけないことです。このままでは先の例(example3.tex)の \textflushouter のような命令を実装することはできません7

:sushi: ラベル名を自動的に生成する

もちろんマッチョTeXプログラマであれば今の問題は容易に解決できるでしょう。既にラベルと一対一に対応する“here”カウンタが存在するので、この値を利用して here:1here:2、……のようなラベル名を自動生成すればよいわけです。

flushouter.sty
%% 変数
\newcounter{my@here}% ユニークIDにも使う
\newcount\my@pageref % ページ番号
\let\my@label@name\relax % ラベル名
%%<*> \textflushouter{<テキスト>}
% 例のアレ.
\newcommand\textflushouter[1]{%
  \refstepcounter{my@here}
  \my@get@pageref
  \ifodd\my@pageref % 奇数ページなら
    \begin{flushright}% 右寄せ
      \label{\my@label@name}% ラベル
      \sffamily #1(\the\my@pageref)%
    \end{flushright}%
  \else             % 偶数ページなら
    \begin{flushleft}% 左寄せ
      \label{\my@label@name}% ラベル
      \sffamily\the\my@pageref)#1%
    \end{flushleft}%
  \fi}
%% \my@get@pageref
% カウンタmy@hereの値の序数に対するラベル名を
% \my@label@name に返し, そのラベル位置のページ番号を
% \my@pageref に返す.
\def\my@get@pageref{%
  % ラベル名を作成する
  \edef\my@label@name{my@here:\arabic{my@here}}%
  % \csname... を2回展開する. ラベル存在の場合は
  % "{カウンタ値}{ページ番号}", 非存在なら \relax.
  \expandafter\expandafter\expandafter
      \my@get@pageref@a\csname r@\my@label@name\endcsname
      {}}%←コレに注意
\def\my@get@pageref@a#1#2{%
  \ifx\relax#1% ラベル非存在なら #1=\relax, #2=空
    \my@pageref=\c@page % 暫定的にpageカウンタの値を使う
  \else % ラベル存在なら #1=カウンタ値, #2=ページ番号
    \my@pageref=#2\relax
  \fi}
example6.tex
% pLaTeX 文書
\documentclass[a5paper,papersize]{jsbook}
\usepackage{flushouter}% 前掲のパッケージを使う
\usepackage{bxjalipsum}
\begin{document}
\textflushouter{ニャーン}
\textflushouter{ニャーン}
\jalipsum[1-4]{wagahai}% ダミーテキスト
\textflushouter{ニャーン}
% ここで改ページが起こる
\textflushouter{ニャーン}
\textflushouter{ニャーン}
\textflushouter{ニャーン}
\jalipsum[5-6]{wagahai}% ダミーテキスト
\end{document}

このflushouterパッケージの実装について補足しておきます。

  • \pageref 命令は取得したページ番号(表記)をその場で出力してしまうので今の用途には使えない。そこで代用となるマクロ \my@get@pageref を自前で実装している。
  • \r@XXXXXXはラベル名)を一回展開すると“{カウンタ値}{ページ番号}”というトークン列になる。XXXが未定義の場合は \r@XXX は未定義である8
  • ページ番号表記が算用数字(\pagenumbering{arabic})であることを前提とする9
  • hyperrefパッケージを読み込むと相互参照の実装が少し変わってしまうため、flushouterパッケージはこのままでは正常動作しなくなる。

※ちなみに、今の課題である「現在のページ番号の偶奇を判定する」という機能に限れば、ifoddpageというパッケージがその機能を提供しているようです。(これは開発者用のパッケージです。)

結局LaTeXユーザはどうすればよいのか

残念ながらLaTeXの範囲内での解決を与えてくれるパッケージは見つかりませんでした。仕方がないので、自分で作ってみました。

このパッケージは、これまでに説明した手法を用いて「現在のページ番号を取得する」という機能を提供するものです。使い方は以下の通りです。

  • \usepackage{bxcurpage} で読み込む。オプションはない。
  • パッケージを読み込むと、“currentpage”という名前のカウンタが用意される。
  • \getcurrentpage 命令を実行すると、(2回目以降のコンパイルでは)currentpageカウンタに正しい「現在のページ番号」が(グローバルに)設定される。
    • つまり \value{currentpage}\thecurrentpage 等が利用できる。
    • 初回のコンパイル時には、正しい値がまだ不明なので、代わりに「現在のpageカウンタの値」を設定する。

このパッケージを利用して \textflushouter を実装してみましょう。

example7.tex
% pLaTeX 文書
\documentclass[a5paper,papersize]{jsbook}
\usepackage{bxcurpage}% "現在のページ番号"したい!
\usepackage{ifthen}
\usepackage{bxjalipsum}
%%<*> \textflushouter{<テキスト>}
% 例のアレ.
\newcommand\textflushouter[1]{%
  \getcurrentpage % currentpageを設定
  % pageの代わりにcurrentpageを使う
  \ifthenelse{\isodd{\value{currentpage}}}{% 奇数ページなら右寄せ
    \begin{flushright}\sffamily #1(\thecurrentpage\end{flushright}%
  }{%else                                    偶数ページなら左寄せ
    \begin{flushleft}\sffamily\thecurrentpage)#1\end{flushleft}%
  }}
\begin{document}
\textflushouter{ニャーン}
\textflushouter{ニャーン}
\jalipsum[1-4]{wagahai}% ダミーテキスト
\textflushouter{ニャーン}
% ここで改ページが起こる
\textflushouter{ニャーン}
\textflushouter{ニャーン}
\textflushouter{ニャーン}
\jalipsum[5-6]{wagahai}% ダミーテキスト
\end{document}

2回コンパイルした結果の2ページ目冒頭は次のようになります。

current-page-9.png

バッチリですね!

まとめ

というわけで、「本文領域で現在のページ番号を参照したい」と思ったときは、ホンキで悩み込まずに、さっさとbxcurpageパッケージを使って解決しましょう!


  1. ページ番号の表記形式(算用数字やローマ数字など)を変更するには \pagenumbering 命令を利用するのが普通です。(直接 \thepage を再定義しても構いませんが。) 

  2. 「数値」であって「(算用)数字」ではありません。ページ番号の数値を(\thepage の定義に関わらず)常に算用数字で出力するには \arabic{page} とします。 

  3. :sushi: 一般的なカウンタと同じく、\value{page} を展開すると \c@page となります。ところがTeXエンジンの仕様として「\count0 がページ番号を表す」と決まっているため、この \c@page\count0 に対するcountdefトークンとなっています。 

  4. 例えば、articleクラスの既定ではフッタ中央にページ番号が出ます。 

  5. これに対して、ヘッダ・フッタ領域の組版は、分割後のページの出力の際の付加処理として行われます。つまり、ページ分割処理の後で行われるため、この時点では“現在のページ番号”が確定しているわけです。 

  6. 実際のLaTeXの動作としては、“参照されるカウンタ”は恐らく必須ではないと思われます。しかし何も準備せずに \label 命令を実行すると、「直前に \refstepcounter で更新された何らかのカウンタ(sectionやenumi等)」がデタラメに参照されることになり少々気持ち悪いことになります。 

  7. 実はもう一つ問題があります。\pageref が返すのはページ番号の表記(\thepage に相当するもの)で、表記設定によってはこれは“xiv”のようなローマ数字であるかもしれません。一方で、\textflushouter の実装で必要なのはページ番号の数値(\value{page})なので、これは \pageref では単純に代用できないのです。 

  8. 従って、XXXが未定義の場合、\csname r@XXX\endcsname の一回展開は \relax と等価なトークンになる。これがそのまま \my@get@pageref@a の引数に渡ると「引数が1つ足りない」ことになるため、\my@get@pageref の末尾に {} を入れている。 

  9. 要するに、LaTeXの相互参照の仕組自体に「\value{page} 相当のものを取得する」機能がないので、TeX言語レベルでも解決しないのである。