\pdfsaveposの闇

  • 6
    Like
  • 3
    Comment

この記事は TeX & LaTeX Advent Calendar 2016 の18日目の記事です。
昨日は hak7a3 さんでした。明日は wtsnjpさんです。

はじめに

先日,アドベントネタのために zref パッケージを利用しようとしたのですが,思わぬ所で沼にはまってしまいアドベントネタが生まれたので,まとめます。

検証に用いた環境

e-pTeX 3.14159265-p3.7-160201-2.6 (TeXLive 2016)

zref-savepos について(前提知識)

TeX文書において,ソースコードのとある部分が実行される時点での紙面上の位置を取得したい場合,zref パッケージの savepos モジュールが提供する\zsavepos{<refname>}および\zposx{<refname>}\zposy{<refname>}という制御綴を使用するのが一般的です1。これらは,内部で pdfTeX のプリミティブである\pdfsaveposおよび\pdflastxpos\pdflastyposを呼び出してその機能を実現しています。

従って,前述の\zrefsavepos{<refname>}などの制御綴は,pdfTeX およびそのプリミティブが継承されている TeX エンジンでないと原理的に使用不可能です。ところが,e-(u)pTeX については近年 pdfTeX の一部の機能が移植され続けており,\pdfsaveposもその例外ではありません。TeXLive 2011 以降の e-(u)pTeX では\pdfsaveposがプリミティブとして実装されています。これにより,日本語 TeX ユーザーも zref-savepos モジュールの恩恵を受けることができるようになったのでした。めでたしめでたし。

texconf16 の一般講演『TeX Live 2016 の pTeX 系列のプリミティブ』より。

\pdfsavepos の闇

(※注 ただの茶番です。本題は次の章から。)

ケース1(papersize 非宣言,usemag)

ということで,e-(u)pTeX に移植された\pdfsaveposを実際に使ってみました。以下のソースを e-pTeX でタイプセットすると,確かに紙面上の位置が取得されている(ような気が)します。

pdfsaveposCheck.tex
\documentclass[a4paper,9pt]{jsarticle}
\begin{document}

\pdfsavepos%%%紙面上の位置を計測

ほげ%%%PDFを生成させるために,適当な中身を入れておく

\newpage

\typeout{pdflastypos = \the\pdflastypos sp}%%%計測位置のy座標をコンソールに出力

\end{document}

pdfsaveposCheck_console.png

なお,\pdfsaveposの値が決定されるのはボックスの shipout 時(すなわち,当該ページが組版される時点)であるため,値を取得するためには少なくとも次のページに進まなければなりません2。従って上のテストソースには\newpageを挿入しています。

ケース2(papersize 宣言,usemag)

ところで,jsarticle 文書を作成しているときpapersizeオプションが無性に恋しくなるのは周知の事実です。ということで,papersizeオプションを宣言して\pdfsaveposを実行してみたところ,その結果は上と変わってしまいました。

pdfsaveposCheck.tex
\documentclass[a4paper,9pt,papersize]{jsarticle}
\begin{document}

\pdfsavepos

ほげ

\newpage

\typeout{pdflastypos = \the\pdflastypos sp}

\end{document}

pdfsaveposCheck_console.png

おそらくpapersizeオプションの何らかの作用が\pdfsaveposの機能に干渉するのでしょう。実際にjsarticle.clsを覗くと,papersizeオプションの有無は以下の\specialの発行に(のみ)関与しています。

jsarticle.cls
(199行目)
\newif\ifpapersize
\papersizefalse
\DeclareOption{papersize}{\papersizetrue}

 ...

(244行目)
\ifpapersize
  \AtBeginDvi{\special{papersize=\the\stockwidth,\the\stockheight}}
\fi

ケース3(papersize の中身ベタ打ち,usemag)

問題の検証のため,\AtBeginDviをプリアンブルに移しpapersizeオプションを外してみました。グローバルに宣言したpapersizeというオプションがどこか他の場所3で悪さをしているのではないかと考えたためです。

pdfsaveposCheck.tex
\documentclass[a4paper,9pt]{jsarticle}
\AtBeginDvi{\special{papersize=\the\stockwidth,\the\stockheight}}%%%この一行がpapersizeオプションと同じ意味のはず。
\begin{document}

\pdfsavepos

ほげ

\newpage

\typeout{pdflastypos = \the\pdflastypos sp}

\end{document}

03.png

??? 上のどちらとも異なる値が出てきました…

ケース4(papersize 非宣言,nomag)

そういえば jsarticle といえば\magによる拡大縮小が特徴であり,それとの相性の悪いパッケージを読み込むと不具合が生じることってあるみたいですよね4。しかし最近のJS文書クラスには\magを使わずに組版を行うnomagオプションというものがあるみたいです。なのでnomagを宣言してもう一度試してみましょう。

pdfsaveposCheck.tex
\documentclass[a4paper,9pt,nomag]{jsarticle}
\begin{document}

\pdfsavepos

ほげ

\newpage

\typeout{pdflastypos = \the\pdflastypos sp}

\end{document}

04.png

上のどちらとも異なる値。まあ\mag使ってないししょうがないのかな…?

ケース5(papersize宣言,nomag)

この状態でpapersizeオプションをつけたらどうなるのでしょうか(もはや怖いもの見たさ)。

pdfsaveposCheck.tex
\documentclass[a4paper,9pt,nomag,papersize]{jsarticle}
\begin{document}

\pdfsavepos

ほげ

\newpage

\typeout{pdflastypos = \the\pdflastypos sp}

\end{document}

05.png

これまでのどのケースとも異なる値。

まとめ

以上の試行をまとめると,次の表のようになります。

     papersizeの有無 usemagの状態 \the\pdflastyposの値
ケース1 なし usemag 52841705sp
ケース2 あり usemag 47969607sp
ケース3 papersize specialをベタ打ち usemag 53246924sp
ケース4 なし nomag 48656271sp
ケース5 あり nomag 48202231sp

結論 : \pdfsavepos(をJS文書クラス内で用いるの)は闇

e-(u)pTeXにおける\pdfsaveposの実装

というわけで,\pdfsaveposがJS文書クラスのオプション次第であまりにも異なる値を吐き続けるため,e-(u)pTeX における\pdfsaveposの実装を調べてみました。

そもそも e-(u)pTeX は DVI ファイルを出力するまでの役割しか負わないはずです。実際に,前章の例では DVI ファイルを出力した時点で(dviware を起動すらしていないのに)\pdflastyposの値が帰ってきています。すなわち\pdfsaveposが呼び出されたとき,e-(u)pTeX は PDF を生成せずに\pdfsaveposの PDF 上における位置を取得している事になります。一見矛盾しているかのように思えるこの機能は,どのようにして実装されているのでしょうか?

仮想的な PDF 上の原点を設定する

pTeX 系エンジンで紙面サイズを直接指定する方法としては,papersize specialを用いるものが一般的です。すなわち,ソース中に

\AtBeginDvi{\special{papersize=<width>,<height>}}

などと記入しておけば,DVI ファイル冒頭に\special命令が挿入され,dviware がそれを解釈することにより目的のサイズの紙面が得られます。ユーザーが直にこの値を指定する事はめったにないと思いますが,JS 文書クラスのpapersizeオプションや geometry パッケージなど,一般的な LaTeX ユーザーがサイズを指定する手段として用いる方法は内部でこれが動いています(こちらこちらの記事が詳しいです)。

TeXLive 2011 以降の e-(u)pTeX では,\special{papersize=<width>,<height>}の形(空白文字なし,単位は必ずptpapersize specialが発行された際に,通常のタイプセットに割り込む形で(すなわち即時に)以下の代入操作が実行され,仮想的な PDF の原点位置が定義されるようです5 6。JS文書クラスのpapersizeオプションや geometry パッケージで発行されるpapersize specialはこの書式を満たしているため,この点についてはユーザーが考慮する必要なく自動的に連携がとれています。

(例1)\special{papersize=10pt,20pt} ←これが発行されると以下の代入操作が実行される

  \pdfpagewidth=10pt
  \pdfpageheight=20pt 


(例2)\special{papersize=10cm,20cm} ←単位がptでないのでダメ

(例3)\special{papersize=10pt, 20pt} ←空白文字が入っているのでダメ

※ 例2・例3の場合,紙面サイズはdviwareに正しく伝わるが,仮想的なPDFの原点位置が設定されない。

\pdfpagewidth \pdfpageheightは PDF 上の原点位置を指定する pdfTeX のプリミティブであり,デフォルト値はともに0です。pdfTeX ではこれらを出力紙面のサイズ指定にも用いますが,e-(u)pTeX においては出力される DVI ファイルに影響を及ぼす事はありません。

このプロセスにより,e-(u)pTeX は DVI ファイルを出力するまでの時点で,最終的に生成される PDF の座標系を(仮想的に)用意することができます(原点位置の決定方法の詳細は後述)。

tc16ptex-1.jpg
『TeX Live 2016 の pTeX 系列のプリミティブ』講演資料より

なお,JS 文書クラスのpapersizeオプションの宣言により発行されるpapersize special\magがかかっていないサイズ,すなわち文字サイズ指定に依らない出力紙面そのもののサイズを指定しています7。すなわち,jsarticle.clsなどで,

\AtBeginDvi{\special{papersize=\the\stockwidth,\the\stockheight}}

というソースの付近が処理(おそらく字句解析)される時点では,\stockwidth\stockheightには\magの効果がかかっていない紙面サイズが代入されています。ただし,JS文書クラスはpapersize specialを発行したのちに,\stockwidth\stockheight\magの逆数をかけ,値を変更しています。

初めの例において「ケース2」と「ケース3」とで違いが生じた理由はこれであると考えられます。「ケース2」の場合は出力紙面そのもののサイズとしてpapersize specialが発行され,その値が即時に代入されますが,「ケース3」のようにpapersize specialを直打ちした場合,すでに\stockwidth\stockheight\magの逆数がかかったサイズに変換されているために,\pdfpagewidth\pdfpageheightには実際よりも「大きめな」(10ptより大きなサイズを指定した場合は「小さめな」)値が代入されてしまい,y 座標を実際よりも大きく計測してしまったのでしょう。すなわち,ページサイズの指定はpapersizeオプションにまかせ,手動指定は避けるのが賢明だと考えられます。

ここまででわかったこと

     papersizeの有無 usemagの状態 \the\pdflastyposの値 その意味
ケース2 あり usemag 47969607sp JS 文書クラスにより指定された紙面サイズに基づき,PDF 原点が設定され,それに対する位置を計算した(?)。
ケース3 papsersize specialの中身をベタ打ち usemag 53246924sp 紙面サイズとしてpapersize specialに代入した値は,JS 文書クラス内ですでに変更されていたものであり,実際の紙面サイズを表してはいなかった。

原点位置の決定方法の詳細

仮想的な PDF 原点の,紙面に対する位置は次のように定められているようです。

紙面左端から1trueinだけ右へ進んだ場所から1inだけ左へ戻った場所を通る垂直線と,紙面上端から1trueinだけ下へ進んだ場所から1inだけ上へ戻り,さらに\pdfpageheightだけ下に下がった場所を通る水平線の交点が PDF 原点位置となる。

LaTeX は,紙面左上から\hoffset+1インチだけ右に進み,\voffset+1インチだけ下に進んだ位置をページの基準点としており,通常の文書クラスでは\hoffset\voffset0ptとされます。実際に,例えば jsarticle にa4paperオプションを与えると,以下のようにページレイアウトが設定されます。

余白.jpg

この図中のone inch1tureinであり,かつ\hoffset\voffset0ptと設定されているならば,上の法則は以下のように書き下すことができます。

ページの基準点から1inだけ左に進み,1inだけ上に進み,\pdfpageheightだけ下に進んだ位置がPDF原点位置となる。

この法則について明示している文献は見当たりませんでしたが,以下のようなテストソースを e-(u)pTeX でタイプセットすることによりこれを確認することが出来ます。

\documentclass[a4paper,9pt,usemag,papersize]{jsarticle}
\makeatletter

%%%%以下の設定で紙面の余白を潰し,本文領域を紙面全体に広げる
\hoffset=-1truein
\voffset=-1truein
\evensidemargin=0pt
\oddsidemargin=0pt
\topmargin=0pt
\headheight=0pt
\headsep=0pt
\textheight=\paperheight
\textwidth=\paperwidth
\marginparsep=0pt
\marginparwidth=0pt
\footskip=0pt

%%%%インデントを潰す
\parindent=0pt

\begin{document}
\leavevmode
\vfill%%%% <- 紙面左下端まで移動
\leavevmode
\pdfsavepos%%%% <- ページ左下点の座標取得

\newpage%%%% <- 1ページ目をshipoutし,位置を確定させる

%%%%以上が測定部分。以下は,法則の検証のためのテストコード
水平方向の位置について\par
ページ左端から原点の距離:\the\pdflastxpos\par
法則から予想される理論値:\the\numexpr\dimexpr1truein-1in\relax\relax\par%%%% <- 1truein - 1inのsp値
それらの差:\the\numexpr\pdflastxpos+\dimexpr1truein-1in\relax\relax


\bigskip

垂直方向の位置について\par
ページ下端から原点の距離:\the\pdflastypos\par
法則から予想される理論値:\the\numexpr\dimexpr\paperheight-1truein+1in-\pdfpageheight\relax\relax\par%%%% <- \paperheight - (1truein - 1in + \pdfpagehegiht) のsp値
それらの差:\the\numexpr\pdflastypos+\dimexpr\paperheight-1truein+1in-\pdfpageheight\relax\relax
\end{document}

ドキュメントクラスのオプションusemag9ptを,nomag8pt12ptなどに入れ替えて実験を行うと,実測値と理論値(の絶対値)が常に一致することがわかります。したがって,上に述べた PDF 原点位置の決定法則はおそらく正しいことが確認されます。また,nomagでは原点位置と紙面左下端が一致することも理解できますね(左下端の座標が(0,0)となるため)。

何が起こっているのか,いくつかの例で図示すると以下のようになります。(以下の図は距離の差を誇張しているため,縮尺は正確ではありません。)

文字サイズ 9pt,usemag の場合

文字サイズ9ptusemagでタイプセットすると,ページ左下端の座標は(-451254,-4826063)となりました。これは,下図の赤点(PDF 原点位置)に対する青点(紙面左下端)の座標を計測しているためと考えられます。

a1.jpg

文字サイズ9pt,nomagの場合

文字サイズ9ptnomagでタイプセットすると,ページ左下端の座標は(0,0)となりました。これは PDF 原点位置と紙面左下端が一致しているためと考えられます。

a2.jpg

原点位置の決定方法の不都合

このような原点位置の決定方法は,JS 文書クラスでusemag,かつ10pt以外の文字サイズを指定した際には不都合を生じます。上の例で見たように,紙面左下から微妙に右上にずれた地点に PDF 原点が指定されてしまうためです。この状況は,前節で紹介した講演資料の図と照らし合わせると,「正しくない位置に PDF 原点が設定されてしまった」とみなして良いでしょう。

したがって,JS 文書クラスで\pdfsaveposを用いる際には,nomagオプションが必須であると考えるべきです。

(追記)\pdfsaveposを,「PDF原点位置が紙面左下端に設定されることを前提として」用いる場合にはnomagオプションが必須であると言えます。例えば,\pdfsaveposにより取得される y 座標を,その点から紙面下端までの距離として利用する場合などです。

しかし TeX の開発者界隈では,(上記のような理由から)『\pdfsaveposの原点位置が「何か特定のものである」ことを仮定してはいけない(二点間の差分を取得するためだけに用いる)』という作法が知られており,この知見に基づき設計されたパッケージ類は,JS 文書クラスのオプションによらず正常に機能すると考えられます。

なお,これから「ケース2」と「ケース5」とで違いが生じた理由を説明できます。「ケース2」はusemagかつ9ptを指定したため,PDF 原点が意図しない位置に設定されてしまったわけですね。

ここまででわかったこと

     papersizeの有無 usemagの状態 \the\pdflastyposの値 その意味
ケース2 あり usemag 47969607sp JS 文書クラスにより指定された紙面サイズに基づき,予期せぬ位置に PDF 原点が設定され,それに対する位置を計算した。
ケース3 papsersize specialの中身をベタ打ち usemag 53246924sp 紙面サイズとしてpapersize specialに代入した値は,JS 文書クラス内ですでに変更されていたものであり,実際の紙面サイズを表してはいなかった。
ケース5 あり nomag 48202231sp JS 文書クラスにより指定された紙面サイズに基づき,正しい位置に PDF 原点が設定され,それに対する位置を計算した。

仮想的な PDF 上の位置を測定する

文書中に\pdfsaveposが現れ,実行された場合,e-(u)pTeX は前節のプロセスにより指定された原点とその地点の距離を計測し,\pdflastxpos\pdflastyposに格納します。なお,実際の距離が決定されるのは shipout 時(該当部分の含まれるページが組版される時点)であることには注意が必要です。つまり,少なくとも\pdfsaveposが含まれるページの次ページに進むまでは\pdflastxposなどは取得できません。

PDF の原点が未定義である場合

PDF の原点位置が定義される前に\pdfsaveposが呼び出された場合,\pdfpagewidth\pdfpageheight\shipoutしたボックスの寸法と\hoffset\voffsetから計算された「現在のページサイズ」に自動的に設定されるようです。

tc16ptex-2.jpg
『TeX Live 2016 の pTeX 系列のプリミティブ』講演資料より

「現在のページサイズ」が具体的にどう計算されるかについては文献が見当たらなかったため,正確なことは言えません。しかし「ケース1」と「ケース2」および「ケース4」と「ケース5」で測定距離がずれている以上,JS 文書クラスが想定する紙面サイズ(すなわちpapersizeオプションにより発行されるpapersize special)と e-pTeX エンジンが自動で計測する「現在のページサイズ」は同一ではないということが確実に言えるでしょう。また,「ケース1」と「ケース4」の測定距離が同一でないことから,この「現在のページサイズ」の計測方法は,ユーザーやパッケージが\magを用いて紙面サイズを拡大・縮小することを想定せず設計されている可能性も考えられます。

この「ページサイズ自動設定機能」はpapersize specialの書式が不完全であった場合や,papersize specialを用いずに紙面サイズ指定を行なった場合にも\pdfsaveposを正しく機能させるための工夫であると考えられるのですが…。これとJS文書クラスの相性は今ひとつなようです。

まとめ

「ケース1」〜「ケース5」で何が起こっていたのか

     papersizeの有無 usemagの状態 \the\pdflastyposの値 その意味
ケース1 なし usemag 52841705sp PDF 原点位置が指定されなかったため,e-(u)pTeX が自動算出した「現在のページサイズ」(≠ JS 文書クラスが想定する紙面サイズ)を元に位置を計算。なお「現在のページサイズ」の計算は\magの影響を受けている。
ケース2 あり usemag 47969607sp JS 文書クラスにより指定された紙面サイズに基づき,予期せぬ位置に PDF 原点が設定され,それに対する位置を計算した。
ケース3 papsersize specialの中身をベタ打ち usemag 53246924sp 紙面サイズとしてpapersize specialに代入した値は,JS 文書クラス内ですでに変更されていたものであり,実際の紙面サイズを表してはいなかった。
ケース4 なし nomag 48656271sp PDF 原点位置が指定されなかったため,e-(u)pTeX が自動算出した「現在のページサイズ」(≠ JS 文書クラスが想定する紙面サイズ)を元に位置を計算。
ケース5(正解) あり nomag 48202231sp JS 文書クラスにより指定された紙面サイズに基づき,正しい位置に PDF 原点が設定され,それに対する位置を計算した。

結論:JS 文書クラスで\pdfsaveposを使うときは

JS 文書クラスで\pdfsaveposを用いて正しい測定を行いたい場合は,papersizeオプションおよびnomagオプションを必ず宣言しなければいけないようです。


  1. linegoal パッケージなどで実際に利用されています。 

  2. shipout 前に値を呼び出すと,1つ前の\pdfsaveposにより計測された座標(なければ初期値0)が返されます。 

  3. 例示ソースにおいては考えにくいですが,当初この問題に直面したとき,作業ファイルに複数のパッケージをロードしており,「どれかのパッケージにもpapersizeオプションが用意されており,予期せぬ動作を引き起こしているのではないか」と考えました。 

  4. これも同様に,当初の作業ファイルで複数のパッケージをロードしていたために思いついた操作です。 

  5. e-pTeX の web2c ソース(pdfutils.ch)より。 

  6. マクロツイーターのコメントより。 

  7. papersize special中の長さ指定は常にtrue付きとして取り扱われる」という dvipdfmx や dvips の仕様に合わせた措置であると考えられます。