この記事は 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 でタイプセットすると,確かに紙面上の位置が取得されている(ような気が)します。
\documentclass[a4paper,9pt]{jsarticle}
\begin{document}
\pdfsavepos%%%紙面上の位置を計測
ほげ%%%PDFを生成させるために,適当な中身を入れておく
\newpage
\typeout{pdflastypos = \the\pdflastypos sp}%%%計測位置のy座標をコンソールに出力
\end{document}
なお,\pdfsavepos
の値が決定されるのはボックスの shipout 時(すなわち,当該ページが組版される時点)であるため,値を取得するためには少なくとも次のページに進まなければなりません2。従って上のテストソースには\newpage
を挿入しています。
###ケース2(papersize 宣言,usemag)
ところで,jsarticle 文書を作成しているときpapersize
オプションが無性に恋しくなるのは周知の事実です。ということで,papersize
オプションを宣言して\pdfsavepos
を実行してみたところ,その結果は上と変わってしまいました。
\documentclass[a4paper,9pt,papersize]{jsarticle}
\begin{document}
\pdfsavepos
ほげ
\newpage
\typeout{pdflastypos = \the\pdflastypos sp}
\end{document}
おそらくpapersize
オプションの何らかの作用が\pdfsavepos
の機能に干渉するのでしょう。実際にjsarticle.cls
を覗くと,papersize
オプションの有無は以下の\special
の発行に(のみ)関与しています。
(199行目)
\newif\ifpapersize
\papersizefalse
\DeclareOption{papersize}{\papersizetrue}
...
(244行目)
\ifpapersize
\AtBeginDvi{\special{papersize=\the\stockwidth,\the\stockheight}}
\fi
###ケース3(papersize の中身ベタ打ち,usemag)
問題の検証のため,\AtBeginDvi
をプリアンブルに移しpapersize
オプションを外してみました。グローバルに宣言したpapersize
というオプションがどこか他の場所3で悪さをしているのではないかと考えたためです。
\documentclass[a4paper,9pt]{jsarticle}
\AtBeginDvi{\special{papersize=\the\stockwidth,\the\stockheight}}%%%この一行がpapersizeオプションと同じ意味のはず。
\begin{document}
\pdfsavepos
ほげ
\newpage
\typeout{pdflastypos = \the\pdflastypos sp}
\end{document}
??? 上のどちらとも異なる値が出てきました…
###ケース4(papersize 非宣言,nomag)
そういえば jsarticle といえば\mag
による拡大縮小が特徴であり,それとの相性の悪いパッケージを読み込むと不具合が生じることってあるみたいですよね4。しかし最近のJS文書クラスには\mag
を使わずに組版を行うnomag
オプションというものがあるみたいです。なのでnomag
を宣言してもう一度試してみましょう。
\documentclass[a4paper,9pt,nomag]{jsarticle}
\begin{document}
\pdfsavepos
ほげ
\newpage
\typeout{pdflastypos = \the\pdflastypos sp}
\end{document}
上のどちらとも異なる値。まあ\mag
使ってないししょうがないのかな…?
###ケース5(papersize宣言,nomag)
この状態でpapersize
オプションをつけたらどうなるのでしょうか(もはや怖いもの見たさ)。
\documentclass[a4paper,9pt,nomag,papersize]{jsarticle}
\begin{document}
\pdfsavepos
ほげ
\newpage
\typeout{pdflastypos = \the\pdflastypos sp}
\end{document}
これまでのどのケースとも異なる値。
###まとめ
以上の試行をまとめると,次の表のようになります。
| |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>}
の形(空白文字なし,単位は必ずpt
)**のpapersize 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 の座標系を(仮想的に)用意することができます(原点位置の決定方法の詳細は後述)。
[『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
の値|その意味|
|:-----:|:-:|:-:|:-:|:---------|:-|
|ケース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
や\voffset
は0pt
とされます。実際に,例えば jsarticle にa4paper
オプションを与えると,以下のようにページレイアウトが設定されます。
この図中のone inch
が1turein
であり,かつ\hoffset
と\voffset
が0pt
と設定されているならば,上の法則は以下のように書き下すことができます。
ページの基準点から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}
ドキュメントクラスのオプションusemag
や9pt
を,nomag
や8pt
,12pt
などに入れ替えて実験を行うと,実測値と理論値(の絶対値)が常に一致することがわかります。したがって,上に述べた PDF 原点位置の決定法則はおそらく正しいことが確認されます。また,nomag
では原点位置と紙面左下端が一致することも理解できますね(左下端の座標が(0,0)
となるため)。
何が起こっているのか,いくつかの例で図示すると以下のようになります。(以下の図は距離の差を誇張しているため,縮尺は正確ではありません。)
####文字サイズ 9pt,usemag の場合
文字サイズ9pt
,usemag
でタイプセットすると,ページ左下端の座標は(-451254,-4826063)
となりました。これは,下図の赤点(PDF 原点位置)に対する青点(紙面左下端)の座標を計測しているためと考えられます。
####文字サイズ9pt,nomagの場合
文字サイズ9pt
,nomag
でタイプセットすると,ページ左下端の座標は(0,0)
となりました。これは PDF 原点位置と紙面左下端が一致しているためと考えられます。
###原点位置の決定方法の不都合
このような原点位置の決定方法は,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
から計算された「現在のページサイズ」に自動的に設定されるようです。
[『TeX Live 2016 の pTeX 系列のプリミティブ』](https://texconf16.tumblr.com)講演資料より
「現在のページサイズ」が具体的にどう計算されるかについては文献が見当たらなかったため,正確なことは言えません。しかし「ケース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
オプションを必ず宣言しなければいけないようです。
-
linegoal パッケージなどで実際に利用されています。 ↩
-
shipout 前に値を呼び出すと,1つ前の
\pdfsavepos
により計測された座標(なければ初期値0)が返されます。 ↩ -
例示ソースにおいては考えにくいですが,当初この問題に直面したとき,作業ファイルに複数のパッケージをロードしており,「どれかのパッケージにも
papersize
オプションが用意されており,予期せぬ動作を引き起こしているのではないか」と考えました。 ↩ -
これも同様に,当初の作業ファイルで複数のパッケージをロードしていたために思いついた操作です。 ↩
-
e-pTeX の web2c ソース(pdfutils.ch)より。 ↩
-
「
papersize special
中の長さ指定は常にtrue
付きとして取り扱われる」という dvipdfmx や dvips の仕様に合わせた措置であると考えられます。 ↩