思わず Lua で LaTeX してみた
~LuaTeX で日本語しない件について~

LuaTeX 上で動く LaTeX である LuaLaTeX を使ってみる。 特に、LaTeX の多少複雑な処理を Lua で(TeX ではなく!)プログラミングする ことで実現することに焦点を当てる。
参考: LuaLaTeX の組版上の拡張機能、 特に 「UTF-8 で入力して OpenType/TrueType の Unicode フォントで出力する」 ことについては 「LuaLaTeX で Unicode してみる」 のページで解説しているのでそちらを参照してほしい。 (「日本語する件」 の情報もあります。)
変更履歴
  • 2011/05/28: 一部の内容を 「LuaLaTeX で Unicode してみる」 のページに移動(& 書き直し)した。
  • 2010/07/24: ixbase0 パッケージを更新(v0.1b)。
  • 2010/07/23: 付録を追加。
  • 2010/07/11: ixbase0 パッケージを更新(v0.1a)。 現状の W32TeX に適応させる。

LuaTeX の紹介

いつも通り能書きから

LuaTeX は TeX の拡張の一つで、 主に Taco Hoekwater、Hartmut Henkel、Hans Hagen によって開発されている。 簡単に言うと、LuaTeX は以下に挙げるものを統合 して成立した TeX エンジンである。

  • pdfTeX ― PDF を直接出力する TeX。 海外で TeX を使う場合、実際にはこの pdfTeX が使われることがほとんどである。
  • Omega ― Unicode を直接扱える TeX として 1990 年代に開発されていた。 縦書きなどの複数な書字方向のサポートや 入力の加工の機能などの様々な先進的な機能を有しているが、 開発は長らく中断されていた。
  • MetaPost ― PostScript 形式の画像を出力する 描画用プログラミング言語で METAFONT の派生として生まれた。
  • Lua インタプリタ ― Lua は軽量のスクリプト言語であり、 他のソフトウェアに(プログラム可能なマクロ機能を持たせるために) 組み込むという用途を対象に開発されており、 また実際にそういう用途で広く用いられている。
  • OpenType フォントの広範なサポート ― この辺りは FontForge のコードを流用しているようである。

この中で、TeX エンジンの拡張として異質なのは 「Lua インタープリタの組入」であろう。 TeX はそれ自体がプログラミング言語であるので、 結局のところ、プログラミング言語(TeX)のインタプリタに 別のプログラミング言語(Lua)を載せていることに なっているのである (いや、MetaPost も立派なプログラミング言語なので、 結局 3 つある…)。 このような構成には次のような目的がある。

  1. TeX の文法が苦手な人でも、現在において「ありふれた」 文法体系を持った言語で TeX の機能を操ることができる。 (LuaTeX の開発の最初の動機はこの点であるらしい…。)
  2. これまで TeX の言語でも触ることのできなかった TeX の内部構造へのアクセスを可能にする (例えば一度組み上がったボックスの中身を加工する等) ことで、TeX の組版機能の根幹を (それ以上のエンジンの改変なしに) 拡張する枠組を提供する。

この 2 の「枠組を提供する」という方針は、 今注目されているもう一つの拡張エンジンである XeTeX と対比すると解り易い。 XeTeX は、OpenType の複雑なレンダリングや 言語に応じた行分割(日本語の禁則処理など)等の拡張機能について、 完成品を提供している。 従って、(TeX のレベルでは)それらの拡張機能はすぐに誰でも 使用可能な状態になっていて、 あとは、LaTeX 等の上位システムでのサポートを整える 作業が残るだけである。 逆に、提供された機能を超える拡張 (日本語の行組版の空き調整や、欧文和文のフォント自動切換など) については、従来通り、TeX の文法を用いて「マクロ」 (命令列の操作)のレベルで実装しなければならない。

LuaTeX では TeX の組版機能を Lua で操作できるように したとともに、一連の処理の中の様々な場面で Lua の処理を自動的に割り込ませる仕組みを与えている。 これにより、元々から備わっている pdfTeX と Omega に加えて、必要に応じて他の拡張(例えば日本語の行組版の処理) を、Lua を用いて、また処理の流れの中の望みの箇所で、 実装することが可能になっている。

LuaTeX の現状

LuaTeX の開発は明確なロードマップをもって行われていて、 それによると最初の正式リリース (バージョン 1.00) は 2012 年の終わりに予定されている。 現在 (2011 年 5 月) のバージョンは 0.70 である。 なお、 現在海外で最も多く使われている TeX エンジンは pdfTeX であるが、 LuaTeX は pdfTeX の後継となることが既に決定している。

LuaTeX 上で動く LaTeX、 つまり 「LuaLaTeX」 についてであるが、 LuaTeX 自体が ConTeXt (LaTeX とは別の著名な TeX 上のフレームワーク) での実践と並行して開発されているという経緯があり、 LaTeX での LuaTeX のサポートはあまり進んでいなかった。 しかし、 少なくとも TeX Live 2009 の途中から、 「LuaLaTeX」 のコマンド lualatex が用意されるようになっている。 併せて、 LuaTeX の拡張機能を LaTeX 上で使えるようにするためのパッケージも少しずつ準備されている (参照)。

この文書の目的

先に述べた通り、LuaTeX の凄みはその果てしない拡張性に あるといえるが、実際に拡張を行うためには、TeX の内部構造に 関する知識が不可欠になり、一般の LaTeX ユーザにとっては 縁遠いものになってしまう。 そこで、ここでは敢えて先述の「もう一方の目的」、すなわち 「文書作成上で遭遇する複雑な処理を『普通の』 スクリプト言語で解決できる」 という所に焦点を当ててみる。 つまりは、

標準の LaTeX のレベルでは実現困難な、 擬実用的な処理を 「訳の判らない TeX マクロ」ではなく Lua 言語でのプログラムで解決しよう

という話である。

参考: いやそもそも「TeX マクロが訳の判らない」というのが 大きな誤解であって、本当は……(以下略)。

従って、この文書では(LaTeX のユーザ命令の範囲を超える)TeX 言語に関する知識は 一切前提としないが、Lua でのプログラミング能力は仮定する。 (現在一般的な何らかのスクリプト言語ができる人なら Lua は容易に習得可能だと思われる。) なお、「TeX のマクロ」は扱わないが、 「LaTeX のマクロ機能」(\newcommand\newenvironment)の知識は必要である (これは LaTeX の参考書に載っている事項である)。

注意: なお「擬実用的」とは要するに「非実用的」ということである。

とにかく LuaLaTeX を使ってみる

(この節の内容は 「LuaLaTeX で Unicode してみる」 のページの 『従来の欧文 LaTeX の代わりに使う』 節に移動しました。)

注意: この文書の初版を公開した時点 (2010/05/27) には LuaLaTeX 対応の fontspec (2.0 版) はまだ公開されていなかった (公開されたのは翌日の 2010/05/28)。 その後も暫くは私が様子見をしている状態だったので、 「Lua で LaTeX してみた」 のシリーズで挙げられているソース文書では全て 「入力は ASCII、 出力は 8 ビットフォント」 に従っている。

Lua で LaTeX してみる

事例その 1: カウンタ値の 16 進表記

LaTeX カウンタの値を \arabic で出力させる (例えば \arabic{section})と、 結果は 10 進表記になるが、時にはこれを 16 進表記 にしたい場合がある(異論は当分保留しておこう)。 これを Lua で実現してみる。 必要なのは次の 2 つの要素で、 これらは LaTeX と Lua の連携において最も基本的なものである。

  • \directlua{<テキスト>}[LaTeX 命令]: <テキスト> を Lua コードとして Lua インタプリタで実行する。
  • tex.print(<文字列> s, ...)[Lua 関数]: 引数の文字列を LaTeX の入力として渡す。 各々の文字列が 1 つの入力行とみなされる。 文字列中に含まれる LaTeX の特殊文字は有効 (特殊性を保つ)である。 (注意:第 1 引数の型が数値であると別の意味になる。)

LuaLaTeX 文書は次のようになる。

\documentclass[a4paper]{article}
\begin{document}
\renewcommand{\theenumi}{\directlua{
local hexform = "0x"..string.char(0x25).."02X"
tex.print(hexform:format(\arabic{enumi}))
}}
\renewcommand{\labelenumi}{{\ttfamily\theenumi:}}
\begin{enumerate}
\item First item.
\item Next item.
\item And more. \item And more. \item And more. \item And more.
\item And more. \item And more. \item And more. \item And more.
\item And more. \item And more. \item And more. \item And more.
\item And more. \item And more. \item And more. \item And more.
\item Last but one,
\item That's all!
\end{enumerate}
\end{document}
項目番号が「0x01:」「0x02:」…「0x0F:」「0x10:」…のようになる。

この文書では、Lua コードと連携した LaTeX マクロを紹介するが、 まず最初に、そのようなマクロを作成する際には、 「LaTeX(TeX)と Lua はどちらも言語であり両者はかなり異なる文法をもって いるので、文字列の受け渡しをする時に細心の注意を払う必要がある」 ということを注意しておく。 \directlua の動作をもう少し詳しく述べると以下のようになる。

  • \directlua の引数の文字列は LaTeX の字句解析規則に従って解釈される。 例えば、行頭の空白は無視され、改行は(原則的に)空白と同じ扱いになり、 % から行末まではコメントとして無視される、等である。 上の例で %02X という(Lua の)文字列を 作るために string.char(0x25) としているのは、 % が直接書けないからである。
    注意: 通常、LaTeX で〈%〉を出力するには \% と書けばよい。 この \% が意味するのは 「〈%〉を出力する命令の実行」であって、「非特殊な % への展開」 ではないので、\directlua 中で \% と書いても % の意味にはならない。 (実際に \% と書いた場合、普通の LaTeX の設定では、 そのまま \% と解釈されるはずだが、 パッケージ読込状況により異なる可能性がある。)
  • \directlua の引数中にある LaTeX の命令やマクロは、 単なる文字列に展開されるものであれば、その展開が実行される。 例えば、\arabic{enumi} はカウンタ enumi の現在の値の 10 進表記に展開される (そしてそれは Lua の有効な数値表現である)。 そうでない命令(\hspace\textbf 等; 大部分の LaTeX 命令はこちらに属する) は原則的に書くことができない。 (なお、LaTeX の命令を含む文字列を Lua の文字列として扱う方法 については後で説明する。) 加えて、もし、\directlua の引数中に マクロ引数 #1#2、… が現れた場合は、それは (LaTeX マクロの)実引数に置換される。
    参考: [TeX レベルの話] 要するに \write でファイルに書き出した文字列と同じになる。 TeX のマクロは通常の方法で展開される。
  • \directlua による Lua コードの呼び出しが終わったら、 制御は LaTeX 側に戻り、 その時、当該の \directlua 呼出内で実行された 全ての tex.print の引数の文字列が順番に 改行区切りでその場に挿入されている(最後に改行文字はない)状態になる。
    参考: [TeX レベルの話] すなわち、\directlua{...} の「展開」 結果が上述のテキストになる。 展開であるので、上の例で \item\label を付して参照を行っても正しく動作する。 テキストのカテゴリコードは \directlua 呼出時のものになる。 なお、tex.write()付録参照) では所謂「\the-文字列」 のカテゴリコードが適用される。
参考: Lua コードを実行する LaTeX 命令は \directlua の他に \latelua がある。 この命令で指定されたコードは、次にページ出力が行われた時に実行される。

ixbase0 パッケージ

先に述べた通り、\directlua の引数の中でも LaTeX の特殊文字(% など)は有効であるので、 このままでは Lua のコードが書きづらい。 勿論、別のファイルに書いて dofile() すればよい (また、LuaLaTeX では別のファイルを用いることが推奨される 見込みである)のだが、 ここでは、LaTeX 文書内に Lua コードを書くことが容易になるように 簡単な(手抜きの)補助パッケージを用意してみた。 (このパッケージが提供する機能の一覧については、 付録を参照)

このパッケージを読み込む (\usepackage{ixbase0}) と、以下の命令が使えるようになる。

  • \begin{execluacodeblock}<テキスト>\end{execluacodeblock}[LaTeX 環境]: 中のテキストを「verbatim な状態」 (LaTeX の字句解析規則や特殊文字を無効化した状態)で読み取りそれを \directlua で実行する。 Lua からの出力(tex.print)は verbatim ではなく LaTeX の通常の状態で解釈される。 注意:verbatim 環境等と同じく、命令やマクロの引数内では使えない。
  • \?\** は英数字でない ASCII 文字)[LaTeX 命令]: 「非特殊」な文字 * に展開される。 例えば \?\% と書くことで Lua コードに % を含められる。
  • \??\** は英数字でない ASCII 文字)[LaTeX 命令]: 「非特殊」な文字列 \* に展開される。 例えば "\??\\""\\" になる。

ixbase0 パッケージを使うと、先の例\theenumi の定義は次のようにできる。

\renewcommand{\theenumi}{\directlua{
tex.print(("0x\?\%02X"):format(\arabic{enumi}))
}}

事例 2: graphicx パッケージで傾き(斜体)変形

垂直座標がy増す毎に水平変位がx変化する場合、「傾き具合」rはx/yと定義される。

ここでいう(水平方向の)傾き変形とは、右の図のように、 各点を水平方向に移動させる(ただし水平方向の距離は不変) 線形変換のことである。 ここで実数 r は「どれだけ傾けるか」を示すパラメタである。 フォントを「機械的に斜体」(synthetic slant)にする場合に この変換が用いられる。 graphicx パッケージでは、回転変形と拡大縮小の機能が 提供されているが、傾き変形はない。 しかし、傾き変形は回転変形と拡大縮小の合成として表現できるので、 graphicx の機能だけで r をパラメタとする 傾き変形の機能が実現できるはずである。 ここでは LuaLaTeX でそれを実装してみる。

パラメタ r の傾き変形は、

  • \rotatebox{θ}
  • \scalebox{a}[b]
  • \rotatebox{θ'}

を順に適用したものと同値になる。 ここで各変数値は r の値から次の式により求められる。

a=(√(r²+4)−r)/2, b=(√(r²+4)+r)/2, θ°=(1/2)arctan(2/r), θ′°=θ°−90°

ただしここで一つ問題がある。 \rotatebox には 「回転後のボックスの大きさを調整」する機能があり、 これは回転を合成する際には邪魔になってしまう。 そこで、回転を施す前に外見の大きさをゼロに設定する処理を加え、 傾き変形の結果について、その大きさを変形前の大きさと同じに設定する。

各パラメタ(a, b, θ, θ') の値が与えられているという前提で考えると、 この部分の処理は次のような LaTeX マクロで表せる。 (ほとんど LaTeX と graphicx のコマンド呼び出しからなる のでこの部分は LaTeX で書いた方が簡明である。

\newsavebox{\grslantBox}%  savebox を用意する
% (savebox 使用は「中身」を 2 回組版するのを避けるため)
\newcommand\grslantSub[5]{% 引数は{θ}{a}{b}{θ'}{中身}
  \savebox{\grslantBox}{#5}% 中身の組版結果を \grslantBox に格納
  % graphicx 命令を組み合わせて変形する
  \rotatebox{#4}{\scalebox{#2}[#3]{\rotatebox{#1}{%
   % \makebox, \smash でボックス寸法をゼロに潰している
   \makebox[0pt][l]{\smash{\usebox{\grslantBox}}}}}}%
   % (幅ゼロなので出力位置は不変)
  % 最後に \phantom を利用して元の「中身」と同じ寸法を持たせる
  \phantom{\usebox{\grslantBox}}}

その上で、全体の実装方針を次のようにする。

  • ユーザ命令を \grslant{<r>}{<テキスト>} とする。
  • r から a, b, θ, θ' への変換を Lua で行う。
  • Lua から \grslantSub の呼び出しコードを返す。

完全な LuaLaTeX ソースは以下のようになる。

\documentclass[a4paper]{article}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage{ixbase0}
\usepackage{graphicx}
\newcommand\grslant[1]{%
  \directlua0{
    local r = #1; local q = math.sqrt(r * r + 4)
    local a, b = (q - r) / 2, (q + r) / 2
    local t0 = math.deg(math.atan(2 / r) / 2); local t1 = t0 - 90
    local s = "\??\\grslantSub{\?\%s}{\?\%s}{\?\%s}{\?\%s}"
    tex.print(s:format(t0, a, b, t1))
  }}
\newsavebox{\grslantBox}
\newcommand\grslantSub[5]{%
  \savebox{\grslantBox}{#5}%
  \rotatebox{#4}{\scalebox{#2}[#3]{\rotatebox{#1}{%
   \makebox[0pt][l]{\smash{\usebox{\grslantBox}}}}}}%
  \phantom{\usebox{\grslantBox}}}
\begin{document}

\begin{center}
  \fbox{A$\otimes$} % anything can be set slanted!
  \grslant{.1}{\fbox{A$\otimes$}} \grslant{.2}{\fbox{A$\otimes$}}
  \grslant{.3}{\fbox{A$\otimes$}} \grslant{.4}{\fbox{A$\otimes$}}
\end{center}

\end{document}
文字Aと記号\otimesを、0%、10%、20%、30%、40%の傾き変形を施して出力する。
参考: 上掲のプログラムでは「\gslant + 1 引数」を 「\gslantSub + 4 引数」に置き換えていて、 テキストの部分は Lua に渡していない。 次小節で見るように、文字列のユーザ引数を Lua で取り扱う のは少々厄介であるので、ここではそれを避けている。

事例 3: 自動化処理を含む表組み

以下のような要件を満たす簡単な「年表作成」用の LaTeX コマンドを作成する。

  • 書式は以下の通り。
    \begin{TimeLine}
    \TLEntry{<年>}{<月>}{<キー>}{<事柄>}
    %……以降同じ形式の \TLEntry が続く
    \end{TimeLine}
    
  • 出力は「年」「月」「事柄」の 3 列からなる。
  • 項目(行)は「年」と「キー」の辞書順昇順に並び替える。
  • 「年」毎に横罫線で区切る。また「年」の表示は該当の「年」 の最初の行にのみ行う。
  • 「年」は 4 桁の数字であり、「キー」はプレーン文字列である。 「月」「事柄」は LaTeX 命令を含みうる。

これを実装したものを以下に使用例とともに示す。 少し長い Lua コードを含むので、execluacodeblock 環境を使っている。

\documentclass[a4paper]{article}
\usepackage{ixbase0}
%% Lua codes
\begin{execluacodeblock}
-- TL_init_table: called at \begin{TimeLine}
function TL_init_table()
  -- The table TL_table holds all data given by \TLEntry;
  -- it is used as an array of maps.
  TL_table = {}
end
-- TL_add_entry: called at \TLEntry
function TL_add_entry(year, mon, key, text)
  -- makes the 'real' key from year and key given
  local fullkey = ("%05d%s"):format(year, key)
  -- adds the entry to TL_table
  local ent = { year = year; mon = mon; key = fullkey; text = text }
  table.insert(TL_table, ent)
end
-- TL_output_table: called at \end{TimeLine}
function TL_output_table()
  -- sorts the array
  local function TL_compare(a, b) return a.key < b.key end
  table.sort(TL_table, TL_compare)
  -- prints data as a tabular
  -- (header lines)
  ixbase.print([=[
\begin{tabular}{cc|p{.72\linewidth}}
\hline
\itshape Year & \itshape Mon. & \multicolumn{1}{c}{\itshape Fact} \\
]=])
  -- (data rows)
  local prev_year = 0
  for _, ent in ipairs(TL_table) do
    local year = ""
    if prev_year ~= ent.year then
      tex.print("\\hline"); year = ent.year; prev_year = year
    end
    local fo = "\\TLStrut\\sffamily\\bfseries %s & %s & %s\\\\"
    local line = fo:format(year, ent.mon, ent.text)
    tex.print(line)
  end
  -- (trail)
  ixbase.print([=[
\hline
\end{tabular}]=])
end
\end{execluacodeblock}%
%% LaTeX stuffs
\newenvironment{TimeLine}%
  {\directlua{TL_init_table()}}%
  {\directlua{TL_output_table()}}
\newcommand\TLStrut{\rule{0pt}{1em}}
\newcommand\asluastring[1]{"\luaescapestring{\detokenize{#1}}"}
\newcommand\TLEntry[4]{\directlua{
  TL_add_entry(#1, \asluastring{#2}, \asluastring{#3}, \asluastring{#4})
}}
\begin{document}

\newcommand*{\AmS}{\textit{AmS}} % here we do with simple logo
\begin{center}\begin{TimeLine}
\TLEntry{1977}{---}{001}
 {Knuth begins his research on typography.}
\TLEntry{1978}{---}{001}
 {Knuth delivers an AMS Gibbs Lecture entitled
  ``Mathematical Typography''
  [Bull.\ AMS, vol.~1 (March 1979), no.~2, pp.~337--372]
  to the AMS membership at its annual meeting.}
\TLEntry{1979}{---}{001}
 {Digital Equipment Corporation and the American Mathematical Society
  jointly publish Knuth's book
  \emph{{\TeX} and METAFONT: New Directions in Typesetting},
  which contains the text of Knuth's Gibbs Lecture.}
\TLEntry{1982}{Sep.}{091}
 {Knuth releases dvitype, a model DVI driver.}
\TLEntry{1980}{Oct.}{101}
 {The first draft of Spivak's ``Joy of TeX'' is announced in
  TUGboat, vol.~1, no.~1.}
\TLEntry{1983}{Dec.}{121}
 {Lamport writes a {\LaTeX} manual, the earliest known {\LaTeX}
   manual in existence.}
\TLEntry{1984}{---}{001}
 {Addison-Wesley publishes Knuth's \emph{The {\TeX}book},
  destined to become the definitive {\TeX} reference.}
\TLEntry{1984}{Sep.}{091}
 {Lamport releases version 2.06a of the {\LaTeX} macros.}
\TLEntry{1985}{Aug.}{081}
 {Lamport releases {\LaTeX} 2.09, his last version of
  the {\LaTeX} macros.}
\TLEntry{1985}{---}{091}
 {Addison-Wesley publishes the first edition of Lamport's
  reference manual
  \emph{{\LaTeX}: A Document Preparation System}.}
\TLEntry{1982}{Jan.}{011}
 {Spivak announces {\AmS}-{\TeX} at the joint math meetings.}
\TLEntry{1982}{Jan.}{012}
 {Version 0 of Spivak's ``Joy of {\TeX}'' is released.}
\end{TimeLine}\end{center}

\end{document}
出力では、例えば、1982 のある行は、その後に「Jan.」「Spivak announces ...」と続く。その下の行は年表示がなく「Jan.」「Version 0 of ...」、その下は「Sep.」「Knuth releases ...」。その他、上述した仕様を満たす出力が得られている。

Lua(或いは他のスクリプト言語)のプログラミングに慣れている人ならば、 Lua の処理自体は容易に理解できると思う。 問題は LaTeX との受け渡しの部分である。 以下で要点を述べる。例えば、

\TLEntry{2013}{Apr.}{401}{\emph{Lua{\TeX}: The Program} is published.}

が実行された場合、以下のような Lua コードが実行されるようにしたい。

TL_add_entry(2013, "Apr.", "401", "\\emph {Lua{\\TeX }: The Program} is published.")

この文字列の形式変換を行うために次のような命令を用いる。

  • \detokenize{<テキスト>}[LaTeX 命令]: LaTeX の特殊文字や命令を含むテキストを、 「この文字列をもう一度 LaTeX の入力として与えるともとのテキストに戻る」 という性質をもつ文字列に変換する。 (つまり、テキスト X について、 \detokenize{X} の結果の 文字列を Lua で tex.print させて LaTeX がそれを読むと X に戻る。)
    参考:[TeX レベルの話] すなわち、\detokenize{<テキスト>} を展開すると、 <テキスト> を \write で出力する形式の文字列 (「\the-文字列」のカテゴリコードをもつ)になる。 この際に命令実行やマクロ展開は一切行われない。 なお、\detokenize は e-TeX のプリミティブである。
  • \luaescapestring{文字列}[LaTeX 命令]: 文字列に対して、Lua の通常文字列リテラルで必要なエスケープ 処理を行う。 (つまり、文字列 S に対し、 "\luaescapestring{S}" の Lua での評価結果は S になる。)

ここから次のようなことがいえる。 次のようなマクロを用意する。

\newcommand\asluastring[1]{"\luaescapestring{\detokenize{#1}}"}

すると、LaTeX テキスト X に対し、\directlua を用いて Lua で tex.print(\asluastring{X}) を実行すると、X を書いたのと同じことになる。 これを利用して、一旦 Lua に渡した LaTeX コードをそのまま 戻すことが可能になる。

これとは別の話として、上の例では Lua コード中に LaTeX ソースを(改行を含む)長形式文字列の形([=[ ... ]=]) で含めているが、 これを tex.print() で出力すると、 改行文字が「LaTeX の改行文字の働き」をしてくれない。 そこで、ixbase0 パッケージで次の補助機能を用意していて、 例ではその関数を用いて出力させている。

  • ixbase.print(<文字列> s,...) [Lua 関数]: 引数の各文字列を改行文字毎に分割して tex.print() に渡す(文字列の末尾の改行は無視する)。 結果的に、文字列の中の各行が LaTeX の 1 つの入力行と扱われる。
    参考: [TeX レベルの話] execluacodeblock 環境では、TeX ソース中の改行文字を 「カテゴリコード 12 の CR」として Lua に渡しているが、 Lua はこれを常に LF と解釈するようである。 そして、LuaTeX の tex.print の 1 つの引数の文字列は (LF のカテゴリコードに関わらず)常に 1 つの行として扱われる。

事例 4: 文字列を指定の字幅で切り捨てる

すなわち次のようなマクロを作成する。

\truncatetowidth{<長さ>}{<文字列>}: 指定された文字列を先頭から順に幅が <長さ> を超えない範囲で』出力する。 簡単のために <文字列> には LaTeX の特殊文字や命令は含まないものとする。

実装方針は次の通り。

  • 切り出した文字列に対して幅を調べることができるので、 二分探索により、幅に収まる最大の文字列長を求める。
  • 切り出した文字列の幅を求めるのは、LaTeX の \settowidth 命令を使う (幅を得るには組版処理を行わなければならないため)。 残りの処理のほぼ全てを Lua で行う。
参考: Lua の中で組版処理を行うのも可能ではあるが、 「TeX の内部処理を自前でやる」ことになるので、 それなりの知識が必要で、かつ非常に面倒である。

いくつか準備をする。 まず、二分探索のアルゴリズムを確認しておく。

  -- str = 入力文字列; wd = 上限の幅
  -- str の先頭 n 文字を組版した時の幅が wd 以下で
  -- あるような最大の n を求める
  local l, u = 0, str:len()
  while l < u do
    local m = math.ceil((l + u) / 2)  -- ※ floor ではダメ
    local w = 【str:sub(1, m) の幅】
    if w <= wd then l = m       -- ※ m + 1 ではない
    else            u = m - 1
    end
  end
  -- ここで l (= u) が求める値である

幅の取得のために、LaTeX の長さ変数の値を Lua で読み取る必要がある。 ixbase0 パッケージで用意された命令を利用する。

  • ixbase.length.<名前>.width [Lua 変数・読書可]: LaTeX の長さ変数 \<名前> の現在の値の自然長。 値は sp 単位の整数で指定する(1pt = 65536sp)。 この ixbase.length.<名前> (これ自体はユーザデータ型)の他のフィールドを 用いると伸縮長(plus/minus)の部分も読み書きできるのだが、 ここでは省略。
  • ixbase.counter.<名前> [Lua 変数・読書可]: LaTeX のカウンタ <名前> の現在の値。 今の例では使わないがついでに紹介しておく。
参考: [TeX レベルの話] TeX のレジスタについては、tex.counttex.dimentex.skip 等を使う。 それぞれ、「レジスタ番号」または「(\countdef 等での)名前」 のインデクスでアクセス可能である。 ixbase.counter.hogetex.count["c@hoge"] と同値となる。

実は、この題目については、比較的簡単かつ素直に 実装できると当初予想していたのであるが、実際には かなり技巧的なコーディングを要するようである。

\documentclass[a4paper]{article}
\usepackage{ixbase0}

%% Lua code
\begin{execluacodeblock}
function truncate_to_width(str)
  local wdt = ixbase.length.ttwTarget.width
  -- binary search written in tail recursion
  function ttw_iterate(l, u)
    if l == u then -- here l is the answer
      tex.print(str:sub(1, l)); return
    end
    local m = math.ceil((l + u) / 2)
    -- Make a closure containing what to do after LaTeX
    -- work finishes (it is referred in ttw_get_width()).
    function ttw_continue()
      local wdm = ixbase.length.ttwMeasured.width
      if wdm <= wdt then ttw_iterate(m, u)
      else               ttw_iterate(l, m - 1)
      end
    end
    ttw_get_width(str:sub(1, m))
    -- ... and exit Lua context for a while!
  end
  ttw_iterate(0, str:len())
end

function ttw_get_width(str)
  -- here promise that you'll go back into loop
  ixbase.print([[
\settowidth{\ttwMeasured}{]]..str..[[}%
\directlua{ttw_continue()}]])
end
\end{execluacodeblock}

%% LaTeX stuff : it is easy!
\newlength{\ttwTarget}
\newlength{\ttwMeasured}
\newcommand*{\truncatetowidth}[2]{% {width}{string}
  \setlength{\ttwTarget}{#1}%
  \directlua{
    truncate_to_width("\luaescapestring{\detokenize{#2}}")
  }%
}

\begin{document}
\begin{itemize}
\item \truncatetowidth{10pt}{Hello world!}
\item \truncatetowidth{20pt}{Hello world!}
\item \truncatetowidth{30pt}{Hello world!}
\item \truncatetowidth{40pt}{Hello world!}
\item \truncatetowidth{50pt}{Hello world!}
\item \truncatetowidth{60pt}{Hello world!}
\item \truncatetowidth{70pt}{Hello world!}
\end{itemize}

\end{document}
上から「H」「Hell」「Hello」「Hello wo」「Hello worl」「Hello world!」「Hello world!」という項目が並ぶ。

truncate_to_width() の中は先に示した 二分探索の実装であるが、何やら不可解なコードに 変わってしまっている。

  • この関数の中では、LaTeX の \settowidth を呼び出すとともにそこで設定された値を読み取る必要がある。 従って、単に LaTeX コードが tex.print で書かれるだけでは不十分でそれが実行される必要があり、 そのためには一旦 Lua の実行コンテキスト (\directlua)を抜ける必要があり、 その上で、\ttwMeasured の値を読むところ から Lua の処理に戻らなければならない。
  • これを何とか実現するために、 LaTeX の実行の完了を待つ箇所について、 それ以降のコードを一旦関数にし、それを実行する \directlua を書き出して、Lua の実行を 終了するという処理をとっている。
    \directlua{
      A(); B(); local r = C()
      tex.print("\\TeXprocess")
      【TeX コードを実行させる】
      D(r); E(); F()
    }
    
    \directlua{
      A(); B(); local r = C()
      tex.print("\\TeXprocess")
      function do_next() --「後の処理」を関数にする
        D(r); E(); F()
      end
      tex.print("\\directlua{do_next()}")
    }% Lua 実行を抜ける。\TeXprocess が
    % 実行され、その後 do_next() に戻る。
    
  • ところが、今の探索過程ではその「待つ箇所」 が while ループの中にあるので、「それ以降の処理」 というのが上手く関数で書けなくなってしまっている。
  • そのため、上掲の実装においては、truncate_to_width の中の反復処理を末尾再帰(tail recursion)の形に書き換えてから、 ttw_get_width 以降の処理を関数にしている。
参考: 一応実装はできたが、もはや「普通のスクリプト言語の書き方」 ではなくなっているのも事実である (関数型言語がもっとポピュラーな存在であればいいのだが…)。 どういうコードの書き方をしていても任意の場所で TeX へ抜け出せる ようにはできないか。 何だか無性に call/cc が欲しくなってくるところだが、 もう少し調べてみると、Lua のコルーチンの機構を使うと 解決できることが判った。 → Lua 内で TeX コードの実行を完了させる

事例 5: コンパイルに 3 日かかる LaTeX 文書を作る

\documentclass[a4paper]{article}
\usepackage[width=545pt,vscale=.9]{geometry}
\usepackage{lmodern}
\usepackage{ixbase0}
\pagestyle{empty}\raggedbottom
\begin{execluacodeblock}
MP_PREC = 1000000    -- number of digits

MP_BDGT = 7
MP_SIZE = math.floor(MP_PREC * 1.001 / MP_BDGT) + 2
MP_RDX = 10^MP_BDGT; MP_SF = "%0"..MP_BDGT.."d"
function mp_make(v)
  local x = { [0] = v }
  for i = 1, MP_SIZE do x[i] = 0 end
  return x
end
function mp_clone(x)
  local t = { [0] = x[0] }
  for i = 1, MP_SIZE do t[i] = x[i] end
  return t
end
function mp_add(x, y)
  local c = 0
  for i = MP_SIZE, 0, -1 do
    local r = x[i] + y[i] + c
    if r < MP_RDX then x[i] = r; c = 0
    else               x[i] = r - MP_RDX; c = 1
    end
  end
end
function mp_sub(x, y)
  local c = 0
  for i = MP_SIZE, 0, -1 do
    local r = x[i] - y[i] - c
    if r >= 0 then x[i] = r; c = 0
    else           x[i] = r + MP_RDX; c = 1
    end
  end
end
function mp_div(x, v)
  local c = 0; local floor = math.floor
  for i = 0, MP_SIZE do
    local r = c * MP_RDX + x[i]
    x[i] = floor(r / v); c = r % v
  end
end
function mp_divx(x, v)
  local c = 0; local floor = math.floor
  local t = {}; local u; local z = true
  for i = 0, MP_SIZE do
    local r = c * MP_RDX + x[i]
    u = floor(r / v); t[i] = u; c = r % v
    if u > 0 then z = false end
  end
  if z then return nil else return t end
end
function mp_string(x)
  local t = {}
  for i = 1, MP_SIZE do t[i] = MP_SF:format(x[i]) end
  local s = table.concat(t):sub(1, MP_PREC)
  return (x[0].."."..s:gsub("0+$", ""))
end

function a_atan_rcp_b(a, b)
  local x = mp_make(a); mp_div(x, b)
  local s = mp_clone(x); local n, tadd = 1, true
  while true do
    n = n + 2; tadd = not tadd
    if n >= MP_RDX then return nil end
    mp_div(x, b * b)
    local t = mp_divx(x, n)
    if not t then return s, n end
    if tadd then mp_add(s, t) else mp_sub(s, t) end
  end
  return s
end

function calc_pi()
  -- Good old Machin formula:
  --  pi/4 = 4atan(1/5) - atan(1/239)
  print("start")
  local t1 = os.time()
  local r1, n1 = a_atan_rcp_b(16, 5)
  print("atan(1/5) needed terms up to n = "..n1)
  local r2, n2 = a_atan_rcp_b(4, 239)
  print("atan(1/239) needed terms up to n = "..n2)
  mp_sub(r1, r2); local pi = mp_string(r1)
  local td = os.difftime(os.time(), t1)
  print("Calculation took "..td.." seconds.")
  local s1, s2 = pi:sub(1, 7), pi:sub(-40)
  print("pi = "..s1.."..."..s2)
  return pi
end

function output_pi ()
  local pi = calc_pi():sub(3) -- discard "3."
  -- pad spaces to an appropriate length
  local len = pi:len()
  if len % 100 ~= 0 then
    pi = pi..(" "):rep(100 - len % 100); len = pi:len()
  end
  -- divide into 10-digit chunks
  local cnk = {}
  for j = 10, len, 10 do
    table.insert(cnk, pi:sub(j - 9, j))
  end
  -- take 10 chunks to form a line
  for j = 10, #cnk, 10 do
    local s = table.concat(cnk, "\\blksep", j - 9, j)
    tex.print("\\oneline{"..s.."}")
  end
end
\end{execluacodeblock}
\begin{document}

\begin{flushleft}
\fontsize{32}{32}\selectfont
$\pi$ = 3.
\end{flushleft}
\newcommand*\blksep{\hspace{5pt plus.5pt minus.5pt}}
\newcommand*\oneline[1]{\noindent#1\par}
\par\directlua{output_pi()}

\end{document}
π=3. 1415926535 8979323846 2643383279 5028841971 6939937510 5820974944 5923078164 0628620899 8628034825 3421170679 8214808651 3282306647 0938446095 5058223172 5359408128 4811174502 8410270193 8521105559 6446229489 5493038196 4428810975 6659334461 2847564823 3786783165 2712019091 4564856692 3460348610 4543266482 1339360726 0249141273 7245870066 0631558817 4881520920 9628292540 9171536436 7892590360 0113305305 4882046652 1384146951 9415116094 …………(159ページまで続く)

計算時間は約 60 時間(Pentium D 2.80GHz/1.00GB RAM)

参考: なお、現在のところ、LuaTeX では LuaJIT 等の Lua 高速化技術を採用する予定はないようである。
参考: LuaJIT を試してみた。もはや TeX とは無関係な話 ;-)
計算の所要時間(単位:秒)
処理系 5万桁10万桁
LuaTeX beta-0.50.0 (W32TeX) 5292137
Lua5.1.4 (LuaBinaries) 3351342
LuaJIT 2.0.0-beta4(自前ビルド) 44182

付録:Lua と LaTeX の連携の命令の一覧

LuaTeX の機能

ここに挙げる「LaTeX 命令」は、実際には LuaTeX のプリミティブである。 最近の LuaLaTeX では、一部のプリミティブ (★印;LuaTeX で追加されたもので \directlua 以外のもの) は本来の名前でなく、前に luatex を付加した名前 (例えば \latelua\luatexlatelua) で定義されている。 LuaLaTeX の方針としては、luatextra パッケージ(後述) で用意される命令(ラッパー)を使うのが適当としている のだと思われる。 今のところ、このページでは luatextra を使わないことに しているので、 ixbase0 パッケージでは、ここに挙げた命令に限って、 元の名前でも使えるようにしてある。

  • \directlua{<テキスト>}[LaTeX 命令]: <テキスト> を Lua コードとして Lua インタプリタで実行する。 (参照
  • \directlua name <名前> {<テキスト>}[LaTeX 命令]: \directlua でチャンクの名前(エラー表示等で使用される) を指定した形式。
  • \latelua{<テキスト>}[★LaTeX 命令]: <テキスト> を Lua コードとして実行するが、 \directlua と異なり、 実行されるのは次にページ出力が行われた時であるる。 name <名前> 指定も可能である。
  • \detokenize{<テキスト>}[LaTeX 命令]: LaTeX の特殊文字や命令を含むテキストを、 「それを表現する文字列」に展開する。 (参照
  • \luaescapestring{<文字列>}[★LaTeX 命令]: 文字列を Lua の文字列リテラルで必要なエスケープ を施したものに展開する。 (参照

  • tex.print(<文字列> s, ...)[Lua 関数]: 引数の文字列を LaTeX の入力として渡す。 (参照
  • tex.write(<文字列> s, ...)[Lua 関数]: 引数の文字列を「verbatim な状態」で LaTeX の入力として渡す。

ixbase0 パッケージの機能

  • \begin{execluacodeblock}[<名前>]<テキスト>\end{execluacodeblock} [LaTeX 環境]: 中のテキストを「verbatim な状態」で読み取りそれを \directlua で実行する。 (参照<名前>\directluaname オプションの値。
  • \begin{execluacodeblock*}[<名前>]<テキスト>\end{execluacodeblock*} [LaTeX 環境]: \directlua の代わりに \latelua で実行する ことを除いて execluacodeblock 環境と同じ。
  • \execluacode[<名前>]{<テキスト>} [LaTeX 命令]: execluacodeblock の命令版として用意しているが、 要するに、\directlua name <名前> {<テキスト>} を直接使うのと同じ(verbatim ではない)で、 しかも完全展開可能(fully-expandable)でないという制限があるので、 こちらはあまり有用でない。 (オプション引数なしの \execluacodeblock{...} だけは完全展開可能である。)
  • \execluacode*[<名前>]<テキスト> [LaTeX 命令]: \execluacode\latelua 版。
  • \?\** は英数字でない ASCII 文字) [LaTeX 命令]: 「非特殊」な文字 * に展開される。 (参照
  • \??\** は英数字でない ASCII 文字) [LaTeX 命令]: 「非特殊」な文字列 \* に展開される。 (参照
  • ixbase.print(<文字列> s,...) [Lua 関数]: 引数の各文字列を改行文字毎に分割して tex.print() に渡す(文字列の末尾の改行は無視する)。 (参照
  • \ixstring{<テキスト>} [LaTeX 命令]: <テキスト> を Lua の文字列リテラル形式に 変換したものに展開。 例:\ixstring{Lua{\TeX}}"Lua\\TeX "
  • \ixescape{<テキスト>} [LaTeX 命令]: 引用符〈""〉を付加しないことを除いて \ixstring と同じ。 例:\ixstring{Lua{\TeX}}Lua\\TeX␣
  • \ixnumber{<テキスト>} [LaTeX 命令]: <テキスト> を Lua で数値として扱うコードに展開する。 例えば、\newcommand*{\valA}{2.0} \directlua{r = math.sqrt(\ixnumber\valA) のように使う。 単純に math.sqrt(\valA) とするのと異なり、 \valA が数値の形式でない場合はエラーになる (つまり変な動作をしない)。
  • \ixvcounter{<カウンタ名>} [LaTeX 命令]: LaTeX カウンタの現在の値を Lua で数値として扱うコードに展開する。 要するに (\arabic{<カウンタ名>}) と同じ。
  • \ixlength{<長さ変数名>} [LaTeX 命令]: 長さ変数の現在の値を Lua でグルー値(ユーザデータ型) として扱うコードに展開する。
  • ixbase.counter.<カウンタ名> [Lua 変数;読書可]: LaTeX のカウンタに Lua でアクセスする。
  • ixbase.length.<長さ変数名> [Lua 変数;読書可]: LaTeX の長さ変数に Lua でアクセスする。 LaTeX のグルー値(A plus B minus C のように伸縮をもつ長さ) は Lua ではユーザデータ型の値として扱われ、 インデクス width がそのグルーの自然長 (sp 単位の整数値)を表す。
  • ixbase.to_dimen(v) [Lua 関数]: v が数値ならば v 自身を返す。 v が文字列ならば、それを LaTeX の長さ表記 (単位付き数値)として解釈して sp 単位の整数値を返す。
  • ixbase.to_skip(v) [Lua 関数]: v が数値の場合、 自然長が v sp で伸縮がゼロのグルー値を返す。 v が文字列の場合、それを LaTeX のグルー値表記 として解釈して結果のグルー値を返す。
(まだ続く…?)