Markdownから素晴しいPDFをTravis CI上で作成することの考察

# はじめに

かつて僕は[GitBook MarkdownからPandoc+LaTeXで美しいPDFを生成する](https://qiita.com/yyu/items/712231009afe434127e6)という記事を書きました。その後、[Scala Text PDF](https://github.com/dwango/scala_text_pdf)の次のプロジェクトとして[“プログラミング言語Rust: 2nd Edition”の日本語版PDF](https://github.com/y-yu/trpl-2nd-pdf)の開発に関わりました。僕がTravis CIを利用してMarkdownからLaTeXの処理系を利用して最終的にPDFを作成するという作業をはじめてから——こればかりをずっとやっていたわけではありませんが——それでも2年ほど経ち、いろいろな知見がアップデートされてきました。2年前からの大きなアップデートとして、最も大きな知見はTravis CI上に生でPandocやTeXLiveを展開するのではなく、Dockerイメージを利用するようにしたなどがあります。また、最近はいろいろな技術文書がMarkdownで執筆されるようになり、これをPDFにしたいという需要もあるでしょう。
この記事ではまず、筆者のMarkdownからPDFを出力するということへの考えを述べます。次にMarkdownで書かれたソースコードをPandocで処理し、SVG形式の画像をInkscapeでLaTeXで取り扱える形にして、最終的にLaTeX文書をコンパイルするという工程がすべて可能なDockerイメージについて述べ、なぜTravis CIの上に生でインストールしないのか？についても説明します。ただ`.travis.yml`の説明といった具体的な技術については省略しましたので、そちらに興味がある方にはちょっと申し訳ないと思います。

この文章を読んでなにか分からないことがあったり、改善するべきことを見つけた場合は気軽にコメントなどでおしらせください。

# MarkdownからPDFを得るということ

ここでは主にPandocパート（MarkdownからLaTeX）とLaTeXパート（LaTeXからPDF）に分けて、それぞれがなぜややこしいのかについて説明します。この文章を読んでいる方の中には「MarkdownのソースコードをPandocで処理して得られたLaTeXファイルをTeX処理系でコンパイルすれば、それでそこそこなPDFが得られる」と考えておられる方もいるでしょう。実際にはこのように何も考えることがない世界が理想的なのですが、まずはこの神話に対して筆者の考えていることをいくつか述べたいと思います。

## MarkdownソースをPandocで処理するには？

### Pandocのバージョン

Pandocはアクティブに開発されていますが、Travis CIのUbuntuはどうしても古いOSを使わざるを得ず、そのパッケージマネージャーでは古いPandocしかインストールできません。一方で開発者のパソコンには最新のPandocがインストールされているため、CI環境と開発環境で差が発生してしまいがちです。CI環境にHaskellをインストールし、cabalといったHaskellのパッケージマネージャーで最新のPandocをインストールするといった方法がありますが、これはすでに面倒そうです。

### SVGファイルの処理

残念ながら、LaTeXの処理系の多くはSVGファイルをそのまま投入することができません。一方でWebの世界ではベクター形式の画像といえばまずSVGの利用が考えられると思います。このギャップを埋めるためには、[Inkscape](https://inkscape.org/ja/)といったソフトウェアでSVG形式の画像をPDFといったLaTeXに埋め込み可能な形式へ変換する必要があります。この時点で、LaTeXとPandocの他にInkscapeのインストールが必要である、ということになります。

### Markdownに埋め込まれたHTMLの処理

Markdownは恐らくHTMLへの変換を念頭に作られた形式ではないと思いますが、Markdownの中には、残念ながらHTMLへの変換を前提としていて、Markdownとして許可されていないHTMLが埋め込まれてしまったものがあります。たとえば、Markdownの画像を埋め込むマークアップにはサイズを指定する方法がありません。そういう時にHTMLの`<img>`マークアップを入れてしまいますが、このようなMarkdownソースをPandocに投入しても適切なLaTeXへは変換されません。従って、[Pandocfilter](http://pandoc.org/filters.html)という機構で処理する必要があります。PandocがHaskellで実装されている一方で、PandocfilterはPythonやJavaScriptなど様々なプログラム言語の処理系で利用できるため、それが幸か不幸かたとえばPandocfilterがPythonで実装されているならば、Pythonの処理系と適切なライブラリーも必要ということになります。

### Markdownの“改変可能性”の処理

Markdownは**拡張**ができるということを言う人がいますが、それは本当なのか？と思うことがあります。とある人が「Markdownは拡張が可能なのではなく**改変**（Markdownを名乗ったまま非互換な変更を入れること）が可能である」と述べていました。このような状況に対応するため、実はPandocにはたくさんのオプションがあり、どういったMarkdownを入力として与えるのかを教えることができます。たとえば脚注があるMarkdownで書かれている場合は、脚注を処理するようなPandocオプションを与える必要があります。この作業はソースとなるMarkdownとPandocのオプションの両方への精通を必要とし、この時点で簡単に変換できるとは到底言えないのではないかと筆者は思います。

## LaTeXソースをコンパイルするには？

### LaTeXの環境構築

実は、LaTeXの環境構築は難しくありません！大昔は相当大変だったと聞いていますが、今となっては[TeXLive](https://www.tug.org/texlive/)というLaTeXの関連ツールが全てはいったパッケージをインストールするだけで簡単に環境を構築できます。しかし残念なことに、今やLaTeX環境のインストールは簡単になった！という事実のプロパガンダはやや失敗していると筆者は思います[^latex]。いろいろな方にLaTeXの話をしても環境構築が大変だ、という認識が今でもあるように感じます。このことから、たとえ何かのドキュメントにLaTeXの環境構築方法について細かく記述したとしても、たぶん読まれずに終わるのではないかと思います。このことは、ドキュメントへのコントリビューターが減ること意味し、なにか組版のミスを発見したとしても直すに至ってくれる方がなかなか現れないという状況になると思います。

[^latex]: Twitterのアンケートを利用して調査をしたところ、次のような結果となりました。<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">LaTeXの環境構築は</p>&mdash; 吉村 優 / Yuu YOSHIMURA (@_yyu_) <a href="https://twitter.com/_yyu_/status/995258235606061057?ref_src=twsrc%5Etfw">2018年5月12日</a></blockquote>従って、2人に1人よりも多い人数がLaTeXの環境構築を難しいと感じているようです。この調査は筆者のフォロワーに偏りがあるでしょうから、参考程度であろうと思います。


### LaTeXのパッケージマネージャーの仕様

RubyやPythonといったプログラム言語に慣れている方々にとって、パッケージ（ライブラリー）の依存解決というのは通常パッケージマネージャーが行うという認識があると思います。ところがLaTeXのパッケージマネージャーは、歴史的な経緯により依存解決という機能を持ちません。従って、ありとあらゆるパッケージをインストールするか、あるいは手で依存を解決する必要があります。全てのパッケージをインストールするのは、手元のパソコンならばよさそうですがCI環境となるとそうはいかず、これも事を複雑にします。

### フォントのインストール

LaTeXでややこしいことの一つとして、フォント周りを挙げる方はそれなりにいるのではないでしょうか。pLaTeXといった処理系ではかつて、フォントを変更するといった作業は大変ややこしいものでした。しかし綺麗なPDFに綺麗なフォントは不可欠ですから、LaTeXをコンパイルする環境には狙ったフォントが確実にインストールされていてほしいです。

# MarkdownをPDFへコンパイルする方法の概要

上述したいようなもろもろのことを考慮して手元に開発環境を整えてくれる方は本当に少ないだろうということで、筆者はこれらが全てはいったDockerイメージを用意するという方法が現状で最もよいという結論に至りました。このような手順でTravis CI上でコンパイルを行います。

1. DockerHubからlatestのイメージをpullする
2. 同梱されている`Dockerfile`からイメージをビルドする
    - このイメージにはPandoc, Inkscape, TeXLiveそしてフォントといった全てがはいっている
    - (1)のイメージをキャッシュとして利用するので、多くの場合はそれほど時間がかからない
3. Dockerイメージを利用してPandoc、Inkscape、LaTeXの処理を実行
4. 成果物のPDFと(2)のDockerイメージをDockerHubへアップロード

この方法の詳細については次のリポジトリを見ることである程度分かると思います。

- https://github.com/y-yu/trpl-2nd-pdf

特に、Dockerイメージを作成している`Dockerfile`は次の場所にあります。

- https://github.com/y-yu/trpl-2nd-pdf/blob/master/docker/Dockerfile

# FAQ

ここでは勝手にFAQを作成してみました。たぶんこういう質問が来るという気持ちです。

## Tarvis CIの上でDockerを利用すると、`sudo`を使わざるを得ないのでは？

その通りです。筆者はかつてはそれを恐れて`sudo`を使わずにTravis CI上にPandoc, Inkscape, TeXLiveを全て構築していましたが、比較してみたところほとんど速度が変らないという結論になりました。
そして、Dockerイメージを利用することで筆者といったLaTeXなどにそこそこ詳しい人以外でもコンパイルできる可能性が高まるというメリットもあることを考えると、この方法が最も良いという判断に至りました。

## Dockerイメージのサイズが巨大になってしまうのでは？

現在、利用しているDockerイメージは次になります。

- https://hub.docker.com/r/yyupw/trpl-2nd-pdf/

サイズは600MBくらいとやや大きいですが、それでもまあそこそこの大きさに収まっていると思います。注意が必要なことは、このイメージは特定のLaTeXドキュメントをコンパイルするために必要なパッケージを手で特定し、それのみをインストールすることで軽量化しています。従ってこのDockerイメージであらゆるLaTeXドキュメントをコンパイルすることはできません。

## Pandocfilterは言うほど巨大なのか？

これは文書によると思いますが、例としてプログラミング言語Rust: 2nd Editionで利用されているPandocfilterは次の2つです。

- https://github.com/y-yu/trpl-2nd-pdf/blob/master/python/filter.py
- https://github.com/y-yu/trpl-2nd-pdf/blob/master/python/fix_headers.py

これらは合わせてだいたい110行程度あり、そこそこ大きいといっていいのではないかと筆者は思います。そもそもこれらでは何をやっているのか？ということを述べましょう。

- `<img>`といったHTMLを適切なLaTeXコマンドへ変換する
- 参照[^ref]が壊れる部分を適切に修正する
- インターネットの画像を参照している部分を抽出し、それをダウンロードする
- LaTeXでは処理できない“壊れたUTF-8文字列”を消す
- Rustのコードハイライティングが適切に行われるようにする

他にも細々したことをやっていますが、だいたいこんな感じです。

[^ref]: LaTeXには参照といって、たとえば「図1.1」のような番号を自動で付ける機能がありますが、Pandocにこの参照付けをまかせると日本語の関係でおかしなことになるため、ここではPandocfilterで処理しています。

## Pandocに与えているオプションを見せてほしい

プログラミング言語Rust: 2nd Editionの日本語版PDFでは次のようなオプションを与えています。

```
FILENAME="$BASE" pandoc -o "./target/$BASE.tex" -f markdown_github+footnotes+header_attributes-hard_line_breaks \
         --pdf-engine=lualatex --top-level-division=chapter --listings $FILTERS $f
```

- https://github.com/y-yu/trpl-2nd-pdf/blob/450ba0f23198edd044e48ae031c0f3525f5823c6/setup.sh#L43-L44

この`markdown_github+footnotes+header_attributes-hard_line_breaks`の部分が、ソースのMarkdownに関してそれがどういう改変なのかを指定する部分です。興味がある方は[Pandocのドキュメント](https://pandoc.org/MANUAL.html)でこれらの意味を調べてみるとよいでしょう。

## DockerイメージのベースにAlpineを使っているが、Ubuntuなどでいいのでは？

たしかにここまでいろいろなパッケージを入れたならば、もはやAlpineでなくてもよいというのはもっともだと思います。そのへんはまだ筆者があまり洗練されていないためのもので、これからベストを模索していきたいと思います。

# まとめ

MarkdownからPDFを作成する方法についていろいろ書かせていただきました。僕自身はドキュメントづくりに価値を感じてやっていますし、文書における組版というのは重要であって、読めればいいというものではないと考えています。これからもいろいろな文書の組版に関わっていけたらいいと思いますので、もしMarkdownで作ったOSSのドキュメントがあり、それのPDFを作成したいと考えている方がいましたら、ぜひ[“プログラミング言語Rust: 2nd Edition”の日本語版PDF](https://github.com/y-yu/trpl-2nd-pdf)を参考にしていただき、そして分からないことなどがありましたら気軽に僕に質問してほしいと思います。常に返信するとお約束はできませんが、可能な限り貢献したいと思います。