ジョブカン事業部のアドベントカレンダー 6 日目です.
ジョブカン採用管理の開発をやってる @Lru です. 今年は二番手を逃しました.
先月技術書典がありましたね. 今回は私もとある技術同人誌に寄稿させてもらっていました.
執筆は 3 年ぶり...? ブランクが凄まじい.
さて技術書を書くにあたって LaTeX は非常に強力なツールです.
今回は LaTeX について, 文書構築を楽にするためのテクニックを紹介します.
TL;DR
この記事で語る目標地点は次の条件を満たす執筆環境です.
- 1 コマンドで LaTeX 文書のビルドを完結できる
- ソースコードの変更を監視し, 自動的に再コンパイルを実行できる
- ファイル間の依存関係を自動的かつ正確に追跡し, 必要な処理のみを実行できる
この記事では LaTeX 本体の処理のみに限らず, Mermaid での図描画, PDF のグレースケール化など, LaTeX 外 / コンパイル後も含めて 1 つのビルドプロセスを構築します.
例としていくつかのツールを取り上げますが, 特定のツールに限らず他のツールを利用する場合も同様の記述で対応可能です.
鍵となる点は次の 4 つです.
-
latexmkを利用すると tex ファイルから pdf へのコンパイルは集約できる -
latexmkは依存ファイルの変更を検知して自動再コンパイルが実行できる -
latexmkが解決できる依存関係はカスタムで追加できる -
Makefileの依存関係の解決にlatexmkが利用できる
先んじて最終的な設定ファイルが見たい方はこちらからどうぞ.
最終的な設定ファイル例
#!/usr/bin/env perl
$silent = 1;
# Output directory
$out_dir = 'build';
$force_mode = 1;
# use LuaLaTeX
$lualatex = 'lualatex -interaction=nonstopmode -file-line-error -synctex=1 %O %S';
$pdflatex = $lualatex;
$pdf_mode = 4; # use lualatex for pdf
# Ignore PS and DVI
$postscript_mode = 0;
$dvi_mode = 0;
# use Biber
$bibtex_use = 2;
$biber = 'biber %O -u -U --output_safechars %B';
# use upmendex
$makeindex = 'upmendex %O -o %D %S';
# use make for custom deps
sub make_deps {
my ($base_name, $ext) = @_;
my $dest_file = "${base_name}.${ext}";
system("make ${dest_file}");
}
# d2 -> png
add_cus_dep('d2', 'png', 0, 'd2_to_png');
sub d2_to_png {
make_deps($_[0], 'png');
}
# mermaid -> png
add_cus_dep('mmd', 'png', 0, 'mmd_to_png');
sub mmd_to_png {
make_deps($_[0], 'png');
}
TARGETS = build/main.pdf build/main_mono.pdf
SOURCE = main.tex
DEPS_DIR = .deps
# loading dependency files
$(foreach file, $(TARGETS), $(eval -include $(DEPS_DIR)/$(file).d))
define escape-deps
sed -i.bak '/^[ \t]/s|:|\\:|g' $(1) && rm -f $(1).bak
endef
all: $(TARGETS)
# main build rule
build/main.pdf: $(SOURCE)
mkdir -p $(dir $(DEPS_DIR)/$@)
latexmk -deps-out=$(DEPS_DIR)/$@.d $<
$(call escape-deps,$(DEPS_DIR)/$@.d)
# monochrome conversion
%_mono.pdf: %.pdf
gs -sOutputFile=$@ -sDEVICE=pdfwrite -sColorConversionStrategy=Gray -sProcessColorModel=DeviceGray -dCompatibilityLevel=1.4 -dNOPAUSE -dBATCH $<
# image generation rules (d2 -> png)
images/%.png: images/%.d2
d2 --pad 10 -l elk $< $@
# image generation rules (mermaid -> png)
images/%.png: images/%.mmd
mmdc -i $< -o $@
.PHONY: preview watch clean
preview:
latexmk -pvc $(SOURCE)
watch:
latexmk -pvc -view=none $(SOURCE)
clean:
rm -rf build print $(DEPS_DIR)
LaTeX とは?
組版ソフトである TeX 上に構築された, 文書を構築するためのシステムです.
「こう表示しろ」という指定を記述することで, その指定に従った文書を構築できるツールです.
例えば次のようなコードを記述すると, A5 サイズの本としての形式で PDF 文書を構築できます.
\documentclass[book,paper=a5]{jlreq}
\usepackage[x11names]{xcolor}
\usepackage{amsmath}
\usepackage{listings}
\lstdefinestyle{codeblock}{
frame=tBLr,
backgroundcolor=\color{white},
showspaces=false,
showstringspaces=false,
}
\lstset{style=codeblock}
\begin{document}
\chapter{サンプル}\label{example}
サンプルの部の始まりです.
\LaTeX ではこのようにテキストを書き連ねて文書を構築していきます.
\section{数式サンプル}
equation 環境などを利用することで, 数式をキレイに盛った文書を構築できます.
オイラーの公式は数式としてキレイですよね.
\begin{equation}
e^{i\theta} = \cos\theta + i\sin\theta
\end{equation}
\section{コードサンプル}
ソースコードのみ特定の形式で表示する場合には listings パッケージなどを利用します.
\begin{lstlisting}[language=Python, caption=サンプルコード]
def hello_world():
print("Hello, World!")
\end{lstlisting}
\end{document}
最近の Qiita などの技術記事サイトでは MathJax や KaTeX が入っていますね. これらは LaTeX の機能の 1 つである数式表示機能を Web 上で提供するライブラリです. そのため「$x^3 + y^3 = z^3$ を満たす自然数の組は?」みたいなリッチな数式を記述できますし, LaTeX の正式記述 $\rm\LaTeX$ も表示できたりします.
LaTeX はこうした特殊な文字や記法も含めてキレイに文書を構築できるシステムであり, テキストファイルがベースとなっていてバージョン管理や共同作業にも適しているのが特徴です.
LaTeX で書くときのダルさ
LaTeX は文書作成において非常に強力なツールですが, その分だけ文書構築のために必要な手順が煩雑になりがちです. 例えば参考文献, 索引, 画像ありの tex ファイルを PDF に変換するまでに必要になる処理は次のようになっています.
lualatex main.tex
biber main
upmendex main.idx
lualatex main.tex
lualatex main.tex
同じコマンドが複数回ありますが, 全てのコマンドは異なる処理を実行しています.
技術書ともなれば, Mermaid や d2 などの図描画ツールを利用したい場合もありますね. この場合は更に合間に画像生成のためのコマンドを追加する必要があります.
lualatex main.tex
d2 --pad 10 -l elk images/diagram.d2 images/diagram.png
mmdc -i images/flowchart.mmd -o images/flowchart.png
biber main
upmendex main.idx
lualatex main.tex
lualatex main.tex
これらのコマンドは何らかのファイルの変更結果を確認する際, 必要に応じてそれっぽいものを毎回叩くことになります.
これらの煩雑な手段を覚えたり, 編集ファイルによって必要なコマンドを覚えたり, そもそもコマンドを叩いて再コンパイルしたりすること自体は極力したくないわけです. 積み重ねで結構な時間を持っていかれますので.
ということでここから本編です.
本記事で必要なもの
この記事では Linux 環境かつ Tex Live, make をインストールしている状態を前提に話を進めます. 他の環境の方は適宜読み替えてください.
tex ファイルから pdf を構築するにあたって, 今回の記事では次のツールの利用を想定します.
| ツール | 用途 |
|---|---|
| LuaLaTeX | 文書処理 |
| BibLaTeX + Biber | 参考文献構築 |
| upmendex | 索引構築 |
上記の構成であればマルチバイト文字にも対応, 日本語での執筆でも問題なく利用できます. 参考文献や索引が不要であれば LuaLaTeX だけで問題ありません.
必要な tlmgr (Tex Live ManaGeR) のパッケージは次のものです.
- collection-basic
- collection-latexrecommended
- collection-langjapanese
- latexmk
- biblatex
- biber
- upmendex
文書のビルドプロセスを組んでいく
LaTeX では基本的にメインとなる 1 つの tex ファイルを決め, そこから文書を構築していきます. 当記事では main.tex という名前でメインの tex ファイルがあるものとして記述します.
latexmk を設定する
latexmk は LaTeX 文書を構築するコマンドの 1 つです. このコマンドはあらかじめ設定ファイルに記述しておくことで, tex ファイルから PDF を得るまでの一連の流れを自動的に行ってくれます.
設定の例として次のようなファイルを用意します.
#!/usr/bin/env perl
$silent = 1;
# LaTeX out directory
$out_dir = 'build';
$force_mode = 1;
# use LuaLaTeX
$lualatex = 'lualatex -interaction=nonstopmode -file-line-error -synctex=1 %O %S';
$pdflatex = $lualatex;
$pdf_mode = 4; # use lualatex for pdf
# Ignore PS and DVI
$postscript_mode = 0;
$dvi_mode = 0;
# use Biber
$bibtex_use = 2; # cleanup bbl files
$biber = 'biber %O -u -U --output_safechars %B';
# use upmendex
$makeindex = 'upmendex %O -o %D %S';
詳細知りたい方はこちら
全ての情報はドキュメント上に記載がありますので, もっと知りたい方はこちらからマニュアル PDF を参照してください.
https://ctan.org/pkg/latexmk/?lang=en
| オプション | 説明 |
|---|---|
$silent = 1; |
処理ログの出力を抑制する |
$out_dir = 'build'; |
出力ディレクトリを build に指定する |
$force_mode = 1; |
エラー発生時でも処理継続 |
$lualatex = ...; |
LuaLaTeX 利用時のコマンド指定 |
$pdflatex = $lualatex; |
PDF 作成は LuaLaTeX で実行 |
$pdf_mode = 4; |
PDF 出力モードを LuaLaTeX に指定 |
$postscript_mode = 0; |
PS 出力を無効化 |
$dvi_mode = 0; |
DVI 出力を無効化 |
$bibtex_use = 2; |
クリーンアップ時に bbl ファイルも破棄する (再生成のため) |
$biber = ...; |
Biber 利用時のコマンド指定 |
$makeindex = ...; |
makeindex 利用時のコマンド指定 |
仔細な意図は以下の通り.
- 中間生成物がありえんほど出てきてめちゃくちゃ邪魔になるので出力は別ディレクトリ
- Debug ログまみれになるのが嫌なのでサイレントモード
- LuaLaTeX 利用の場合は PS や DVI はそもそも出力されないので無効化してよい
- 後は使うと決めたツールのコマンドを指定しているだけ
この設定ファイルを配置しておくことで, 次のように 1 コマンドを叩くだけで PDF への変換に必要な処理が全て実行されます.
latexmk main.tex
ここまででもう LaTeX 関連の処理は 1 コマンドに集約されました.
変更監視モードを利用してリアルタイムに再コンパイルする
latexmk にはソースコードの変更を検知して必要な処理のみを自動で実行し続ける -pvc オプションが存在します. これを実行することで, ソースコード変更のたびに必要な処理のみが実行され続け, PDF Viewer でその変更を即座に確認できます.
latexmk -pvc main.tex
Viewer が不要な場合は -view=none オプションを追加します.
latexmk -pvc -view=none main.tex
latexmk を利用する場合にはデフォルトで fls ファイルが自動的に生成されるようになっています. そこに指定した tex ファイルから参照したファイルが記録されるような形で依存関係が追跡されます.
LaTeX が対応していないファイルを処理対象にする
Mermaid などの図描画ツールのソースコードが tex ファイルから直接参照されることは (Mermaid 自体の紹介とかでなければ) ほぼないでしょう.
tex ファイルから参照されているのが生成後の画像である場合, デフォルトではソースコードは latexmkの依存関係に入ってきません. このため監視対象にもならず, 変更しても再コンパイルは実行されません.
また, これらのツールは LaTeX の処理系とは別途, 画像に変換するためのコマンドを自動的に実行する必要があります.
latexmk では add_cus_dep サブルーチンを利用することで, LaTeX 単体では対応していないファイルの依存関係を追加できます. .latexmkrcは単なる Perl スクリプトなので, 自由にサブルーチンを定義して処理を記述できます.
add_cus_dep('d2', 'png', 0, 'd2_to_png');
sub d2_to_png {
my ($slug) = @_;
my $src = "$slug.d2";
my $dst = "$slug.png";
system("d2 --pad 10 -l elk $src $dst");
}
add_cus_dep('mmd', 'png', 0, 'mmd_to_png');
sub mmd_to_png {
my ($slug) = @_;
my $src = "$slug.mmd";
my $dst = "$slug.png";
system("mmdc -i $src -o $dst");
}
この設定により, tex ファイル内で \includegraphics{images/sample.png} のような記述がある場合, 以下のように処理が自動的に実行されるようになります.
-
images/sample.d2が存在する場合-
d2コマンドでimages/sample.pngが生成される
-
-
images/sample.mmdが存在する場合-
mmdcコマンドでimages/sample.pngが生成される
-
追加された依存関係は変更監視モード時の変更検知対象として追加されます.
これにより Mermaid や d2 のソースファイルを変更した場合にも自動的に PNG が再生成され, その後必要な lualatex などのコマンドも自動的に再実行されるようになります.
Makefile と連動させる
latexmk で PDF ファイルを生成するまでのコンパイルの自動化は行えました.
ここから更に生成された PDF を加工する処理を考えてみます.
LaTeX で完結できない処理としてグレースケール処理があります. 文字列や Tikz 描画などはカラーモードで対応が効きますが, 取り込んだ画像の直接変換に LaTeX は対応していません.
このため実施したい場合は, GhostScript などを利用して PDF をグレースケール化する必要があります.
こういった処理を記述するなら Makefile が使いたいわけですが, 単純に次のように記述してしまうとせっかくlatexmkが解決してくれる依存関係をうまく活用できません.
build/main.pdf: main.tex
latexmk $<
build/main_mono.pdf: build/main.pdf
gs -sOutputFile=$@ -sDEVICE=pdfwrite -sColorConversionStrategy=Gray -sProcessColorModel=DeviceGray -dCompatibilityLevel=1.4 -dNOPAUSE -dBATCH $<
# ビルドされる
make build/main.pdf
# `main.tex`が変更されていない限りビルドされない
make build/main.pdf
この状態では make とは別に latexmk を実行する必要が生じてしまいます.
latexmk はこうした際に Makefile との連動も可能なように様々なサポートを提供しています. その 1 つが Tex ファイル処理中に認識した依存関係を全て出力するという-deps-outオプションです.
例えば次のような Makefile が記述できます.
TARGETS = build/main.pdf
SOURCE = main.tex
DEPS_DIR = .deps
$(foreach file, $(TARGETS), $(eval -include $(DEPS_DIR)/$(file).d))
define escape-deps
sed -i.bak '/^[ \t]/s|:|\\:|g' $(1) && rm -f $(1).bak
endef
all: $(TARGETS)
build/main.pdf: $(SOURCE)
mkdir -p $(dir $(DEPS_DIR)/$@)
# 依存関係の出力処理付きで latexmk を実行
latexmk -deps-out=$(DEPS_DIR)/$@.d $<
$(call escape-deps,$(DEPS_DIR)/$@.d)
%_mono.pdf: %.pdf
gs -sOutputFile=$@ -sDEVICE=pdfwrite -sColorConversionStrategy=Gray -sProcessColorModel=DeviceGray -dCompatibilityLevel=1.4 -dNOPAUSE -dBATCH $<
依存関係の自動追跡
latexmkの-deps-outオプションが指定された場合, その tex ファイルを処理するにあたって必要になったファイルを全て列挙した依存関係ファイルが出力されます.
#===Dependents, and related info, for main.tex:
build/main.pdf :\
/home/lru/.fonts/b/BIZUDPGothic_Regular.ttf\
/home/lru/.fonts/s/SourceSans3_VariableFont_wght.ttf\
...
images/archi.d2\
images/flow.mmd\
lrubook.sty\
main.tex
#===End dependents for main.tex:
このオプションを利用して出力されたファイルを, Makefile 内で -include の対象とすることで, 指定されたファイルのビルドについての依存関係が追加で読み込ませることができます.
依存関係のみのルールはビルドルールとはコンフリクトせず, 依存関係のみを追加できます.
# latexmk が認識した依存関係が全て読み込まれる
$(foreach file, $(TARGETS), $(eval -include $(DEPS_DIR)/$(file).d))
# 必要に応じて依存関係ファイルの内容をエスケープする
define escape-deps
sed -i.bak '/^[ \t]/s|:|\\:|g' $(1) && rm -f $(1).bak
endef
# build/main.pdf: main.tex + ビルドルールのみ. 実行されるビルドルールはこっちに記載されているもの
build/main.pdf: $(SOURCE)
mkdir -p $(dir $(DEPS_DIR)/$@)
# 依存関係の出力処理付きで latexmk を実行
latexmk -deps-out=$(DEPS_DIR)/$@.d $<
$(call escape-deps,$(DEPS_DIR)/$@.d)
初回のビルド時には依存関係ファイルが存在しませんが, PDF も構築されていない状態のため確定でビルドルールが実行されます. 初回の実行時に生成された依存関係ファイルにはその時依存していたファイル群が全て出力されます. ここから他のファイルを読み込む, 画像を埋め込む, 内容を編集するなどの更新があればビルドすべきという判定が行わるようになります.
こういった仕組みを利用することで, 特定の PDF を出力する際に必要な依存関係を正確に追跡しつつ, 追加で必要な処理を記述できます.
依存関係のエスケープ:
latexmkが出力する依存関係ファイルは, 依存関係にあるファイルのパス内にコロン(:)が含まれる場合でもそのままの内容を出力します.
この状態に陥るとそのまま Makefile に取り込めないため, Makefile 内で sed コマンドを利用して依存ファイルに関してのみコロンをエスケープする処理を追加しています.
他に動作しなくなるケースとして「空白」を含むパスもありますが, 完全に対応する事自体がそこそこ困難なので今回の記事では省略しています.
空白は可能な限り避けることを推奨します.
ここまでたどり着ければ, Makefile での LaTeX 系の依存関係の自己記述は不要になります.
依存関係は自動的に tex ファイルから追跡され, make build/main_mono.pdf は常に最新の内容で, 必要な処理のみを実行して PDF を出力してくれます.
latexmk から Makefile に処理を委譲する
ここは単なる整理フェーズです. 飛ばしても良いです.
ここまでで, LaTeX 処理の自動化と Makefile での PDF 後処理の取りまとめができました.
まず.latexmkrc内に画像の変換ロジックを記述しましたが, これは 1 行で終わる単純なものであったためです. 画像生成自体に複数行のコマンドが必要であったり, 画像も生成後に変換処理をかけたかったりする場合には Makefile で書いたほうがシンプルに書けるでしょう.
make から latexmk を呼び出せるように, latexmk からも make を呼び出せます. LaTeX 処理以外の処理は Makefile に記述しつつ, 依存関係だけ latexmk に認識させる形態が良いでしょう.
sub make_deps {
my ($base_name, $ext) = @_;
my $dest_file = "${base_name}.${ext}";
system("make ${dest_file}");
}
add_cus_dep('d2', 'png', 0, 'd2_to_png');
sub d2_to_png {
make_deps($_[0], 'png');
}
add_cus_dep('mmd', 'png', 0, 'mmd_to_png');
sub mmd_to_png {
make_deps($_[0], 'png');
}
# image generation rules (d2 -> png)
images/%.png: images/%.d2
d2 --pad 10 -l elk $< $@
# image generation rules (mermaid -> png)
images/%.png: images/%.mmd
mmdc -i $< -o $@
-use-makeオプション:
latexmk には -use-makeのオプションがあり, LaTeX の処理中に存在しないファイルの処理を行う最終手段として Makefile のビルドルールを呼び出すことができます.
こちらは依存関係を追跡することはなく, 存在しない場合にファイルを作成するのみで処理を終了します. latexmk 側での依存関係の追跡が働かず, 変更監視モードでの変更検知が働かなくなりますので, 明確に方法がわかっているのであれば add_cus_dep を利用したほうが良いでしょう.
変更監視モードで構築後処理
PDF 化後の処理については Makefile 側で記述しましたが, 変更監視モードでの自動再コンパイル時にもグレースケール化を実行したい場合は latexmk 側から実行する方法があります.
$success_cmd = 'make build/main_mono.pdf';
$success_cmd は本来コンパイル状況をわかりやすく表示したりするためのオプションですが, コマンド自体は任意のものを指定できます.
このコマンドでは依存関係の追跡は行われませんが, make 側で依存関係が正しく追跡されているため, 必要な場合にのみグレースケール化が実行されます.
まとめ
最終的にはこのような設定ファイルを記述できます.
- 最終成果物は
makeで常に最新のものを出力 - 作業中は
make watchまたはmake previewでリアルタイム追従コンパイル - 依存関係は
latexmkが自動的に追跡し, Makefile へと連携
#!/usr/bin/env perl
$silent = 1;
# LaTeX out directory
$out_dir = 'build';
$force_mode = 1;
# use LuaLaTeX
$lualatex = 'lualatex -interaction=nonstopmode -file-line-error -synctex=1 %O %S';
$pdflatex = $lualatex;
$pdf_mode = 4; # use lualatex for pdf
# Ignore PS and DVI
$postscript_mode = 0;
$dvi_mode = 0;
# use Biber
$bibtex_use = 2; # cleanup bbl files
$biber = 'biber %O -u -U --output_safechars %B';
# use upmendex
$makeindex = 'upmendex %O -o %D %S';
# use make for custom deps
sub make_deps {
my ($base_name, $ext) = @_;
my $dest_file = "${base_name}.${ext}";
system("make ${dest_file}");
}
# d2 -> png
add_cus_dep('d2', 'png', 0, 'd2_to_png');
sub d2_to_png {
make_deps($_[0], 'png');
}
# mermaid -> png
add_cus_dep('mmd', 'png', 0, 'mmd_to_png');
sub mmd_to_png {
make_deps($_[0], 'png');
}
TARGETS = build/main.pdf build/main_mono.pdf
SOURCE = main.tex
DEPS_DIR = .deps
# loading dependency files
$(foreach file, $(TARGETS), $(eval -include $(DEPS_DIR)/$(file).d))
define escape-deps
sed -i.bak '/^[ \t]/s|:|\\:|g' $(1) && rm -f $(1).bak
endef
all: $(TARGETS)
# main build rule
build/main.pdf: $(SOURCE)
mkdir -p $(dir $(DEPS_DIR)/$@)
latexmk -deps-out=$(DEPS_DIR)/$@.d $<
$(call escape-deps,$(DEPS_DIR)/$@.d)
# monochrome conversion
%_mono.pdf: %.pdf
gs -sOutputFile=$@ -sDEVICE=pdfwrite -sColorConversionStrategy=Gray -sProcessColorModel=DeviceGray -dCompatibilityLevel=1.4 -dNOPAUSE -dBATCH $<
# image generation rules (d2 -> png)
images/%.png: images/%.d2
d2 --pad 10 -l elk $< $@
# image generation rules (mermaid -> png)
images/%.png: images/%.mmd
mmdc -i $< -o $@
.PHONY: preview watch clean
preview:
latexmk -pvc $(SOURCE)
watch:
latexmk -pvc -view=none $(SOURCE)
clean:
rm -rf build print $(DEPS_DIR)
初手に書くものとしては少し重ためですが, 1 度書いてさえしまえば, 後の手間は大幅に減らせます.
今回の記事では例として Makefile への連携なども含めましたが, コンパイル後処理が不要であれば latexmk のみで完結させても良いでしょう.
必要に応じてカスタムし, 快適な執筆環境を目指す参考になれば幸いです.
宣伝
DONUTS では新卒中途問わず積極的に採用活動を行っています。
(情報アウトプットの好きな方がもっと増えたら会社発行の技術書とかも出せませんかね?)
ジョブカン事業部の求人はこちらです.
参考資料
公式マニュアルとかはここから.
本記事で記述している依存関係ファイルの読み込み処理とかがサンプル例としてあるくらいには makeとの連携を意識した作りになっています.
