楽天KoboやiBooksはMathMLに対応しており,レンダリングもかなりよくなっていますが,最大手のAmazonのKindleは対応しておらず(費用対効果の関係なのか,おそらく今後対応させる気はなさそう),リフロー型の数学っぽいKindle本を作るには,数式を画像化するのが不本意ながら定石といえます.
数式を画像にする上で,せめてベクトル画像のsvgを用いたいところですが,これもKindleは対応しているのかしてないのかよく分からない状況(iOSのアプリだと対応していない?)なので,おとなしくpngにするのが無難そうです.しかし,数百以上の数式を盛り込んだtexなどの文書をすべて画像化して,それをepub3などのフォーマットにするのを手作業でやるわけにもいかないので,オートメイトするのがこの記事の主旨です.
注意点
環境はmacOSです.変数命名のセンスやディレクトリなどの取り扱いが素人丸出しかもしれませんが,ご了承ください.あとでもう少しスマートに書き換えるかもしれません.
特別に用いたもの
- python 3.5
- pandoc
- texlive
- imagemagick
- Kindle Previewer(or kindlegen)
大まかな手順
- texで文書作成
- pandocでtex->html+mathjaxに変換
- mathjaxの部分をpngにする
- pandocでhtml->epub3に変換
- epub3を解凍して手直しし,再圧縮
- Kindle Previewerにepub3を突っ込んで,mobiを作成(kindlegenでもOK)
とりあえずコード
import subprocess, lxml.html, lxml.etree, imagesize, re, tempfile
def tex2html(source, target): #pandocのオプションは適宜変えてください
subprocess.call(['pandoc',
'-s',
'-t', 'html5',
'--mathjax',
'--css=stylesheet.css',
'-o', target, source])
def get_html(filename, encoding='utf-8', xml=False):
# xhtmlをパースするときは lxml.etree を,htmlをパースするときは lxml.html を使う
with open(filename, 'r', encoding=encoding) as f:
if xml == True:
html = lxml.etree.parse(f).getroot()
else:
html = lxml.html.parse(f).getroot()
return html
def write_html(html, filename, encoding='utf-8', xml=False):
# xhtmlをパースするときは lxml.etree を,htmlをパースするときは lxml.html を使う
if xml == True:
src = lxml.etree.tostring(html,
encoding=encoding,
xml_declaration=True,
doctype='<!DOCTYPE html>',
method='xml',
pretty_print=True).decode(encoding)
else:
src = lxml.html.tostring(html,
encoding=encoding,
doctype='<!DOCTYPE html>',
pretty_print=True).decode(encoding)
with open(filename, 'w', encoding=encoding) as f:
f.write(src)
def convert2png(filename, newfilename, convert=True):
html= get_html(filename)
textemplate = r'''\documentclass[a0paper, uplatex]{jsarticle}
\usepackage[dvipdfmx]{graphicx}
\usepackage[margin=1cm]{geometry}
\usepackage{amsmath,amssymb}
\pagestyle{empty}
\begin{document}
\scalebox{4}{\parbox{.25\linewidth}{MATH}}
\end{document}
'''
imgs = {} #同じ画像を作らないように,texコードと画像ファイルを記録しておく用
for span in html.xpath(r'//span[@class="math inline" or @class="math display"]'):
tex = span.text
if tex in imgs:
imgsrc = imgs[tex]
else:
imgsrc = r'math{0:04d}.png'.format(len(imgs)+1)
imgs[tex] = imgsrc
if convert == True:
with open('tmp.tex', 'w') as texf:
texf.write(textemplate.replace('MATH', tex))
subprocess.call('uplatex tmp.tex', shell=True)
subprocess.call('dvipdfmx tmp.dvi', shell=True)
subprocess.call('convert -trim tmp.pdf '+imgsrc, shell=True)
span.tag = 'img'
span.text = None
span.attrib['src'] = imgsrc
span.attrib['alt'] = tex
width, height = imagesize.get(imgsrc)
span.attrib['height'] = str(height)
write_html(html, newfilename)
def html2epub(source, target, css, cover):
subprocess.call(['pandoc',
'-t', 'epub3',
'--toc',
'--epub-chapter-level=1',
'--epub-stylesheet', css,
'--epub-cover-image', cover,
'-o', target, source])
def extract_epub(filename, tmpdir):
subprocess.call('mkdir {0}'.format(tmpdir), shell=True)
subprocess.call('unzip -d {0} {1}'.format(tmpdir, filename), shell=True)
def make_epub(filename, tmpdir):
subprocess.os.chdir(tmpdir)
subprocess.call(r'zip -0 ../{filename} mimetype;zip -XrD ../{filename} *'.format(filename=filename, tmpdir = tmpdir),shell=True)
subprocess.os.chdir('../')
def make_epub_for_kindle(name, css, cover, convert=True):
tmpdir = name + '_tmpdir'
if not subprocess.os.path.isdir(tmpdir):
subprocess.os.mkdir(tmpdir)
subprocess.os.chdir(tmpdir)
tex2html('../'+name+'.tex', name+'.html')
convert2png(name+'.html', name+'2.html', convert)
html2epub(name+'2.html', name+'0.epub', '../'+css, '../'+cover)
epubdir = tempfile.TemporaryDirectory(dir='./')
extract_epub(name+'0.epub', epubdir.name)
ns = {'xhtml': 'http://www.w3.org/1999/xhtml'}
for filename in [fn for fn in subprocess.os.listdir(epubdir.name) if re.match(r'ch[\d]{3}.xhtml', fn)]:
xhtml = get_html(epubdir.name+'/'+filename, xml=True)
xhtml.attrib['{http://www.idpf.org/2007/ops}lang'] = 'ja'
xhtml.attrib['lang'] = 'ja'
for img in xhtml.xpath('//xhtml:img', namespaces=ns):
height = int(img.attrib['height'])
img.attrib['style'] = 'height: ' + str(round(height/40, 2)) + 'em;'
del img.attrib['height']
write_html(xhtml, epubdir.name+'/'+filename, xml=True)
make_epub(name+'.epub', epubdir.name)
epubdir.cleanup()
subprocess.os.chdir('../')
各関数の説明
tex2html(source, target)
pandoc
を用いて,TeXファイルをHTML5に変換します.--mathjax
オプションによって,数式の部分は
<span class="math inline">\(e^{\pi i} = -1\)</span>
<span class="math display">\[e^{\pi i} = -1\]</span>
といった形で出力されます.この部分をlxml
で抽出して順次画像化します.
get_html(filename, encoding='utf-8', xml=False)
HTML文書をlxml
でパースし,ルート要素であるhtml
要素を返します.デフォルトではHTMLを想定していますが,XHTMLをパースする場合はxml=True
のオプションを追加します.
write_html(html, filename, encoding='utf-8', xml=False)
html
要素を引数にして,HTMLファイルを書き出します.
convert2png(filename, newfilename, convert=True)
HTML文書内の
<span class="math inline">\(e^{\pi i} = -1\)</span>
といった部分をimagemagickのconvert
で画像化し,
<img src="math0001.png" class="math inline" alt="\(e^{\pi i} = -1\)" height="30">
という形式のimg
要素に置き換えていきます.height
属性は,最後の数式の高さ調整で使うかなり重要な値です.これはimagesize
というpythonのライブラリで拾っています.数式が多いと変換に時間がかかります.convert=False
とすると,画像変換の手順をスキップします.手直しするときなど,再び変換する必要がない場合はFalse
にしておくと時間短縮になります.
ちなみに生成する画像は少し大きめに作っています(初期設定の4倍で多分40ptくらい).初期設定のまま画像を作ると,潰れて読めないので,大きめにして縮小するという手法を使っています.
ところで,texを画像化するのにあたってググってみると dvipng
というものがヒットしますが,日本語変換に対応させるのは面倒くさそうなので,pdfをimagemagickのconvert
でpngに変換する方法がお手軽です.
html2epub(source, target, css, cover)
数式の画像化が完了したHTML文書をpandocでepubに変換します.css
は埋め込むstylesheetのファイル名,cover
は埋め込む表紙画像のファイル名をいれます.
extract_epub(filename, tmpdir)
html2epub
で出来上がったepubを手直しするためにtmpdir
へ解凍します.epub3はzipファイルであることは割と有名だと思います.
make_epub(filename, tmpdir)
手直しの終わったファイル群をepubにまとめます(こちらを参考にしました:ターミナルを使ってEPUBに圧縮する).
make_epub_for_kindle(name, css, cover, convert=True)
一連の過程をワンタッチにしたものです.カレントディレクトリにあるname.tex
をソースとして,最終的にname.epub
ファイルを生成します.その過程で多くのファイルを生成するので,name_tmpdir
というディレクトリを作成し,そこに生成されるファイルを出力する仕様になっています.css
とcover
のファイルはname.tex
と同じディレクトリに配置してください.
手直しって?
一度pandocでepubを作ってから,解凍して手直しをし,再びepubにしています.この手直しで一体何をしているかというと,数式化した画像の高さ調整をしています.MathMLが対応しているのであればこの心配はいらないのですが,数式を画像化するとなると,幅または高さの調整が必要で,絶対指定(あるいは特に指定しない)をしてしまうと,epubビューアでフォントサイズを変更しても数式の大きさが変わらないという事態になります.これを回避するためには,
<img src="math001.png" style='height: 1.0em;'>
という風に,style
属性で相対指定をするしかないと思われます(他にもっといい方法があったら是非教えてください).その部分を設定しているコードを抜粋します:
for img in xhtml.xpath('//xhtml:img', namespaces=ns):
height = int(img.attrib['height'])
img.attrib['style'] = 'height: ' + str(round(height/40, 2)) + 'em;'
具体的には,実際のpng画像の高さを40で割った値に単位em
を付けています.場合によっては調節してください.
html2epub()
の段階で,style
属性を設定しておけば,わざわざ手直ししなくても済むのではないかと思うかもしれませんが,なんと,pandoc
でhtmlをepubに変換すると,img
要素のstyle
属性が削除されてしまうというお節介仕様があります.
まあ,そうは言っても,他にpandoc
が生成したepub
を手直しする要素は意外とあるので,この解凍して圧縮するという作業は無駄ではないと思います.例えば,html
要素にlang
属性やepub:lang
属性を付加するのはやはりこの手直しのタイミングでしかありません.他にもsection
要素のid
属性など,(人によっては)手直しした方が良い要素は結構あります.
stylesheet.css
class
属性がmath display
のものは,display: block
が必須でしょう.
img.math.display{
display: block;
margin: .5em auto; /*中央寄せ*/
}
img.math.inline{
margin-left: .2em;
margin-right: .2em;
}
余白は好みです.その他,見出しや行間などは各自で調整してください.
サンプル
\documentclass[uplatex]{jsbook}
\usepackage{amsmath,amssymb}
\begin{document}
\title{数の体系}
\chapter{自然数}
無限公理で保証されている集合で,もっとも小さいものを$\omega$とすると,$\emptyset\in\omega$であり,任意の$x\in\omega$に対して
\begin{align*}
\sigma: x\mapsto x\cup\{x\}
\end{align*}
とすることで,$\omega$から$\omega$への写像が定義される.
\section{ペアノの公理系}
$(\omega, \emptyset, \sigma)$ はいわゆるペアノの公理系を満足する.
\chapter{整数}
2つの自然数$m, n$ に対して, 対$(m, n)$を整数の$m-n$とみなす,というのが整数構成のヒントである.
\section{同値関係}
$(1, 0)$と$(2, 1)$は同じ整数とみるので,同値関係が必要になる.
\end{document}
make_epub_for_kindle('sample', 'stylesheet.css', 'cover.png')
おそらく
今回はTeXファイルをベースにしましたが,流行りのマークダウンからでもいけそうです.ただ,画像化するのにTeXを使うので,TeX環境は必須でしょう.