pukiwiki/数式プラグイン のバックアップ(No.3)

更新


公開メモ

pukiwiki 用の数式プラグイン

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 の周りに空白が・・・)

コメント





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