🌊

ブラウザだけで TeX がコンパイルできる WebTeX サーバーを整備した

2020/09/30に公開

世の中にはブラウザだけで 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