Help us understand the problem. What is going on with this article?

GitBook MarkdownからPandoc+LaTeXで美しいPDFを生成する

More than 3 years have passed since last update.

はじめに

GitBookはMarkdownなどで記述したドキュメントをHTMLやPDF、EPUBなどへ変換するツールである。GitBookにはPDFを作成する機能が標準で備わっているが、それの組版のできがそれほどよくなかったので、今回はPandocとLaTeXなどを用いて美しい組版のPDFドキュメントを作成することを目指す。また、このドキュメントはTravis CI上でコンパイルができるようにする。
この記事にある内容を行うことで、次のようなPDFが作成できる。

https://dwango.github.io/scala_text_pdf/scala_text.pdf

また、今回の成果は次のリポジトリにある。

https://github.com/dwango/scala_text_pdf

対象としたドキュメント

今回はScala TextというドキュメントのPDF版を作成することにした。このドキュメントはプログラム言語Scalaの入門テキストであるが、Markdownに埋め込まれたScalaコードをtutというプログラムで実行していることが特徴である。

変換

次のような流れでPDFへ変換する。

  1. sbtでtutを実行し、Scalaコードの実行結果が埋め込まれたMarkdownを生成
  2. book.jsonをコピー
  3. 画像ファイルをコピー
  4. ソースコードをコピー
  5. SVG形式の画像をInkscapeでPDFへ変換
  6. PandocでLaTeXへ変換
  7. LuaLaTeXでbook.jsonを解析し、目次と本文を生成
  8. LuaLaTeXファイルのコンパイル

この流れは、次のシェルスクリプトにまとまっている。

https://github.com/dwango/scala_text_pdf/blob/master/setup.sh

sbtの実行

これは次のようにsbt tutをScala Textのルートディレクトリで実行するだけでよい。

$ cd ./scala_text
$ sbt tut
$ cd -

終了すると、./scala_text/gitbook/にScalaのコードを実行した結果が埋め込まれたMarkdownファイルが生成される。

book.jsonをコピー

book.jsonとはGitBookのメタ情報を表わすJSONファイルであり、Scala Textでは次のようになっている。

./scala_text/book.json
{
  "structure": {
    "readme": "INTRODUCTION.md",
    "summary": "SUMMARY.md"
  },
  "plugins": [
    "-search",
    "include-codeblock",
    "japanese-support",
    "footnote-string-to-number",
    "anchors",
    "regexplace"
  ],
  "pluginsConfig": {
    "regexplace": {
      "substitutes": [
        {"pattern": "<!-- begin answer id=\"(.*)\" style=\"(.*)\" -->", "flags": "g", "substitute": "<div><button type=\"button\" id=\"$1_show_answer_button\" style=\"display:block\" onclick=\"document.getElementById('$1').style.display='block'; document.getElementById('$1_show_answer_button').style.display='none'; document.getElementById('$1_hide_answer_button').style.display='block'; \">解答例を表示する</button><button type=\"button\" id=\"$1_hide_answer_button\" style=\"display:none\" onclick=\"document.getElementById('$1').style.display='none'; document.getElementById('$1_show_answer_button').style.display='block'; document.getElementById('$1_hide_answer_button').style.display='none'; \">解答例を隠す</button></div><div id=\"$1\" style=\"$2\">"},
        {"pattern": "<!-- end answer -->", "flags": "g", "substitute": "</div>"}
      ]
    }
  },
  "title": "Scala研修テキスト"
}

ここで重要なのは、目次を表わすstructure.summaryと序文を表すstructure.readmeである。このbook.jsonをLaTeXファイルから読み込み、structure.summaryにある目次ファイルを利用して本文を作成する。

SVG形式の画像をInkscapeでPDFへ変換

LaTeXではSVG形式の画像を読み込むことができないので、Inkscapeを用いてPDFへ変換しそれを読み込むことにする。次のようなシェルスクリプトでSVG画像を変換する。

for f in ./img/*.svg
do
  if [[ $f =~ \./img/(.*)\.svg ]]; then
    inkscape -z -D --file=`pwd`/img/${BASH_REMATCH[1]}.svg --export-pdf=`pwd`/${BASH_REMATCH[1]}.pdf --export-latex=`pwd`/${BASH_REMATCH[1]}.pdf_tex
  fi
doneB

PandocでLaTeXへ変換

GitBookのMarkdownを全てPandocを用いてTeXファイルへ変換する。

mkdir target
for f in ./scala_text/gitbook/*.md
do
  if [[ $f =~ \./scala_text/gitbook/(.*)\.md ]]; then
    cp $f ./target/
    pandoc -o "./target/${BASH_REMATCH[1]}.tex" -f markdown_github+footnotes+header_attributes-hard_line_breaks-intraword_underscores --latex-engine=lualatex --chapters --listings --filter=filter.py $f
  fi
done

まず、Pandocは多彩なMarkdown拡張に対応している。markdown_github+footnotes+header_attributes-hard_line_breaks-intraword_underscoresというのはGitHub Markdownに追加で次のようなものを有効にする。

+footnote
脚注を書ける
+header_attributes
# hoge { headers }のような、ヘッダ情報を書ける
-hard_line_breaks
改行をパラグラフの終了とみなさない
-intraword_underscores
単語内の_による強調を許可する

また、それ以外のオプションの意味は次のようになっている。

--latex-engine=lualatex
LaTeXのエンジンとしてLuaTeXを使う
--chapters
LaTeXのマークアップを`\chapter`からにする
--listings
ソースコードハイライティングにListingsを使う

そして、最後の--filter=filter.pyというのは、Pandocの汎用性の高さを示す機能のひとつであると考えている。これは、ソースファイルから一旦それのASTをJSON形式で出力し、それに対してfilterで指定したスクリプトでツリーを操作し、操作されたJSONを最終的にターゲットへ変換する、ということが可能になっている。Pandocのマニュアルから図を引用する。
http://pandoc.org/scripting.html

                         source format
                              ↓
                           (pandoc)
                              ↓
                      JSON-formatted AST
                              ↓
                           (filter)
                              ↓
                      JSON-formatted AST
                              ↓
                           (pandoc)
                              ↓
                        target format

このfilterでは今回次のような仕事を行う。

  • ListingsでScalaのスタイルを使うようにする
  • GitBookのファイルのインクルード1機能に対応する
  • 画像がURLの場合、ダウンロードする
  • 波ダッシュ(U+301C)を全角チルダ(U+FF5E)へ変換する2

このような処理をするPythonスクリプトは次のようになる。

filter.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
from pandocfilters import toJSONFilter, CodeBlock, RawBlock, Str, RawInline

def mkListingsEnvironment(code):
    return RawBlock('latex', "\\begin{lstlisting}[style=scala]\n" + code + "\n\\end{lstlisting}\n")

def mkInputListings(src):
    return RawInline('latex', "\\lstinputlisting[style=scala]{" + src + "}")

def mkIncludegraphics(src):
    return RawInline('latex', "\\includegraphics{img/" + src + "}")

def filter(key, value, fmt, meta):
    if key == 'CodeBlock':
        [[ident, classes, kvs], code] = value
        if 'scala' in classes:
            return mkListingsEnvironment(code)
    elif key == 'Link':
        [_, text, [href, _]] = value
        if text == [Str("include")]:
            return mkInputListings(href)
    elif key == 'Image':
        [_, _, [src, _]] = value
        if src.startswith("http"):
            fileName = src.split("/")[-1]
            os.system("cd img && curl -O " + src)
            return mkIncludegraphics(fileName)
    elif key == 'Str':
        return(Str(value.replace(u"〜", u"~")))

if __name__ == "__main__":
    toJSONFilter(filter)

LuaLaTeXでbook.jsonを解析し、目次と本文を生成

今回はLaTeXの処理系にLuaTeXを採用したため、この部分はLuaコードで次のように実装する。よいことに、LuaTeXではJSONパーザーが提供されているのでそれを使えばよく、目次のMarkdownは簡単な形式だったので今回はやや手抜きだが正規表現で抜き出すこととした。

require("lualibs-util-jsn.lua")

local target = "./target/"
local bookJsonFile = "book.json"

function read(file)
  local handler = io.open(file, "rb")
  local content = handler:read("*all")
  handler:close()
  return content
end

function readSummary(file)
  local handler = io.open(file, "r")
  local summary = {}

  for l in handler:lines() do
    local n = string.match(l, "%[.*%]%((.*)%.md%)")
    table.insert(summary, n)
  end
  handler:close()

  return summary
end

function getTeXFile(target, name)
  return target .. name .. ".tex"
end

function getFileName(f)
  return string.match(f, "(.*)%.md")
end

local bookJson = utilities.json.tolua(read(bookJsonFile))
local readmeFile  = getTeXFile(target, getFileName(bookJson['structure']['readme']))
local summaryFile = target .. bookJson['structure']['summary']

tex.print("\\frontmatter")
tex.print("\\input{" .. readmeFile .. "}")

tex.print("\\tableofcontents")

tex.print("\\mainmatter")
for k, v in pairs(readSummary(summaryFile)) do
  tex.print("\\input{" .. getTeXFile(target, v) .. "}")
end

LuaLaTeXファイルのコンパイル

LuaLaTeXファイルのコンパイルにはlatexmkとMakeを用いた。latexmkは簡単な設定ファイルを作成すると、それによって適切にLaTeXファイルをコンパイルするプログラムである。次のようなlatexmkの設定ファイルとMakefileを用いた。

latexmkrc
#!/usr/bin/env perl

$pdflatex = 'lualatex %O %S --interaction=nonstopmode';
$pdf_mode = 3;
$bibtex = 'pbibtex';
Makeflie
.PHONY : all clean

all : scala_text.pdf

clean :
    latexmk -C
    rm -r img target example_projects *.pdf *.pdf_tex book.json

scala_text.pdf :
    latexmk -pdf scala_text.tex

Travis CI上によるコンパイル

さて、これらでGitBook MarkdownからPDFを作れるようになったので、Travis CI上でコンパイルできるようにする。LaTeXをTravis CI上でコンパイルすることに関しては以前の記事を参照して欲しい。
なお、今回の.tarivs.ymlは次のようになっている。

https://github.com/dwango/scala_text_pdf/blob/master/.travis.yml

まとめ

このようにすることで、最終的にはある程度まともなPDFを作ることができた。もし、このPDFをよりよくする方法を思いついたり、知っていたりする場合は、この記事のコメントもしくはScala Text PDFのIssue、またはScala TextのIssueのどれでもよいので、どれかで教えて欲しいと思う。


  1. GitBookはMarkdownのリンクを用いて、ファイルをインクルードする機能がある。 

  2. 波ダッシュはLaTeXでは意図した見た目にならない。http://www1.pm.tokushima-u.ac.jp/~kohda/tex/texlive.html 

yyu
暗号やプログラム言語の記事をよく書きます。 I'm interested in Programming and Cryptography.
https://twitter.com/_yyu_
recruitmp
結婚・カーライフ・進学の情報サイトや『スタディサプリ』などの学びを支援するサービスなど、ライフイベント領域に関わるサービスを提供するリクルートグループの中核企業
http://www.recruit-mp.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした