なかがわ電機サービス
homeに戻る ブログ
2020-12-18

DiscordでLaTeXを書きたい

工学研究部のドベカレ(UEC koken Advent Calendar 2020)の記事です。

昨年、私が浪人していた頃、はこだて未来大学に在籍している私の友人にとあるLT会に誘われました。夏休みの自由研究発表といった形で行われた1年生のLT会で、そこで私は初めてdiscordのbotとpythonに出会いました。以来、自作の謎botを高校のオタク友人らのサーバで動かしていました。そしてつい先日、金曜の夕方vcをしていたところ…

友人「お前のbotでlatex書けねえの?」 血迷った私「できるんじゃね?日曜までに作るわ!」

他人事ですよね。作るのは私なんですけど!!そんなこんなでtex文書をコンパイルしてpngで返すbotを作りました。 動作環境はlubuntu18.04、python3.7.6です。ちなみに金曜日なので部室で部会に参加していたのですが、部屋にいた部員の皆さんの反応は「要らなくね?笑」一色でした。まあ私も要らないと思います。それでは作り始めます。とりあえず構想はこんな形です。

メッセージうけとる ↓ 何かに渡す ↓ 生成されたpngを送り返す

できました。

if message.content.startswith("latex"):
    text = message.content
    text = text[8:-3]

    imgpath = "./latexondiscord/image.png"
    #生成される画像のパス

    formerupdatetime = os.path.getctime(imgpath) #画像の更新時刻を確認
    latexmessage(text) #画像作成(「何か」の部分)
    nowupdatetime = os.path.getctime(imgpath) #画像の更新時刻を確認
    if formerupdatetime == nowupdatetime: #画像の更新がされていないとき
        await message.channel.send("Syntax error!!")
    else: #画像がきちんと更新されているとき
        await message.channel.send(file=discord.File(imgpath))

では「何か」を作っていきます。計画は次のような感じです。

テキストを受け取る ↓ 用意してあるテンプレの\begin{document}〜\end{document}の中に入れる ↓ なんか ↓ できた画像のパスを返す

「なんか」の部分の候補はいくつかありましたが、今回は

platex→dvipdfmx→pdfcrop→mutool

という形にしました。pdfcropで余白を切り取って、mutoolでpngにしています。dviからいきなりpngにしてしまうやり方など、他にも色々あるようですがこれで動いたのでヨシとしました。

「なんか」を書きました。

#!/bin/sh
cd ./latexondiscord &&
yes x | platex ./latex-on-discord.tex >/dev/null 2>&1 &&
dvipdfmx ./latex-on-discord.dvi >/dev/null 2>&1 &&
pdfcrop --margins "5 5 5 5" ./latex-on-discord.pdf ./croped.pdf >/dev/null 2>&1 &&
mutool draw -r 600 -o ./image.png ./croped.pdf >/dev/null 2>&1

yes xの部分はエラーが出てもplatexの処理が止まらないようにgottiくんがつけてくれました。ありがとうございます。

さて、問題はここからです。みなさんご存知のようにlatexではバックスラッシュを用いますが、これをpythonで文字列として扱おうとすると\bや\nなどがうまく書けません。そこで次のようにしました。

def latexmessage(message):
    text = repr(message) #バックスラッシュを2倍にしてくれるらしい
    text = text[1:-1] #reprでつけられた''の囲いを外す

    #テンプレを開く
    with open("./latexondiscord/latex-on-discord-template.tex") as file:
        nakami = file.read()
        #文章を挿入
        onew = re.sub("ここに文章を挿入",text, nakami,flags=re.DOTALL)

    #コンパイルする方のファイルを開く
    with open("./latexondiscord/latex-on-discord.tex", mode="w") as file:
        file.write(onew) #書き込む

    #シェルスクリプトにコンパイルしてもらう
    subprocess.run('./latexondiscord/textopng.sh')

あらかじめ用意したテンプレートには「ここに文章を挿入」と記した部分が\begin{document}〜\end{document}で囲われているのでそこを置換して別のファイルに保存します。それでは全体像です。

    import discord
    import re
    import subprocess
    import os

    # 自分のBotのアクセストークン
    with open("bot.token") as tokenfile:
        TOKEN = tokenfile.read()

    with open("./latexondiscord/image.png", mode="w") as file:
        file.write("") #仮の画像ファイル

    # 接続に必要なオブジェクトを生成
    client = discord.Client()

    # 起動時に動作する処理
    @client.event
    async def on_ready():
        # 起動したらターミナルにログイン通知が表示される
        print('LaTeX botがログインしました')


    # メッセージ受信時に動作する処理
    @client.event
    async def on_message(message):

        # メッセージ送信者がBotだった場合は無視する
        if message.author.bot:
            return

        #latex文法
        if message.content.startswith("```latex"):
            text = message.content
            text = text[8:-3]
            #```latex〜```を取り外す

            imgpath = "./latexondiscord/image.png"
            #生成される画像のパス

            formerupdatetime = os.path.getctime(imgpath) #画像の更新時刻を確認
            latexmessage(text) #画像作成
            nowupdatetime = os.path.getctime(imgpath) #画像の更新時刻を確認
            if formerupdatetime == nowupdatetime: #画像の更新がされていないとき
                await message.channel.send("Syntax error!!")
            else: #画像がきちんと更新されているとき
                await message.channel.send(file=discord.File(imgpath))


    def latexmessage(message):
        text = repr(message) #バックスラッシュを2倍にしてくれるらしい
        text = text[1:-1] #reprでつけられた''の囲いを外す

        #テンプレを開く
        with open("./latexondiscord/latex-on-discord-template.tex") as file:
            nakami = file.read()
            #文章を挿入
            onew = re.sub("ここに文章を挿入",text, nakami,flags=re.DOTALL)

        #コンパイルする方のファイルを開く
        with open("./latexondiscord/latex-on-discord.tex", mode="w") as file:
            file.write(onew) #書き込む

        #シェルスクリプトにコンパイルしてもらう
        subprocess.run('./latexondiscord/textopng.sh')

    # Botの起動とDiscordサーバーへの接続
    client.run(TOKEN)

githubにあげておきました。 https://github.com/ybasviel/latex-on-discord

使ってみた。(このころは文字サイズを変更していたので少し大きいです。)

{% asset_img 001.png twitterで「僕がつくりました」ってイキっている画像 %}

このセリフ、気持ち良すぎて絶頂しました。