\pdfsaveposの闇

>この記事は [TeX & LaTeX Advent Calendar 2016](http://www.adventar.org/calendars/1734) の18日目の記事です。
>昨日は [hak7a3](http://hak7a3.hatenablog.com/entry/2016/12/17/122422) さんでした。明日は [wtsnjp](https://blog.wtsnjp.com/2016/12/19/tex-and-latex/)さんです。

#はじめに

先日，アドベントネタのために 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 系列のプリミティブ』](https://texconf16.tumblr.com)より。

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

#\pdfsavepos の闇

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

###ケース1（papersize 非宣言，usemag）

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

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

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

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

\newpage

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

\end{document}
~~~

<img width="514" alt="pdfsaveposCheck_console.png" src="https://qiita-image-store.s3.amazonaws.com/0/77110/58925431-af63-5435-52c3-42adb2efe70d.png">

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

[^2]: shipout 前に値を呼び出すと，１つ前の`\pdfsavepos`により計測された座標（なければ初期値0）が返されます。

###ケース2（papersize 宣言，usemag）

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

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

\pdfsavepos

ほげ

\newpage

\typeout{pdflastypos = \the\pdflastypos sp}

\end{document}
~~~

<img width="514" alt="pdfsaveposCheck_console.png" src="https://qiita-image-store.s3.amazonaws.com/0/77110/8f02c857-67d4-cb9e-bdfd-dc699383008b.png">

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

~~~ LaTeX: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]で悪さをしているのではないかと考えたためです。

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

~~~ LaTeX: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}
~~~

<img width="514" alt="03.png" src="https://qiita-image-store.s3.amazonaws.com/0/77110/d7ccb681-6538-3071-c8a7-bc9dbb93b4ea.png">

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

###ケース4（papersize 非宣言，nomag）

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

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

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

\pdfsavepos

ほげ

\newpage

\typeout{pdflastypos = \the\pdflastypos sp}

\end{document}
~~~

<img width="516" alt="04.png" src="https://qiita-image-store.s3.amazonaws.com/0/77110/d93de7c3-1df6-46ec-876a-6a25fbeb3510.png">

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

###ケース5（papersize宣言，nomag）

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

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

\pdfsavepos

ほげ

\newpage

\typeout{pdflastypos = \the\pdflastypos sp}

\end{document}
~~~

<img width="517" alt="05.png" src="https://qiita-image-store.s3.amazonaws.com/0/77110/74c37cca-c676-72a5-05e2-6508e0abc7d2.png">

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

###まとめ

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


|  　　　　|`papersize`の有無| `usemag`の状態|  `\the\pdflastypos`の値|
|:-----:|:-:|:-:|:-:|:---------|
|ケース１|なし|`usemag`|52841705sp|
|ケース２|あり|`usemag`|47969607sp|
|ケース３|`papersize special`をベタ打ち|`usemag`|53246924sp|
|ケース４|なし|`nomag`|48656271sp|
|ケース５|あり|`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`を用いるものが一般的です。すなわち，ソース中に

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

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

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

~~~ LaTeX
（例１）\special{papersize=10pt,20pt} ←これが発行されると以下の代入操作が実行される

  \pdfpagewidth=10pt
  \pdfpageheight=20pt 


（例２）\special{papersize=10cm,20cm} ←単位がptでないのでダメ

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

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

[^5]: e-pTeX の web2c ソース（[pdfutils.ch](https://www.tug.org/svn/texlive/trunk/Build/source/texk/web2c/eptexdir/pdfutils.ch?view=log)）より。

[^6]: マクロツイーターの[コメント](http://d.hatena.ne.jp/zrbabbler/20111102/1320253720#c)より。

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

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

> <img width="500" alt="tc16ptex-1.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/2515f668-a48b-a512-1955-06b7253fef0c.jpeg">
> [『TeX Live 2016 の pTeX 系列のプリミティブ』](https://texconf16.tumblr.com)講演資料より

なお，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`の値|その意味|
|:-----:|:-:|:-:|:-:|:---------|:-|
|ケース２|あり|`usemag`|47969607sp|JS 文書クラスにより指定された紙面サイズに基づき，PDF 原点が設定され，それに対する位置を計算した（？）。|
|ケース３|`papsersize special`の中身をベタ打ち|`usemag`|53246924sp|紙面サイズとして`papersize special`に代入した値は，JS 文書クラス内ですでに変更されていたものであり，実際の紙面サイズを表してはいなかった。|



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

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

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

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

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

<img width="500" alt="余白.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/de1f28b4-07f9-081f-9e4c-362c39790c8d.jpeg">

この図中の`one inch`が`1turein`であり，かつ`\hoffset`と`\voffset`が`0pt`と設定されているならば，上の法則は以下のように書き下すことができます。

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

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

~~~LaTeX
\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}
~~~

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

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

####文字サイズ 9pt，usemag の場合

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

<img width="1000" alt="a1.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/c6e3dfbd-a888-abfc-a648-a74e6e37af7f.jpeg">

####文字サイズ9pt，nomagの場合

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

<img width="450" alt="a2.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/f526aee6-7800-3b88-c5ee-64ca5500e3b9.jpeg">

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

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

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

>（追記）`\pdfsavepos`を，「PDF原点位置が紙面左下端に設定されることを前提として」用いる場合には`nomag`オプションが必須であると言えます。例えば，`\pdfsavepos`により取得される y 座標を，その点から紙面下端までの距離として利用する場合などです。
>
>しかし TeX の開発者界隈では，（上記のような理由から）『`\pdfsavepos`の原点位置が「何か特定のものである」ことを仮定してはいけない（二点間の差分を取得するためだけに用いる）』という作法が[知られており](https://twitter.com/zr_tex8r/status/810407343909457920)，この知見に基づき設計されたパッケージ類は，JS 文書クラスのオプションによらず正常に機能すると考えられます。

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

###ここまででわかったこと

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



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

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

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

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

> <img width="500" alt="tc16ptex-2.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/d98ff14f-545a-4640-abaa-8b8d54a721cd.jpeg">
> [『TeX Live 2016 の pTeX 系列のプリミティブ』](https://texconf16.tumblr.com)講演資料より

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


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


#まとめ

###「ケース１」〜「ケース５」で何が起こっていたのか


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


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

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