pukiwiki/数式プラグイン

(2086d) 更新


公開メモ

pukiwiki 用の数式プラグイン

MathJax を使うバージョンは、これとは別に下の方にあります。

pukiwiki に TeX で書いた数式を入れられるようにするプラグインです。

特に、数式をインライン svg として表示できるようにしたので、 対応するブラウザでは拡大時や印刷時に高い品質が得られます。

現在、ユーザーエージェントに Mozilla/5 を含む場合にのみ、 インライン svg で数式を表示するようになっています。

それ以外では png ファイルとして数式を表示します。

使用例

&math(y=ax^2+bx+c);

y=ax^2+bx+c

&math(x=\frac{-b\pm\sqrt{b^2-4ac}}{2a});

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 .);

\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 .);

\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 への変換がうまく行かず止まってる。

あと minbfg とか打つと文字間がおかしいような気がするんだけど、 これも保留中。(文字幅情報が真四角を基準に取られているため? 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, \set : 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'

とか。

いろいろバージョンが上がって動かなくなりました

上記の latex を使って svg を作成するコードがうまく動かなくなったため、より良い方法を検討しました。

TeX ソースから svg への変換

LANG:console
$ 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}
%

この結果はこうなります。

tex2svg_1.png

余白を取り除いた svg を作るためには数式の大きさが分からなければならないのですが、 普通に作った svg ファイルには数式の大きさの情報は入ってきません。

そこで TeX ソースを書き換えて、数式を枠で囲っておくことにしました。

LANG:TeX
\begin{align*}\boxed{
e^{i\pi}=-1
}\end{align*}
\end{document}

tex2svg_2.png

あとは 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 の定義があれば再定義をしない工夫をします。

副作用

ただ、そのようにするとブラウザで「選択領域のみ印刷」した際、 選択領域に Glyph の定義が入っていないと文字が出力されずに空白ができてしまいます。

あと、Glyph 置き換えはサーバーへの負担も大きいし、一長一短ある感じかもしれません。

ソースコード

一応、以下のコードで動いているみたい?

作った svg ファイルは次回表示する際に負荷を軽減するためファイルにキャッシュしています。 このキャッシュファイルの数が膨大になるため(現状で1万以上になっています・・・)、 1フォルダあたりのファイル数を少なく保つよう工夫しました。

具体的には、数式自身をハッシュ化して決めたファイル名の先頭の2文字を元にフォルダを掘り、 その中へ入れています。1万ファイルが256フォルダに分けられるので、 1フォルダあたりに40ファイル程度になっています。

LANG:html(linenumber)
<?php
//
// pukiwiki用 数式プラグイン (math.inc.php)
//   Copywrite 2011 Osamu Takeuchi <osamu@big.jp>
//
// [履歴]
//   2011.05.04 初期リリース
//   2014.04.14 svg 変換を最新版 pstoedit に対応するため大幅書き直し
//              svg 中の glyph を展開せずに使うことで svg をスリム化
//              さらに同じ glyph を複数の svg で使い回すことでスリム化
//              namespace を追加して関数名等の重複を避ける
//
// [インストール]
//   ソースファイルを (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, braket, color, multirow,
//   bigdelim,graphicx パッケージを利用し、align* 環境で評価される
//
// [設定]
//
// 画像ファイルの保存場所
// constant MATHIMG_DIR
//
// function usepackage()             // 利用するパッケージ
// function preamble_commands()      // プリアンブルに追加するコード
// function declare_math_operators() // MathOperator の定義
// function in_document_commands()   // document 内に追加するコード
// function in_equation_commands()   // equation 環境内に追加するコード
// function math2tex($math)          // tex ソースのひな形
//

namespace math2html {

// 画像ファイルの保存場所
define('MATH2HTML_DIR', 'mathimg');

// プリアンブルに追加するコード
function preamble_commands()
{
return 
'';
}

// MathOperator の定義
function declare_math_operators()
{
    $math_operators = array (
        'Res', 
        'rot', 
        'grad', 
        'rank', 
        'tr', 
        array('Image', 'Im'),
        array('Kernel', 'Ker'),
        array('DIV', 'div'), 
        array('ROT', 'rot'), 
        array('GRAD', 'grad'),
        array('trace', 'tr')
    );

    $declare_math_operators = '';

    for($i=0; $i<count($math_operators); $i=$i+1){
        $name = '';
        $decl = '';
        if( is_array($math_operators[$i]) ) {
            $name = $math_operators[$i][0];
            $decl = $math_operators[$i][1];
        } else {
            $name = $math_operators[$i];
            $decl = $math_operators[$i];
        }
        $declare_math_operators .= 
            sprintf("\\DeclareMathOperator*{\\$name}{%s}", $decl);
    }

    return $declare_math_operators;
}

// document 内に追加するコード
function in_document_commands()
{
return
'\newcommand{\MARU}[1]
{{\ooalign{\hfil#1\/\hfil\crcr\raise.167ex\hbox{\mathhexbox20D}}}}
\newcommand{\transpose}[0]{\,{}^t\!}
\newcommand{\D}[0]{{\rm d}}
\newcommand{\PD}[0]{\partial}
\newcommand{\comment}[1]{}';
}

// equation 環境内に追加するコード
function in_equation_commands()
{
return
'%%コーシーの主値積分%%%%
\def\Xint#1{\mathchoice
{\XXint\displaystyle\textstyle{#1}}%
{\XXint\textstyle\scriptstyle{#1}}%
{\XXint\scriptstyle\scriptscriptstyle{#1}}%
{\XXint\scriptscriptstyle\scriptscriptstyle{#1}}%
\!\int}
\def\XXint#1#2#3{{\setbox0=\hbox{$#1{#2#3}{\int}$}
\vcenter{\hbox{$#2#3$}}\kern-.5\wd0}}
\def\ddashint{\Xint=}
\def\dashint{\Xint-}
%%%%%%%%%%%%%%%%
\makeatletter
\def\@tvsp{\mathchoice{{}\mkern-4.5mu}{{}\mkern-4.5mu}{{}\mkern-2.5mu}{}}
\def\llangle{\left<\@tvsp\left<}
\def\rrangle{\right>\@tvsp\right>}
\def\bigllangle{\left<\@tvsp\bigl<}
\def\bigrrangle{\bigr>\@tvsp\right>}
\def\hsymbu#1{\smash{\lower1.8ex\hbox{\scalebox{2}{$#1$}}}}
\def\hsymbl#1{\smash{\hbox{\scalebox{2}{$#1$}}}}
\makeatother
%
\def\red#1{\textcolor{red}{#1}}
%';
}

// 利用するパッケージ
function usepackage()
{
    return 'amsmath,amssymb,bm,braket,color,multirow,bigdelim,graphicx';
}

// tex ソースを作成
function math2tex($math)
{
$format =
'\documentclass[12pt,fleqn]{jarticle}
\pagestyle{empty}
\usepackage{' . usepackage() . '}
%%
%% preamble commands
%%
%s
%%
%% declare math operators
%%
%s
\begin{document}
%%
\setlength{\\hoffset}{-1in}
\setlength{\\voffset}{-1in}
\setlength{\\topmargin}{0mm}
\setlength{\\headheight}{0mm}
\setlength{\\headsep}{0mm}
\setlength{\\oddsidemargin}{0mm}
\setlength{\\mathindent}{0mm}
%%
%% in document commands
%%
%s
\begin{equation*}
%%
%% in equation commands
%%
%s
%%
\boxed{
\begin{split}
%s
\end{split}
}
%%
\end{equation*}
\end{document}
%%';

    return sprintf(
        $format,
        preamble_commands(),
        declare_math_operators(),
        in_document_commands(),
        in_equation_commands(),
        str_replace("\n", "", $math)
        );
}

// svg の transform matrix を再現
function transform_by_matrix($x, $y, $a, $b, $c, $d, $e, $f)
{
    return [
        $a * $x + $b * $y + $e,
        $c * $x + $d * $y + $f
    ];
}

// svg ソースを整形する
//  - 外枠サイズにトリムする
//  - 外枠を外す
//  - Glyph に一意な名前を付ける
//  - <title> を付加する
function format_svg($svg, $math)
{
    // $math は TeX ソース
    // $svg は改行で split されて渡される
    
    $glyphs = array();  // ローカル Glyph 名から一意な Glyph 名への連想配列
    
    $after_defs = 0;       // </defs> 以前を探索中
    $top_line = 0;      // 外形線の上線の書かれた行
    $bottom_line = 0;   // 外形線の下線の書かれた行
    for( $i = 0; $i < count($svg); ++$i ) {
        if (!$after_defs) {
            // </defs> 以前を探索中
            if (preg_match('/\A<symbol .*id="([^"]+)"/', $svg[$i], $matches)) {
                // Glyph 定義を発見
                $glyph_id = $matches[1];
                // path 情報から一意な名前を算出
                $glyph_path = $svg[$i+1];
                for( $j = $i+2; mb_substr($svg[$j],0,5)=="<path"; ++$j){
                  $glyph_path .= $svg[$j];
                }
                $glyph_hash = sha1($glyph_path);
                // 名前を登録&書き換え
                $glyphs[$glyph_id] = $glyph_hash;
                $svg[$i] = preg_replace('/ id="[^"]+"/', " id=\"{$glyph_hash}\"", $svg[$i]);
                // 処理済み行をスキップ
                $i = $j; // $j は </symbol> を指している (後で ++$i される)
            }
            if (mb_substr($svg[$i],0,7)=="</defs>") {
                $after_defs = 1;
            }
        } else {
            // </defs> 以降を探索中
            if (preg_match('/\A *<use .*xlink:href="#([^"]+)"/', $svg[$i], $matches)) {
                // Glyph 名を一意な物に書き換える
                $svg[$i] = preg_replace('/ xlink:href="#[^"]+"/', 
                               " xlink:href=\"#{$glyphs[$matches[1]]}\"", $svg[$i]);
            } else
            if (mb_substr($svg[$i],0,5)=="<path") {
                if ($top_line==0) {
                    // 初めての <path は上部外形線
                    $top_line = $i;
                }
                // 最後の <path は下部外形線
                $bottom_line = $i;
            }
        }
    }

    // 上部外形線から左上座標を得る
    preg_match('/d=" *M +([^ ]+) +([^ ]+) +L +([^ ]+) +([^ ]+) *" +'.
                'transform *= *" *matrix\\(([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)\\) *"/', 
               $svg[$top_line], $matches);
    list($left, $top) = transform_by_matrix(
        intval($matches[1]), intval($matches[2]),
        intval($matches[5]), intval($matches[6]),
        intval($matches[7]), intval($matches[8]),
        intval($matches[9]), intval($matches[10])
    );

    // 下部外形線から右下座標を得る
    preg_match('/d=" *M +([^ ]+) +([^ ]+) +L +([^ ]+) +([^ ]+) *" +'.
                'transform *= *" *matrix\\(([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)\\) *"/', 
               $svg[$bottom_line], $matches);
    list($right, $bottom) = transform_by_matrix(
        intval($matches[3]), intval($matches[4]),
        intval($matches[5]), intval($matches[6]),
        intval($matches[7]), intval($matches[8]),
        intval($matches[9]), intval($matches[10])
    );
    
    // 外形寸法を算出
    $width  = $right - $left;
    $height = $bottom - $top;
    
    // <?xml を上書き
    $svg[0] = $svg[1];

    // <title> を挿入
    $svg[1] = "<title>" . htmlspecialchars($math, ENT_COMPAT | ENT_HTML401, "UTF-8") . "</title>";

    // 外形線を削除
    $svg[$bottom_line] = '';
    $svg[$bottom_line-1] = '';
    $svg[$top_line+1] = '';
    $svg[$top_line] = '';

    // svg 画像をトリミング
    $svg[0] = preg_replace('/width="[^"]+"/', "width=\"{$width}pt\"", $svg[0]);
    $svg[0] = preg_replace('/height="[^"]+"/', "height=\"{$height}pt\"", $svg[0]);
    $svg[0] = preg_replace('/viewBox="[^"]+"/', 
                  "viewBox=\"{$left} {$top} {$width} {$height}\"", $svg[0]);

    return $svg;
}

// math から svg を作成する
function math2svg($math)
{
    $tex_src = math2tex($math);

    $tmp_dir = sys_get_temp_dir();
    $tmp_base = $tmp_dir . "/math_edit" . getmypid();
    $tmp_tex =  $tmp_base . ".tex";
    $tmp_dvi =  $tmp_base . ".dvi";
    $tmp_pdf =  $tmp_base . ".pdf";
    $tmp_svg =  $tmp_base . ".svg";
    $tmp_any =  $tmp_base . ".*";

    $out = array();
    # create tex src file
    $fh = fopen($tmp_tex, 'w');
    fwrite($fh, $tex_src);
    fclose($fh);
    # convert tex to dvi
    exec("platex -interaction=nonstopmode -output-directory={$tmp_dir} {$tmp_tex}", $out, $ret);
    if ($ret) return $out;
    # convert dvi to pdf
    exec("dvipdfmx -o {$tmp_pdf} {$tmp_base} 2>&1", $out, $ret);
    if ($ret) return $out;
    # convert pdf to svg
    exec("pdf2svg {$tmp_pdf} {$tmp_svg}", $out, $ret);
    if ($ret) return $out;
    # read svg
    $svg = file( $tmp_svg );
    # remove temporary files
    exec("rm {$tmp_any}", $out, $ret);

    // svg ソースを整形する
    //  - 外枠サイズにトリムする
    //  - 外枠を外す
    //  - Glyph に一意な名前を付ける
    //  - <title> を付加する
    return format_svg($svg, $math);
}

// 同じページに配置した svg にてすでに定義済みの glyph を取り除く
function remove_duplicated_glyphs($svg)
{
    // 定義済み Glyph を覚えておく
    global $defined_glyphs;
    for( $i = 0; $i < count($svg); ++$i ) {
        if (preg_match('/\A<symbol .*id="([^"]+)"/', $svg[$i], $matches)) {
            $glyph_id = $matches[1];
            if(!isset($defined_glyphs)) {
                $defined_glyphs = array();
            }
            if(!isset($defined_glyphs[$glyph_id])) {
                $defined_glyphs[$glyph_id] = 1;
            } else {
                // 定義済みなので削除する
                $svg[$i] = '';
                $svg[$i+1] = '';
                $svg[$i+2] = '';
            }
        }
        if (mb_substr($svg[$i],0,7)=="</defs>") {
            break;
        }
    }
    return $svg;
}

// $math から html (svg) を作成する
// 結果をファイルにキャッシュする
function convert($math)
{
    global $error_log_written;
    $math = mb_convert_encoding($math, "UTF-8", "EUC");

    $image_base = sha1($math);
    $image_dir = MATH2HTML_DIR . "/{$image_base[0]}/{$image_base[1]}";
    $image_svg = "{$image_dir}/{$image_base}.svg";

    if(!file_exists($image_svg)) {
        $svg = math2svg($math);
        
        if (mb_substr($svg[0],0,4)=="<svg") {
            if(!file_exists($image_dir)){
                mkdir($image_dir, 0770, true);
            }
            $fh = fopen($image_svg, "w");
            fwrite($fh, join("", $svg));
            fclose($fh);
        }
    } else {
        $svg = file($image_svg);
    }

    if (mb_substr($svg[0],0,4)=="<svg") {
        // 同じページに配置した svg にてすでに定義済みの glyph を取り除く
        return join("", remove_duplicated_glyphs($svg));
    } else {
        if (!isset($error_log_written) && !preg_match('/math_edit\/editor.html/', $_SERVER['REQUEST_URI'])) {
            error_log ('Math2html conversion error in ' . $_SERVER['REQUEST_URI']);
            $error_log_written = 1;
        }
        // エラーメッセージを表示する
        return 
        '<span class="math-error">[Math Conversion Error]<div><br>' .
        preg_replace("/\\n/", "<br>\n", htmlspecialchars(
	    mb_convert_encoding(join("\n", $svg), "UTF-8", "EUC"), 
	    ENT_COMPAT | ENT_HTML401, "UTF-8")) .
	    '</div></span>';
    }
}

} // end of namespace math2html

namespace {

    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.
        return math2html\convert($math);
    }

}

?>

現状このコードで試してみています。

問題がなさそうであれば、このページ全体の構成を見直して書き直す予定です。

不具合

現状では、カラーを使っているところで表示が崩れるようです?

LANG:latex
{\color{red}赤}\ 黒\ {\color{blue}青}

の結果で COLOR{RED}{赤黒} となってしまいます。。。

カラーが崩れるところを調べてみた

あ、上記コードにはない

LANG:php
  $svg = preg_replace('/<\/g>\n<g>/', "\n", $svg);

何て訳分からないのを入れていたのがまずかったようでした。

とりあえずこれをコメントアウトして、あとは不具合はなさそう?

KaTeX について

MathJax よりも動作がずっと軽いと評判の KaTeX を使えるようにしてみました。

pukiwiki/数式プラグイン/KaTeX

コメント





添付ファイル: filetex2svg_2.png 1434件 [詳細] filetex2svg_1.png 1486件 [詳細]

Counter: 15393 (from 2010/06/03), today: 4, yesterday: 0