ブラウザだけで TeX がコンパイルできる WebTeX サーバーを整備した
世の中にはブラウザだけで TeX がコンパイルできるサービスがいくつかある。
普段は Cloud LaTeX を好んで使っているのだけど、このサービスは保存してある TeX 文書が消えても困らないようにバックアップはとっとけとか、上限が 999個までサイズが〇〇まで、とかいった脅しや制限が窮屈に感じていたので、できれば Google Docs 上で TeX 文書を書いておいて、そこから PDF が取得できれば、と考えていた。
これを実現するためには、 Web 上で TeX 文書を PDF に変換できるサービスが使えればいいのだけど、結構な機密文書を作成するので、一般のサービスにはちょっと頼りづらい。
調べてみると、 TeX を使ってみよう のように、自前でサービスを作る方法も公開されていた。これは PHP を利用していて、構造も簡単だったので、これを真似て、 Go言語で TeX 文書から PDF に変換する Web サービスを作ってみた。
今回選んだサーバーは Debian10 。まずは apt で TeX 環境をインストール。
apt-get install texlive texlive-lang-cjk
コンパイルには ClutTeX を使用したかったので、 ClutTeX もインストール。 ClutTeX を使用するのは、余計なゴミファイルが残りにくいため。
apt-get install texlive-extra-utils
そして、以下のような簡単な Go言語プログラムを作成。
package main
import (
"crypto/rand"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"text/template"
)
func makeRandomStr(digit uint32) (string, error) {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// 乱数を生成
b := make([]byte, digit)
if _, err := rand.Read(b); err != nil {
return "", errors.New("unexpected error")
}
// letters からランダムに取り出して文字列を生成
var result string
for _, v := range b {
// index が letters の長さに収まるように調整
result += string(letters[int(v)%len(letters)])
}
return result, nil
}
func makeFile(data, filename string) {
file, err := os.Create(filename)
if err != nil {
log.Fatal(err) //ファイルが開けなかったときエラー出力
}
defer file.Close()
file.Write([]byte(data))
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("index.html"))
tmpl.Execute(w, nil)
})
http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
data := r.PostFormValue("data")
randomStr, _ := makeRandomStr(16)
filename := "./tmp/" + randomStr + ".tex"
// fmt.Println(data)
makeFile(data, filename)
pdffile := "./tmp/" + randomStr + ".pdf"
cmd := exec.Command("/usr/bin/cluttex",
"-e", "platex",
"-o", pdffile,
filename)
err := cmd.Run()
if err != nil {
fmt.Println("Command Exec Error: " + err.Error())
http.Error(w, err.Error(),
http.StatusInternalServerError)
return
}
_ = os.Remove(filename)
w.Header().Set("Content-Type", "application/pdf")
reader, err := os.Open(pdffile)
if err != nil {
http.Error(w, err.Error(),
http.StatusInternalServerError)
log.Fatal(err)
return
}
_, err = io.Copy(w, reader)
if err != nil {
http.Error(w, err.Error(),
http.StatusInternalServerError)
log.Fatal(err)
return
}
_ = os.Remove(pdffile)
})
http.ListenAndServe(":8080", nil)
}
このプログラムは、トップページで HTML を表示するので、表示用の index.html ファイルも以下の内容で作成。
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<form action="/post" method="post" target="_blank">
<textarea name="data" rows="60" cols="80">
\documentclass{jarticle}
\begin{document}
This is TEST.
\end{document}
</textarea>
<input type="submit" name="submit" value="PDF">
</form>
</body>
</html>
これで、 http://サーバー:8080/ にアクセスして、テキストエリアに TeX 文書を打ち込んで submit すれば、 PDF が得られるようになった。
ここまで出来たら、 Google Docs からは、もうコピペでこの Web サービスに流し込むのでいいんじゃね?という感じになってきたので、しばらくはこれで運用することにした。
更新:上の HTML ではなく、下の HTML を index.html として使うと、同じ画面で PDF の preview を見ながら編集できるようになります。
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style type="text/css">
.wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
}
.main, .pdf {
height: 95%;
padding: 20px;
}
.pdf {
width: 100%;
}
.tex {
height: 95%;
}
</style>
</head>
<body>
<div class="wrapper">
<section class="main">
<form action="/post" method="post" target="pdfframe">
<textarea name="data" cols="80" class="tex">
\documentclass{jarticle}
\begin{document}
This is TEST.
\end{document}
</textarea>
<br/>
<input type="submit" name="submit" value="compile">
</form>
</section>
<section class="pdf">
<iframe name="pdfframe" width="100%" height="100%"></iframe>
</section>
</div>
</body>
</html>
Discussion