pukiwiki/数式プラグイン の履歴(No.16)
更新- 履歴一覧
- 差分 を表示
- 現在との差分 を表示
- ソース を表示
- ソフトウェア/pukiwiki/数式プラグイン へ行く。
pukiwiki 用の数式プラグイン†
MathJax を使うバージョンは、これとは別に下の方にあります。
pukiwiki に TeX で書いた数式を入れられるようにするプラグインです。
特に、数式をインライン svg として表示できるようにしたので、 対応するブラウザでは拡大時や印刷時に高い品質が得られます。
現在、ユーザーエージェントに Mozilla/5 を含む場合にのみ、 インライン svg で数式を表示するようになっています。
それ以外では png ファイルとして数式を表示します。
使用例†
&math(y=ax^2+bx+c);
&math(x=\frac{-b\pm\sqrt{b^2-4ac}}{2a});
&math(\left\{\begin{array}{c@{\ }l@{\ \ \ \ \ \ \ \ \ \ \ \ }l}\displaystyle \ROT\bm E+\frac{\PD \bm B}{\PD t}&=\bm 0&\MARU{1}\vspace{2mm} \\\DIV \bm B &= 0&\MARU{2}\vspace{2mm}\\\displaystyle\frac{1}{\mu_0}\ROT \bm B -\varepsilon_0 \frac{\PD \bm E}{\PD t} &= \bm i&\MARU{3}\vspace{2mm}\\ \varepsilon_0\DIV \bm E &= \rho&\MARU{4}\\\end{array}\right .);
※長い数式も1行で書かなければならないのがつらいところ
&math(\left\{\begin{array}{c@{}c@{}c@{}c@{}c@{}c@{}c} a_{11}x_1&+&a_{12}x_2&+\cdots+&a_{1n}x_n&=&b_1 \\ a_{21}x_1&+ &a_{22}x_2&+\cdots+&a_{2n}x_n&=&b_2 \\ \vdots&&\vdots&&\vdots &&\vdots\\ a_{m1}x_1&+&a_{m2}x_2&+\cdots+&a_{mn}x_n&=&b_m \\ \end{array}\right .);
ソースコード†
math.inc.php
LANG:php(linenumber) <?php // // pukiwiki用 数式プラグイン (math.inc.php) // Copywrite 2011 Osamu Takeuchi <osamu@big.jp> // // [履歴] // 2011.05.04 初期リリース // // [インストール] // ソースファイルを (pukiwiki)/plugin/math.inc.php として保存 // // [設定] // MATHIMG_DIR に画像ファイルが作成される // 標準では (pukiwiki)/mathimg に画像ファイルが作成される // function user_macros() に TeX マクロを記述できる // function use_svg() でインライン svg を使うかどうかを判断する // // [使い方] // &math(y=ax^2+bx+c); のように、&math(); 中に TeX ソース形式で // 数式を記述する // TeX ソースとしては amsmath, amssymb, bm パッケージを利用し、 // equation + split 環境で評価される // //math mode image repository define("MATHIMG_DIR", "./mathimg"); function use_svg() { return strstr( getenv("HTTP_USER_AGENT"), "Mozilla/5" ) != false; } function user_macros() { return '\newcommand{\MARU}[1] {{\ooalign{\hfil#1\/\hfil\crcr\raise.167ex\hbox{\mathhexbox20D}}}} \newcommand{\D}[0]{{\rm d}} \newcommand{\PD}[0]{\partial} \newcommand{\ROT}[0]{{\rm rot}\,} \newcommand{\DIV}[0]{{\rm div}\,} \newcommand{\GRAD}[0]{{\rm grad}\,} '; } function math2tex($math){ return '\documentclass[12pt,fleqn]{jarticle} \pagestyle{empty} \usepackage{amsmath,amssymb,bm} \begin{document} \setlength{\hoffset}{-1in} \setlength{\voffset}{-1in} \setlength{\topmargin}{0mm} \setlength{\headheight}{0mm} \setlength{\headsep}{0mm} \setlength{\oddsidemargin}{0mm} \setlength{\mathindent}{0mm} ' . user_macros() . '\begin{equation*} \begin{split} ' . $math . '\end{split} \end{equation*} \end{document}'; } class MinMax { var $min = null; var $max = null; function update($x){ if (is_null($this->min)) { $this->min = $x; $this->max = $x; } else if ($x<$this->min) { $this->min = $x; } else if ($this->max<$x) { $this->max = $x; } } function min() { return $this->min; } function max() { return $this->max; } } function calc_bounding_box($math, $srch) { $xmm = new MinMax(); $ymm = new MinMax(); $converted = array(); while(!feof($srch)) { $line = fgets($srch); if (preg_match("/<g /", $line, $matches)) { $converted[] = preg_replace("/ transform=\"[^\"]*\"/", "", $line); } else if (preg_match("/<\/g>/", $line, $matches)) { $converted[] = $line; } else if (preg_match("/<line (.*)/", $line, $matches)) { $params = $matches[1]; if (preg_match("/x1=\"([^\"]+)\"/", $params, $matches)) $xmm->update($matches[1]); if (preg_match("/y1=\"([^\"]+)\"/", $params, $matches)) $ymm->update($matches[1]); if (preg_match("/x2=\"([^\"]+)\"/", $params, $matches)) $xmm->update($matches[1]); if (preg_match("/y2=\"([^\"]+)\"/", $params, $matches)) $ymm->update($matches[1]); $converted[] = $line; } else if (preg_match("/<polygon points=\"([^\"]+)\"/", $line, $matches)) { foreach (explode(" ", $matches[1]) as $p) { foreach (explode(",", $p) as $i => $v) { if ($i % 2 == 0) { $xmm->update($v); } else { $ymm->update($v); } } } $converted[] = $line; } else if (preg_match("/<path d=\"([^\"]+)\"/", $line, $matches)) { foreach (explode(" ", $matches[1]) as $c) { if (preg_match("/^(M|L|C|S|Q|T)([0-9\\.,]+)/", $c, $matches)) { foreach (explode(",", $matches[2]) as $i => $v) { if ($i % 2 == 0) { $xmm->update($v); } else { $ymm->update($v); } } } else if (preg_match("/^H([0-9\\.]+)/", $c, $matches)) { $xmm->update($matches[1]); } else if (preg_match("/^V([0-9\\.]+)/", $c, $matches)) { $ymm->update($matches[1]); } } $converted[] = $line; } } return array( $xmm, $ymm, implode($converted) ); } function format_svg($math, $srch) { list( $xmm, $ymm, $converted ) = calc_bounding_box($math, $srch); $margin= 0.02; $scale= 1.35; $math_encoded = htmlspecialchars($math); $width = ( $xmm->max() - $xmm->min() + $margin*2 ) * $scale; $height = ( $ymm->max() - $ymm->min() + $margin*2 ) * $scale; $transx = - ( $xmm->min() - $margin ); $transy = - ( $ymm->max() + $margin ); return "<svg width=\"{$width}\" height=\"{$height}\" " . "xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" " . "style=\"vertical-align:middle;\" >\n" . "<title>{$math_encoded}</title>\n" . "<g transform=\"scale({$scale},-{$scale}) translate({$transx}, {$transy})\">\n" . "{$converted}\n" . "</g></svg>\n"; } function create_image_files($math, $image_dir, $image_png, $image_svg) { if(!file_exists($image_dir)) mkdir($image_dir, 0770, true); $tmp_dir = sys_get_temp_dir(); $tmp_base = $tmp_dir . "/pukiwiki_math_plugin" . getmypid(); $tmp_tex = $tmp_base . ".tex"; $tmp_dvi = $tmp_base . ".dvi"; $tmp_any = $tmp_base . ".*"; try { # create tex src $texh = fopen($tmp_tex, 'w'); fwrite($texh, math2tex($math)); fclose($texh); # convert tex to dvi system("jlatex -interaction=nonstopmode -output-directory={$tmp_dir} {$tmp_tex} > /dev/null"); # create png system("dvipng -q -bdpi 1440 -o {$image_png} {$tmp_dvi} > /dev/null"); # create svg $pp = popen("dvips -q -Ppdf -E {$tmp_dvi} -o - " . "|pstoedit -q -f plot-svg -dt -sclip -ssp - -", 'r'); $svg = format_svg($math, $pp); pclose($pp); # output formatted svg $svgh = fopen($image_svg, "w"); fwrite($svgh, $svg); fclose($svgh); } catch(Exception $e) { # cleanup temporary files system("rm " . $tmp_any); throw $e; } } function math2html($math) { # if(strlen(encode($math))<40){ # $image_base = encode($math); # }else{ $image_base = sha1($math); # } $image_dir = MATHIMG_DIR . "/{$image_base[0]}/{$image_base[1]}"; $image_png = "{$image_dir}/{$image_base}.png"; $image_svg = "{$image_dir}/{$image_base}.svg"; if(!file_exists($image_svg)) create_image_files($math, $image_dir, $image_png, $image_svg); if(use_svg()) { $fh = fopen($image_svg, "r"); $svg = fread($fh, filesize($image_svg)); fclose($fh); return $svg; } else { list($w, $h) = getimagesize( $image_png ); $math_encoded = htmlspecialchars($math); if($w > 0 && $h > 0){ return "<img src=\"{$image_png}\" alt=\"{$math_encoded}\" " . "width=\"{$w}\" height=\"{$h}\">"; }else{ return "<img src=\"{$image_png}\" alt=\"{$math_encoded}\">"; } } } function plugin_math_convert() { return plugin_math_inline(); } function plugin_math_inline() { $aryargs = func_get_args(); $math = join(",", $aryargs); $math = rtrim($math, ","); //remove extra comma at the end. $html = math2html($math); return $html; } ?>
本当は†
日本語も入れられるようにしたいところなんだけど、 svg への変換がうまく行かず止まってる。
あと とか打つと文字間がおかしいような気がするんだけど、 これも保留中。(文字幅情報が真四角を基準に取られているため? f の周りに空白が・・・)
MathJax†
MathJax というのがあるらしい。
MathJax http://www.mathjax.org/
MathJaxの使い方 http://genkuroki.web.fc2.com/
MathJax の導入試験 http://www.str.ce.akita-u.ac.jp/~gotou/mathjax/
こっちに切換えることを検討したいかも。
現在、MathJax を使用する形にして様子見中†
\bm が見にくい点が、線形代数をやるのに致命的か?
いろいろ変換をミスっているところがありそうなので、見直さなければ。
これ、重〜い!†
最新の PC ならまだ良いけれど、タブレットで スピントロニクス理論の基礎/8-10 なんかを開くと分単位で待たされる?!
サーバーの負担は軽くなるし、見た目はまあ悪くないし、もうしばらく経ったら行けそうだけど、 どうするかなあ・・・
非互換性†
- \ooalign → 丸付文字は代わりに \enclose を使えば良さそう
- \vspace
- \bra, \ket, \braket : from braket.sty
- 不等号の直後に変数が入って <t のようになっているときにうまく変換されない?
- プラグインからの出力を htmlspecialchars に通していなかったのが原因だった
- 互換性情報 http://www.wikidot.com/doc:math
元に戻した†
悪くはないのだけれど、
- 重すぎるのと
- \bm が見にくいこと
とが気になって元に戻した。
mathjax プラグイン†
LANG:php <?php // // pukiwiki用 数式プラグイン (mathjax.inc.php) // Copywrite 2013 Osamu Takeuchi <osamu@big.jp> // // [履歴] // 2013.04.08 初期リリース // // [インストール] // ソースファイルを (pukiwiki)/plugin/mathjax.inc.php として保存 // // [使い方] // #mathjax; として一回呼んでおくと、その後の TeX ソース内に // 記述された mathjax 形式の tex ソースを maxjax で処理する // ようになる // function plugin_mathjax_header() { return <<<'EOS' <meta http-equiv="X-UA-Compatible" CONTENT="IE=EmulateIE7" /> <script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script> <script type="text/x-mathjax-config"> MathJax.Hub.Config({ tex2jax: { inlineMath: [ ['$','$'], ["\\(","\\)"] ], displayMath: [ ['$$','$$'], ["\\[","\\]"] ] }, TeX: { extensions: ["cancel.js","enclose.js","mhchem.js"], Macros: { C: '{\\mathbb C}', R: '{\\mathbb R}', Q: '{\\mathbb Q}', Z: '{\\mathbb Z}', diag: '\\mathop{\\mathrm{diag}}\\nolimits' } } }); </script> EOS; } function plugin_mathjax_convert() { return plugin_math_inline(); } function plugin_mathjax_inline() { $header = ""; if (!defined("PLUGIN_MATHJAX_LOADED")) { define("PLUGIN_MATHJAX_LOADED", "LOADED"); $header = plugin_mathjax_header(); } $aryargs = func_get_args(); $math = join(",", $aryargs); $math = rtrim($math, ","); //remove extra comma at the end. $math = htmlspecialchars($math); if($math===""){ return $header; }else{ return $header . "\\(\\displaystyle\\begin{split}" . $math . "\\end{split}\\)"; } } ?>
これを入れるだけで、\[x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}\] などと書けるようになる。
事前に定義しておきたい内容があれば Macros のところに追加すればいい。
たとえば、
MARU: ['\\enclose{circle}{\\scriptsize #1}', 1], tr: '\\mathop{\\mathrm{tr}}\\nolimits', rank: '\\mathop{\\mathrm{rank}}\\nolimits', grad: '\\mathop{\\mathrm{grad}}\\nolimits', rot: '\\mathop{\\mathrm{rot}}\\nolimits', divergence: '\\mathop{\\mathrm{div}}\\nolimits'
とか。
いろいろバージョンが上がって動かなくなりました†
上記のコードがうまく動かなくなったため、より良い方法を検討しました。
TeX コードから svg への変換†
LANG:sh $ platex This is e-pTeX, Version 3.1415926-p3.3-110825-2.4 (utf8.euc) (TeX Live 2012/Debian) $ platex -interaction=nonstopmode math.tex -> math.dvi $ dvipdfmx math.dvi -> math.pdf $ pdf2svg math.pdf math.svg
とすることで、TeX ソースから math.svg を作れます。 ただし、TeX ソースの文字コードは utf8 にしておきます。
数式の書かれた部分を切り出す†
上記のようにして作った math.svg は、 例えば A4 サイズの大きな用紙の左上に数式が書かれた画像になっているため、 そのまま html 内に貼り付けられる物ではありません。
例えば、
LANG:TeX \documentclass[12pt,fleqn]{jarticle} \pagestyle{empty} \usepackage{amsmath,amssymb,bm,braket,color,multirow,bigdelim,graphicx} % \begin{document} \setlength{\hoffset}{-1in} \setlength{\voffset}{-1in} \setlength{\topmargin}{0mm} \setlength{\headheight}{0mm} \setlength{\headsep}{0mm} \setlength{\oddsidemargin}{0mm} \setlength{\mathindent}{0mm} % \begin{align*} \vbox{\hrule \hbox{\strut\vrule {} \begin{matrix}a\\b\end{matrix} \vrule} \hrule} \end{align*} \end{document} %
この結果はこうなります。
余白を取り除いた svg を作るためには数式の大きさが分からなければならないのですが、 普通に作った svg ファイルには数式の大きさの情報は入ってきません。
そこで TeX ソースを書き換えて、数式を枠で囲っておくことにしました。
LANG:TeX \begin{align*}\boxed{ e^{i\pi}=-1 }\end{align*} \end{document}
あとは svg を解析して外枠情報を読み出し、 それに合わせてトリミングしてやればいいはず。
できた svg はこんな感じ。
LANG:xml <?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="595.28pt" height="841.89pt" viewBox="0 0 595.28 841.89" version="1.1"> <defs> <g> <symbol overflow="visible" id="glyph0-0"> <path style="stroke:none;" d=""/> </symbol> <symbol overflow="visible" id="glyph0-1"> <path style="stroke:none;" d="M 2.140625 -2.765625 C 2.46875 -2.765625 3.28125 -2.796875 3.84375 -3.015625 C 4.75 ... "/> </symbol> <symbol overflow="visible" id="glyph1-0"> <path style="stroke:none;" d=""/> </symbol> <symbol overflow="visible" id="glyph1-1"> <path style="stroke:none;" d="M 2.375 -4.96875 C 2.375 -5.140625 2.25 -5.28125 2.0625 -5.28125 C 1.859375 -5.28125 ... "/> </symbol> <symbol overflow="visible" id="glyph1-2"> <path style="stroke:none;" d="M 2.265625 -2.90625 L 3.171875 -2.90625 C 3.015625 -2.1875 2.875 -1.59375 2.875 -1 C ... "/> </symbol> <symbol overflow="visible" id="glyph2-0"> <path style="stroke:none;" d=""/> </symbol> <symbol overflow="visible" id="glyph2-1"> <path style="stroke:none;" d="M 8.0625 -3.875 C 8.234375 -3.875 8.453125 -3.875 8.453125 -4.09375 C 8.453125 -4.3125 ... "/> </symbol> <symbol overflow="visible" id="glyph2-2"> <path style="stroke:none;" d="M 3.4375 -7.65625 C 3.4375 -7.9375 3.4375 -7.953125 3.203125 -7.953125 C 2.921875 -7.625 ... "/> </symbol> <symbol overflow="visible" id="glyph3-0"> <path style="stroke:none;" d=""/> </symbol> <symbol overflow="visible" id="glyph3-1"> <path style="stroke:none;" d="M 7.875 -2.75 C 8.078125 -2.75 8.296875 -2.75 8.296875 -2.984375 C 8.296875 -3.234375 ... "/> </symbol> </g> </defs> <g id="surface1"> <path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -71.5 47.5 L -20.5 47.5 " transform="matrix(1,0,0,-1,72,72)"/> <path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -71.5 30.5 L -71.5 47.5 " transform="matrix(1,0,0,-1,72,72)"/> <g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> <use xlink:href="#glyph0-1" x="3.39" y="37.69"/> </g> <g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> <use xlink:href="#glyph1-1" x="8.81" y="32.75"/> <use xlink:href="#glyph1-2" x="11.692749" y="32.75"/> </g> <g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> <use xlink:href="#glyph2-1" x="20.66" y="37.69"/> </g> <g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> <use xlink:href="#glyph3-1" x="33.09" y="37.69"/> </g> <g style="fill:rgb(0%,0%,0%);fill-opacity:1;"> <use xlink:href="#glyph2-2" x="42.39" y="37.69"/> </g> <path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -20.5 30.5 L -20.5 47.5 " transform="matrix(1,0,0,-1,72,72)"/> <path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -71.5 30.5 L -20.5 30.5 " transform="matrix(1,0,0,-1,72,72)"/> </g> </svg>
Symbol として文字を定義して、それを use で置いて数式を書いていることが分かります。
数式を囲むように存在する、次の部分が枠を表わすはず。
LANG:xml <g id="surface1"> <path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -71.5 47.5 L -20.5 47.5 " transform="matrix(1,0,0,-1,72,72)"/> <path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -71.5 30.5 L -71.5 47.5 " transform="matrix(1,0,0,-1,72,72)"/> ... <path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -20.5 30.5 L -20.5 47.5 " transform="matrix(1,0,0,-1,72,72)"/> <path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M -71.5 30.5 L -20.5 30.5 " transform="matrix(1,0,0,-1,72,72)"/> </g>
試しにこの4つの <path> を削除してみると、思った通り枠が消えます。
1本ずつ消してみると線の順番は 上 左 (内容) 右 下 になっていることも分かります。
path> は始点が M で、終点が L で指定されているようで、例えば1つ目であれば 座標 (-71.5, 47.5) から 座標 (-20.5, 47.5) へ線が引かれます。
ただし、この <path> には transform="matrix(1,0,0,-1,72,72)" が指定されているので、実際には (x, y) という座標は ( 1 x + 72, -1 y + 72) という座標に変換されます。
したがって、変換後の座標は以下の通りです。
- 上線: (0.5, 24.5)-(51.5, 24.5)
- 下線: (0.5, 41.5)-(51.5, 41.5)
ということで、これら4本の線を除き、<svg> タグの指定を width="51pt" height="17pt" viewBox="0.5 24.5 51 17" としてやると・・・うまくいった。viewBox の後ろの2つのパラメータは右下座標ではなく幅と高さなので、width, height パラメータと同じ値を入れればいい。
あとはこれを php で自動化すればいいはず。
php による切り出し†
まず見つけるのは、
LANG:xml </defs> <g id="surface1">
の直後から2行の <path> と、最後の
LANG:xml </g> </svg>
の直前の2行の <path>
特に、</defs> 後の初めての <path> と、ファイルの最後に現れる <path> とを見ることで、 数式のサイズを知ることができる。
1つの画面に複数の数式を貼る場合†
1つの画面に複数の数式を貼る場合、 同じ名前で異なる Glyph が大量に定義されてしまい、 困ったことになる。
そこで、自動生成された svg の Glyph 名に、数式をハッシュ化した文字列を追加することで Glyph 名の重複を避けることにした。
本当は、同じ Glyph を異なる名前で複数回登録する無駄を省くために、 同じ名前の Glyph を検索して再利用すればよいのだけれど・・・ それは時間があればまた後ほど。